From 541b1cfa3c65303a6a74881d07c5030319a77d64 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Tue, 27 Dec 2022 18:14:05 -0800 Subject: [PATCH] backend compiled --- backend/controllers/cln/balance.js | 30 + backend/controllers/cln/channels.js | 160 ++++++ backend/controllers/cln/fees.js | 24 + backend/controllers/cln/getInfo.js | 74 +++ backend/controllers/cln/invoices.js | 55 ++ backend/controllers/cln/network.js | 92 +++ backend/controllers/cln/offers.js | 99 ++++ backend/controllers/cln/onchain.js | 53 ++ backend/controllers/cln/payments.js | 141 +++++ backend/controllers/cln/peers.js | 65 +++ backend/controllers/cln/utility.js | 97 ++++ backend/controllers/cln/webSocketClient.js | 116 ++++ backend/controllers/eclair/channels.js | 158 ++++++ backend/controllers/eclair/fees.js | 144 +++++ backend/controllers/eclair/getInfo.js | 50 ++ backend/controllers/eclair/invoices.js | 124 +++++ backend/controllers/eclair/network.js | 22 + backend/controllers/eclair/onchain.js | 96 ++++ backend/controllers/eclair/payments.js | 128 +++++ backend/controllers/eclair/peers.js | 128 +++++ backend/controllers/eclair/webSocketClient.js | 130 +++++ backend/controllers/lnd/balance.js | 35 ++ backend/controllers/lnd/channels.js | 267 +++++++++ backend/controllers/lnd/channelsBackup.js | 239 ++++++++ backend/controllers/lnd/fees.js | 48 ++ backend/controllers/lnd/getInfo.js | 56 ++ backend/controllers/lnd/graph.js | 164 ++++++ backend/controllers/lnd/invoices.js | 84 +++ backend/controllers/lnd/message.js | 43 ++ backend/controllers/lnd/newAddress.js | 21 + backend/controllers/lnd/payments.js | 98 ++++ backend/controllers/lnd/peers.js | 87 +++ backend/controllers/lnd/switch.js | 58 ++ backend/controllers/lnd/transactions.js | 46 ++ backend/controllers/lnd/wallet.js | 213 +++++++ backend/controllers/lnd/webSocketClient.js | 122 ++++ backend/controllers/shared/RTLConf.js | 391 +++++++++++++ backend/controllers/shared/authenticate.js | 133 +++++ backend/controllers/shared/boltz.js | 163 ++++++ backend/controllers/shared/loop.js | 241 ++++++++ backend/controllers/shared/pageSettings.js | 33 ++ backend/models/config.model.js | 66 +++ backend/models/database.model.js | 139 +++++ backend/models/ecl.model.js | 12 + backend/routes/cln/balance.js | 7 + backend/routes/cln/channels.js | 14 + backend/routes/cln/fees.js | 7 + backend/routes/cln/getInfo.js | 7 + backend/routes/cln/index.js | 31 ++ backend/routes/cln/invoices.js | 9 + backend/routes/cln/network.js | 11 + backend/routes/cln/offers.js | 12 + backend/routes/cln/onchain.js | 9 + backend/routes/cln/payments.js | 8 + backend/routes/cln/peers.js | 9 + backend/routes/cln/utility.js | 11 + backend/routes/eclair/channels.js | 11 + backend/routes/eclair/fees.js | 8 + backend/routes/eclair/getInfo.js | 7 + backend/routes/eclair/index.js | 25 + backend/routes/eclair/invoices.js | 9 + backend/routes/eclair/network.js | 7 + backend/routes/eclair/onchain.js | 10 + backend/routes/eclair/payments.js | 10 + backend/routes/eclair/peers.js | 9 + backend/routes/lnd/balance.js | 7 + backend/routes/lnd/channels.js | 13 + backend/routes/lnd/channelsBackup.js | 10 + backend/routes/lnd/fees.js | 7 + backend/routes/lnd/getInfo.js | 7 + backend/routes/lnd/graph.js | 13 + backend/routes/lnd/index.js | 37 ++ backend/routes/lnd/invoices.js | 9 + backend/routes/lnd/message.js | 8 + backend/routes/lnd/newAddress.js | 7 + backend/routes/lnd/payments.js | 11 + backend/routes/lnd/peers.js | 9 + backend/routes/lnd/switch.js | 7 + backend/routes/lnd/transactions.js | 8 + backend/routes/lnd/wallet.js | 14 + backend/routes/shared/RTLConf.js | 17 + backend/routes/shared/authenticate.js | 9 + backend/routes/shared/boltz.js | 14 + backend/routes/shared/index.js | 19 + backend/routes/shared/loop.js | 16 + backend/routes/shared/pageSettings.js | 8 + backend/utils/app.js | 90 +++ backend/utils/authCheck.js | 67 +++ backend/utils/common.js | 522 ++++++++++++++++++ backend/utils/config.js | 352 ++++++++++++ backend/utils/cors.js | 25 + backend/utils/csrf.js | 20 + backend/utils/database.js | 296 ++++++++++ backend/utils/logger.js | 81 +++ backend/utils/webSocketServer.js | 179 ++++++ 95 files changed, 6858 insertions(+) create mode 100644 backend/controllers/cln/balance.js create mode 100644 backend/controllers/cln/channels.js create mode 100644 backend/controllers/cln/fees.js create mode 100644 backend/controllers/cln/getInfo.js create mode 100644 backend/controllers/cln/invoices.js create mode 100644 backend/controllers/cln/network.js create mode 100644 backend/controllers/cln/offers.js create mode 100644 backend/controllers/cln/onchain.js create mode 100644 backend/controllers/cln/payments.js create mode 100644 backend/controllers/cln/peers.js create mode 100644 backend/controllers/cln/utility.js create mode 100644 backend/controllers/cln/webSocketClient.js create mode 100644 backend/controllers/eclair/channels.js create mode 100644 backend/controllers/eclair/fees.js create mode 100644 backend/controllers/eclair/getInfo.js create mode 100644 backend/controllers/eclair/invoices.js create mode 100644 backend/controllers/eclair/network.js create mode 100644 backend/controllers/eclair/onchain.js create mode 100644 backend/controllers/eclair/payments.js create mode 100644 backend/controllers/eclair/peers.js create mode 100644 backend/controllers/eclair/webSocketClient.js create mode 100644 backend/controllers/lnd/balance.js create mode 100644 backend/controllers/lnd/channels.js create mode 100644 backend/controllers/lnd/channelsBackup.js create mode 100644 backend/controllers/lnd/fees.js create mode 100644 backend/controllers/lnd/getInfo.js create mode 100644 backend/controllers/lnd/graph.js create mode 100644 backend/controllers/lnd/invoices.js create mode 100644 backend/controllers/lnd/message.js create mode 100644 backend/controllers/lnd/newAddress.js create mode 100644 backend/controllers/lnd/payments.js create mode 100644 backend/controllers/lnd/peers.js create mode 100644 backend/controllers/lnd/switch.js create mode 100644 backend/controllers/lnd/transactions.js create mode 100644 backend/controllers/lnd/wallet.js create mode 100644 backend/controllers/lnd/webSocketClient.js create mode 100644 backend/controllers/shared/RTLConf.js create mode 100644 backend/controllers/shared/authenticate.js create mode 100644 backend/controllers/shared/boltz.js create mode 100644 backend/controllers/shared/loop.js create mode 100644 backend/controllers/shared/pageSettings.js create mode 100644 backend/models/config.model.js create mode 100644 backend/models/database.model.js create mode 100644 backend/models/ecl.model.js create mode 100644 backend/routes/cln/balance.js create mode 100644 backend/routes/cln/channels.js create mode 100644 backend/routes/cln/fees.js create mode 100644 backend/routes/cln/getInfo.js create mode 100644 backend/routes/cln/index.js create mode 100644 backend/routes/cln/invoices.js create mode 100644 backend/routes/cln/network.js create mode 100644 backend/routes/cln/offers.js create mode 100644 backend/routes/cln/onchain.js create mode 100644 backend/routes/cln/payments.js create mode 100644 backend/routes/cln/peers.js create mode 100644 backend/routes/cln/utility.js create mode 100644 backend/routes/eclair/channels.js create mode 100644 backend/routes/eclair/fees.js create mode 100644 backend/routes/eclair/getInfo.js create mode 100644 backend/routes/eclair/index.js create mode 100644 backend/routes/eclair/invoices.js create mode 100644 backend/routes/eclair/network.js create mode 100644 backend/routes/eclair/onchain.js create mode 100644 backend/routes/eclair/payments.js create mode 100644 backend/routes/eclair/peers.js create mode 100644 backend/routes/lnd/balance.js create mode 100644 backend/routes/lnd/channels.js create mode 100644 backend/routes/lnd/channelsBackup.js create mode 100644 backend/routes/lnd/fees.js create mode 100644 backend/routes/lnd/getInfo.js create mode 100644 backend/routes/lnd/graph.js create mode 100644 backend/routes/lnd/index.js create mode 100644 backend/routes/lnd/invoices.js create mode 100644 backend/routes/lnd/message.js create mode 100644 backend/routes/lnd/newAddress.js create mode 100644 backend/routes/lnd/payments.js create mode 100644 backend/routes/lnd/peers.js create mode 100644 backend/routes/lnd/switch.js create mode 100644 backend/routes/lnd/transactions.js create mode 100644 backend/routes/lnd/wallet.js create mode 100644 backend/routes/shared/RTLConf.js create mode 100644 backend/routes/shared/authenticate.js create mode 100644 backend/routes/shared/boltz.js create mode 100644 backend/routes/shared/index.js create mode 100644 backend/routes/shared/loop.js create mode 100644 backend/routes/shared/pageSettings.js create mode 100644 backend/utils/app.js create mode 100644 backend/utils/authCheck.js create mode 100644 backend/utils/common.js create mode 100644 backend/utils/config.js create mode 100644 backend/utils/cors.js create mode 100644 backend/utils/csrf.js create mode 100644 backend/utils/database.js create mode 100644 backend/utils/logger.js create mode 100644 backend/utils/webSocketServer.js diff --git a/backend/controllers/cln/balance.js b/backend/controllers/cln/balance.js new file mode 100644 index 00000000..c621d31c --- /dev/null +++ b/backend/controllers/cln/balance.js @@ -0,0 +1,30 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getBalance = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Balance', msg: 'Getting Balance..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/getBalance'; + request(options).then((body) => { + if (!body.totalBalance) { + body.totalBalance = 0; + } + if (!body.confBalance) { + body.confBalance = 0; + } + if (!body.unconfBalance) { + body.unconfBalance = 0; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Balance', msg: 'Balance Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Balance', 'Get Balance Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/channels.js b/backend/controllers/cln/channels.js new file mode 100644 index 00000000..c56e7611 --- /dev/null +++ b/backend/controllers/cln/channels.js @@ -0,0 +1,160 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const listChannels = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channels..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/listChannels'; + request(options).then((body) => { + body?.map((channel) => { + if (!channel.alias || channel.alias === '') { + channel.alias = channel.id.substring(0, 20); + } + const local = (channel.msatoshi_to_us) ? channel.msatoshi_to_us : 0; + const remote = (channel.msatoshi_to_them) ? channel.msatoshi_to_them : 0; + const total = channel.msatoshi_total ? channel.msatoshi_total : 0; + channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3); + return channel; + }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channels List Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'List Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const openChannel = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Opening Channel..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/openChannel'; + options.body = req.body; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Open Channel Options', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Opened', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Open Channel Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const setChannelFee = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Setting Channel Fee..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/setChannelFee'; + options.body = req.body; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Update Channel Policy Options', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Updated Channel Policy', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Update Channel Policy Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const closeChannel = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' }); + req.setTimeout(60000 * 10); // timeout 10 mins + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + const unilateralTimeoutQuery = req.query.force ? '?unilateralTimeout=1' : ''; + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/closeChannel/' + req.params.channelId + unilateralTimeoutQuery; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Closing Channel', data: options.url }); + request.delete(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Closed', data: body }); + res.status(204).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Close Channel Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getLocalRemoteBalance = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Local & Remote Balances..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/localremotebal'; + request(options).then((body) => { + if (!body.localBalance) { + body.localBalance = 0; + } + if (!body.remoteBalance) { + body.remoteBalance = 0; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Local Remote Balance Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Local Remote Balance Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listForwards = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channel List Forwards..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/listForwards?status=' + (req.query.status ? req.query.status : 'settled'); + request.get(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Forwarding History Received For Status ' + req.query.status, data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Forwarding History Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const funderUpdatePolicy = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting or Updating Funder Policy..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/funderUpdate'; + if (req.body && req.body.policy) { + options.body = req.body; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Funder Update Body', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Funder Policy Received', data: body }); + body.channel_fee_max_base_msat = (body.channel_fee_max_base_msat && typeof body.channel_fee_max_base_msat === 'string' && body.channel_fee_max_base_msat.includes('msat')) ? +body.channel_fee_max_base_msat?.replace('msat', '') : body.channel_fee_max_base_msat; + body.lease_fee_base_msat = (body.lease_fee_base_msat && typeof body.lease_fee_base_msat === 'string' && body.lease_fee_base_msat.includes('msat')) ? +body.lease_fee_base_msat?.replace('msat', '') : body.channel_fee_max_base_msat; + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Funder Policy Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listForwardsPaginated = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Paginated List Forwards..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + const { status, maxLen, offset } = req.query; + let queryStr = '?status=' + (status ? status : 'settled'); + queryStr = queryStr + '&maxLen=' + (maxLen ? maxLen : '10'); + queryStr = queryStr + '&offset=' + (offset ? offset : '0'); + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/listForwardsPaginated' + queryStr; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Paginated Forwarding History url' + options.url }); + request.get(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Paginated Forwarding History Received For Status ' + req.query.status, data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Paginated Forwarding History Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/fees.js b/backend/controllers/cln/fees.js new file mode 100644 index 00000000..07f4ad82 --- /dev/null +++ b/backend/controllers/cln/fees.js @@ -0,0 +1,24 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getFees = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Getting Fees..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/getFees'; + request(options).then((body) => { + if (!body.feeCollected) { + body.feeCollected = 0; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Fee Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Fees', 'Get Fees Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/getInfo.js b/backend/controllers/cln/getInfo.js new file mode 100644 index 00000000..6263c542 --- /dev/null +++ b/backend/controllers/cln/getInfo.js @@ -0,0 +1,74 @@ +import request from 'request-promise'; +import { Database } from '../../utils/database.js'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { CLWSClient } from './webSocketClient.js'; +let options = null; +const logger = Logger; +const common = Common; +const clWsClient = CLWSClient; +const databaseService = Database; +export const getInfo = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Getting Core Lightning Node Information..' }); + common.logEnvVariables(req); + common.setOptions(req); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/getinfo'; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Selected Node ' + req.session.selectedNode.ln_node }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Calling Info from Core Lightning server url ' + options.url }); + if (!options.headers || !options.headers.macaroon) { + const errMsg = 'Core lightning get info failed due to bad or missing macaroon!'; + const err = common.handleError({ statusCode: 502, message: 'Bad Macaroon', error: errMsg }, 'GetInfo', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + return request(options).then((body) => { + const body_str = (!body) ? '' : JSON.stringify(body); + const search_idx = (!body) ? -1 : body_str.search('Not Found'); + if (!body || search_idx > -1 || body.error) { + if (body && !body.error) { + body.error = 'Error From Server!'; + } + const err = common.handleError(body, 'GetInfo', 'Get Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Node Information Before Update', data: body }); + body.lnImplementation = 'Core Lightning'; + const chainObj = { chain: '', network: '' }; + if (body.network.includes('litecoin') || body.network.includes('feathercoin')) { + chainObj.chain = ''; + chainObj.network = ''; + } + else if (body.network.includes('liquid')) { + chainObj.chain = 'Liquid'; + chainObj.network = common.titleCase(body.network); + } + else { + chainObj.chain = 'Bitcoin'; + chainObj.network = common.titleCase(body.network); + } + body.chains = [chainObj]; + body.uris = []; + if (body.address && body.address.length > 0) { + body.address.forEach((addr) => { + body.uris.push(body.id + '@' + addr.address + ':' + addr.port); + }); + } + req.session.selectedNode.api_version = body.api_version || ''; + req.session.selectedNode.ln_version = body.version || ''; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Core Lightning\'s Websocket Server.' }); + clWsClient.updateSelectedNode(req.session.selectedNode); + databaseService.loadDatabase(req.session); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body }); + return res.status(200).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'GetInfo', 'Get Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; diff --git a/backend/controllers/cln/invoices.js b/backend/controllers/cln/invoices.js new file mode 100644 index 00000000..770cec12 --- /dev/null +++ b/backend/controllers/cln/invoices.js @@ -0,0 +1,55 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const deleteExpiredInvoice = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Deleting Expired Invoices..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + const queryStr = req.query.maxExpiry ? '?maxexpiry=' + req.query.maxExpiry : ''; + options.url = req.session.selectedNode.ln_server_url + '/v1/invoice/delExpiredInvoice' + queryStr; + request.delete(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoices Deleted', data: body }); + res.status(204).json({ status: 'Invoice Deleted Successfully' }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listInvoices = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting Invoices..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + const labelQuery = req.query.label ? '?label=' + req.query.label : ''; + options.url = req.session.selectedNode.ln_server_url + '/v1/invoice/listInvoices' + labelQuery; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List URL', data: options.url }); + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoice', 'List Invoices Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const addInvoice = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/invoice/genInvoice'; + options.body = req.body; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Created', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoice', 'Add Invoice Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/network.js b/backend/controllers/cln/network.js new file mode 100644 index 00000000..b65d0c35 --- /dev/null +++ b/backend/controllers/cln/network.js @@ -0,0 +1,92 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getRoute = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Getting Network Routes..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/network/getRoute/' + req.params.destPubkey + '/' + req.params.amount; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Network Routes Received', data: body }); + res.status(200).json({ routes: body }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Network', 'Query Routes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listNode = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Node Lookup..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/network/listNode/' + req.params.id; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Node Lookup Finished', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Network', 'Node Lookup Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listChannel = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Channel Lookup..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/network/listChannel/' + req.params.channelShortId; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Channel Lookup Finished', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Network', 'Channel Lookup Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const feeRates = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Getting Network Fee Rates..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/network/feeRates/' + req.params.feeRateStyle; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Network Fee Rates Received for ' + req.params.feeRateStyle, data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Network', 'Fee Rates Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listNodes = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'List Nodes..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + const queryStr = req.query.liquidity_ads ? '?liquidity_ads=' + req.query.liquidity_ads : ''; + options.url = req.session.selectedNode.ln_server_url + '/v1/network/listNodes' + queryStr; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Network', msg: 'List Nodes URL' + options.url }); + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'List Nodes Finished', data: body }); + body.forEach((node) => { + if (node.option_will_fund) { + node.option_will_fund.lease_fee_base_msat = (node.option_will_fund.lease_fee_base_msat && typeof node.option_will_fund.lease_fee_base_msat === 'string' && + node.option_will_fund.lease_fee_base_msat.includes('msat')) ? node.option_will_fund.lease_fee_base_msat?.replace('msat', '') : node.option_will_fund.lease_fee_base_msat; + node.option_will_fund.channel_fee_max_base_msat = (node.option_will_fund.channel_fee_max_base_msat && typeof node.option_will_fund.channel_fee_max_base_msat === 'string' && + node.option_will_fund.channel_fee_max_base_msat.includes('msat')) ? node.option_will_fund.channel_fee_max_base_msat?.replace('msat', '') : node.option_will_fund.channel_fee_max_base_msat; + } + return node; + }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Network', 'Node Lookup Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/offers.js b/backend/controllers/cln/offers.js new file mode 100644 index 00000000..ad158bcb --- /dev/null +++ b/backend/controllers/cln/offers.js @@ -0,0 +1,99 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { Database } from '../../utils/database.js'; +import { CollectionFieldsEnum, CollectionsEnum } from '../../models/database.model.js'; +let options = null; +const logger = Logger; +const common = Common; +const databaseService = Database; +export const listOfferBookmarks = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Bookmarks..' }); + databaseService.find(req.session.selectedNode, CollectionsEnum.OFFERS).then((offers) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmarks Received', data: offers }); + res.status(200).json(offers); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Offers', 'Offer Bookmarks Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const deleteOfferBookmark = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Deleting Offer Bookmark..' }); + databaseService.remove(req.session.selectedNode, CollectionsEnum.OFFERS, CollectionFieldsEnum.BOLT12, req.params.offerStr).then((deleteRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmark Deleted', data: deleteRes }); + res.status(204).json(req.params.offerStr); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Offers', 'Offer Bookmark Delete Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listOffers = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offers..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/offers/listoffers'; + if (req.query.offer_id) { + options.url = options.url + '?offer_id=' + req.query.offer_id; + } + if (req.query.active_only) { + options.url = options.url + '?active_only=' + req.query.active_only; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Offers', msg: 'Offers List URL', data: options.url }); + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offers List Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Offers', 'List Offers Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const createOffer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Creating Offer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/offers/offer'; + options.body = req.body; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Created', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Offer', 'Create Offer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const fetchOfferInvoice = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Invoice..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/offers/fetchInvoice'; + options.body = req.body; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Offers', msg: 'Offer Invoice Body', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Invoice Received', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Offers', 'Get Offer Invoice Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const disableOffer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Disabling Offer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/offers/disableOffer/' + req.params.offerID; + request.delete(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Disabled', data: body }); + res.status(202).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Offers', 'Disable Offer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/onchain.js b/backend/controllers/cln/onchain.js new file mode 100644 index 00000000..fac8756a --- /dev/null +++ b/backend/controllers/cln/onchain.js @@ -0,0 +1,53 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getNewAddress = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Generating New Address..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/newaddr?addrType=' + req.query.type; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'New Address Generated', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'OnChain', 'New Address Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const onChainWithdraw = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Withdrawing from On Chain..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/withdraw'; + options.body = req.body; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'OnChain', msg: 'OnChain Withdraw Options', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Withdraw Finished', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'OnChain', 'Withdraw Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getUTXOs = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Listing Funds..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/listFunds'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Funds List Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'OnChain', 'List Funds Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/payments.js b/backend/controllers/cln/payments.js new file mode 100644 index 00000000..294517d2 --- /dev/null +++ b/backend/controllers/cln/payments.js @@ -0,0 +1,141 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { Database } from '../../utils/database.js'; +import { CollectionFieldsEnum, CollectionsEnum } from '../../models/database.model.js'; +let options = null; +const logger = Logger; +const common = Common; +const databaseService = Database; +function paymentReducer(accumulator, currentPayment) { + const currPayHash = currentPayment.payment_hash; + if (!currentPayment.partid) { + currentPayment.partid = 0; + } + if (!accumulator[currPayHash]) { + accumulator[currPayHash] = [currentPayment]; + } + else { + accumulator[currPayHash].push(currentPayment); + } + return accumulator; +} +function summaryReducer(accumulator, mpp) { + if (mpp.status === 'complete') { + accumulator.msatoshi = accumulator.msatoshi + mpp.msatoshi; + accumulator.msatoshi_sent = accumulator.msatoshi_sent + mpp.msatoshi_sent; + accumulator.status = mpp.status; + } + if (mpp.bolt11) { + accumulator.bolt11 = mpp.bolt11; + } + if (mpp.bolt12) { + accumulator.bolt12 = mpp.bolt12; + } + return accumulator; +} +function groupBy(payments) { + const paymentsInGroups = payments?.reduce(paymentReducer, {}); + const paymentsGrpArray = Object.keys(paymentsInGroups)?.map((key) => ((paymentsInGroups[key].length && paymentsInGroups[key].length > 1) ? common.sortDescByKey(paymentsInGroups[key], 'partid') : paymentsInGroups[key])); + return paymentsGrpArray?.reduce((acc, curr) => { + let temp = {}; + if (curr.length && curr.length === 1) { + temp = JSON.parse(JSON.stringify(curr[0])); + temp.is_group = false; + temp.is_expanded = false; + temp.total_parts = 1; + delete temp.partid; + } + else { + const paySummary = curr?.reduce(summaryReducer, { msatoshi: 0, msatoshi_sent: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' }); + temp = { + is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash, + destination: curr[0].destination, msatoshi: paySummary.msatoshi, msatoshi_sent: paySummary.msatoshi_sent, created_at: curr[0].created_at, + mpps: curr + }; + if (paySummary.bolt11) { + temp.bolt11 = paySummary.bolt11; + } + if (paySummary.bolt12) { + temp.bolt12 = paySummary.bolt12; + } + } + return acc.concat(temp); + }, []); +} +export const listPayments = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'List Payments..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/pay/listPayments'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body.payments }); + res.status(200).json(groupBy(body.payments)); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postPayment = (req, res, next) => { + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + if (req.body.paymentType === 'KEYSEND') { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Keysend Payment..' }); + options.url = req.session.selectedNode.ln_server_url + '/v1/pay/keysend'; + options.body = req.body; + } + else { + if (req.body.paymentType === 'OFFER') { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sending Offer Payment..' }); + options.body = { invoice: req.body.invoice }; + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sending Invoice Payment..' }); + options.body = req.body; + } + options.url = req.session.selectedNode.ln_server_url + '/v1/pay'; + } + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent', data: body }); + if (req.body.paymentType === 'OFFER') { + if (req.body.saveToDB && req.body.bolt12) { + const offerToUpdate = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() }; + if (req.body.vendor) { + offerToUpdate['vendor'] = req.body.vendor; + } + if (req.body.description) { + offerToUpdate['description'] = req.body.description; + } + // eslint-disable-next-line arrow-body-style + return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => { + return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, req.body.bolt12).then((updatedOffer) => { + logger.log({ level: 'DEBUG', fileName: 'Payments', msg: 'Offer Updated', data: updatedOffer }); + return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer }); + }).catch((errDB) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB update error', error: errDB }); + return res.status(201).json({ paymentResponse: body, saveToDBError: errDB }); + }); + }).catch((errValidation) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB validation error', error: errValidation }); + return res.status(201).json({ paymentResponse: body, saveToDBError: errValidation }); + }); + } + else { + return res.status(201).json({ paymentResponse: body, saveToDBResponse: 'NA' }); + } + } + if (req.body.paymentType === 'INVOICE') { + return res.status(201).json({ paymentResponse: body, saveToDBResponse: 'NA' }); + } + if (req.body.paymentType === 'KEYSEND') { + return res.status(201).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Send Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/peers.js b/backend/controllers/cln/peers.js new file mode 100644 index 00000000..21bac143 --- /dev/null +++ b/backend/controllers/cln/peers.js @@ -0,0 +1,65 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getPeers = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'List Peers..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/peer/listPeers'; + request(options).then((body) => { + body.forEach((peer) => { + if (!peer.alias || peer.alias === '') { + peer.alias = peer.id.substring(0, 20); + } + }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers with Alias Received', data: body }); + res.status(200).json(body || []); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postPeer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Connecting Peer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/peer/connect'; + options.body = req.body; + request.post(options).then((connectRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: connectRes }); + options.url = req.session.selectedNode.ln_server_url + '/v1/peer/listPeers'; + request(options).then((listPeersRes) => { + const peers = listPeersRes ? common.newestOnTop(listPeersRes, 'id', req.body.id) : []; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers }); + res.status(201).json(peers); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const deletePeer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Disconnecting Peer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/peer/disconnect/' + req.params.peerId + '?force=' + req.query.force; + request.delete(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconnected', data: body }); + res.status(204).json({}); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Detach Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/utility.js b/backend/controllers/cln/utility.js new file mode 100644 index 00000000..f8e866d9 --- /dev/null +++ b/backend/controllers/cln/utility.js @@ -0,0 +1,97 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const decodePaymentFromPaymentRequest = (selNode, payment) => { + options.url = selNode.ln_server_url + '/v1/utility/decode/' + payment; + return request(options).then((res) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment Decode Received', data: res }); + return res; + }).catch((err) => { }); +}; +export const decodePayments = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Decoding Payments List..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + if (req.body.payments) { + const paymentsArr = req.body.payments.split(','); + return Promise.all(paymentsArr?.map((payment) => decodePaymentFromPaymentRequest(req.session.selectedNode, payment))). + then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment List Decoded', data: values }); + res.status(200).json(values); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Decode Payments Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Empty Payment List Decoded' }); + return res.status(200).json([]); + } +}; +export const decodePayment = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Decoding Payment..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/utility/decode/' + req.params.payReq; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Decoded', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Decode Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const signMessage = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Signing Message..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/utility/signMessage'; + options.form = { message: req.body.message }; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Signed', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Message', 'Sign Message Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const verifyMessage = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Verifying Message..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/utility/checkMessage/' + req.body.message + '/' + req.body.signature; + request.get(options, (error, response, body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Verified', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Message', 'Verify Message Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listConfigs = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Utility', msg: 'List Configs..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/utility/listConfigs'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Utility', msg: 'List Configs Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Utility', 'List Configs Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/cln/webSocketClient.js b/backend/controllers/cln/webSocketClient.js new file mode 100644 index 00000000..7270bb95 --- /dev/null +++ b/backend/controllers/cln/webSocketClient.js @@ -0,0 +1,116 @@ +import * as fs from 'fs'; +import { join } from 'path'; +import WebSocket from 'ws'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { WSServer } from '../../utils/webSocketServer.js'; +export class CLWebSocketClient { + constructor() { + this.logger = Logger; + this.common = Common; + this.wsServer = WSServer; + this.webSocketClients = []; + this.reconnectTimeOut = null; + this.waitTime = 0.5; + this.reconnet = (clWsClt) => { + if (this.reconnectTimeOut) { + return; + } + this.waitTime = (this.waitTime >= 64) ? 64 : (this.waitTime * 2); + this.reconnectTimeOut = setTimeout(() => { + if (clWsClt.selectedNode) { + this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'INFO', fileName: 'CLWebSocket', msg: 'Reconnecting to the Core Lightning\'s Websocket Server..' }); + this.connect(clWsClt.selectedNode); + } + this.reconnectTimeOut = null; + }, this.waitTime * 1000); + }; + this.connect = (selectedNode) => { + try { + const clientExists = this.webSocketClients.find((wsc) => wsc.selectedNode.index === selectedNode.index); + if (!clientExists) { + if (selectedNode.ln_server_url) { + const newWebSocketClient = { selectedNode: selectedNode, reConnect: true, webSocketClient: null }; + this.connectWithClient(newWebSocketClient); + this.webSocketClients.push(newWebSocketClient); + } + } + else { + if ((!clientExists.webSocketClient || clientExists.webSocketClient.readyState !== WebSocket.OPEN) && selectedNode.ln_server_url) { + clientExists.reConnect = true; + this.connectWithClient(clientExists); + } + } + } + catch (err) { + throw new Error(err); + } + }; + this.connectWithClient = (clWsClt) => { + this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'INFO', fileName: 'CLWebSocket', msg: 'Connecting to the Core Lightning\'s Websocket Server..' }); + const WS_LINK = (clWsClt.selectedNode.ln_server_url)?.replace(/^http/, 'ws') + '/v1/ws'; + const mcrnHexEncoded = Buffer.from(fs.readFileSync(join(clWsClt.selectedNode.macaroon_path, 'access.macaroon'))).toString('hex'); + clWsClt.webSocketClient = new WebSocket(WS_LINK, [mcrnHexEncoded, 'hex'], { rejectUnauthorized: false }); + clWsClt.webSocketClient.onopen = () => { + this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'INFO', fileName: 'CLWebSocket', msg: 'Connected to the Core Lightning\'s Websocket Server..' }); + this.waitTime = 0.5; + }; + clWsClt.webSocketClient.onclose = (e) => { + if (clWsClt && clWsClt.selectedNode && clWsClt.selectedNode.ln_implementation === 'CLN') { + this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'INFO', fileName: 'CLWebSocket', msg: 'Web socket disconnected, will reconnect again...' }); + clWsClt.webSocketClient.close(); + if (clWsClt.reConnect) { + this.reconnet(clWsClt); + } + } + }; + clWsClt.webSocketClient.onmessage = (msg) => { + this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'DEBUG', fileName: 'CLWebSocket', msg: 'Received message from the server..', data: msg.data }); + msg = (typeof msg.data === 'string') ? JSON.parse(msg.data) : msg.data; + msg['source'] = 'CLN'; + const msgStr = JSON.stringify(msg); + this.wsServer.sendEventsToAllLNClients(msgStr, clWsClt.selectedNode); + }; + clWsClt.webSocketClient.onerror = (err) => { + if (clWsClt.selectedNode.api_version === '' || !clWsClt.selectedNode.api_version || this.common.isVersionCompatible(clWsClt.selectedNode.api_version, '0.6.0')) { + this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'ERROR', fileName: 'CLWebSocket', msg: 'Web socket error', error: err }); + const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message }) : (typeof err === 'object') ? JSON.stringify({ error: err }) : ('{ "error": ' + err + ' }')); + this.wsServer.sendErrorToAllLNClients(errStr, clWsClt.selectedNode); + clWsClt.webSocketClient.close(); + if (clWsClt.reConnect) { + this.reconnet(clWsClt); + } + } + else { + clWsClt.reConnect = false; + } + }; + }; + this.disconnect = (selectedNode) => { + const clientExists = this.webSocketClients.find((wsc) => wsc.selectedNode.index === selectedNode.index); + if (clientExists && clientExists.webSocketClient && clientExists.webSocketClient.readyState === WebSocket.OPEN) { + this.logger.log({ selectedNode: clientExists.selectedNode, level: 'INFO', fileName: 'CLWebSocket', msg: 'Disconnecting from the Core Lightning\'s Websocket Server..' }); + clientExists.reConnect = false; + clientExists.webSocketClient.close(); + const clientIdx = this.webSocketClients.findIndex((wsc) => wsc.selectedNode.index === selectedNode.index); + this.webSocketClients.splice(clientIdx, 1); + } + }; + this.updateSelectedNode = (newSelectedNode) => { + const clientIdx = this.webSocketClients.findIndex((wsc) => +wsc.selectedNode.index === +newSelectedNode.index); + let newClient = this.webSocketClients[clientIdx]; + if (!newClient) { + newClient = { selectedNode: null, reConnect: true, webSocketClient: null }; + } + newClient.selectedNode = JSON.parse(JSON.stringify(newSelectedNode)); + this.webSocketClients[clientIdx] = newClient; + }; + this.wsServer.eventEmitterCLN.on('CONNECT', (nodeIndex) => { + this.connect(this.common.findNode(+nodeIndex)); + }); + this.wsServer.eventEmitterCLN.on('DISCONNECT', (nodeIndex) => { + this.disconnect(this.common.findNode(+nodeIndex)); + }); + } +} +export const CLWSClient = new CLWebSocketClient(); diff --git a/backend/controllers/eclair/channels.js b/backend/controllers/eclair/channels.js new file mode 100644 index 00000000..bd2edb7f --- /dev/null +++ b/backend/controllers/eclair/channels.js @@ -0,0 +1,158 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const simplifyAllChannels = (selNode, channels) => { + let channelNodeIds = ''; + const simplifiedChannels = []; + channels.forEach((channel) => { + channelNodeIds = channelNodeIds + ',' + channel.nodeId; + simplifiedChannels.push({ + nodeId: channel.nodeId ? channel.nodeId : '', + channelId: channel.channelId ? channel.channelId : '', + state: channel.state ? channel.state : '', + announceChannel: channel.data && channel.data.commitments && channel.data.commitments.channelFlags && channel.data.commitments.channelFlags.announceChannel ? channel.data.commitments.channelFlags.announceChannel : false, + toLocal: (channel.data.commitments.localCommit.spec.toLocal) ? Math.round(+channel.data.commitments.localCommit.spec.toLocal / 1000) : 0, + toRemote: (channel.data.commitments.localCommit.spec.toRemote) ? Math.round(+channel.data.commitments.localCommit.spec.toRemote / 1000) : 0, + shortChannelId: channel.data && channel.data.channelUpdate && channel.data.channelUpdate.shortChannelId ? channel.data.channelUpdate.shortChannelId : '', + isFunder: channel.data && channel.data.commitments && channel.data.commitments.localParams && channel.data.commitments.localParams.isFunder ? channel.data.commitments.localParams.isFunder : false, + buried: channel.data && channel.data.buried ? channel.data.buried : false, + feeBaseMsat: channel.data && channel.data.channelUpdate && channel.data.channelUpdate.feeBaseMsat ? channel.data.channelUpdate.feeBaseMsat : 0, + feeRatePerKw: (channel.data.commitments.localCommit.spec.feeratePerKw) ? channel.data.commitments.localCommit.spec.feeratePerKw : 0, + feeProportionalMillionths: channel.data && channel.data.channelUpdate && channel.data.channelUpdate.feeProportionalMillionths ? channel.data.channelUpdate.feeProportionalMillionths : 0, + alias: '' + }); + }); + channelNodeIds = channelNodeIds.substring(1); + options.url = selNode.ln_server_url + '/nodes'; + options.form = { nodeIds: channelNodeIds }; + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Node Ids to find alias', data: channelNodeIds }); + return request.post(options).then((nodes) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Filtered Nodes Received', data: nodes }); + let foundPeer = null; + simplifiedChannels?.map((channel) => { + foundPeer = nodes.find((channelWithAlias) => channel.nodeId === channelWithAlias.nodeId); + channel.alias = foundPeer ? foundPeer.alias : channel.nodeId.substring(0, 20); + return channel; + }); + return simplifiedChannels; + }).catch((err) => simplifiedChannels); +}; +export const getChannels = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'List Channels..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/channels'; + options.form = {}; + if (req.query && req.query.nodeId) { + options.form = req.query; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels Node Id', data: options.form }); + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: options }); + if (common.read_dummy_data) { + common.getDummyData('Channels', req.session.selectedNode.ln_implementation).then((data) => { res.status(200).json(data); }); + } + else { + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels List Received', data: body }); + if (body && body.length) { + return simplifyAllChannels(req.session.selectedNode, body).then((simplifiedChannels) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Simplified Channels with Alias Received', data: simplifiedChannels }); + res.status(200).json(simplifiedChannels); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Empty Channels List Received' }); + res.status(200).json([]); + } + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'List Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; +export const getChannelStats = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channel States..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/channelstats'; + const today = new Date(Date.now()); + const tillToday = (Math.round(today.getTime() / 1000)).toString(); + const fromLastMonth = (Math.round(new Date(today.getFullYear(), today.getMonth() - 1, today.getDate() + 1, 0, 0, 0).getTime() / 1000)).toString(); + options.form = { + from: fromLastMonth, + to: tillToday + }; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel States Received', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Get Channel Stats Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const openChannel = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Opening Channel..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/open'; + options.form = req.body; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Open Channel Params', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Opened', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Open Channel Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const updateChannelRelayFee = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Updating Channel Relay Fee..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/updaterelayfee'; + options.form = req.query; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Update Relay Fee Params', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Relay Fee Updated', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Update Relay Fee Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const closeChannel = (req, res, next) => { + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + if (req.query.force !== 'true') { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' }); + options.url = req.session.selectedNode.ln_server_url + '/close'; + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Force Closing Channel..' }); + options.url = req.session.selectedNode.ln_server_url + '/forceclose'; + } + options.form = { channelId: req.query.channelId }; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Close URL', data: options.url }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Close Params', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Closed', data: body }); + res.status(204).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Close Channel Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/eclair/fees.js b/backend/controllers/eclair/fees.js new file mode 100644 index 00000000..7c60516f --- /dev/null +++ b/backend/controllers/eclair/fees.js @@ -0,0 +1,144 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const arrangeFees = (selNode, body, current_time) => { + const fees = { daily_fee: 0, daily_txs: 0, weekly_fee: 0, weekly_txs: 0, monthly_fee: 0, monthly_txs: 0 }; + const week_start_time = current_time - 604800000; + const day_start_time = current_time - 86400000; + let fee = 0; + body.relayed.forEach((relayedEle) => { + fee = Math.round((relayedEle.amountIn - relayedEle.amountOut) / 1000); + if (relayedEle.timestamp) { + if (relayedEle.timestamp.unix) { + if ((relayedEle.timestamp.unix * 1000) >= day_start_time) { + fees.daily_fee = fees.daily_fee + fee; + fees.daily_txs = fees.daily_txs + 1; + } + if ((relayedEle.timestamp.unix * 1000) >= week_start_time) { + fees.weekly_fee = fees.weekly_fee + fee; + fees.weekly_txs = fees.weekly_txs + 1; + } + } + else { + if (relayedEle.timestamp >= day_start_time) { + fees.daily_fee = fees.daily_fee + fee; + fees.daily_txs = fees.daily_txs + 1; + } + if (relayedEle.timestamp >= week_start_time) { + fees.weekly_fee = fees.weekly_fee + fee; + fees.weekly_txs = fees.weekly_txs + 1; + } + } + } + fees.monthly_fee = fees.monthly_fee + fee; + fees.monthly_txs = fees.monthly_txs + 1; + }); + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Fee Received', data: fees }); + return fees; +}; +export const arrangePayments = (selNode, body) => { + const payments = { + sent: body && body.sent ? body.sent : [], + received: body && body.received ? body.received : [], + relayed: body && body.relayed ? body.relayed : [] + }; + payments.sent.forEach((sentEle) => { + if (sentEle.recipientAmount) { + sentEle.recipientAmount = Math.round(sentEle.recipientAmount / 1000); + } + sentEle.parts.forEach((part) => { + if (part.amount) { + part.amount = Math.round(part.amount / 1000); + } + if (part.feesPaid) { + part.feesPaid = Math.round(part.feesPaid / 1000); + } + if (part.timestamp.unix) { + part.timestamp = part.timestamp.unix * 1000; + } + }); + if (sentEle.parts && sentEle.parts.length > 0) { + sentEle.firstPartTimestamp = sentEle.parts[0].timestamp; + } + }); + payments.received.forEach((receivedEle) => { + receivedEle.parts.forEach((part) => { + if (part.amount) { + part.amount = Math.round(part.amount / 1000); + } + if (part.timestamp.unix) { + part.timestamp = part.timestamp.unix * 1000; + } + }); + if (receivedEle.parts && receivedEle.parts.length > 0) { + receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp; + } + }); + payments.relayed.forEach((relayedEle) => { + if (relayedEle.timestamp.unix) { + relayedEle.timestamp = relayedEle.timestamp.unix * 1000; + } + if (relayedEle.amountIn) { + relayedEle.amountIn = Math.round(relayedEle.amountIn / 1000); + } + if (relayedEle.amountOut) { + relayedEle.amountOut = Math.round(relayedEle.amountOut / 1000); + } + }); + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Payments Received', data: payments }); + return payments; +}; +export const getFees = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Getting Fees..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/audit'; + const today = new Date(Date.now()); + const tillToday = (Math.round(today.getTime() / 1000)).toString(); + const fromLastMonth = (Math.round(new Date(today.getFullYear(), today.getMonth() - 1, today.getDate() + 1, 0, 0, 0).getTime() / 1000)).toString(); + options.form = { + from: fromLastMonth, + to: tillToday + }; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Fee Audit Options', data: options.form }); + if (common.read_dummy_data) { + common.getDummyData('Fees', req.session.selectedNode.ln_implementation).then((data) => { res.status(200).json(arrangeFees(req.session.selectedNode, data, Math.round((new Date().getTime())))); }); + } + else { + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Fee Received', data: body }); + res.status(200).json(arrangeFees(req.session.selectedNode, body, Math.round((new Date().getTime())))); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Fees', 'Get Fees Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; +export const getPayments = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Getting Payments..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/audit'; + const tillToday = (Math.round(new Date(Date.now()).getTime() / 1000)).toString(); + options.form = { from: 0, to: tillToday }; + if (common.read_dummy_data) { + common.getDummyData('Payments', req.session.selectedNode.ln_implementation).then((data) => { res.status(200).json(arrangePayments(req.session.selectedNode, data)); }); + } + else { + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Payments Received', data: body }); + res.status(200).json(arrangePayments(req.session.selectedNode, body)); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Fees', 'Get Payments Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; diff --git a/backend/controllers/eclair/getInfo.js b/backend/controllers/eclair/getInfo.js new file mode 100644 index 00000000..dbf11cf9 --- /dev/null +++ b/backend/controllers/eclair/getInfo.js @@ -0,0 +1,50 @@ +import request from 'request-promise'; +import { Database } from '../../utils/database.js'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { ECLWSClient } from './webSocketClient.js'; +let options = null; +const logger = Logger; +const common = Common; +const eclWsClient = ECLWSClient; +const databaseService = Database; +export const getInfo = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Getting Eclair Node Information..' }); + common.logEnvVariables(req); + common.setOptions(req); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/getinfo'; + options.form = {}; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Selected Node ' + req.session.selectedNode.ln_node }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Calling Info from Eclair server url ' + options.url }); + if (common.read_dummy_data) { + common.getDummyData('GetInfo', req.session.selectedNode.ln_implementation).then((data) => { + data.lnImplementation = 'Eclair'; + return res.status(200).json(data); + }); + } + else { + if (!options.headers || !options.headers.authorization) { + const errMsg = 'Eclair Get info failed due to missing or wrong password!'; + const err = common.handleError({ statusCode: 502, message: 'Missing or Wrong Password', error: errMsg }, 'GetInfo', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + return request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Eclair\'s Websocket Server.' }); + body.lnImplementation = 'Eclair'; + req.session.selectedNode.ln_version = body.version.split('-')[0] || ''; + eclWsClient.updateSelectedNode(req.session.selectedNode); + databaseService.loadDatabase(req.session); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body }); + return res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'GetInfo', 'Get Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + } +}; diff --git a/backend/controllers/eclair/invoices.js b/backend/controllers/eclair/invoices.js new file mode 100644 index 00000000..6992db93 --- /dev/null +++ b/backend/controllers/eclair/invoices.js @@ -0,0 +1,124 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +let pendingInvoices = []; +export const getReceivedPaymentInfo = (lnServerUrl, invoice) => { + let idx = -1; + invoice.expiresAt = (!invoice.expiry) ? null : (+invoice.timestamp + +invoice.expiry); + if (invoice.amount) { + invoice.amount = Math.round(invoice.amount / 1000); + } + idx = pendingInvoices.findIndex((pendingInvoice) => invoice.serialized === pendingInvoice.serialized); + if (idx < 0) { + options.url = lnServerUrl + '/getreceivedinfo'; + options.form = { paymentHash: invoice.paymentHash }; + return request(options).then((response) => { + invoice.status = response.status.type; + if (response.status && response.status.type === 'received') { + invoice.amountSettled = response.status.amount ? Math.round(response.status.amount / 1000) : 0; + invoice.receivedAt = response.status.receivedAt ? Math.round(response.status.receivedAt / 1000) : 0; + } + return invoice; + }).catch((err) => { + invoice.status = 'unknown'; + return invoice; + }); + } + else { + pendingInvoices.splice(idx, 1); + invoice.status = 'unpaid'; + return invoice; + } +}; +export const getInvoice = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Invoice..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/getinvoice'; + options.form = { paymentHash: req.params.paymentHash }; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Found', data: body }); + const current_time = (Math.round(new Date(Date.now()).getTime() / 1000)); + body.amount = body.amount ? body.amount / 1000 : 0; + body.expiresAt = body.expiresAt ? body.expiresAt : (body.timestamp + body.expiry); + body.status = body.status ? body.status : (+body.expiresAt < current_time ? 'expired' : 'unknown'); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoices', 'Get Invoice Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listInvoices = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting List Invoices..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + const tillToday = (Math.round(new Date(Date.now()).getTime() / 1000)).toString(); + options.form = { from: 0, to: tillToday }; + const options1 = JSON.parse(JSON.stringify(options)); + options1.url = req.session.selectedNode.ln_server_url + '/listinvoices'; + options1.form = { from: 0, to: tillToday }; + const options2 = JSON.parse(JSON.stringify(options)); + options2.url = req.session.selectedNode.ln_server_url + '/listpendinginvoices'; + options2.form = { from: 0, to: tillToday }; + if (common.read_dummy_data) { + return common.getDummyData('Invoices', req.session.selectedNode.ln_implementation).then((body) => { + const invoices = (!body[0] || body[0].length <= 0) ? [] : body[0]; + pendingInvoices = (!body[1] || body[1].length <= 0) ? [] : body[1]; + return Promise.all(invoices?.map((invoice) => getReceivedPaymentInfo(req.session.selectedNode.ln_server_url, invoice))). + then((values) => res.status(200).json(invoices)); + }); + } + else { + return Promise.all([request(options1), request(options2)]). + then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body }); + const invoices = (!body[0] || body[0].length <= 0) ? [] : body[0]; + pendingInvoices = (!body[1] || body[1].length <= 0) ? [] : body[1]; + if (invoices && invoices.length > 0) { + return Promise.all(invoices?.map((invoice) => getReceivedPaymentInfo(req.session.selectedNode.ln_server_url, invoice))). + then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Sorted Invoices List Received', data: invoices }); + return res.status(200).json(invoices); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Invoices', 'List Invoices Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Empty List Invoice Received' }); + return res.status(200).json([]); + } + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Invoices', 'List Invoices Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; +export const createInvoice = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/createinvoice'; + options.form = req.body; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Created', data: body }); + if (body.amount) { + body.amount = Math.round(body.amount / 1000); + } + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoices', 'Create Invoice Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/eclair/network.js b/backend/controllers/eclair/network.js new file mode 100644 index 00000000..bd5f7001 --- /dev/null +++ b/backend/controllers/eclair/network.js @@ -0,0 +1,22 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getNodes = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Node Lookup..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/nodes'; + options.form = { nodeIds: req.params.id }; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Node Lookup Finished', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Network', 'Node Lookup Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/eclair/onchain.js b/backend/controllers/eclair/onchain.js new file mode 100644 index 00000000..d69b1de2 --- /dev/null +++ b/backend/controllers/eclair/onchain.js @@ -0,0 +1,96 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const arrangeBalances = (body) => { + if (!body.confirmed) { + body.confirmed = 0; + } + if (!body.unconfirmed) { + body.unconfirmed = 0; + } + body.total = +body.confirmed + +body.unconfirmed; + body.btc_total = +body.btc_confirmed + +body.btc_unconfirmed; + return body; +}; +export const getNewAddress = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Generating New Address..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/getnewaddress'; + options.form = {}; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'New Address Generated', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'OnChain', 'Get New Address Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getBalance = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Getting On Chain Balance..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/onchainbalance'; + options.form = {}; + if (common.read_dummy_data) { + common.getDummyData('OnChainBalance', req.session.selectedNode.ln_implementation).then((data) => { res.status(200).json(arrangeBalances(data)); }); + } + else { + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'On Chain Balance Received', data: body }); + res.status(200).json(arrangeBalances(body)); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'OnChain', 'Get Balance Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; +export const getTransactions = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Getting On Chain Transactions..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/onchaintransactions'; + options.form = { + count: req.query.count, + skip: req.query.skip + }; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Getting On Chain Transactions Options', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'On Chain Transactions Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'OnChain', 'Get Transactions Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const sendFunds = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Sending On Chain Funds..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/sendonchain'; + options.form = { + address: req.body.address, + amountSatoshis: req.body.amount, + confirmationTarget: req.body.blocks + }; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Onchain', msg: 'Send Funds Options', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Onchain', msg: 'On Chain Funds Sent', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'OnChain', 'Send Funds Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/eclair/payments.js b/backend/controllers/eclair/payments.js new file mode 100644 index 00000000..a54e1a87 --- /dev/null +++ b/backend/controllers/eclair/payments.js @@ -0,0 +1,128 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getSentInfoFromPaymentRequest = (selNode, payment) => { + options.url = selNode.ln_server_url + '/getsentinfo'; + options.form = { paymentHash: payment }; + return request.post(options).then((body) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment Sent Information Received', data: body }); + body.forEach((sentPayment) => { + if (sentPayment.amount) { + sentPayment.amount = Math.round(sentPayment.amount / 1000); + } + if (sentPayment.recipientAmount) { + sentPayment.recipientAmount = Math.round(sentPayment.recipientAmount / 1000); + } + }); + return body; + }).catch((err) => err); +}; +export const getQueryNodes = (selNode, nodeIds) => { + options.url = selNode.ln_server_url + '/nodes'; + options.form = { nodeIds: nodeIds?.reduce((acc, curr) => acc + ',' + curr) }; + return request.post(options).then((nodes) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Payments', msg: 'Query Nodes Received', data: nodes }); + return nodes; + }).catch((err) => []); +}; +export const decodePayment = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Decoding Payment..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/parseinvoice'; + options.form = { invoice: req.params.invoice }; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Decoded', data: body }); + if (body.amount) { + body.amount = Math.round(body.amount / 1000); + } + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Decode Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postPayment = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Paying Invoice..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/payinvoice'; + options.form = req.body; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Send Payment Options', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Invoice Paid', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Send Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const queryPaymentRoute = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Querying Payment Route..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/findroutetonode'; + options.form = { + nodeId: req.query.nodeId, + amountMsat: req.query.amountMsat + }; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Query Payment Route Options', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Query Payment Route Received', data: body }); + if (body && body.routes && body.routes.length) { + let allRoutesNodeIds = []; + allRoutesNodeIds = body.routes?.reduce((accRoutes, currRoute) => [...new Set([...accRoutes, ...currRoute.nodeIds])], []); + return getQueryNodes(req.session.selectedNode, allRoutesNodeIds).then((nodesWithAlias) => { + let foundPeer = null; + body.routes.forEach((route, i) => { + route.nodeIds?.map((node, j) => { + foundPeer = nodesWithAlias.find((nodeWithAlias) => node === nodeWithAlias.nodeId); + body.routes[i].nodeIds[j] = { nodeId: node, alias: foundPeer ? foundPeer.alias : '' }; + return node; + }); + }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Query Routes with Alias Received', data: body }); + res.status(200).json(body); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Empty Payment Route Information Received' }); + res.status(200).json({ routes: [] }); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Query Route Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getSentPaymentsInformation = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Getting Sent Payment Information..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + if (req.body.payments) { + const paymentsArr = req.body.payments.split(','); + return Promise.all(paymentsArr?.map((payment) => getSentInfoFromPaymentRequest(req.session.selectedNode, payment))). + then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent Information Received', data: values }); + return res.status(200).json(values); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Sent Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Empty Sent Payment Information Received' }); + return res.status(200).json([]); + } +}; diff --git a/backend/controllers/eclair/peers.js b/backend/controllers/eclair/peers.js new file mode 100644 index 00000000..0436f2ec --- /dev/null +++ b/backend/controllers/eclair/peers.js @@ -0,0 +1,128 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getFilteredNodes = (selNode, peersNodeIds) => { + options.url = selNode.ln_server_url + '/nodes'; + options.form = { nodeIds: peersNodeIds }; + return request.post(options).then((nodes) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Peers', msg: 'Filtered Nodes Received', data: nodes }); + return nodes; + }).catch((err) => []); +}; +export const getPeers = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Getting Peers..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/peers'; + options.form = {}; + if (common.read_dummy_data) { + common.getDummyData('Peers', req.session.selectedNode.ln_implementation).then((data) => { res.status(200).json(data); }); + } + else { + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List Received', data: body }); + if (body && body.length) { + let peersNodeIds = ''; + body.forEach((peer) => { peersNodeIds = peersNodeIds + ',' + peer.nodeId; }); + peersNodeIds = peersNodeIds.substring(1); + return getFilteredNodes(req.session.selectedNode, peersNodeIds).then((peersWithAlias) => { + let foundPeer = null; + body?.map((peer) => { + foundPeer = peersWithAlias.find((peerWithAlias) => peer.nodeId === peerWithAlias.nodeId); + peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20); + return peer; + }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body }); + res.status(200).json(body); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Empty Peers Received' }); + res.status(200).json([]); + } + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; +export const connectPeer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Conneting Peer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/connect'; + options.form = {}; + if (req.query) { + options.form = req.query; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Connect Peer Params', data: options.form }); + } + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: body }); + if (typeof body === 'string' && body.includes('already connected')) { + const err = common.handleError({ statusCode: 500, message: 'Connect Peer Error', error: body }, 'Peers', body, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else if (typeof body === 'string' && body.includes('connection failed')) { + const err = common.handleError({ statusCode: 500, message: 'Connect Peer Error', error: body }, 'Peers', body, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/peers'; + options.form = {}; + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List after Connect', data: body }); + if (body && body.length) { + let peersNodeIds = ''; + body.forEach((peer) => { peersNodeIds = peersNodeIds + ',' + peer.nodeId; }); + peersNodeIds = peersNodeIds.substring(1); + return getFilteredNodes(req.session.selectedNode, peersNodeIds).then((peersWithAlias) => { + let foundPeer = null; + body?.map((peer) => { + foundPeer = peersWithAlias.find((peerWithAlias) => peer.nodeId === peerWithAlias.nodeId); + peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20); + return peer; + }); + const peers = common.newestOnTop(body || [], 'nodeId', req.query.nodeId ? req.query.nodeId : req.query.uri ? req.query.uri.substring(0, req.query.uri.indexOf('@')) : ''); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers }); + res.status(201).json(peers); + }); + } + else { + res.status(201).json([]); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const deletePeer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Disconneting Peer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/disconnect'; + options.form = {}; + if (req.params.nodeId) { + options.form = { nodeId: req.params.nodeId }; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Disconnect Peer Params', data: options.form }); + } + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconnected', data: body }); + res.status(204).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Disconnect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/eclair/webSocketClient.js b/backend/controllers/eclair/webSocketClient.js new file mode 100644 index 00000000..9f3b31c2 --- /dev/null +++ b/backend/controllers/eclair/webSocketClient.js @@ -0,0 +1,130 @@ +import * as WebSocket from 'ws'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { WSServer } from '../../utils/webSocketServer.js'; +import { ECLWSEventsEnum } from '../../models/ecl.model.js'; +export class ECLWebSocketClient { + constructor() { + this.logger = Logger; + this.common = Common; + this.wsServer = WSServer; + this.webSocketClients = []; + this.reconnectTimeOut = null; + this.waitTime = 0.5; + this.reconnet = (eclWsClt) => { + if (this.reconnectTimeOut) { + return; + } + this.waitTime = (this.waitTime >= 64) ? 64 : (this.waitTime * 2); + this.reconnectTimeOut = setTimeout(() => { + if (eclWsClt.selectedNode) { + this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'INFO', fileName: 'ECLWebSocket', msg: 'Reconnecting to the Eclair\'s Websocket Server..' }); + this.connect(eclWsClt.selectedNode); + } + this.reconnectTimeOut = null; + }, this.waitTime * 1000); + }; + this.connect = (selectedNode) => { + try { + const clientExists = this.webSocketClients.find((wsc) => wsc.selectedNode.index === selectedNode.index); + if (!clientExists) { + if (selectedNode.ln_server_url) { + const newWebSocketClient = { selectedNode: selectedNode, reConnect: true, webSocketClient: null }; + this.connectWithClient(newWebSocketClient); + this.webSocketClients.push(newWebSocketClient); + } + } + else { + if ((!clientExists.webSocketClient || clientExists.webSocketClient.readyState !== WebSocket.OPEN) && selectedNode.ln_server_url) { + clientExists.reConnect = true; + this.connectWithClient(clientExists); + } + } + } + catch (err) { + throw new Error(err); + } + }; + this.connectWithClient = (eclWsClt) => { + this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'INFO', fileName: 'ECLWebSocket', msg: 'Connecting to the Eclair\'s Websocket Server..' }); + const UpdatedLNServerURL = (eclWsClt.selectedNode.ln_server_url)?.replace(/^http/, 'ws'); + const firstSubStrIndex = (UpdatedLNServerURL.indexOf('//') + 2); + const WS_LINK = UpdatedLNServerURL.slice(0, firstSubStrIndex) + ':' + eclWsClt.selectedNode.ln_api_password + '@' + UpdatedLNServerURL.slice(firstSubStrIndex) + '/ws'; + eclWsClt.webSocketClient = new WebSocket(WS_LINK); + eclWsClt.webSocketClient.onopen = () => { + this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'INFO', fileName: 'ECLWebSocket', msg: 'Connected to the Eclair\'s Websocket Server..' }); + this.waitTime = 0.5; + this.heartbeat(eclWsClt); + }; + eclWsClt.webSocketClient.onclose = (e) => { + if (eclWsClt && eclWsClt.selectedNode && eclWsClt.selectedNode.ln_implementation === 'ECL') { + this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'INFO', fileName: 'ECLWebSocket', msg: 'Web socket disconnected, will reconnect again...' }); + eclWsClt.webSocketClient.close(); + if (eclWsClt.reConnect) { + this.reconnet(eclWsClt); + } + } + }; + eclWsClt.webSocketClient.onmessage = (msg) => { + this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Received message from the server..', data: msg.data }); + msg = (typeof msg.data === 'string') ? JSON.parse(msg.data) : msg.data; + if (msg.type && msg.type !== ECLWSEventsEnum.PAY_RELAYED && msg.type !== ECLWSEventsEnum.PAY_SETTLING_ONCHAIN && msg.type !== ECLWSEventsEnum.ONION_MESSAGE_RECEIVED) { + msg['source'] = 'ECL'; + const msgStr = JSON.stringify(msg); + this.wsServer.sendEventsToAllLNClients(msgStr, eclWsClt.selectedNode); + } + }; + eclWsClt.webSocketClient.onerror = (err) => { + if (eclWsClt.selectedNode.ln_version === '' || !eclWsClt.selectedNode.ln_version || this.common.isVersionCompatible(eclWsClt.selectedNode.ln, '0.5.0')) { + this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'ERROR', fileName: 'ECLWebSocket', msg: 'Web socket error', error: err }); + const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message }) : (typeof err === 'object') ? JSON.stringify({ error: err }) : ('{ "error": ' + err + ' }')); + this.wsServer.sendErrorToAllLNClients(errStr, eclWsClt.selectedNode); + eclWsClt.webSocketClient.close(); + if (eclWsClt.reConnect) { + this.reconnet(eclWsClt); + } + } + else { + eclWsClt.reConnect = false; + } + }; + }; + this.disconnect = (selectedNode) => { + const clientExists = this.webSocketClients.find((wsc) => wsc.selectedNode.index === selectedNode.index); + if (clientExists && clientExists.webSocketClient && clientExists.webSocketClient.readyState === WebSocket.OPEN) { + this.logger.log({ selectedNode: clientExists.selectedNode, level: 'INFO', fileName: 'ECLWebSocket', msg: 'Disconnecting from the Eclair\'s Websocket Server..' }); + clientExists.reConnect = false; + clientExists.webSocketClient.close(); + const clientIdx = this.webSocketClients.findIndex((wsc) => wsc.selectedNode.index === selectedNode.index); + this.webSocketClients.splice(clientIdx, 1); + } + }; + this.updateSelectedNode = (newSelectedNode) => { + const clientIdx = this.webSocketClients.findIndex((wsc) => +wsc.selectedNode.index === +newSelectedNode.index); + let newClient = this.webSocketClients[clientIdx]; + if (!newClient) { + newClient = { selectedNode: null, reConnect: true, webSocketClient: null }; + } + newClient.selectedNode = JSON.parse(JSON.stringify(newSelectedNode)); + this.webSocketClients[clientIdx] = newClient; + }; + this.heartbeat = (eclWsClt) => { + this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Websocket Server Heartbeat..' }); + if (!eclWsClt.webSocketClient) + return; + if (eclWsClt.webSocketClient.readyState !== 1) + return; + eclWsClt.webSocketClient.ping(); + setTimeout(() => { + this.heartbeat(eclWsClt); + }, 59 * 1000); + }; + this.wsServer.eventEmitterECL.on('CONNECT', (nodeIndex) => { + this.connect(this.common.findNode(+nodeIndex)); + }); + this.wsServer.eventEmitterECL.on('DISCONNECT', (nodeIndex) => { + this.disconnect(this.common.findNode(+nodeIndex)); + }); + } +} +export const ECLWSClient = new ECLWebSocketClient(); diff --git a/backend/controllers/lnd/balance.js b/backend/controllers/lnd/balance.js new file mode 100644 index 00000000..a73898d5 --- /dev/null +++ b/backend/controllers/lnd/balance.js @@ -0,0 +1,35 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getBlockchainBalance = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Balance', msg: 'Getting Balance..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/balance/blockchain'; + options.qs = req.query; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Balance', msg: 'Request params', data: req.params }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Balance', msg: 'Request Query', data: req.query }); + request(options).then((body) => { + if (body) { + if (!body.total_balance) { + body.total_balance = 0; + } + if (!body.confirmed_balance) { + body.confirmed_balance = 0; + } + if (!body.unconfirmed_balance) { + body.unconfirmed_balance = 0; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Balance', msg: 'Balance Received', data: body }); + res.status(200).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Balance', 'Get Balance Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/channels.js b/backend/controllers/lnd/channels.js new file mode 100644 index 00000000..30653793 --- /dev/null +++ b/backend/controllers/lnd/channels.js @@ -0,0 +1,267 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getAliasForChannel = (selNode, channel) => { + const pubkey = (channel.remote_pubkey) ? channel.remote_pubkey : (channel.remote_node_pub) ? channel.remote_node_pub : ''; + options.url = selNode.ln_server_url + '/v1/graph/node/' + pubkey; + return request(options).then((aliasBody) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Alias Received', data: aliasBody.node.alias }); + channel.remote_alias = aliasBody.node.alias && aliasBody.node.alias !== '' ? aliasBody.node.alias : aliasBody.node.pub_key.slice(0, 20); + return channel; + }).catch((err) => { + channel.remote_alias = pubkey.slice(0, 20); + return channel; + }); +}; +export const getAllChannels = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channels..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channels'; + options.qs = req.query; + let local = 0; + let remote = 0; + let total = 0; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels List Received', data: body }); + if (body.channels) { + return Promise.all(body.channels?.map((channel) => { + local = (channel.local_balance) ? +channel.local_balance : 0; + remote = (channel.remote_balance) ? +channel.remote_balance : 0; + total = local + remote; + channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3); + return getAliasForChannel(req.session.selectedNode, channel); + })).then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body }); + return res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Get All Channel Aliases Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + body.channels = []; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Empty Channels List Received' }); + return res.status(200).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'List Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getPendingChannels = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Pending Channels..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/pending'; + options.qs = req.query; + request(options).then((body) => { + if (!body.total_limbo_balance) { + body.total_limbo_balance = 0; + } + const promises = []; + if (body.pending_open_channels && body.pending_open_channels.length > 0) { + body.pending_open_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel))); + } + if (body.pending_force_closing_channels && body.pending_force_closing_channels.length > 0) { + body.pending_force_closing_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel))); + } + if (body.pending_closing_channels && body.pending_closing_channels.length > 0) { + body.pending_closing_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel))); + } + if (body.waiting_close_channels && body.waiting_close_channels.length > 0) { + body.waiting_close_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel))); + } + return Promise.all(promises).then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Pending Channels List Received', data: body }); + return res.status(200).json(body); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Get Pending Channel Aliases Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'List Pending Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getClosedChannels = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Closed Channels..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/closed'; + options.qs = req.query; + request(options).then((body) => { + if (body.channels && body.channels.length > 0) { + return Promise.all(body.channels?.map((channel) => { + channel.close_type = (!channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type; + return getAliasForChannel(req.session.selectedNode, channel); + })).then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body }); + return res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Get Closed Channel Aliases Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + body.channels = []; + return res.status(200).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'List Closed Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postChannel = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Opening Channel..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channels'; + options.form = { + node_pubkey_string: req.body.node_pubkey, + local_funding_amount: req.body.local_funding_amount, + private: req.body.private, + spend_unconfirmed: req.body.spend_unconfirmed + }; + if (req.body.trans_type === '1') { + options.form.target_conf = req.body.trans_type_value; + } + else if (req.body.trans_type === '2') { + options.form.sat_per_byte = req.body.trans_type_value; + } + options.form = JSON.stringify(options.form); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Opened', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Open Channel Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postTransactions = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sending Payment..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/transaction-stream'; + options.form = { payment_request: req.body.paymentReq }; + if (req.body.paymentAmount) { + options.form.amt = req.body.paymentAmount; + } + if (req.body.feeLimit) { + options.form.fee_limit = req.body.feeLimit; + } + if (req.body.outgoingChannel) { + options.form.outgoing_chan_id = req.body.outgoingChannel; + } + if (req.body.allowSelfPayment) { + options.form.allow_self_payment = req.body.allowSelfPayment; + } + if (req.body.lastHopPubkey) { + options.form.last_hop_pubkey = Buffer.from(req.body.lastHopPubkey, 'hex').toString('base64'); + } + options.form = JSON.stringify(options.form); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form }); + request.post(options).then((body) => { + if (body.payment_error) { + const err = common.handleError(body.payment_error, 'Channels', 'Send Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Payment Sent', data: body }); + res.status(201).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Send Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const closeChannel = (req, res, next) => { + try { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' }); + if (!req.session.selectedNode) { + const err = common.handleError({ message: 'Session Expired after a day\'s inactivity.', statusCode: 401 }, 'Session Expired', 'Session Expiry Error', null); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + const channelpoint = req.params.channelPoint?.replace(':', '/'); + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/' + channelpoint + '?force=' + req.query.force; + if (req.query.target_conf) { + options.url = options.url + '&target_conf=' + req.query.target_conf; + } + if (req.query.sat_per_byte) { + options.url = options.url + '&sat_per_byte=' + req.query.sat_per_byte; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Closing Channel Options URL', data: options.url }); + request.delete(options); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Close Requested' }); + res.status(202).json({ message: 'Close channel request has been submitted.' }); + } + catch (error) { + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Close Channel Error', error: error.message }); + return res.status(500).json({ message: 'Close Channel Error', error: error.message }); + } +}; +export const postChanPolicy = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Updating Channel Policy..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/chanpolicy'; + if (req.body.chanPoint === 'all') { + options.form = JSON.stringify({ + global: true, + base_fee_msat: req.body.baseFeeMsat, + fee_rate: parseFloat((req.body.feeRate / 1000000).toString()), + time_lock_delta: parseInt(req.body.timeLockDelta) + }); + } + else { + const breakPoint = req.body.chanPoint.indexOf(':'); + const txid_str = req.body.chanPoint.substring(0, breakPoint); + const output_idx = req.body.chanPoint.substring(breakPoint + 1, req.body.chanPoint.length); + const optionsBody = { + base_fee_msat: req.body.baseFeeMsat, + fee_rate: parseFloat((req.body.feeRate / 1000000).toString()), + time_lock_delta: parseInt(req.body.timeLockDelta), + chan_point: { funding_txid_str: txid_str, output_index: parseInt(output_idx) } + }; + if (req.body.max_htlc_msat) { + optionsBody['max_htlc_msat'] = req.body.max_htlc_msat; + } + if (req.body.min_htlc_msat) { + optionsBody['min_htlc_msat'] = req.body.min_htlc_msat; + optionsBody['min_htlc_msat_specified'] = true; + } + options.form = JSON.stringify(optionsBody); + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Update Channel Policy Options', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Policy Updated', data: body }); + if (body.failed_updates && body.failed_updates.length && body.failed_updates[0].update_error) { + const err = common.handleError({ error: body.failed_updates[0].update_error }, 'Channels', 'Update Channel Policy Error', req.session.selectedNode); + return res.status(500).json({ message: err.message, error: err.error }); + } + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Update Channel Policy Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/channelsBackup.js b/backend/controllers/lnd/channelsBackup.js new file mode 100644 index 00000000..489deb5f --- /dev/null +++ b/backend/controllers/lnd/channelsBackup.js @@ -0,0 +1,239 @@ +import * as fs from 'fs'; +import { sep } from 'path'; +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +function getFilesList(channelBackupPath, callback) { + const files_list = []; + let all_restore_exists = false; + let response = { all_restore_exists: false, files: [] } || { message: '', error: {}, statusCode: 500 }; + fs.readdir(channelBackupPath + sep + 'restore', (err, files) => { + if (err && err.code !== 'ENOENT' && err.errno !== -4058) { + response = { message: 'Channels Restore List Failed!', error: err, statusCode: 500 }; + } + if (files && files.length > 0) { + files.forEach((file) => { + if (!file.includes('.restored')) { + if (file.toLowerCase() === 'channel-all.bak' || file.toLowerCase() === 'backup-channel-all.bak') { + all_restore_exists = true; + } + else { + files_list.push({ channel_point: file.substring(8, file.length - 4)?.replace('-', ':') }); + } + } + }); + } + response = { all_restore_exists: all_restore_exists, files: files_list }; + callback(response); + }); +} +export const getBackup = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'ChannelBackup', msg: 'Getting Channel Backup..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + let channel_backup_file = ''; + let message = ''; + if (req.params.channelPoint === 'ALL') { + channel_backup_file = req.session.selectedNode.channel_backup_path + sep + 'channel-all.bak'; + message = 'All Channels Backup Successful.'; + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/backup'; + } + else { + channel_backup_file = req.session.selectedNode.channel_backup_path + sep + 'channel-' + req.params.channelPoint?.replace(':', '-') + '.bak'; + message = 'Channel Backup Successful.'; + const channelpoint = req.params.channelPoint?.replace(':', '/'); + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/backup/' + channelpoint; + const exists = fs.existsSync(channel_backup_file); + if (exists) { + fs.writeFile(channel_backup_file, '', () => { }); + } + else { + try { + const createStream = fs.createWriteStream(channel_backup_file); + createStream.end(); + } + catch (errRes) { + const err = common.handleError(errRes, 'ChannelsBackup', 'Backup Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + } + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'ChannelsBackup', msg: 'Channel Backup Received', data: body }); + fs.writeFile(channel_backup_file, JSON.stringify(body), (errRes) => { + if (errRes) { + const err = common.handleError(errRes, 'ChannelsBackup', 'Backup Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'ChannelBackup', msg: 'Channel Backed up and Saved', data: body }); + res.status(200).json({ message: message }); + } + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'ChannelsBackup', 'Backup Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postBackupVerify = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'ChannelBackup', msg: 'Verifying Channel Backup..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/backup/verify'; + let channel_verify_file = ''; + let message = ''; + let verify_backup = ''; + if (req.params.channelPoint === 'ALL') { + message = 'All Channels Verify Successful.'; + channel_verify_file = req.session.selectedNode.channel_backup_path + sep + 'channel-all.bak'; + const exists = fs.existsSync(channel_verify_file); + if (exists) { + verify_backup = fs.readFileSync(channel_verify_file, 'utf-8'); + if (verify_backup !== '') { + const verify_backup_json = JSON.parse(verify_backup); + delete verify_backup_json.single_chan_backups; + options.form = JSON.stringify(verify_backup_json); + } + else { + const errMsg = 'Channel backup to verify does not Exist.'; + const err = common.handleError({ statusCode: 404, message: 'Verify Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + else { + verify_backup = ''; + const errMsg = 'Channel backup to verify does not Exist.'; + const err = common.handleError({ statusCode: 404, message: 'Verify Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + else { + message = 'Channel Verify Successful.'; + channel_verify_file = req.session.selectedNode.channel_backup_path + sep + 'channel-' + req.params.channelPoint?.replace(':', '-') + '.bak'; + const exists = fs.existsSync(channel_verify_file); + if (exists) { + verify_backup = fs.readFileSync(channel_verify_file, 'utf-8'); + options.form = JSON.stringify({ single_chan_backups: { chan_backups: [JSON.parse(verify_backup)] } }); + } + else { + verify_backup = ''; + const errMsg = 'Channel backup to verify does not Exist.'; + const err = common.handleError({ statusCode: 404, message: 'Verify Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + if (verify_backup !== '') { + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'ChannelBackup', msg: 'Channel Backup Verified', data: body }); + res.status(201).json({ message: message }); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'ChannelsBackup', 'Verify Channels Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; +export const postRestore = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'ChannelBackup', msg: 'Restoring Channel Backup..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channels/backup/restore'; + let channel_restore_file = ''; + let message = ''; + let restore_backup = ''; + if (req.params.channelPoint === 'ALL') { + message = 'All Channels Restore Successful.'; + channel_restore_file = req.session.selectedNode.channel_backup_path + sep + 'restore' + sep; + const exists = fs.existsSync(channel_restore_file + 'channel-all.bak'); + const downloaded_exists = fs.existsSync(channel_restore_file + 'backup-channel-all.bak'); + if (exists) { + restore_backup = fs.readFileSync(channel_restore_file + 'channel-all.bak', 'utf-8'); + if (restore_backup !== '') { + const restore_backup_json = JSON.parse(restore_backup); + options.form = JSON.stringify({ multi_chan_backup: restore_backup_json.multi_chan_backup.multi_chan_backup }); + } + else { + const errMsg = 'Channel backup to restore does not Exist.'; + const err = common.handleError({ statusCode: 404, message: 'Restore Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + else if (downloaded_exists) { + restore_backup = fs.readFileSync(channel_restore_file + 'backup-channel-all.bak', 'utf-8'); + if (restore_backup !== '') { + const restore_backup_json = JSON.parse(restore_backup); + options.form = JSON.stringify({ multi_chan_backup: restore_backup_json.multi_chan_backup.multi_chan_backup }); + } + else { + const errMsg = 'Channel backup to restore does not Exist.'; + const err = common.handleError({ statusCode: 404, message: 'Restore Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + else { + restore_backup = ''; + const errMsg = 'Channel backup to restore does not Exist.'; + const err = common.handleError({ statusCode: 404, message: 'Restore Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + else { + message = 'Channel Restore Successful.'; + channel_restore_file = req.session.selectedNode.channel_backup_path + sep + 'restore' + sep + 'channel-' + req.params.channelPoint?.replace(':', '-') + '.bak'; + const exists = fs.existsSync(channel_restore_file); + if (exists) { + restore_backup = fs.readFileSync(channel_restore_file, 'utf-8'); + options.form = JSON.stringify({ chan_backups: { chan_backups: [JSON.parse(restore_backup)] } }); + } + else { + restore_backup = ''; + const errMsg = 'Channel backup to restore does not Exist.'; + const err = common.handleError({ statusCode: 404, message: 'Restore Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + if (restore_backup !== '') { + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'ChannelBackup', msg: 'Channel Restored', data: body }); + if (req.params.channelPoint === 'ALL') { + channel_restore_file = channel_restore_file + 'channel-all.bak'; + } + fs.rename(channel_restore_file, channel_restore_file + '.restored', () => { + getFilesList(req.session.selectedNode.channel_backup_path, (getFilesListRes) => { + if (getFilesListRes.error) { + const errMsg = getFilesListRes.error; + const err = common.handleError({ statusCode: 500, message: 'Restore Channel Error', error: errMsg }, 'ChannelBackup', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, list: getFilesListRes }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'ChannelBackup', msg: 'Channel Restored and Saved' }); + return res.status(201).json({ message: message, list: getFilesListRes }); + } + }); + }); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'ChannelsBackup', 'Restore Channel Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; +export const getRestoreList = (req, res, next) => { + getFilesList(req.session.selectedNode.channel_backup_path, (getFilesListRes) => { + if (getFilesListRes.error) { + return res.status(getFilesListRes.statusCode).json(getFilesListRes); + } + else { + return res.status(200).json(getFilesListRes); + } + }); +}; diff --git a/backend/controllers/lnd/fees.js b/backend/controllers/lnd/fees.js new file mode 100644 index 00000000..68766789 --- /dev/null +++ b/backend/controllers/lnd/fees.js @@ -0,0 +1,48 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { getAllForwardingEvents } from './switch.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getFees = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Getting Fees..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/fees'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Fee Received', data: body }); + const today = new Date(Date.now()); + const start_date = new Date(today.getFullYear(), today.getMonth(), 1, 0, 0, 0); + const current_time = (Math.round(today.getTime() / 1000)); + const month_start_time = (Math.round(start_date.getTime() / 1000)); + const week_start_time = current_time - 604800; + const day_start_time = current_time - 86400; + return getAllForwardingEvents(req, month_start_time, current_time, 0, 'fees', (history) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Forwarding History Received', data: history }); + const daily_sum = history.forwarding_events?.reduce((acc, curr) => ((curr.timestamp >= day_start_time) ? [(acc[0] + 1), (acc[1] + +curr.fee_msat)] : acc), [0, 0]); + const weekly_sum = history.forwarding_events?.reduce((acc, curr) => ((curr.timestamp >= week_start_time) ? [(acc[0] + 1), (acc[1] + +curr.fee_msat)] : acc), [0, 0]); + const monthly_sum = history.forwarding_events?.reduce((acc, curr) => [(acc[0] + 1), (acc[1] + +curr.fee_msat)], [0, 0]); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Daily Sum (Transactions, Fee)', data: daily_sum }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Weekly Sum (Transactions, Fee)', data: weekly_sum }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Monthly Sum (Transactions, Fee)', data: monthly_sum }); + body.daily_tx_count = daily_sum[0]; + body.weekly_tx_count = weekly_sum[0]; + body.monthly_tx_count = monthly_sum[0]; + body.day_fee_sum = (daily_sum[1] / 1000).toFixed(2); + body.week_fee_sum = (weekly_sum[1] / 1000).toFixed(2); + body.month_fee_sum = (monthly_sum[1] / 1000).toFixed(2); + body.forwarding_events_history = history; + if (history.error) { + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Fees', msg: 'Fetch Forwarding Events Error', error: history.error }); + } + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Fees Received', data: body }); + res.status(200).json(body); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Fees', 'Get Forwarding Events Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/getInfo.js b/backend/controllers/lnd/getInfo.js new file mode 100644 index 00000000..b34c1800 --- /dev/null +++ b/backend/controllers/lnd/getInfo.js @@ -0,0 +1,56 @@ +import request from 'request-promise'; +import { Database } from '../../utils/database.js'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { LNDWSClient } from './webSocketClient.js'; +let options = null; +const logger = Logger; +const common = Common; +const lndWsClient = LNDWSClient; +const databaseService = Database; +export const getInfo = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Getting LND Node Information..' }); + common.logEnvVariables(req); + common.setOptions(req); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/getinfo'; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Selected Node ' + req.session.selectedNode.ln_node }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Calling Info from LND server url ' + options.url }); + if (!options.headers || !options.headers['Grpc-Metadata-macaroon']) { + const errMsg = 'LND Get info failed due to bad or missing macaroon! Please check RTL-Config.json to verify the setup!'; + const err = common.handleError({ statusCode: 502, message: 'Bad or Missing Macaroon', error: errMsg }, 'GetInfo', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + common.nodes?.map((node) => { + if (node.ln_implementation === 'LND') { + common.getAllNodeAllChannelBackup(node); + } + return node; + }); + return request(options).then((body) => { + const body_str = (!body) ? '' : JSON.stringify(body); + const search_idx = (!body) ? -1 : body_str.search('Not Found'); + if (!body || search_idx > -1 || body.error) { + if (body && !body.error) { + body.error = 'Error From Server!'; + } + const err = common.handleError(body, 'GetInfo', 'Get Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + req.session.selectedNode.ln_version = body.version.split('-')[0] || ''; + lndWsClient.updateSelectedNode(req.session.selectedNode); + databaseService.loadDatabase(req.session); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body }); + return res.status(200).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'GetInfo', 'Get Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } +}; diff --git a/backend/controllers/lnd/graph.js b/backend/controllers/lnd/graph.js new file mode 100644 index 00000000..37f99221 --- /dev/null +++ b/backend/controllers/lnd/graph.js @@ -0,0 +1,164 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getAliasFromPubkey = (selNode, pubkey) => { + options.url = selNode.ln_server_url + '/v1/graph/node/' + pubkey; + return request(options).then((res) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Graph', msg: 'Alias Received', data: res.node.alias }); + return res.node.alias; + }). + catch((err) => pubkey.substring(0, 20)); +}; +export const getDescribeGraph = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Getting Network Graph..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/graph'; + request.get(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Network Graph Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Describe Graph Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getGraphInfo = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Getting Graph Information..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/graph/info'; + request.get(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Information Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Graph Information Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getGraphNode = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Getting Graph Node Information..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/graph/node/' + req.params.pubKey; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Node Information Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Get Node Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getGraphEdge = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Getting Graph Edge Information..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/graph/edge/' + req.params.chanid; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Edge Information Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Get Edge Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getQueryRoutes = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Getting Graph Routes..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/graph/routes/' + req.params.destPubkey + '/' + req.params.amount; + if (req.query.outgoing_chan_id) { + options.url = options.url + '?outgoing_chan_id=' + req.query.outgoing_chan_id; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Graph', msg: 'Query Routes URL', data: options.url }); + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Graph', msg: 'Query Routes Received', data: body }); + if (body.routes && body.routes.length && body.routes.length > 0 && body.routes[0].hops && body.routes[0].hops.length && body.routes[0].hops.length > 0) { + return Promise.all(body.routes[0].hops?.map((hop) => getAliasFromPubkey(req.session.selectedNode, hop.pub_key))). + then((values) => { + body.routes[0].hops?.map((hop, i) => { + hop.hop_sequence = i + 1; + hop.pubkey_alias = values[i]; + return hop; + }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Routes with Alias Received', data: body }); + res.status(200).json(body); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Get Query Routes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Routes Received', data: body }); + return res.status(200).json(body); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Get Query Routes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getRemoteFeePolicy = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Getting Remote Fee Policy..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/graph/edge/' + req.params.chanid; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Graph', msg: 'Edge Info Received', data: body }); + let remoteNodeFee = {}; + if (body.node1_pub === req.params.localPubkey) { + remoteNodeFee = { + time_lock_delta: body.node2_policy.time_lock_delta, + fee_base_msat: body.node2_policy.fee_base_msat, + fee_rate_milli_msat: body.node2_policy.fee_rate_milli_msat + }; + } + else if (body.node2_pub === req.params.localPubkey) { + remoteNodeFee = { + time_lock_delta: body.node1_policy.time_lock_delta, + fee_base_msat: body.node1_policy.fee_base_msat, + fee_rate_milli_msat: body.node1_policy.fee_rate_milli_msat + }; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Remote Fee Policy Received', data: remoteNodeFee }); + res.status(200).json(remoteNodeFee); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Remote Fee Policy Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getAliasesForPubkeys = (req, res, next) => { + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + if (req.query.pubkeys) { + const pubkeyArr = req.query.pubkeys.split(','); + return Promise.all(pubkeyArr?.map((pubkey) => getAliasFromPubkey(req.session.selectedNode, pubkey))). + then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Node Alias', data: values }); + res.status(200).json(values); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'Graph', 'Get Aliases for Pubkeys Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + return res.status(200).json([]); + } +}; diff --git a/backend/controllers/lnd/invoices.js b/backend/controllers/lnd/invoices.js new file mode 100644 index 00000000..433f398e --- /dev/null +++ b/backend/controllers/lnd/invoices.js @@ -0,0 +1,84 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { LNDWSClient } from './webSocketClient.js'; +let options = null; +const logger = Logger; +const common = Common; +const lndWsClient = LNDWSClient; +export const invoiceLookup = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Getting Invoice Information..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v2/invoices/lookup'; + if (req.query.payment_addr) { + options.url = options.url + '?payment_addr=' + req.query.payment_addr; + } + else { + options.url = options.url + '?payment_hash=' + req.query.payment_hash; + } + request(options).then((body) => { + body.r_preimage = body.r_preimage ? Buffer.from(body.r_preimage, 'base64').toString('hex') : ''; + body.r_hash = body.r_hash ? Buffer.from(body.r_hash, 'base64').toString('hex') : ''; + body.description_hash = body.description_hash ? Buffer.from(body.description_hash, 'base64').toString('hex') : null; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Information Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoices', 'Invoice Lookup Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listInvoices = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Getting List Invoices..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/invoices?num_max_invoices=' + req.query.num_max_invoices + '&index_offset=' + req.query.index_offset + + '&reversed=' + req.query.reversed; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body }); + if (body.invoices && body.invoices.length > 0) { + body.invoices.forEach((invoice) => { + invoice.r_preimage = invoice.r_preimage ? Buffer.from(invoice.r_preimage, 'base64').toString('hex') : ''; + invoice.r_hash = invoice.r_hash ? Buffer.from(invoice.r_hash, 'base64').toString('hex') : ''; + invoice.description_hash = invoice.description_hash ? Buffer.from(invoice.description_hash, 'base64').toString('hex') : null; + }); + } + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Sorted Invoices List Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoices', 'List Invoices Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const addInvoice = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Adding Invoice..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/invoices'; + options.form = JSON.stringify(req.body); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Added', data: body }); + try { + if (body.r_hash) { + lndWsClient.subscribeToInvoice(options, req.session.selectedNode, body.r_hash); + } + } + catch (errRes) { + const err = common.handleError(errRes, 'Invoices', 'Subscribe to Newly Added Invoice Error', req.session.selectedNode); + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Invoice', msg: 'Subscribe to Newly Added Invoice Error', error: err }); + } + body.r_preimage = body.r_preimage ? Buffer.from(body.r_preimage, 'base64').toString('hex') : ''; + body.r_hash = body.r_hash ? Buffer.from(body.r_hash, 'base64').toString('hex') : ''; + body.description_hash = body.description_hash ? Buffer.from(body.description_hash, 'base64').toString('hex') : null; + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Invoices', 'Add Invoice Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/message.js b/backend/controllers/lnd/message.js new file mode 100644 index 00000000..19a62aa3 --- /dev/null +++ b/backend/controllers/lnd/message.js @@ -0,0 +1,43 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const signMessage = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Signing Message..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/signmessage'; + options.form = JSON.stringify({ + msg: Buffer.from(req.body.message).toString('base64') + }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Signed', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Messages', 'Sign Message Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const verifyMessage = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Verifying Message..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/verifymessage'; + options.form = JSON.stringify({ + msg: Buffer.from(req.body.message).toString('base64'), + signature: req.body.signature + }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Verified', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Messages', 'Verify Message Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/newAddress.js b/backend/controllers/lnd/newAddress.js new file mode 100644 index 00000000..ac2f9e15 --- /dev/null +++ b/backend/controllers/lnd/newAddress.js @@ -0,0 +1,21 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getNewAddress = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'NewAddress', msg: 'Getting New Address..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/newaddress?type=' + req.query.type; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'NewAddress', msg: 'New Address Generated', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'NewAddress', 'New Address Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/payments.js b/backend/controllers/lnd/payments.js new file mode 100644 index 00000000..1e34ce95 --- /dev/null +++ b/backend/controllers/lnd/payments.js @@ -0,0 +1,98 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const decodePaymentFromPaymentRequest = (selNode, payment) => { + options.url = selNode.ln_server_url + '/v1/payreq/' + payment; + return request(options).then((res) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'PayReq', msg: 'Description Received', data: res.description }); + return res; + }).catch((err) => { }); +}; +export const decodePayment = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'PayRequest', msg: 'Decoding Payment..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/payreq/' + req.params.payRequest; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'PayRequest', msg: 'Payment Decoded', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'PayRequest', 'Decode Payment Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const decodePayments = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'PayRequest', msg: 'Decoding Payments List..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + if (req.body.payments) { + const paymentsArr = req.body.payments.split(','); + return Promise.all(paymentsArr?.map((payment) => decodePaymentFromPaymentRequest(req.session.selectedNode, payment))). + then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'PayRequest', msg: 'Payment List Decoded', data: values }); + res.status(200).json(values); + }). + catch((errRes) => { + const err = common.handleError(errRes, 'PayRequest', 'Decode Payments Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'PayRequest', msg: 'Empty Payment List Decoded' }); + return res.status(200).json([]); + } +}; +export const getPayments = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Getting Payments List..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/payments?max_payments=' + req.query.max_payments + '&index_offset=' + req.query.index_offset + '&reversed=' + req.query.reversed; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getAllLightningTransactions = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Getting All Lightning Transactions..' }); + const options1 = JSON.parse(JSON.stringify(common.getOptions(req))); + const options2 = JSON.parse(JSON.stringify(common.getOptions(req))); + // options1.url = req.session.selectedNode.ln_server_url + '/v1/payments?max_payments=100000&index_offset=0&reversed=true'; + options2.url = req.session.selectedNode.ln_server_url + '/v1/invoices?num_max_invoices=100000&index_offset=0&reversed=true'; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'All Payments Options', data: options1 }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'All Invoices Options', data: options2 }); + // return Promise.all([request(options1), request(options2)]).then((values) => { + return Promise.all([{ payments: [] }, request(options2)]).then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'All Lightning Transactions Received', data: ({ totalPayments: values[0].payments.length || 0, totalInvoices: values[1].invoices.length || 0 }) }); + res.status(200).json({ listPaymentsAll: values[0], listInvoicesAll: values[1] }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'All Lightning Transactions Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const paymentLookup = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Looking up Payment..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v2/router/track/' + req.params.paymentHash; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Information Received for ' + req.params.paymentHash, data: body }); + res.status(200).json(body.result || body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Payments', 'Payment Lookup Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/peers.js b/backend/controllers/lnd/peers.js new file mode 100644 index 00000000..9920860a --- /dev/null +++ b/backend/controllers/lnd/peers.js @@ -0,0 +1,87 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getAliasForPeers = (selNode, peer) => { + options.url = selNode.ln_server_url + '/v1/graph/node/' + peer.pub_key; + return request(options).then((aliasBody) => { + logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Peers', msg: 'Alias Received', data: aliasBody.node.alias }); + peer.alias = aliasBody.node.alias; + return aliasBody.node.alias; + }).catch((err) => { + peer.alias = peer.pub_key.slice(0, 20); + return peer.pub_key; + }); +}; +export const getPeers = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Getting Peers..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/peers'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List Received', data: body }); + const peers = !body.peers ? [] : body.peers; + return Promise.all(peers?.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body.peers }); + res.status(200).json(body.peers); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postPeer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Connecting Peer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/peers'; + options.form = JSON.stringify({ + addr: { host: req.body.host, pubkey: req.body.pubkey }, + perm: req.body.perm + }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: body }); + options.url = req.session.selectedNode.ln_server_url + '/v1/peers'; + request(options).then((body) => { + const peers = (!body.peers) ? [] : body.peers; + return Promise.all(peers?.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => { + if (body.peers) { + body.peers = common.newestOnTop(body.peers, 'pub_key', req.body.pubkey); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: body }); + } + res.status(201).json(body.peers); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const deletePeer = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Disconnecting Peer..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/peers/' + req.params.peerPubKey; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconnect Pubkey', data: req.params.peerPubKey }); + request.delete(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconneted', data: body }); + res.status(204).json({}); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Peers', 'Disconnect Peer Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/switch.js b/backend/controllers/lnd/switch.js new file mode 100644 index 00000000..fb16f1d4 --- /dev/null +++ b/backend/controllers/lnd/switch.js @@ -0,0 +1,58 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +const responseData = { switch: { forwarding_events: [], last_offset_index: 0 }, fees: { forwarding_events: [], last_offset_index: 0 } }; +const num_max_events = 100; +export const forwardingHistory = (req, res, next) => { + getAllForwardingEvents(req, req.body.start_time, req.body.end_time, 0, 'switch', (eventsResponse) => { + if (eventsResponse.error) { + res.status(eventsResponse.error.statusCode).json(eventsResponse); + } + else { + res.status(201).json(eventsResponse); + } + }); +}; +export const getAllForwardingEvents = (req, start, end, offset, caller, callback) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Switch', msg: 'Getting Forwarding Events..' }); + if (offset === 0) { + responseData[caller] = { forwarding_events: [], last_offset_index: 0 }; + } + if (!req.session.selectedNode) { + const err = common.handleError({ message: 'Session Expired after a day\'s inactivity.', statusCode: 401 }, 'Balance', 'Get Balance Error', req.session.selectedNode); + return callback({ message: err.message, error: err.error, statusCode: err.statusCode }); + } + options = common.getOptions(req); + options.url = req.session.selectedNode.ln_server_url + '/v1/switch'; + options.form = {}; + if (start) { + options.form.start_time = start; + } + if (end) { + options.form.end_time = end; + } + options.form.num_max_events = num_max_events; + options.form.index_offset = offset; + options.form = JSON.stringify(options.form); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Switch', msg: 'Forwarding Events Params', data: options.form }); + return request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Switch', msg: 'Forwarding Events Received', data: body }); + if (body.forwarding_events) { + responseData[caller].forwarding_events.push(...body.forwarding_events); + responseData[caller].last_offset_index = body.last_offset_index ? body.last_offset_index : 0; + } + if (!body.last_offset_index || body.last_offset_index < offset + num_max_events) { + responseData[caller].last_offset_index = body.last_offset_index ? body.last_offset_index : 0; + return callback(responseData[caller]); + } + else { + return getAllForwardingEvents(req, start, end, offset + num_max_events, caller, callback); + } + }).catch((errRes) => { + const err = common.handleError(errRes, 'Switch', 'Get All Forwarding Events Error', req.session.selectedNode); + return callback({ message: err.message, error: err.error, statusCode: err.statusCode }); + }); +}; diff --git a/backend/controllers/lnd/transactions.js b/backend/controllers/lnd/transactions.js new file mode 100644 index 00000000..0b4684d8 --- /dev/null +++ b/backend/controllers/lnd/transactions.js @@ -0,0 +1,46 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getTransactions = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Transactions', msg: 'Getting Transactions..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/transactions'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Transactions', msg: 'Transactions List Received', data: body }); + res.status(200).json(body.transactions); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Transactions', 'List Transactions Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const postTransactions = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Transactions', msg: 'Sending Transaction..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/transactions'; + options.form = { + amount: req.body.amount, + addr: req.body.address, + sat_per_byte: req.body.fees, + target_conf: req.body.blocks + }; + if (req.body.sendAll) { + options.form.send_all = req.body.sendAll; + } + options.form = JSON.stringify(options.form); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Transactions', msg: 'Transaction Sent', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Transactions', 'Send Transaction Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/wallet.js b/backend/controllers/lnd/wallet.js new file mode 100644 index 00000000..61b1c4e4 --- /dev/null +++ b/backend/controllers/lnd/wallet.js @@ -0,0 +1,213 @@ +import atob from 'atob'; +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const genSeed = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Generating Seed..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + if (req.params.passphrase) { + options.url = req.session.selectedNode.ln_server_url + '/v1/genseed?aezeed_passphrase=' + Buffer.from(atob(req.params.passphrase)).toString('base64'); + } + else { + options.url = req.session.selectedNode.ln_server_url + '/v1/genseed'; + } + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Seed Generated', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Wallet', 'Gen Seed Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const operateWallet = (req, res, next) => { + let err_message = ''; + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.method = 'POST'; + if (!req.params.operation || req.params.operation === 'unlockwallet') { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Unlocking Wallet..' }); + options.url = req.session.selectedNode.ln_server_url + '/v1/unlockwallet'; + options.form = JSON.stringify({ + wallet_password: Buffer.from(atob(req.body.wallet_password)).toString('base64') + }); + err_message = 'Unlocking wallet failed! Verify that lnd is running and the wallet is locked!'; + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Initializing Wallet..' }); + options.url = req.session.selectedNode.ln_server_url + '/v1/initwallet'; + if (req.body.aezeed_passphrase && req.body.aezeed_passphrase !== '') { + options.form = JSON.stringify({ + wallet_password: Buffer.from(atob(req.body.wallet_password)).toString('base64'), + cipher_seed_mnemonic: req.body.cipher_seed_mnemonic, + aezeed_passphrase: Buffer.from(atob(req.body.aezeed_passphrase)).toString('base64') + }); + } + else { + options.form = JSON.stringify({ + wallet_password: Buffer.from(atob(req.body.wallet_password)).toString('base64'), + cipher_seed_mnemonic: req.body.cipher_seed_mnemonic + }); + } + err_message = 'Initializing wallet failed!'; + } + request(options).then((body) => { + const body_str = (!body) ? '' : JSON.stringify(body); + const search_idx = (!body) ? -1 : body_str.search('Not Found'); + if (!body) { + const err = common.handleError({ statusCode: 500, message: 'Wallet Error', error: err_message }, 'Wallet', err_message, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } + else if (search_idx > -1) { + const err = common.handleError({ statusCode: 500, message: 'Wallet Error', error: err_message }, 'Wallet', err_message, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } + else if (body.error) { + if ((body.code === 1 && body.error === 'context canceled') || (body.code === 14 && body.error === 'transport is closing')) { + res.status(201).json('Successful'); + } + else { + const errMsg = (body.error && typeof body.error === 'object') ? JSON.stringify(body.error) : (body.error && typeof body.error === 'string') ? body.error : err_message; + const err = common.handleError({ statusCode: 500, message: 'Wallet Error', error: errMsg }, 'Wallet', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Wallet Unlocked/Initialized', data: body }); + res.status(201).json('Successful'); + } + }).catch((errRes) => { + if ((errRes.error.code === 1 && errRes.error.error === 'context canceled') || (errRes.error.code === 14 && errRes.error.error === 'transport is closing')) { + res.status(201).json('Successful'); + } + else { + const err = common.handleError(errRes, 'Wallet', err_message, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + }); +}; +export const updateSelNodeOptions = (req, res, next) => { + const response = common.updateSelectedNodeOptions(req); + res.status(response.status).json({ updateMessage: response.message }); +}; +export const getUTXOs = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Getting UTXOs..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v2/wallet/utxos'; + if (common.isVersionCompatible(req.session.selectedNode.ln_version, '0.14.0')) { + options.form = JSON.stringify({ max_confs: req.query.max_confs }); + } + else { + options.url = options.url + '?max_confs=' + req.query.max_confs; + } + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'UTXOs List Received', data: body }); + res.status(200).json(body.utxos ? body.utxos : []); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Wallet', 'List UTXOs Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const bumpFee = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Bumping Fee..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v2/wallet/bumpfee'; + options.form = {}; + options.form.outpoint = { + txid_str: req.body.txid, + output_index: req.body.outputIndex + }; + if (req.body.targetConf) { + options.form.target_conf = req.body.targetConf; + } + else if (req.body.satPerByte) { + options.form.sat_per_byte = req.body.satPerByte; + } + options.form = JSON.stringify(options.form); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Fee Bumped', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Wallet', 'Bump Fee Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const labelTransaction = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Labelling Transaction..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v2/wallet/tx/label'; + options.form = {}; + options.form.txid = req.body.txid; + options.form.label = req.body.label; + options.form.overwrite = req.body.overwrite; + options.form = JSON.stringify(options.form); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Wallet', msg: 'Label Transaction Options', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Transaction Labelled', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Wallet', 'Label Transaction Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const leaseUTXO = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Leasing UTXO..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v2/wallet/utxos/lease'; + options.form = {}; + options.form.id = req.body.txid; + options.form.outpoint = { + txid_bytes: req.body.txid, + output_index: req.body.outputIndex + }; + options.form = JSON.stringify(options.form); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Wallet', msg: 'UTXO Lease Options', data: options.form }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'UTXO Leased', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Wallet', 'Lease UTXO Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const releaseUTXO = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Releasing UTXO..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v2/wallet/utxos/release'; + options.form = {}; + options.form.id = req.body.txid; + options.form.outpoint = { + txid_bytes: req.body.txid, + output_index: req.body.outputIndex + }; + options.form = JSON.stringify(options.form); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'UTXO Released', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Wallet', 'Release UTXO Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/lnd/webSocketClient.js b/backend/controllers/lnd/webSocketClient.js new file mode 100644 index 00000000..450eb2b3 --- /dev/null +++ b/backend/controllers/lnd/webSocketClient.js @@ -0,0 +1,122 @@ +import request from 'request-promise'; +import * as fs from 'fs'; +import { join } from 'path'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { WSServer } from '../../utils/webSocketServer.js'; +export class LNDWebSocketClient { + constructor() { + this.logger = Logger; + this.common = Common; + this.wsServer = WSServer; + this.webSocketClients = []; + this.connect = (selectedNode) => { + try { + const clientExists = this.webSocketClients.find((wsc) => wsc.selectedNode.index === selectedNode.index); + if (!clientExists && selectedNode.ln_server_url) { + const newWebSocketClient = { selectedNode: selectedNode }; + this.webSocketClients.push(newWebSocketClient); + } + } + catch (err) { + throw new Error(err); + } + }; + this.fetchUnpaidInvoices = (selectedNode) => { + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Getting Unpaid Invoices..' }); + const options = this.setOptionsForSelNode(selectedNode); + options.url = selectedNode.ln_server_url + '/v1/invoices?pending_only=true'; + return request(options).then((body) => { + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Unpaid Invoices Received', data: body }); + if (body.invoices && body.invoices.length > 0) { + body.invoices.forEach((invoice) => { + if (invoice.state === 'OPEN') { + this.subscribeToInvoice(options, selectedNode, invoice.r_hash); + } + }); + } + return null; + }).catch((errRes) => { + const err = this.common.handleError(errRes, 'WebSocketClient', 'Pending Invoices Error', selectedNode); + return ({ message: err.message, error: err.error }); + }); + }; + this.subscribeToInvoice = (options, selectedNode, rHash) => { + rHash = rHash?.replace(/\+/g, '-')?.replace(/[/]/g, '_'); + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Subscribing to Invoice ' + rHash + ' ..' }); + options.url = selectedNode.ln_server_url + '/v2/invoices/subscribe/' + rHash; + request(options).then((msg) => { + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Invoice Information Received for ' + rHash }); + if (typeof msg === 'string') { + const results = msg.split('\n'); + msg = (results.length && results.length > 1) ? JSON.parse(results[1]) : JSON.parse(msg); + msg.result.r_preimage = msg.result.r_preimage ? Buffer.from(msg.result.r_preimage, 'base64').toString('hex') : ''; + msg.result.r_hash = msg.result.r_hash ? Buffer.from(msg.result.r_hash, 'base64').toString('hex') : ''; + msg.result.description_hash = msg.result.description_hash ? Buffer.from(msg.result.description_hash, 'base64').toString('hex') : null; + } + msg['type'] = 'invoice'; + msg['source'] = 'LND'; + const msgStr = JSON.stringify(msg); + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Invoice Info Received', data: msgStr }); + this.wsServer.sendEventsToAllLNClients(msgStr, selectedNode); + }).catch((errRes) => { + const err = this.common.handleError(errRes, 'Invoices', 'Subscribe to Invoice Error for ' + rHash, selectedNode); + const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message + ' ' + rHash }) : (typeof err === 'object') ? JSON.stringify({ error: err + ' ' + rHash }) : ('{ "error": ' + err + ' ' + rHash + ' }')); + this.wsServer.sendErrorToAllLNClients(errStr, selectedNode); + }); + }; + this.subscribeToPayment = (options, selectedNode, paymentHash) => { + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Subscribing to Payment ' + paymentHash + ' ..' }); + options.url = selectedNode.ln_server_url + '/v2/router/track/' + paymentHash; + request(options).then((msg) => { + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Payment Information Received for ' + paymentHash }); + msg['type'] = 'payment'; + msg['source'] = 'LND'; + const msgStr = JSON.stringify(msg); + this.logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'WebSocketClient', msg: 'Payment Info Received', data: msgStr }); + this.wsServer.sendEventsToAllLNClients(msgStr, selectedNode); + }).catch((errRes) => { + const err = this.common.handleError(errRes, 'Payment', 'Subscribe to Payment Error for ' + paymentHash, selectedNode); + const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message + ' ' + paymentHash }) : (typeof err === 'object') ? JSON.stringify({ error: err + ' ' + paymentHash }) : ('{ "error": ' + err + ' ' + paymentHash + ' }')); + this.wsServer.sendErrorToAllLNClients(errStr, selectedNode); + }); + }; + this.setOptionsForSelNode = (selectedNode) => { + const options = { url: '', rejectUnauthorized: false, json: true, form: null }; + try { + options['headers'] = { 'Grpc-Metadata-macaroon': fs.readFileSync(join(selectedNode.macaroon_path, 'admin.macaroon')).toString('hex') }; + } + catch (err) { + this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: 'WebSocketClient', msg: 'Set Options Error', error: JSON.stringify(err) }); + } + return options; + }; + this.disconnect = (selectedNode) => { + const clientExists = this.webSocketClients.find((wsc) => wsc.selectedNode.index === selectedNode.index); + if (clientExists) { + this.logger.log({ selectedNode: clientExists.selectedNode, level: 'INFO', fileName: 'CLWebSocket', msg: 'Disconnecting from the LND\'s Websocket Server..' }); + const clientIdx = this.webSocketClients.findIndex((wsc) => wsc.selectedNode.index === selectedNode.index); + this.webSocketClients.splice(clientIdx, 1); + } + }; + this.updateSelectedNode = (newSelectedNode) => { + const clientIdx = this.webSocketClients.findIndex((wsc) => +wsc.selectedNode.index === +newSelectedNode.index); + let newClient = this.webSocketClients[clientIdx]; + if (!newClient) { + newClient = { selectedNode: null }; + } + newClient.selectedNode = JSON.parse(JSON.stringify(newSelectedNode)); + this.webSocketClients[clientIdx] = newClient; + if (this.webSocketClients[clientIdx].selectedNode.ln_version === '' || !this.webSocketClients[clientIdx].selectedNode.ln_version || this.common.isVersionCompatible(this.webSocketClients[clientIdx].selectedNode.ln_version, '0.11.0')) { + this.fetchUnpaidInvoices(this.webSocketClients[clientIdx].selectedNode); + } + }; + this.wsServer.eventEmitterLND.on('CONNECT', (nodeIndex) => { + this.connect(this.common.findNode(+nodeIndex)); + }); + this.wsServer.eventEmitterLND.on('DISCONNECT', (nodeIndex) => { + this.disconnect(this.common.findNode(+nodeIndex)); + }); + } +} +export const LNDWSClient = new LNDWebSocketClient(); diff --git a/backend/controllers/shared/RTLConf.js b/backend/controllers/shared/RTLConf.js new file mode 100644 index 00000000..973752cd --- /dev/null +++ b/backend/controllers/shared/RTLConf.js @@ -0,0 +1,391 @@ +import * as fs from 'fs'; +import { sep } from 'path'; +import ini from 'ini'; +import parseHocon from 'hocon-parser'; +import request from 'request-promise'; +import { Database } from '../../utils/database.js'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { WSServer } from '../../utils/webSocketServer.js'; +const options = { url: '' }; +const logger = Logger; +const common = Common; +const wsServer = WSServer; +const databaseService = Database; +export const updateSelectedNode = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Updating Selected Node..' }); + const selNodeIndex = req.params.currNodeIndex ? +req.params.currNodeIndex : common.initSelectedNode ? +common.initSelectedNode.index : 1; + req.session.selectedNode = common.findNode(selNodeIndex); + if (req.headers && req.headers.authorization && req.headers.authorization !== '') { + wsServer.updateLNWSClientDetails(req.session.id, +req.session.selectedNode.index, +req.params.prevNodeIndex); + if (req.params.prevNodeIndex !== -1) { + databaseService.unloadDatabase(req.params.prevNodeIndex, req.session.id); + } + } + const responseVal = !req.session.selectedNode.ln_node ? '' : req.session.selectedNode.ln_node; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Selected Node Updated To ' + responseVal }); + res.status(200).json({ status: 'Selected Node Updated To: ' + JSON.stringify(responseVal) + '!' }); +}; +export const getRTLConfigInitial = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Initial RTL Configuration..' }); + const confFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + fs.readFile(confFile, 'utf8', (errRes, data) => { + if (errRes) { + if (errRes.code === 'ENOENT') { + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'RTLConf', msg: 'Node config does not exist!', error: { error: 'Node config does not exist.' } }); + res.status(200).json({ defaultNodeIndex: 0, selectedNodeIndex: 0, sso: {}, nodes: [] }); + } + else { + const errMsg = 'Get Node Config Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } + } + else { + const nodeConfData = JSON.parse(data); + const sso = { rtlSSO: common.rtl_sso, logoutRedirectLink: common.logout_redirect_link }; + const enable2FA = !!common.rtl_secret2fa; + const allowPasswordUpdate = common.flg_allow_password_update; + const nodesArr = []; + if (common.nodes && common.nodes.length > 0) { + common.nodes.forEach((node, i) => { + const settings = { unannouncedChannels: false }; + settings.userPersona = node.user_persona ? node.user_persona : 'MERCHANT'; + settings.themeMode = (node.theme_mode) ? node.theme_mode : 'DAY'; + settings.themeColor = (node.theme_color) ? node.theme_color : 'PURPLE'; + settings.unannouncedChannels = !!node.unannounced_channels || false; + settings.fiatConversion = (node.fiat_conversion) ? !!node.fiat_conversion : false; + settings.currencyUnit = node.currency_unit; + nodesArr.push({ + index: node.index, + lnNode: node.ln_node, + lnImplementation: node.ln_implementation, + settings: settings, + authentication: {} + }); + }); + } + const body = { defaultNodeIndex: nodeConfData.defaultNodeIndex, selectedNodeIndex: (req.session.selectedNode && req.session.selectedNode.index ? req.session.selectedNode.index : common.initSelectedNode.index), sso: sso, enable2FA: enable2FA, allowPasswordUpdate: allowPasswordUpdate, nodes: nodesArr }; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Initial RTL Configuration Received', data: body }); + res.status(200).json(body); + } + }); +}; +export const getRTLConfig = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting RTL Configuration..' }); + const confFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + fs.readFile(confFile, 'utf8', (errRes, data) => { + if (errRes) { + if (errRes.code === 'ENOENT') { + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'RTLConf', msg: 'Node config does not exist!', error: { error: 'Node config does not exist.' } }); + res.status(200).json({ defaultNodeIndex: 0, selectedNodeIndex: 0, sso: {}, nodes: [] }); + } + else { + const errMsg = 'Get Node Config Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } + } + else { + const nodeConfData = JSON.parse(data); + const sso = { rtlSSO: common.rtl_sso, logoutRedirectLink: common.logout_redirect_link }; + const enable2FA = !!common.rtl_secret2fa; + const allowPasswordUpdate = common.flg_allow_password_update; + const nodesArr = []; + if (common.nodes && common.nodes.length > 0) { + common.nodes.forEach((node, i) => { + const authentication = {}; + authentication.configPath = (node.config_path) ? node.config_path : ''; + authentication.swapMacaroonPath = (node.swap_macaroon_path) ? node.swap_macaroon_path : ''; + authentication.boltzMacaroonPath = (node.boltz_macaroon_path) ? node.boltz_macaroon_path : ''; + const settings = { unannouncedChannels: false }; + settings.userPersona = node.user_persona ? node.user_persona : 'MERCHANT'; + settings.themeMode = (node.theme_mode) ? node.theme_mode : 'DAY'; + settings.themeColor = (node.theme_color) ? node.theme_color : 'PURPLE'; + settings.unannouncedChannels = !!node.unannounced_channels || false; + settings.fiatConversion = (node.fiat_conversion) ? !!node.fiat_conversion : false; + settings.bitcoindConfigPath = node.bitcoind_config_path; + settings.logLevel = node.log_level ? node.log_level : 'ERROR'; + settings.lnServerUrl = node.ln_server_url; + settings.swapServerUrl = node.swap_server_url; + settings.boltzServerUrl = node.boltz_server_url; + settings.enableOffers = node.enable_offers; + settings.enablePeerswap = node.enable_peerswap; + settings.channelBackupPath = node.channel_backup_path; + settings.currencyUnit = node.currency_unit; + nodesArr.push({ + index: node.index, + lnNode: node.ln_node, + lnImplementation: node.ln_implementation, + settings: settings, + authentication: authentication + }); + }); + } + const body = { defaultNodeIndex: nodeConfData.defaultNodeIndex, selectedNodeIndex: (req.session.selectedNode && req.session.selectedNode.index ? req.session.selectedNode.index : common.initSelectedNode.index), sso: sso, enable2FA: enable2FA, allowPasswordUpdate: allowPasswordUpdate, nodes: nodesArr }; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'RTL Configuration Received', data: body }); + res.status(200).json(body); + } + }); +}; +export const updateUISettings = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Updating UI Settings..' }); + const RTLConfFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); + const node = config.nodes.find((node) => (node.index === req.session.selectedNode.index)); + if (node && node.Settings) { + node.Settings.userPersona = req.body.updatedSettings.userPersona; + node.Settings.themeMode = req.body.updatedSettings.themeMode; + node.Settings.themeColor = req.body.updatedSettings.themeColor; + node.Settings.unannouncedChannels = req.body.updatedSettings.unannouncedChannels; + node.Settings.fiatConversion = req.body.updatedSettings.fiatConversion; + if (req.body.updatedSettings.fiatConversion) { + node.Settings.currencyUnit = req.body.updatedSettings.currencyUnit ? req.body.updatedSettings.currencyUnit : 'USD'; + } + else { + delete node.Settings.currencyUnit; + } + const selectedNode = common.findNode(req.session.selectedNode.index); + selectedNode.user_persona = req.body.updatedSettings.userPersona; + selectedNode.theme_mode = req.body.updatedSettings.themeMode; + selectedNode.theme_color = req.body.updatedSettings.themeColor; + selectedNode.unannounced_channels = req.body.updatedSettings.unannouncedChannels; + selectedNode.fiat_conversion = req.body.updatedSettings.fiatConversion; + if (req.body.updatedSettings.fiatConversion) { + selectedNode.currency_unit = req.body.updatedSettings.currencyUnit ? req.body.updatedSettings.currencyUnit : 'USD'; + } + else { + delete selectedNode.currency_unit; + } + common.replaceNode(req, selectedNode); + } + try { + fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8'); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'UI Settings Updated', data: maskPasswords(config) }); + res.status(201).json({ message: 'Node Settings Updated Successfully' }); + } + catch (errRes) { + const errMsg = 'Update Node Settings Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } +}; +export const update2FASettings = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Updating 2FA Settings..' }); + const RTLConfFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); + if (req.body.secret2fa && req.body.secret2fa.trim() !== '') { + config.secret2fa = req.body.secret2fa; + } + else { + delete config.secret2fa; + } + const message = req.body.secret2fa.trim() === '' ? 'Two factor authentication disabled successfully.' : 'Two factor authentication enabled successfully.'; + try { + fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8'); + common.rtl_secret2fa = config.secret2fa; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: message }); + res.status(201).json({ message: message }); + } + catch (errRes) { + const errMsg = 'Update 2FA Settings Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } +}; +export const updateDefaultNode = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Updating Default Node..' }); + const RTLConfFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); + config.defaultNodeIndex = req.body.defaultNodeIndex; + try { + fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8'); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Default Node Updated', data: maskPasswords(config) }); + res.status(201).json({ message: 'Default Node Updated Successfully' }); + } + catch (errRes) { + const errMsg = 'Update Default Node Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } +}; +export const getConfig = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Reading Configuration File..' }); + let confFile = ''; + let fileFormat = 'INI'; + switch (req.params.nodeType) { + case 'ln': + confFile = req.session.selectedNode.config_path; + break; + case 'bitcoind': + confFile = req.session.selectedNode.bitcoind_config_path; + break; + case 'rtl': + fileFormat = 'JSON'; + confFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + break; + default: + confFile = ''; + break; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'RTLConf', msg: 'Node Type', data: req.params.nodeType }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'RTLConf', msg: 'File Path', data: confFile }); + fs.readFile(confFile, 'utf8', (errRes, data) => { + if (errRes) { + const errMsg = 'Reading Config Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } + else { + let jsonConfig = {}; + if (fileFormat === 'JSON') { + jsonConfig = JSON.parse(data); + } + else { + fileFormat = 'INI'; + data = data?.replace('color=#', 'color='); + jsonConfig = ini.parse(data); + if (jsonConfig['Application Options'] && jsonConfig['Application Options'].color) { + jsonConfig['Application Options'].color = '#' + jsonConfig['Application Options'].color; + } + if (req.params.nodeType === 'ln' && req.session.selectedNode.ln_implementation === 'ECL' && !jsonConfig['eclair.api.password']) { + fileFormat = 'HOCON'; + jsonConfig = parseHocon(data); + } + } + jsonConfig = maskPasswords(jsonConfig); + const responseJSON = (fileFormat === 'JSON') ? jsonConfig : ini.stringify(jsonConfig)?.replace('color=\\#', 'color=#'); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Configuration File Data Received', data: responseJSON }); + res.status(200).json({ format: fileFormat, data: responseJSON }); + } + }); +}; +export const getFile = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting File..' }); + const file = req.query.path ? req.query.path : (req.session.selectedNode.channel_backup_path + sep + 'channel-' + req.query.channel?.replace(':', '-') + '.bak'); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'RTLConf', msg: 'Channel Point', data: req.query.channel }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'RTLConf', msg: 'File Path', data: file }); + fs.readFile(file, 'utf8', (errRes, data) => { + if (errRes) { + if (errRes.code && errRes.code === 'ENOENT') { + errRes.code = 'File Not Found!'; + } + const errMsg = 'Reading File Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'File Data Received', data: data }); + res.status(200).json(data); + } + }); +}; +export const getCurrencyRates = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Currency Rates..' }); + options.url = 'https://blockchain.info/ticker'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Currency Rates Received', data: body }); + res.status(200).json(JSON.parse(body)); + }).catch((errRes) => { + const errMsg = 'Get Rates Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + }); +}; +export const updateSSO = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Updating SSO Settings..' }); + const RTLConfFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); + delete config.SSO; + config.SSO = req.body.SSO; + try { + fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8'); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'SSO Setting Updated', data: maskPasswords(config) }); + res.status(201).json({ message: 'SSO Updated Successfully' }); + } + catch (errRes) { + const errMsg = 'Update SSO Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } +}; +export const updateServiceSettings = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Updating Service Settings..' }); + const RTLConfFile = common.rtl_conf_file_path + sep + 'RTL-Config.json'; + const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); + const selectedNode = common.findNode(req.session.selectedNode.index); + config.nodes.forEach((node) => { + if (node.index === req.session.selectedNode.index) { + switch (req.body.service) { + case 'LOOP': + if (req.body.settings.enable) { + node.Settings.swapServerUrl = req.body.settings.serverUrl; + node.Authentication.swapMacaroonPath = req.body.settings.macaroonPath; + selectedNode.swap_server_url = req.body.settings.serverUrl; + selectedNode.swap_macaroon_path = req.body.settings.macaroonPath; + } + else { + delete node.Settings.swapServerUrl; + delete node.Authentication.swapMacaroonPath; + delete selectedNode.swap_server_url; + delete selectedNode.swap_macaroon_path; + } + break; + case 'BOLTZ': + if (req.body.settings.enable) { + node.Settings.boltzServerUrl = req.body.settings.serverUrl; + node.Authentication.boltzMacaroonPath = req.body.settings.macaroonPath; + selectedNode.boltz_server_url = req.body.settings.serverUrl; + selectedNode.boltz_macaroon_path = req.body.settings.macaroonPath; + } + else { + delete node.Settings.boltzServerUrl; + delete node.Authentication.boltzMacaroonPath; + delete selectedNode.boltz_server_url; + delete selectedNode.boltz_macaroon_path; + } + break; + case 'OFFERS': + node.Settings.enableOffers = req.body.settings.enableOffers; + selectedNode.enable_offers = req.body.settings.enableOffers; + break; + case 'PEERSWAP': + node.Settings.enablePeerswap = req.body.settings.enablePeerswap; + selectedNode.enable_peerswap = req.body.settings.enablePeerswap; + break; + default: + break; + } + common.replaceNode(req, selectedNode); + } + return node; + }); + try { + fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8'); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Service Settings Updated', data: maskPasswords(config) }); + res.status(201).json({ message: 'Service Settings Updated Successfully' }); + } + catch (errRes) { + const errMsg = 'Update Service Settings Error'; + const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.error, error: err.error }); + } +}; +export const maskPasswords = (obj) => { + const keys = Object.keys(obj); + const length = keys.length; + if (length !== 0) { + for (let i = 0; i < length; i++) { + if (typeof obj[keys[i]] === 'object') { + keys[keys[i]] = maskPasswords(obj[keys[i]]); + } + if (typeof keys[i] === 'string' && + (keys[i].toLowerCase().includes('password') || keys[i].toLowerCase().includes('multipass') || + keys[i].toLowerCase().includes('rpcpass') || keys[i].toLowerCase().includes('rpcpassword') || + keys[i].toLowerCase().includes('rpcuser'))) { + obj[keys[i]] = '********************'; + } + } + } + return obj; +}; diff --git a/backend/controllers/shared/authenticate.js b/backend/controllers/shared/authenticate.js new file mode 100644 index 00000000..4325ede8 --- /dev/null +++ b/backend/controllers/shared/authenticate.js @@ -0,0 +1,133 @@ +import jwt from 'jsonwebtoken'; +import * as otplib from 'otplib'; +import * as crypto from 'crypto'; +import { Database } from '../../utils/database.js'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +const logger = Logger; +const common = Common; +const ONE_MINUTE = 60000; +const LOCKING_PERIOD = 30 * ONE_MINUTE; // HALF AN HOUR +const ALLOWED_LOGIN_ATTEMPTS = 5; +const failedLoginAttempts = {}; +const databaseService = Database; +const loginInterval = setInterval(() => { + for (const ip in failedLoginAttempts) { + if (new Date().getTime() > (failedLoginAttempts[ip].lastTried + LOCKING_PERIOD)) { + delete failedLoginAttempts[ip]; + clearInterval(loginInterval); + } + } +}, LOCKING_PERIOD); +export const getFailedInfo = (reqIP, currentTime) => { + let failed = { count: 0, lastTried: currentTime }; + if ((!failedLoginAttempts[reqIP]) || (currentTime > (failed.lastTried + LOCKING_PERIOD))) { + failed = { count: 0, lastTried: currentTime }; + failedLoginAttempts[reqIP] = failed; + } + else { + failed = failedLoginAttempts[reqIP]; + } + return failed; +}; +const handleMultipleFailedAttemptsError = (failed, currentTime, errMsg) => { + if (failed.count >= ALLOWED_LOGIN_ATTEMPTS && (currentTime <= (failed.lastTried + LOCKING_PERIOD))) { + return { + message: 'Multiple Failed Login Attempts!', + error: 'Application locked for ' + (LOCKING_PERIOD / ONE_MINUTE) + ' minutes due to multiple failed attempts!\nTry again after ' + common.convertTimestampToTime((failed.lastTried + LOCKING_PERIOD) / 1000) + '!' + }; + } + else { + return { + message: 'Authentication Failed!', + error: errMsg + '\nApplication will be locked after ' + (ALLOWED_LOGIN_ATTEMPTS - failed.count) + ' more unsuccessful attempts!' + }; + } +}; +export const verifyToken = (twoFAToken) => !!(common.rtl_secret2fa && common.rtl_secret2fa !== '' && otplib.authenticator.check(twoFAToken, common.rtl_secret2fa)); +export const authenticateUser = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Authenticating User..' }); + if (+common.rtl_sso) { + if (req.body.authenticateWith === 'JWT' && jwt.verify(req.body.authenticationValue, common.secret_key)) { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'User Authenticated' }); + res.status(406).json({ message: 'SSO Authentication Error', error: 'Login with Password is not allowed with SSO.' }); + } + else if (req.body.authenticateWith === 'PASSWORD') { + if (common.cookie_value.trim().length >= 32 && crypto.timingSafeEqual(Buffer.from(crypto.createHash('sha256').update(common.cookie_value).digest('hex'), 'utf-8'), Buffer.from(req.body.authenticationValue, 'utf-8'))) { + common.refreshCookie(); + if (!req.session.selectedNode) { + req.session.selectedNode = common.initSelectedNode; + } + const token = jwt.sign({ user: 'SSO_USER' }, common.secret_key); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'User Authenticated' }); + res.status(200).json({ token: token }); + } + else { + const errMsg = 'SSO Authentication Failed! Access key too short or does not match.'; + const err = common.handleError({ statusCode: 406, message: 'SSO Authentication Error', error: errMsg }, 'Authenticate', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } + } + else { + const currentTime = new Date().getTime(); + const reqIP = common.getRequestIP(req); + const failed = getFailedInfo(reqIP, currentTime); + const password = req.body.authenticationValue; + if (common.rtl_pass === password && failed.count < ALLOWED_LOGIN_ATTEMPTS) { + if (req.body.twoFAToken && req.body.twoFAToken !== '') { + // if (!verifyToken(req.body.twoFAToken)) { + // logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Authenticate', msg: 'Invalid Token! Failed IP ' + reqIP, error: { error: 'Invalid token.' } }); + // failed.count = failed.count + 1; + // failed.lastTried = currentTime; + // return res.status(401).json(handleMultipleFailedAttemptsError(failed, currentTime, 'Invalid 2FA Token!')); + // } + } + if (!req.session.selectedNode) { + req.session.selectedNode = common.initSelectedNode; + } + delete failedLoginAttempts[reqIP]; + const token = jwt.sign({ user: 'NODE_USER' }, common.secret_key); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'User Authenticated' }); + res.status(200).json({ token: token }); + } + else { + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Authenticate', msg: 'Invalid Password! Failed IP ' + reqIP, error: { error: 'Invalid password.' } }); + failed.count = common.rtl_pass !== password ? (failed.count + 1) : failed.count; + failed.lastTried = common.rtl_pass !== password ? currentTime : failed.lastTried; + return res.status(401).json(handleMultipleFailedAttemptsError(failed, currentTime, 'Invalid Password!')); + } + } +}; +export const resetPassword = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Resetting Password..' }); + if (+common.rtl_sso) { + const errMsg = 'Password cannot be reset for SSO authentication'; + const err = common.handleError({ statusCode: 401, message: 'Password Reset Error', error: errMsg }, 'Authenticate', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + else { + const currPassword = req.body.currPassword; + if (common.rtl_pass === currPassword) { + common.rtl_pass = common.replacePasswordWithHash(req.body.newPassword); + const token = jwt.sign({ user: 'NODE_USER' }, common.secret_key); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Password Reset Successful' }); + res.status(200).json({ token: token }); + } + else { + const errMsg = 'Incorrect Old Password'; + const err = common.handleError({ statusCode: 401, message: 'Password Reset Error', error: errMsg }, 'Authenticate', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + } +}; +export const logoutUser = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Logged out' }); + if (req.session.selectedNode && req.session.selectedNode.index) { + databaseService.unloadDatabase(+req.session.selectedNode.index, req.session.id); + } + req.session.destroy((err) => { + res.clearCookie('connect.sid'); + res.status(200).json({ loggedout: true }); + }); +}; diff --git a/backend/controllers/shared/boltz.js b/backend/controllers/shared/boltz.js new file mode 100644 index 00000000..716b9fc7 --- /dev/null +++ b/backend/controllers/shared/boltz.js @@ -0,0 +1,163 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const getInfo = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Getting Boltz Information..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Get Info Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/info'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz Information Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'Get Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getServiceInfo = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Getting Service Information..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Get Service Information Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/serviceinfo'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz Get Service Info Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'Get Service Information Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const listSwaps = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Getting List Swaps..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'List Swaps Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/listswaps'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz List Swaps Received', data: body }); + if (body && body.swaps && body.swaps.length && body.swaps.length > 0) { + body.swaps = body.swaps.reverse(); + } + if (body && body.reverseSwaps && body.reverseSwaps.length && body.reverseSwaps.length > 0) { + body.reverseSwaps = body.reverseSwaps.reverse(); + } + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'List Swaps Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const getSwapInfo = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Getting Swap Information..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Get Swap Information Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/swap/' + req.params.swapId; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Swap Information Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'Swap Info Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const createSwap = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Creating Swap..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Create Swap Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/createswap'; + options.body = { amount: req.body.amount }; + if (req.body.address !== '') { + options.body.address = req.body.address; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Create Swap Options Body', data: options.body }); + request.post(options).then((createSwapRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Swap Created', data: createSwapRes }); + res.status(201).json(createSwapRes); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'Create Swap Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const createReverseSwap = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Creating Reverse Swap..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Create Reverse Swap Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/createreverseswap'; + options.body = { amount: req.body.amount }; + if (req.body.address !== '') { + options.body.address = req.body.address; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Create Reverse Swap Body', data: options.body }); + request.post(options).then((createReverseSwapRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Reverse Swap Created', data: createReverseSwapRes }); + res.status(201).json(createReverseSwapRes); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'Create Reverse Swap Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const createChannel = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Creating Boltz Channel..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Create Channel Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/createchannel'; + options.body = { amount: req.body.amount }; + if (req.body.address !== '') { + options.body.address = req.body.address; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Create Channel Options Body', data: options.body }); + request.post(options).then((createChannelRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz Channel Created', data: createChannelRes }); + res.status(201).json(createChannelRes); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'Create Channel Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const deposit = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz Deposit Start..' }); + options = common.getBoltzServerOptions(req); + if (options.url === '') { + const errMsg = 'Boltz Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Deposit Error', error: errMsg }, 'Boltz', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/deposit'; + request.post(options).then((depositRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz Deposit Finished', data: depositRes }); + res.status(201).json(depositRes); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Boltz', 'Deposit Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/shared/loop.js b/backend/controllers/shared/loop.js new file mode 100644 index 00000000..290817fb --- /dev/null +++ b/backend/controllers/shared/loop.js @@ -0,0 +1,241 @@ +import request from 'request-promise'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +let options = null; +const logger = Logger; +const common = Common; +export const loopOut = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Looping Out..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop Out Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/out'; + options.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, + initiator: 'RTL' + }; + if (req.body.chanId !== '') { + options.body['loop_out_channel'] = req.body.chanId; + } + if (req.body.destAddress !== '') { + options.body['dest'] = req.body.destAddress; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Body', data: options.body }); + request.post(options).then((loopOutRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Looped Out', data: loopOutRes }); + res.status(201).json(loopOutRes); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop Out Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const loopOutTerms = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting Loop Out Terms..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop Out Terms Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/out/terms'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Out Terms Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop Out Terms Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const loopOutQuote = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting Loop Out Quotes..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop Out Quotes Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/out/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Quote URL', data: options.url }); + request(options).then((quoteRes) => { + quoteRes.amount = +req.params.amount; + quoteRes.swap_payment_dest = quoteRes.swap_payment_dest ? Buffer.from(quoteRes.swap_payment_dest, 'base64').toString('hex') : ''; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Out Quote Received', data: quoteRes }); + res.status(200).json(quoteRes); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop Out Quotes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const loopOutTermsAndQuotes = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting Loop Out Terms & Quotes..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop Out Terms & Quotes Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/out/terms'; + request(options).then((terms) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Terms Received', data: terms }); + const options1 = common.getSwapServerOptions(req); + const options2 = common.getSwapServerOptions(req); + options1.url = options1.url + '/v1/loop/out/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; + options2.url = options2.url + '/v1/loop/out/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Min Quote Options', data: options1 }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Max Quote Options', data: options2 }); + return Promise.all([request(options1), request(options2)]).then((values) => { + 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.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Out Quotes 1 Received', data: values[0] }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Out Quotes 2 Received', data: values[1] }); + res.status(200).json(values); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop Out Terms & Quotes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop Out Terms & Quotes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const loopIn = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Looping In..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop In Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/in'; + options.body = { + amt: req.body.amount, + max_swap_fee: req.body.swapFee, + max_miner_fee: req.body.minerFee, + initiator: 'RTL' + }; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Body', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Looped In', data: body }); + res.status(201).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop In Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const loopInTerms = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting Loop In Terms..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop In Terms Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/in/terms'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Terms Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop In Terms Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const loopInQuote = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting Loop In Quotes..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop In Quotes Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/in/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop In Quote Options', data: options.url }); + request(options).then((body) => { + body.amount = +req.params.amount; + body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : ''; + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Qoutes Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop In Quote Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const loopInTermsAndQuotes = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting Loop In Terms & Quotes..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Loop In Terms & Quotes Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/in/terms'; + request(options).then((terms) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop In Terms Received', data: terms }); + const options1 = common.getSwapServerOptions(req); + const options2 = common.getSwapServerOptions(req); + options1.url = options1.url + '/v1/loop/in/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; + options2.url = options2.url + '/v1/loop/in/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop In Min Quote Options', data: options1 }); + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop In Max Quote Options', data: options2 }); + return Promise.all([request(options1), request(options2)]).then((values) => { + 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.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Quotes 1 Received', data: values[0] }); + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Quotes 2 Received', data: values[1] }); + res.status(200).json(values); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop In Terms & Quotes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Loop In Terms & Quotes Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const swaps = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting List Swaps..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'List Swaps Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/swaps'; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Swaps Received', data: body }); + res.status(200).json(body.swaps); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'List Swaps Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const swap = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Getting Swap Information..' }); + options = common.getSwapServerOptions(req); + if (options.url === '') { + const errMsg = 'Loop Server URL is missing in the configuration.'; + const err = common.handleError({ statusCode: 500, message: 'Get Swap Error', error: errMsg }, 'Loop', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } + options.url = options.url + '/v1/loop/swap/' + req.params.id; + request(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Swap Information Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Loop', 'Get Swap Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/controllers/shared/pageSettings.js b/backend/controllers/shared/pageSettings.js new file mode 100644 index 00000000..6040a8ab --- /dev/null +++ b/backend/controllers/shared/pageSettings.js @@ -0,0 +1,33 @@ +import { Database } from '../../utils/database.js'; +import { Logger } from '../../utils/logger.js'; +import { Common } from '../../utils/common.js'; +import { CollectionsEnum } from '../../models/database.model.js'; +const logger = Logger; +const common = Common; +const databaseService = Database; +export const getPageSettings = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Getting Page Settings..' }); + databaseService.find(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS).then((settings) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Received', data: settings }); + return res.status(200).json(settings); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Page Settings', 'Page Settings Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; +export const savePageSettings = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Saving Page Settings..' }); + // eslint-disable-next-line arrow-body-style + return Promise.all(req.body.map((page) => databaseService.validateDocument(CollectionsEnum.PAGE_SETTINGS, page))).then((values) => { + return databaseService.insert(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS, req.body).then((insertRes) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Updated', data: insertRes }); + res.status(201).json(insertRes); + }).catch((insertErrRes) => { + const err = common.handleError(insertErrRes, 'Page Settings', 'Page Settings Update Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Page Settings', 'Page Settings Validation Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/models/config.model.js b/backend/models/config.model.js new file mode 100644 index 00000000..78cb9cfb --- /dev/null +++ b/backend/models/config.model.js @@ -0,0 +1,66 @@ +export class CommonSelectedNode { + constructor(options, ln_server_url, macaroon_path, ln_api_password, swap_server_url, boltz_server_url, config_path, rtl_conf_file_path, swap_macaroon_path, boltz_macaroon_path, bitcoind_config_path, channel_backup_path, log_level, log_file, index, ln_node, ln_implementation, user_persona, theme_mode, theme_color, unannounced_channels, fiat_conversion, currency_unit, ln_version, api_version, enable_offers, enable_peerswap) { + this.options = options; + this.ln_server_url = ln_server_url; + this.macaroon_path = macaroon_path; + this.ln_api_password = ln_api_password; + this.swap_server_url = swap_server_url; + this.boltz_server_url = boltz_server_url; + this.config_path = config_path; + this.rtl_conf_file_path = rtl_conf_file_path; + this.swap_macaroon_path = swap_macaroon_path; + this.boltz_macaroon_path = boltz_macaroon_path; + this.bitcoind_config_path = bitcoind_config_path; + this.channel_backup_path = channel_backup_path; + this.log_level = log_level; + this.log_file = log_file; + this.index = index; + this.ln_node = ln_node; + this.ln_implementation = ln_implementation; + this.user_persona = user_persona; + this.theme_mode = theme_mode; + this.theme_color = theme_color; + this.unannounced_channels = unannounced_channels; + this.fiat_conversion = fiat_conversion; + this.currency_unit = currency_unit; + this.ln_version = ln_version; + this.api_version = api_version; + this.enable_offers = enable_offers; + this.enable_peerswap = enable_peerswap; + } +} +export class AuthenticationConfiguration { + constructor(configPath, swapMacaroonPath, boltzMacaroonPath) { + this.configPath = configPath; + this.swapMacaroonPath = swapMacaroonPath; + this.boltzMacaroonPath = boltzMacaroonPath; + } +} +export class NodeSettingsConfiguration { + constructor(userPersona, themeMode, themeColor, unannouncedChannels, fiatConversion, currencyUnit, bitcoindConfigPath, logLevel, lnServerUrl, swapServerUrl, boltzServerUrl, channelBackupPath, enableOffers, enablePeerswap) { + this.userPersona = userPersona; + this.themeMode = themeMode; + this.themeColor = themeColor; + this.unannouncedChannels = unannouncedChannels; + this.fiatConversion = fiatConversion; + this.currencyUnit = currencyUnit; + this.bitcoindConfigPath = bitcoindConfigPath; + this.logLevel = logLevel; + this.lnServerUrl = lnServerUrl; + this.swapServerUrl = swapServerUrl; + this.boltzServerUrl = boltzServerUrl; + this.channelBackupPath = channelBackupPath; + this.enableOffers = enableOffers; + this.enablePeerswap = enablePeerswap; + } +} +export class LogJSONObj { + constructor(level, msg, data, error, fileName, selectedNode) { + this.level = level; + this.msg = msg; + this.data = data; + this.error = error; + this.fileName = fileName; + this.selectedNode = selectedNode; + } +} diff --git a/backend/models/database.model.js b/backend/models/database.model.js new file mode 100644 index 00000000..e9be4ce9 --- /dev/null +++ b/backend/models/database.model.js @@ -0,0 +1,139 @@ +export var OfferFieldsEnum; +(function (OfferFieldsEnum) { + OfferFieldsEnum["BOLT12"] = "bolt12"; + OfferFieldsEnum["AMOUNTMSAT"] = "amountMSat"; + OfferFieldsEnum["TITLE"] = "title"; + OfferFieldsEnum["VENDOR"] = "vendor"; + OfferFieldsEnum["DESCRIPTION"] = "description"; +})(OfferFieldsEnum || (OfferFieldsEnum = {})); +export class Offer { + constructor(bolt12, amountMSat, title, vendor, description, lastUpdatedAt) { + this.bolt12 = bolt12; + this.amountMSat = amountMSat; + this.title = title; + this.vendor = vendor; + this.description = description; + this.lastUpdatedAt = lastUpdatedAt; + } +} +export const validateDocument = (collectionName, documentToValidate) => { + switch (collectionName) { + case CollectionsEnum.OFFERS: + return validateOffer(documentToValidate); + case CollectionsEnum.PAGE_SETTINGS: + return validatePageSettings(documentToValidate); + default: + return ({ isValid: false, error: 'Collection does not exist' }); + } +}; +export const validateOffer = (documentToValidate) => { + if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.BOLT12)) { + return ({ isValid: false, error: 'Bolt12 is mandatory.' }); + } + if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.AMOUNTMSAT)) { + return ({ isValid: false, error: 'Amount mSat is mandatory.' }); + } + if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.TITLE)) { + return ({ isValid: false, error: 'Title is mandatory.' }); + } + if ((typeof documentToValidate[CollectionFieldsEnum.AMOUNTMSAT] !== 'number')) { + return ({ isValid: false, error: 'Amount mSat should be a number.' }); + } + return ({ isValid: true }); +}; +export var SortOrderEnum; +(function (SortOrderEnum) { + SortOrderEnum["ASCENDING"] = "asc"; + SortOrderEnum["DESCENDING"] = "desc"; +})(SortOrderEnum || (SortOrderEnum = {})); +export var PageSettingsFieldsEnum; +(function (PageSettingsFieldsEnum) { + PageSettingsFieldsEnum["PAGE_ID"] = "pageId"; + PageSettingsFieldsEnum["TABLES"] = "tables"; +})(PageSettingsFieldsEnum || (PageSettingsFieldsEnum = {})); +export var TableSettingsFieldsEnum; +(function (TableSettingsFieldsEnum) { + TableSettingsFieldsEnum["TABLE_ID"] = "tableId"; + TableSettingsFieldsEnum["RECORDS_PER_PAGE"] = "recordsPerPage"; + TableSettingsFieldsEnum["SORT_BY"] = "sortBy"; + TableSettingsFieldsEnum["SORT_ORDER"] = "sortOrder"; + TableSettingsFieldsEnum["COLUMN_SELECTION"] = "columnSelection"; + TableSettingsFieldsEnum["COLUMN_SELECTION_SM"] = "columnSelectionSM"; +})(TableSettingsFieldsEnum || (TableSettingsFieldsEnum = {})); +export class TableSetting { + constructor(tableId, recordsPerPage, sortBy, sortOrder, columnSelection) { + this.tableId = tableId; + this.recordsPerPage = recordsPerPage; + this.sortBy = sortBy; + this.sortOrder = sortOrder; + this.columnSelection = columnSelection; + } +} +export class PageSettings { + constructor(pageId, tables) { + this.pageId = pageId; + this.tables = tables; + } +} +export const validatePageSettings = (documentToValidate) => { + let errorMessages = ''; + if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.PAGE_ID)) { + errorMessages = errorMessages + 'Page ID is mandatory.'; + } + if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.TABLES)) { + errorMessages = errorMessages + 'Tables is mandatory.'; + } + const tablesMessages = documentToValidate.tables.reduce((tableAcc, table, tableIdx) => { + let errMsg = ''; + if (!table.hasOwnProperty(CollectionFieldsEnum.TABLE_ID)) { + errMsg = errMsg + 'Table ID is mandatory.'; + } + if (!table.hasOwnProperty(CollectionFieldsEnum.SORT_BY)) { + errMsg = errMsg + 'Sort By is mandatory.'; + } + if (!table.hasOwnProperty(CollectionFieldsEnum.SORT_ORDER)) { + errMsg = errMsg + 'Sort Order is mandatory.'; + } + if (!table.hasOwnProperty(CollectionFieldsEnum.COLUMN_SELECTION_SM)) { + errMsg = errMsg + 'Column Selection (Mobile Resolution) is mandatory.'; + } + if (table[CollectionFieldsEnum.COLUMN_SELECTION_SM].length < 1) { + errMsg = errMsg + 'Column Selection (Mobile Resolution) should have at least 1 field.'; + } + if (table[CollectionFieldsEnum.COLUMN_SELECTION_SM].length > 3) { + errMsg = errMsg + 'Column Selection (Mobile Resolution) should have maximum 3 fields.'; + } + if (!table.hasOwnProperty(CollectionFieldsEnum.COLUMN_SELECTION)) { + errMsg = errMsg + 'Column Selection (Desktop Resolution) is mandatory.'; + } + if (table[CollectionFieldsEnum.COLUMN_SELECTION].length < 2) { + errMsg = errMsg + 'Column Selection (Desktop Resolution) should have at least 2 fields.'; + } + if (errMsg.trim() !== '') { + tableAcc.push({ table: (table.hasOwnProperty(CollectionFieldsEnum.TABLE_ID) ? table[CollectionFieldsEnum.TABLE_ID] : (tableIdx + 1)), message: errMsg }); + } + return tableAcc; + }, []); + if (errorMessages.trim() === '' && tablesMessages.length === 0) { + return ({ isValid: true }); + } + else { + const errObj = { page: (documentToValidate.hasOwnProperty(CollectionFieldsEnum.PAGE_ID) ? documentToValidate[CollectionFieldsEnum.PAGE_ID] : 'Unknown') }; + if (errorMessages.trim() !== '') { + errObj['message'] = errorMessages; + } + if (tablesMessages.length && tablesMessages.length > 0) { + errObj['tables'] = tablesMessages; + } + return ({ isValid: false, error: JSON.stringify(errObj) }); + } +}; +export var CollectionsEnum; +(function (CollectionsEnum) { + CollectionsEnum["OFFERS"] = "Offers"; + CollectionsEnum["PAGE_SETTINGS"] = "PageSettings"; +})(CollectionsEnum || (CollectionsEnum = {})); +export const CollectionFieldsEnum = { ...OfferFieldsEnum, ...PageSettingsFieldsEnum, ...TableSettingsFieldsEnum }; +export const LNDCollection = [CollectionsEnum.PAGE_SETTINGS]; +export const ECLCollection = [CollectionsEnum.PAGE_SETTINGS]; +export const CLNCollection = [CollectionsEnum.PAGE_SETTINGS, CollectionsEnum.OFFERS]; diff --git a/backend/models/ecl.model.js b/backend/models/ecl.model.js new file mode 100644 index 00000000..ed7c2931 --- /dev/null +++ b/backend/models/ecl.model.js @@ -0,0 +1,12 @@ +export var ECLWSEventsEnum; +(function (ECLWSEventsEnum) { + ECLWSEventsEnum["PAY_RECEIVED"] = "payment-received"; + ECLWSEventsEnum["PAY_RELAYED"] = "payment-relayed"; + ECLWSEventsEnum["PAY_SENT"] = "payment-sent"; + ECLWSEventsEnum["PAY_SETTLING_ONCHAIN"] = "payment-settling-onchain"; + ECLWSEventsEnum["PAY_FAILED"] = "payment-failed"; + ECLWSEventsEnum["CHANNEL_OPENED"] = "channel-opened"; + ECLWSEventsEnum["CHANNEL_STATE_CHANGED"] = "channel-state-changed"; + ECLWSEventsEnum["CHANNEL_CLOSED"] = "channel-closed"; + ECLWSEventsEnum["ONION_MESSAGE_RECEIVED"] = "onion-message-received"; +})(ECLWSEventsEnum || (ECLWSEventsEnum = {})); diff --git a/backend/routes/cln/balance.js b/backend/routes/cln/balance.js new file mode 100644 index 00000000..719afb47 --- /dev/null +++ b/backend/routes/cln/balance.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getBalance } from '../../controllers/cln/balance.js'; +const router = Router(); +router.get('/', isAuthenticated, getBalance); +export default router; diff --git a/backend/routes/cln/channels.js b/backend/routes/cln/channels.js new file mode 100644 index 00000000..d4550d77 --- /dev/null +++ b/backend/routes/cln/channels.js @@ -0,0 +1,14 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards, funderUpdatePolicy, listForwardsPaginated } from '../../controllers/cln/channels.js'; +const router = Router(); +router.get('/listChannels', isAuthenticated, listChannels); +router.post('/', isAuthenticated, openChannel); +router.post('/setChannelFee', isAuthenticated, setChannelFee); +router.delete('/:channelId', isAuthenticated, closeChannel); +router.get('/localRemoteBalance', isAuthenticated, getLocalRemoteBalance); +router.get('/listForwards', isAuthenticated, listForwards); +router.get('/listForwardsPaginated', isAuthenticated, listForwardsPaginated); +router.post('/funderUpdate', isAuthenticated, funderUpdatePolicy); +export default router; diff --git a/backend/routes/cln/fees.js b/backend/routes/cln/fees.js new file mode 100644 index 00000000..a1cd8eee --- /dev/null +++ b/backend/routes/cln/fees.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getFees } from '../../controllers/cln/fees.js'; +const router = Router(); +router.get('/', isAuthenticated, getFees); +export default router; diff --git a/backend/routes/cln/getInfo.js b/backend/routes/cln/getInfo.js new file mode 100644 index 00000000..4a9aedea --- /dev/null +++ b/backend/routes/cln/getInfo.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getInfo } from '../../controllers/cln/getInfo.js'; +const router = Router(); +router.get('/', isAuthenticated, getInfo); +export default router; diff --git a/backend/routes/cln/index.js b/backend/routes/cln/index.js new file mode 100644 index 00000000..39865034 --- /dev/null +++ b/backend/routes/cln/index.js @@ -0,0 +1,31 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import infoCLRoutes from './getInfo.js'; +import feesCLRoutes from './fees.js'; +import balanceCLRoutes from './balance.js'; +import channelsCLRoutes from './channels.js'; +import invoicesCLRoutes from './invoices.js'; +import onChainCLRoutes from './onchain.js'; +import paymentsCLRoutes from './payments.js'; +import peersCLRoutes from './peers.js'; +import networkCLRoutes from './network.js'; +import offersCLRoutes from './offers.js'; +import utilityCLRoutes from './utility.js'; +const router = Router(); +const clRoutes = [ + { path: '/getinfo', route: infoCLRoutes }, + { path: '/fees', route: feesCLRoutes }, + { path: '/balance', route: balanceCLRoutes }, + { path: '/channels', route: channelsCLRoutes }, + { path: '/invoices', route: invoicesCLRoutes }, + { path: '/onchain', route: onChainCLRoutes }, + { path: '/payments', route: paymentsCLRoutes }, + { path: '/peers', route: peersCLRoutes }, + { path: '/network', route: networkCLRoutes }, + { path: '/offers', route: offersCLRoutes }, + { path: '/utility', route: utilityCLRoutes } +]; +clRoutes.forEach((route) => { + router.use(route.path, route.route); +}); +export default router; diff --git a/backend/routes/cln/invoices.js b/backend/routes/cln/invoices.js new file mode 100644 index 00000000..3141b4ec --- /dev/null +++ b/backend/routes/cln/invoices.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { listInvoices, addInvoice, deleteExpiredInvoice } from '../../controllers/cln/invoices.js'; +const router = Router(); +router.get('/', isAuthenticated, listInvoices); +router.post('/', isAuthenticated, addInvoice); +router.delete('/', isAuthenticated, deleteExpiredInvoice); +export default router; diff --git a/backend/routes/cln/network.js b/backend/routes/cln/network.js new file mode 100644 index 00000000..7fa20690 --- /dev/null +++ b/backend/routes/cln/network.js @@ -0,0 +1,11 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getRoute, listNode, listChannel, feeRates, listNodes } from '../../controllers/cln/network.js'; +const router = Router(); +router.get('/getRoute/:destPubkey/:amount', isAuthenticated, getRoute); +router.get('/listNode/:id', isAuthenticated, listNode); +router.get('/listChannel/:channelShortId', isAuthenticated, listChannel); +router.get('/feeRates/:feeRateStyle', isAuthenticated, feeRates); +router.get('/listNodes', isAuthenticated, listNodes); +export default router; diff --git a/backend/routes/cln/offers.js b/backend/routes/cln/offers.js new file mode 100644 index 00000000..fe123949 --- /dev/null +++ b/backend/routes/cln/offers.js @@ -0,0 +1,12 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { listOfferBookmarks, deleteOfferBookmark, listOffers, disableOffer, createOffer, fetchOfferInvoice } from '../../controllers/cln/offers.js'; +const router = Router(); +router.get('/offerbookmarks', isAuthenticated, listOfferBookmarks); +router.delete('/offerbookmark/:offerStr', isAuthenticated, deleteOfferBookmark); +router.get('/', isAuthenticated, listOffers); +router.post('/', isAuthenticated, createOffer); +router.post('/fetchOfferInvoice', isAuthenticated, fetchOfferInvoice); +router.delete('/:offerID', isAuthenticated, disableOffer); +export default router; diff --git a/backend/routes/cln/onchain.js b/backend/routes/cln/onchain.js new file mode 100644 index 00000000..f95be2c7 --- /dev/null +++ b/backend/routes/cln/onchain.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getNewAddress, onChainWithdraw, getUTXOs } from '../../controllers/cln/onchain.js'; +const router = Router(); +router.get('/', isAuthenticated, getNewAddress); +router.post('/', isAuthenticated, onChainWithdraw); +router.get('/utxos/', isAuthenticated, getUTXOs); +export default router; diff --git a/backend/routes/cln/payments.js b/backend/routes/cln/payments.js new file mode 100644 index 00000000..6f9c2f42 --- /dev/null +++ b/backend/routes/cln/payments.js @@ -0,0 +1,8 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { listPayments, postPayment } from '../../controllers/cln/payments.js'; +const router = Router(); +router.get('/', isAuthenticated, listPayments); +router.post('/', isAuthenticated, postPayment); +export default router; diff --git a/backend/routes/cln/peers.js b/backend/routes/cln/peers.js new file mode 100644 index 00000000..4ccaeb07 --- /dev/null +++ b/backend/routes/cln/peers.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getPeers, postPeer, deletePeer } from '../../controllers/cln/peers.js'; +const router = Router(); +router.get('/', isAuthenticated, getPeers); +router.post('/', isAuthenticated, postPeer); +router.delete('/:peerId', isAuthenticated, deletePeer); +export default router; diff --git a/backend/routes/cln/utility.js b/backend/routes/cln/utility.js new file mode 100644 index 00000000..1114df54 --- /dev/null +++ b/backend/routes/cln/utility.js @@ -0,0 +1,11 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { decodePayments, decodePayment, signMessage, verifyMessage, listConfigs } from '../../controllers/cln/utility.js'; +const router = Router(); +router.get('/', isAuthenticated, decodePayments); +router.get('/decode/:payReq', isAuthenticated, decodePayment); +router.post('/sign', isAuthenticated, signMessage); +router.post('/verify', isAuthenticated, verifyMessage); +router.get('/listConfigs', isAuthenticated, listConfigs); +export default router; diff --git a/backend/routes/eclair/channels.js b/backend/routes/eclair/channels.js new file mode 100644 index 00000000..f51d8388 --- /dev/null +++ b/backend/routes/eclair/channels.js @@ -0,0 +1,11 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getChannels, getChannelStats, openChannel, updateChannelRelayFee, closeChannel } from '../../controllers/eclair/channels.js'; +const router = Router(); +router.get('/', isAuthenticated, getChannels); +router.get('/stats', isAuthenticated, getChannelStats); +router.post('/', isAuthenticated, openChannel); +router.post('/updateRelayFee', isAuthenticated, updateChannelRelayFee); +router.delete('/', isAuthenticated, closeChannel); +export default router; diff --git a/backend/routes/eclair/fees.js b/backend/routes/eclair/fees.js new file mode 100644 index 00000000..48e3ab08 --- /dev/null +++ b/backend/routes/eclair/fees.js @@ -0,0 +1,8 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getFees, getPayments } from '../../controllers/eclair/fees.js'; +const router = Router(); +router.get('/fees', isAuthenticated, getFees); +router.get('/payments', isAuthenticated, getPayments); +export default router; diff --git a/backend/routes/eclair/getInfo.js b/backend/routes/eclair/getInfo.js new file mode 100644 index 00000000..d542f3e1 --- /dev/null +++ b/backend/routes/eclair/getInfo.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getInfo } from '../../controllers/eclair/getInfo.js'; +const router = Router(); +router.get('/', isAuthenticated, getInfo); +export default router; diff --git a/backend/routes/eclair/index.js b/backend/routes/eclair/index.js new file mode 100644 index 00000000..7fcb4cd0 --- /dev/null +++ b/backend/routes/eclair/index.js @@ -0,0 +1,25 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import infoECLRoutes from './getInfo.js'; +import feesECLRoutes from './fees.js'; +import channelsECLRoutes from './channels.js'; +import onChainECLRoutes from './onchain.js'; +import peersECLRoutes from './peers.js'; +import invoicesECLRoutes from './invoices.js'; +import paymentsECLRoutes from './payments.js'; +import networkECLRoutes from './network.js'; +const router = Router(); +const eclRoutes = [ + { path: '/getinfo', route: infoECLRoutes }, + { path: '/fees', route: feesECLRoutes }, + { path: '/channels', route: channelsECLRoutes }, + { path: '/onchain', route: onChainECLRoutes }, + { path: '/peers', route: peersECLRoutes }, + { path: '/invoices', route: invoicesECLRoutes }, + { path: '/payments', route: paymentsECLRoutes }, + { path: '/network', route: networkECLRoutes } +]; +eclRoutes.forEach((route) => { + router.use(route.path, route.route); +}); +export default router; diff --git a/backend/routes/eclair/invoices.js b/backend/routes/eclair/invoices.js new file mode 100644 index 00000000..1770e6a4 --- /dev/null +++ b/backend/routes/eclair/invoices.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { listInvoices, getInvoice, createInvoice } from '../../controllers/eclair/invoices.js'; +const router = Router(); +router.get('/', isAuthenticated, listInvoices); +router.get('/:paymentHash', isAuthenticated, getInvoice); +router.post('/', isAuthenticated, createInvoice); +export default router; diff --git a/backend/routes/eclair/network.js b/backend/routes/eclair/network.js new file mode 100644 index 00000000..ee6a8dd6 --- /dev/null +++ b/backend/routes/eclair/network.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getNodes } from '../../controllers/eclair/network.js'; +const router = Router(); +router.get('/nodes/:id', isAuthenticated, getNodes); +export default router; diff --git a/backend/routes/eclair/onchain.js b/backend/routes/eclair/onchain.js new file mode 100644 index 00000000..4370cd59 --- /dev/null +++ b/backend/routes/eclair/onchain.js @@ -0,0 +1,10 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getNewAddress, getBalance, getTransactions, sendFunds } from '../../controllers/eclair/onchain.js'; +const router = Router(); +router.get('/', isAuthenticated, getNewAddress); +router.get('/balance/', isAuthenticated, getBalance); +router.get('/transactions/', isAuthenticated, getTransactions); +router.post('/', isAuthenticated, sendFunds); +export default router; diff --git a/backend/routes/eclair/payments.js b/backend/routes/eclair/payments.js new file mode 100644 index 00000000..c25db27e --- /dev/null +++ b/backend/routes/eclair/payments.js @@ -0,0 +1,10 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { queryPaymentRoute, decodePayment, getSentPaymentsInformation, postPayment } from '../../controllers/eclair/payments.js'; +const router = Router(); +router.get('/route/', isAuthenticated, queryPaymentRoute); +router.get('/decode/:invoice', isAuthenticated, decodePayment); +router.post('/getsentinfos', isAuthenticated, getSentPaymentsInformation); +router.post('/', isAuthenticated, postPayment); +export default router; diff --git a/backend/routes/eclair/peers.js b/backend/routes/eclair/peers.js new file mode 100644 index 00000000..7bbcde5e --- /dev/null +++ b/backend/routes/eclair/peers.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getPeers, connectPeer, deletePeer } from '../../controllers/eclair/peers.js'; +const router = Router(); +router.get('/', isAuthenticated, getPeers); +router.post('/', isAuthenticated, connectPeer); +router.delete('/:nodeId', isAuthenticated, deletePeer); +export default router; diff --git a/backend/routes/lnd/balance.js b/backend/routes/lnd/balance.js new file mode 100644 index 00000000..6b8e0eb7 --- /dev/null +++ b/backend/routes/lnd/balance.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getBlockchainBalance } from '../../controllers/lnd/balance.js'; +const router = Router(); +router.get('/', isAuthenticated, getBlockchainBalance); +export default router; diff --git a/backend/routes/lnd/channels.js b/backend/routes/lnd/channels.js new file mode 100644 index 00000000..25bf6461 --- /dev/null +++ b/backend/routes/lnd/channels.js @@ -0,0 +1,13 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getAllChannels, getPendingChannels, getClosedChannels, postChannel, postTransactions, closeChannel, postChanPolicy } from '../../controllers/lnd/channels.js'; +const router = Router(); +router.get('/', isAuthenticated, getAllChannels); +router.get('/pending', isAuthenticated, getPendingChannels); +router.get('/closed', isAuthenticated, getClosedChannels); +router.post('/', isAuthenticated, postChannel); +router.post('/transactions', isAuthenticated, postTransactions); +router.delete('/:channelPoint', isAuthenticated, closeChannel); +router.post('/chanPolicy', isAuthenticated, postChanPolicy); +export default router; diff --git a/backend/routes/lnd/channelsBackup.js b/backend/routes/lnd/channelsBackup.js new file mode 100644 index 00000000..814b4c91 --- /dev/null +++ b/backend/routes/lnd/channelsBackup.js @@ -0,0 +1,10 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getBackup, getRestoreList, postBackupVerify, postRestore } from '../../controllers/lnd/channelsBackup.js'; +const router = Router(); +router.get('/:channelPoint', isAuthenticated, getBackup); +router.get('/restore/list', isAuthenticated, getRestoreList); +router.post('/verify/:channelPoint', isAuthenticated, postBackupVerify); +router.post('/restore/:channelPoint', isAuthenticated, postRestore); +export default router; diff --git a/backend/routes/lnd/fees.js b/backend/routes/lnd/fees.js new file mode 100644 index 00000000..bb90bfa3 --- /dev/null +++ b/backend/routes/lnd/fees.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getFees } from '../../controllers/lnd/fees.js'; +const router = Router(); +router.get('/', isAuthenticated, getFees); +export default router; diff --git a/backend/routes/lnd/getInfo.js b/backend/routes/lnd/getInfo.js new file mode 100644 index 00000000..1c4e45fe --- /dev/null +++ b/backend/routes/lnd/getInfo.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getInfo } from '../../controllers/lnd/getInfo.js'; +const router = Router(); +router.get('/', isAuthenticated, getInfo); +export default router; diff --git a/backend/routes/lnd/graph.js b/backend/routes/lnd/graph.js new file mode 100644 index 00000000..8e452f9a --- /dev/null +++ b/backend/routes/lnd/graph.js @@ -0,0 +1,13 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getDescribeGraph, getGraphInfo, getAliasesForPubkeys, getGraphNode, getGraphEdge, getRemoteFeePolicy, getQueryRoutes } from '../../controllers/lnd/graph.js'; +const router = Router(); +router.get('/', isAuthenticated, getDescribeGraph); +router.get('/info', isAuthenticated, getGraphInfo); +router.get('/nodes', isAuthenticated, getAliasesForPubkeys); +router.get('/node/:pubKey', isAuthenticated, getGraphNode); +router.get('/edge/:chanid', isAuthenticated, getGraphEdge); +router.get('/edge/:chanid/:localPubkey', isAuthenticated, getRemoteFeePolicy); +router.get('/routes/:destPubkey/:amount', isAuthenticated, getQueryRoutes); +export default router; diff --git a/backend/routes/lnd/index.js b/backend/routes/lnd/index.js new file mode 100644 index 00000000..9c517183 --- /dev/null +++ b/backend/routes/lnd/index.js @@ -0,0 +1,37 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import infoRoutes from './getInfo.js'; +import channelsRoutes from './channels.js'; +import channelsBackupRoutes from './channelsBackup.js'; +import peersRoutes from './peers.js'; +import feesRoutes from './fees.js'; +import balanceRoutes from './balance.js'; +import walletRoutes from './wallet.js'; +import graphRoutes from './graph.js'; +import newAddressRoutes from './newAddress.js'; +import transactionsRoutes from './transactions.js'; +import paymentsRoutes from './payments.js'; +import invoiceRoutes from './invoices.js'; +import switchRoutes from './switch.js'; +import messageRoutes from './message.js'; +const router = Router(); +const lndRoutes = [ + { path: '/getinfo', route: infoRoutes }, + { path: '/channels', route: channelsRoutes }, + { path: '/channels/backup', route: channelsBackupRoutes }, + { path: '/peers', route: peersRoutes }, + { path: '/fees', route: feesRoutes }, + { path: '/balance', route: balanceRoutes }, + { path: '/wallet', route: walletRoutes }, + { path: '/network', route: graphRoutes }, + { path: '/newaddress', route: newAddressRoutes }, + { path: '/transactions', route: transactionsRoutes }, + { path: '/payments', route: paymentsRoutes }, + { path: '/invoices', route: invoiceRoutes }, + { path: '/switch', route: switchRoutes }, + { path: '/message', route: messageRoutes } +]; +lndRoutes.forEach((route) => { + router.use(route.path, route.route); +}); +export default router; diff --git a/backend/routes/lnd/invoices.js b/backend/routes/lnd/invoices.js new file mode 100644 index 00000000..eaec4746 --- /dev/null +++ b/backend/routes/lnd/invoices.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { listInvoices, invoiceLookup, addInvoice } from '../../controllers/lnd/invoices.js'; +const router = Router(); +router.get('/', isAuthenticated, listInvoices); +router.get('/lookup/', isAuthenticated, invoiceLookup); +router.post('/', isAuthenticated, addInvoice); +export default router; diff --git a/backend/routes/lnd/message.js b/backend/routes/lnd/message.js new file mode 100644 index 00000000..4bf132d5 --- /dev/null +++ b/backend/routes/lnd/message.js @@ -0,0 +1,8 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { signMessage, verifyMessage } from '../../controllers/lnd/message.js'; +const router = Router(); +router.post('/sign', isAuthenticated, signMessage); +router.post('/verify', isAuthenticated, verifyMessage); +export default router; diff --git a/backend/routes/lnd/newAddress.js b/backend/routes/lnd/newAddress.js new file mode 100644 index 00000000..0f74e083 --- /dev/null +++ b/backend/routes/lnd/newAddress.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getNewAddress } from '../../controllers/lnd/newAddress.js'; +const router = Router(); +router.get('/', isAuthenticated, getNewAddress); +export default router; diff --git a/backend/routes/lnd/payments.js b/backend/routes/lnd/payments.js new file mode 100644 index 00000000..368e2ae7 --- /dev/null +++ b/backend/routes/lnd/payments.js @@ -0,0 +1,11 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { decodePayment, decodePayments, getPayments, getAllLightningTransactions, paymentLookup } from '../../controllers/lnd/payments.js'; +const router = Router(); +router.get('/', isAuthenticated, getPayments); +router.get('/alltransactions', isAuthenticated, getAllLightningTransactions); +router.get('/decode/:payRequest', isAuthenticated, decodePayment); +router.get('/lookup/:paymentHash', isAuthenticated, paymentLookup); +router.post('/', isAuthenticated, decodePayments); +export default router; diff --git a/backend/routes/lnd/peers.js b/backend/routes/lnd/peers.js new file mode 100644 index 00000000..9dfb2b38 --- /dev/null +++ b/backend/routes/lnd/peers.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getPeers, postPeer, deletePeer } from '../../controllers/lnd/peers.js'; +const router = Router(); +router.get('/', isAuthenticated, getPeers); +router.post('/', isAuthenticated, postPeer); +router.delete('/:peerPubKey', isAuthenticated, deletePeer); +export default router; diff --git a/backend/routes/lnd/switch.js b/backend/routes/lnd/switch.js new file mode 100644 index 00000000..c4c60385 --- /dev/null +++ b/backend/routes/lnd/switch.js @@ -0,0 +1,7 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { forwardingHistory } from '../../controllers/lnd/switch.js'; +const router = Router(); +router.post('/', isAuthenticated, forwardingHistory); +export default router; diff --git a/backend/routes/lnd/transactions.js b/backend/routes/lnd/transactions.js new file mode 100644 index 00000000..98c63687 --- /dev/null +++ b/backend/routes/lnd/transactions.js @@ -0,0 +1,8 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getTransactions, postTransactions } from '../../controllers/lnd/transactions.js'; +const router = Router(); +router.get('/', isAuthenticated, getTransactions); +router.post('/', isAuthenticated, postTransactions); +export default router; diff --git a/backend/routes/lnd/wallet.js b/backend/routes/lnd/wallet.js new file mode 100644 index 00000000..f4b09aa4 --- /dev/null +++ b/backend/routes/lnd/wallet.js @@ -0,0 +1,14 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { genSeed, updateSelNodeOptions, getUTXOs, operateWallet, bumpFee, labelTransaction, leaseUTXO, releaseUTXO } from '../../controllers/lnd/wallet.js'; +const router = Router(); +router.get('/genseed/:passphrase?', isAuthenticated, genSeed); +router.get('/updateSelNodeOptions', isAuthenticated, updateSelNodeOptions); +router.get('/getUTXOs', isAuthenticated, getUTXOs); +router.post('/wallet/:operation', isAuthenticated, operateWallet); +router.post('/bumpfee', isAuthenticated, bumpFee); +router.post('/label', isAuthenticated, labelTransaction); +router.post('/lease', isAuthenticated, leaseUTXO); +router.post('/release', isAuthenticated, releaseUTXO); +export default router; diff --git a/backend/routes/shared/RTLConf.js b/backend/routes/shared/RTLConf.js new file mode 100644 index 00000000..13c1860e --- /dev/null +++ b/backend/routes/shared/RTLConf.js @@ -0,0 +1,17 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getRTLConfigInitial, getRTLConfig, updateUISettings, update2FASettings, getConfig, getFile, updateSelectedNode, updateDefaultNode, updateServiceSettings, updateSSO, getCurrencyRates } from '../../controllers/shared/RTLConf.js'; +const router = Router(); +router.get('/rtlconfinit', getRTLConfigInitial); +router.get('/rtlconf', isAuthenticated, getRTLConfig); +router.post('/', isAuthenticated, updateUISettings); +router.post('/update2FA', isAuthenticated, update2FASettings); +router.get('/config/:nodeType', isAuthenticated, getConfig); +router.get('/file', isAuthenticated, getFile); +router.get('/updateSelNode/:currNodeIndex/:prevNodeIndex', updateSelectedNode); +router.post('/updateDefaultNode', updateDefaultNode); +router.post('/updateServiceSettings', updateServiceSettings); +router.post('/updateSSO', updateSSO); +router.get('/rates', getCurrencyRates); +export default router; diff --git a/backend/routes/shared/authenticate.js b/backend/routes/shared/authenticate.js new file mode 100644 index 00000000..34cb73f2 --- /dev/null +++ b/backend/routes/shared/authenticate.js @@ -0,0 +1,9 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { authenticateUser, verifyToken, resetPassword, logoutUser } from '../../controllers/shared/authenticate.js'; +const router = Router(); +router.post('/', authenticateUser); +router.post('/token', verifyToken); +router.post('/reset', resetPassword); +router.get('/logout', logoutUser); +export default router; diff --git a/backend/routes/shared/boltz.js b/backend/routes/shared/boltz.js new file mode 100644 index 00000000..3961d675 --- /dev/null +++ b/backend/routes/shared/boltz.js @@ -0,0 +1,14 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getInfo, getServiceInfo, listSwaps, getSwapInfo, createSwap, createReverseSwap, createChannel, deposit } from '../../controllers/shared/boltz.js'; +const router = Router(); +router.get('/info', isAuthenticated, getInfo); +router.get('/serviceInfo', isAuthenticated, getServiceInfo); +router.get('/listSwaps', isAuthenticated, listSwaps); +router.get('/swapInfo/:swapId', isAuthenticated, getSwapInfo); +router.post('/createSwap', isAuthenticated, createSwap); +router.post('/createReverseSwap', isAuthenticated, createReverseSwap); +router.post('/createChannel', isAuthenticated, createChannel); +router.post('/deposit', isAuthenticated, deposit); +export default router; diff --git a/backend/routes/shared/index.js b/backend/routes/shared/index.js new file mode 100644 index 00000000..1d8f0138 --- /dev/null +++ b/backend/routes/shared/index.js @@ -0,0 +1,19 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import authenticateRoutes from './authenticate.js'; +import boltzRoutes from './boltz.js'; +import loopRoutes from './loop.js'; +import RTLConfRoutes from './RTLConf.js'; +import pageSettingsRoutes from './pageSettings.js'; +const router = Router(); +const sharedRoutes = [ + { path: '/authenticate', route: authenticateRoutes }, + { path: '/boltz', route: boltzRoutes }, + { path: '/loop', route: loopRoutes }, + { path: '/conf', route: RTLConfRoutes }, + { path: '/pagesettings', route: pageSettingsRoutes } +]; +sharedRoutes.forEach((route) => { + router.use(route.path, route.route); +}); +export default router; diff --git a/backend/routes/shared/loop.js b/backend/routes/shared/loop.js new file mode 100644 index 00000000..ddc3ddd3 --- /dev/null +++ b/backend/routes/shared/loop.js @@ -0,0 +1,16 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { loopInTerms, loopInQuote, loopInTermsAndQuotes, loopIn, loopOutTerms, loopOutQuote, loopOutTermsAndQuotes, loopOut, swaps, swap } from '../../controllers/shared/loop.js'; +const router = Router(); +router.get('/in/terms', isAuthenticated, loopInTerms); +router.get('/in/quote/:amount', isAuthenticated, loopInQuote); +router.get('/in/termsAndQuotes', isAuthenticated, loopInTermsAndQuotes); +router.post('/in', isAuthenticated, loopIn); +router.get('/out/terms', isAuthenticated, loopOutTerms); +router.get('/out/quote/:amount', isAuthenticated, loopOutQuote); +router.get('/out/termsAndQuotes', isAuthenticated, loopOutTermsAndQuotes); +router.post('/out', isAuthenticated, loopOut); +router.get('/swaps', isAuthenticated, swaps); +router.get('/swap/:id', isAuthenticated, swap); +export default router; diff --git a/backend/routes/shared/pageSettings.js b/backend/routes/shared/pageSettings.js new file mode 100644 index 00000000..61c47a8c --- /dev/null +++ b/backend/routes/shared/pageSettings.js @@ -0,0 +1,8 @@ +import * as exprs from 'express'; +const { Router } = exprs; +import { isAuthenticated } from '../../utils/authCheck.js'; +import { getPageSettings, savePageSettings } from '../../controllers/shared/pageSettings.js'; +const router = Router(); +router.get('/', isAuthenticated, getPageSettings); +router.post('/', isAuthenticated, savePageSettings); +export default router; diff --git a/backend/utils/app.js b/backend/utils/app.js new file mode 100644 index 00000000..290eb7f8 --- /dev/null +++ b/backend/utils/app.js @@ -0,0 +1,90 @@ +import express from 'express'; +import sessions from 'express-session'; +import cookieParser from 'cookie-parser'; +import bodyParser from 'body-parser'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import CORS from './cors.js'; +import CSRF from './csrf.js'; +import sharedRoutes from '../routes/shared/index.js'; +import lndRoutes from '../routes/lnd/index.js'; +import clnRoutes from '../routes/cln/index.js'; +import eclRoutes from '../routes/eclair/index.js'; +import { Common } from './common.js'; +import { Logger } from './logger.js'; +import { Config } from './config.js'; +import { CLWSClient } from '../controllers/cln/webSocketClient.js'; +import { ECLWSClient } from '../controllers/eclair/webSocketClient.js'; +import { LNDWSClient } from '../controllers/lnd/webSocketClient.js'; +const ONE_DAY = 1000 * 60 * 60 * 24; +export class ExpressApplication { + constructor() { + this.app = express(); + this.logger = Logger; + this.common = Common; + this.config = Config; + this.eclWsClient = ECLWSClient; + this.clWsClient = CLWSClient; + this.lndWsClient = LNDWSClient; + this.directoryName = dirname(fileURLToPath(import.meta.url)); + this.getApp = () => this.app; + this.loadConfiguration = () => { + this.config.setServerConfiguration(); + }; + this.setCORS = () => { CORS.mount(this.app); }; + this.setCSRF = () => { CSRF.mount(this.app); }; + this.setApplicationRoutes = () => { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'App', msg: 'Setting up Application Routes..' }); + this.app.use(this.common.baseHref + '/api', sharedRoutes); + this.app.use(this.common.baseHref + '/api/lnd', lndRoutes); + this.app.use(this.common.baseHref + '/api/cln', clnRoutes); + this.app.use(this.common.baseHref + '/api/ecl', eclRoutes); + this.app.use(this.common.baseHref, express.static(join(this.directoryName, '../..', 'frontend'))); + this.app.use((req, res, next) => { + res.cookie('XSRF-TOKEN', req.csrfToken ? req.csrfToken() : (req.cookies && req.cookies._csrf) ? req.cookies._csrf : ''); // RTL Angular Frontend + res.setHeader('XSRF-TOKEN', req.csrfToken ? req.csrfToken() : (req.cookies && req.cookies._csrf) ? req.cookies._csrf : ''); // RTL Quickpay JQuery + res.sendFile(join(this.directoryName, '../..', 'frontend', 'index.html')); + }); + this.app.use((err, req, res, next) => { + this.handleApplicationErrors(err, res); + next(); + }); + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'App', msg: 'Application Routes Set' }); + }; + this.handleApplicationErrors = (err, res) => { + switch (err.code) { + case 'EACCES': + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'App', msg: 'Server requires elevated privileges' }); + res.status(406).send('Server requires elevated privileges.'); + break; + case 'EADDRINUSE': + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'App', msg: 'Server is already in use' }); + res.status(409).send('Server is already in use.'); + break; + case 'ECONNREFUSED': + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'App', msg: 'Server is down/locked' }); + res.status(401).send('Server is down/locked.'); + break; + case 'EBADCSRFTOKEN': + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'App', msg: 'Invalid CSRF token. Form tempered.' }); + res.status(403).send('Invalid CSRF token, form tempered.'); + break; + default: + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'App', msg: 'DEFUALT ERROR', error: err }); + res.status(400).send(JSON.stringify(err)); + break; + } + }; + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'App', msg: 'Starting Express Application..' }); + this.app.set('trust proxy', true); + this.app.use(sessions({ secret: this.common.secret_key, saveUninitialized: true, cookie: { secure: false, maxAge: ONE_DAY }, resave: false })); + this.app.use(cookieParser(this.common.secret_key)); + this.app.use(bodyParser.json({ limit: '25mb' })); + this.app.use(bodyParser.urlencoded({ extended: false, limit: '25mb' })); + this.loadConfiguration(); + this.setCORS(); + this.setCSRF(); + this.setApplicationRoutes(); + } +} +export default ExpressApplication; diff --git a/backend/utils/authCheck.js b/backend/utils/authCheck.js new file mode 100644 index 00000000..aa3d4e04 --- /dev/null +++ b/backend/utils/authCheck.js @@ -0,0 +1,67 @@ +import jwt from 'jsonwebtoken'; +import csurf from 'csurf/index.js'; +import { Common } from './common.js'; +import { Logger } from './logger.js'; +const common = Common; +const logger = Logger; +const csurfProtection = csurf({ cookie: true }); +export const isAuthenticated = (req, res, next) => { + try { + const token = req.headers.authorization.split(' ')[1]; + jwt.verify(token, common.secret_key); + next(); + } + catch (error) { + const errMsg = 'Authentication Failed! Please Login First!'; + const err = common.handleError({ statusCode: 401, message: 'Authentication Error', error: errMsg }, 'AuthCheck', errMsg, req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + } +}; +export const verifyWSUser = (info, next) => { + const headers = JSON.parse(JSON.stringify(info.req.headers)); + const protocols = !info.req.headers['sec-websocket-protocol'] ? [] : info.req.headers['sec-websocket-protocol'].split(',')?.map((s) => s.trim()); + const jwToken = (protocols && protocols.length > 0) ? protocols[0] : ''; + if (!jwToken || jwToken === '') { + next(false, 401, 'Authentication Failed! Please Login First!'); + } + else { + jwt.verify(jwToken, common.secret_key, (verificationErr) => { + if (verificationErr) { + next(false, 401, 'Authentication Failed! Please Login First!'); + } + else { + try { + let updatedReq = null; + try { + updatedReq = JSON.parse(JSON.stringify(info.req)); + } + catch (err) { + updatedReq = info.req; + } + let cookies = null; + try { + cookies = '{"' + headers.cookie?.replace(/ /g, '')?.replace(/;/g, '","').trim()?.replace(/[=]/g, '":"') + '"}'; + updatedReq['cookies'] = JSON.parse(cookies); + } + catch (err) { + cookies = {}; + updatedReq['cookies'] = JSON.parse(cookies); + logger.log({ selectedNode: common.initSelectedNode, level: 'WARN', fileName: 'AuthCheck', msg: '403 Unable to read CSRF token cookie', data: err }); + } + csurfProtection(updatedReq, null, (err) => { + if (err) { + next(false, 403, 'Invalid CSRF token!'); + } + else { + next(true); + } + }); + } + catch (err) { + logger.log({ selectedNode: common.initSelectedNode, level: 'WARN', fileName: 'AuthCheck', msg: '403 Unable to verify CSRF token', data: err }); + next(true); + } + } + }); + } +}; diff --git a/backend/utils/common.js b/backend/utils/common.js new file mode 100644 index 00000000..4a4b9733 --- /dev/null +++ b/backend/utils/common.js @@ -0,0 +1,522 @@ +import * as fs from 'fs'; +import { join, dirname, isAbsolute, resolve, sep } from 'path'; +import { fileURLToPath } from 'url'; +import * as crypto from 'crypto'; +import request from 'request-promise'; +import { Logger } from './logger.js'; +export class CommonService { + constructor() { + this.logger = Logger; + this.nodes = []; + this.initSelectedNode = null; + this.rtl_conf_file_path = ''; + this.port = 3000; + this.host = null; + this.rtl_pass = ''; + this.flg_allow_password_update = true; + this.rtl_secret2fa = ''; + this.rtl_sso = 0; + this.rtl_cookie_path = ''; + this.logout_redirect_link = ''; + this.cookie_value = ''; + this.api_version = ''; + this.secret_key = crypto.randomBytes(64).toString('hex'); + this.read_dummy_data = false; + this.baseHref = '/rtl'; + this.dummy_data_array_from_file = []; + this.MONTHS = [ + { name: 'JAN', days: 31 }, { name: 'FEB', days: 28 }, { name: 'MAR', days: 31 }, { name: 'APR', days: 30 }, { name: 'MAY', days: 31 }, { name: 'JUN', days: 30 }, + { name: 'JUL', days: 31 }, { name: 'AUG', days: 31 }, { name: 'SEP', days: 30 }, { name: 'OCT', days: 31 }, { name: 'NOV', days: 30 }, { name: 'DEC', days: 31 } + ]; + this.getSwapServerOptions = (req) => { + const swapOptions = { + url: req.session.selectedNode.swap_server_url, + rejectUnauthorized: false, + json: true, + headers: { 'Grpc-Metadata-macaroon': '' } + }; + if (req.session.selectedNode.swap_macaroon_path) { + try { + swapOptions.headers = { 'Grpc-Metadata-macaroon': fs.readFileSync(join(req.session.selectedNode.swap_macaroon_path, 'loop.macaroon')).toString('hex') }; + } + catch (err) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Loop macaroon Error', error: err }); + } + } + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Swap Options', data: swapOptions }); + return swapOptions; + }; + this.getBoltzServerOptions = (req) => { + const boltzOptions = { + url: req.session.selectedNode.boltz_server_url, + rejectUnauthorized: false, + json: true, + headers: { 'Grpc-Metadata-macaroon': '' } + }; + if (req.session.selectedNode.boltz_macaroon_path) { + try { + boltzOptions.headers = { 'Grpc-Metadata-macaroon': fs.readFileSync(join(req.session.selectedNode.boltz_macaroon_path, 'admin.macaroon')).toString('hex') }; + } + catch (err) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Boltz macaroon Error', error: err }); + } + } + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Boltz Options', data: boltzOptions }); + return boltzOptions; + }; + this.getOptions = (req) => { + if (req.session.selectedNode && req.session.selectedNode.options) { + req.session.selectedNode.options.method = (req.session.selectedNode.ln_implementation && req.session.selectedNode.ln_implementation.toUpperCase() !== 'ECL') ? 'GET' : 'POST'; + delete req.session.selectedNode.options.form; + req.session.selectedNode.options.qs = {}; + return req.session.selectedNode.options; + } + return this.handleError({ statusCode: 401, message: 'Session expired after a day\'s inactivity' }, 'Session Expired', 'Session Expiry Error', this.initSelectedNode); + }; + this.updateSelectedNodeOptions = (req) => { + if (!req.session.selectedNode) { + req.session.selectedNode = {}; + } + req.session.selectedNode.options = { + url: '', + rejectUnauthorized: false, + json: true, + form: null + }; + try { + if (req.session.selectedNode && req.session.selectedNode.ln_implementation) { + switch (req.session.selectedNode.ln_implementation.toUpperCase()) { + case 'CLN': + req.session.selectedNode.options.headers = { macaroon: Buffer.from(fs.readFileSync(join(req.session.selectedNode.macaroon_path, 'access.macaroon'))).toString('base64') }; + break; + case 'ECL': + req.session.selectedNode.options.headers = { authorization: 'Basic ' + Buffer.from(':' + req.session.selectedNode.ln_api_password).toString('base64') }; + break; + default: + req.session.selectedNode.options.headers = { 'Grpc-Metadata-macaroon': fs.readFileSync(join(req.session.selectedNode.macaroon_path, 'admin.macaroon')).toString('hex') }; + break; + } + } + if (req.session.selectedNode) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Updated Node Options for ' + req.session.selectedNode.ln_node, data: req.session.selectedNode.options }); + } + return { status: 200, message: 'Updated Successfully' }; + } + catch (err) { + req.session.selectedNode.options = { + url: '', + rejectUnauthorized: false, + json: true, + form: null + }; + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Update Selected Node Options Error', error: err }); + return { status: 502, message: err }; + } + }; + this.setOptions = (req) => { + if (this.nodes[0].options && this.nodes[0].options.headers) { + return; + } + if (this.nodes && this.nodes.length > 0) { + this.nodes.forEach((node) => { + node.options = { + url: '', + rejectUnauthorized: false, + json: true, + form: null + }; + try { + if (node.ln_implementation) { + switch (node.ln_implementation.toUpperCase()) { + case 'CLN': + node.options.headers = { macaroon: Buffer.from(fs.readFileSync(join(node.macaroon_path, 'access.macaroon'))).toString('base64') }; + break; + case 'ECL': + node.options.headers = { authorization: 'Basic ' + Buffer.from(':' + node.ln_api_password).toString('base64') }; + break; + default: + node.options.headers = { 'Grpc-Metadata-macaroon': fs.readFileSync(join(node.macaroon_path, 'admin.macaroon')).toString('hex') }; + break; + } + } + } + catch (err) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Common Set Options Error', error: err }); + node.options = { + url: '', + rejectUnauthorized: false, + json: true, + form: '' + }; + } + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Set Node Options for ' + node.ln_node, data: node.options }); + }); + this.updateSelectedNodeOptions(req); + } + }; + this.findNode = (selNodeIndex) => this.nodes.find((node) => node.index === selNodeIndex); + this.replaceNode = (req, newNode) => { + const foundIndex = this.nodes.findIndex((node) => node.index === req.session.selectedNode.index); + this.nodes.splice(foundIndex, 1, newNode); + req.session.selectedNode = this.findNode(req.session.selectedNode.index); + }; + this.convertTimeToEpoch = (timeToConvert) => Math.floor(timeToConvert.getTime() / 1000); + this.convertTimestampToTime = (num) => { + const myDate = new Date(+num * 1000); + let days = myDate.getDate().toString(); + days = +days < 10 ? '0' + days : days; + let hours = myDate.getHours().toString(); + hours = +hours < 10 ? '0' + hours : hours; + let minutes = myDate.getMinutes().toString(); + minutes = +minutes < 10 ? '0' + minutes : minutes; + let seconds = myDate.getSeconds().toString(); + seconds = +seconds < 10 ? '0' + seconds : seconds; + return days + '/' + this.MONTHS[myDate.getMonth()].name + '/' + myDate.getFullYear() + ' ' + hours + ':' + minutes + ':' + seconds; + }; + this.sortAscByKey = (array, key) => array.sort((a, b) => { + const x = +a[key]; + const y = +b[key]; + return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + }); + this.sortAscByStrKey = (array, key) => array.sort((a, b) => { + const x = a[key] ? a[key].toUpperCase() : ''; + const y = b[key] ? b[key].toUpperCase() : ''; + return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + }); + this.sortDescByKey = (array, key) => { + const temp = array.sort((a, b) => { + const x = +a[key] ? +a[key] : 0; + const y = +b[key] ? +b[key] : 0; + return (x > y) ? -1 : ((x < y) ? 1 : 0); + }); + return temp; + }; + this.sortDescByStrKey = (array, key) => { + const temp = array.sort((a, b) => { + const x = a[key] ? a[key].toUpperCase() : ''; + const y = b[key] ? b[key].toUpperCase() : ''; + return (x > y) ? -1 : ((x < y) ? 1 : 0); + }); + return temp; + }; + this.newestOnTop = (array, key, value) => { + const newlyAddedRecord = array.splice(array.findIndex((item) => item[key] === value), 1); + array?.unshift(newlyAddedRecord[0]); + return array; + }; + this.camelCase = (str) => str?.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (word.toUpperCase()))?.replace(/\s+/g, '')?.replace(/-/g, ' '); + this.titleCase = (str) => { + if (str.indexOf('!\n') > 0 || str.indexOf('.\n') > 0) { + return str.split('\n')?.reduce((accumulator, currentStr) => accumulator + currentStr.charAt(0).toUpperCase() + currentStr.substring(1).toLowerCase() + '\n', ''); + } + else { + if (str.indexOf(' ') > 0) { + return str.split(' ')?.reduce((accumulator, currentStr) => accumulator + currentStr.charAt(0).toUpperCase() + currentStr.substring(1).toLowerCase() + ' ', ''); + } + else { + return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); + } + } + }; + this.handleError = (errRes, fileName, errMsg, selectedNode) => { + const err = JSON.parse(JSON.stringify(errRes)); + if (!selectedNode) { + selectedNode = this.initSelectedNode; + } + switch (selectedNode.ln_implementation) { + case 'LND': + if (err.options && err.options.headers && err.options.headers['Grpc-Metadata-macaroon']) { + delete err.options.headers['Grpc-Metadata-macaroon']; + } + if (err.response && err.response.request && err.response.request.headers && err.response.request.headers['Grpc-Metadata-macaroon']) { + delete err.response.request.headers['Grpc-Metadata-macaroon']; + } + break; + case 'CLN': + if (err.options && err.options.headers && err.options.headers.macaroon) { + delete err.options.headers.macaroon; + } + if (err.response && err.response.request && err.response.request.headers && err.response.request.headers.macaroon) { + delete err.response.request.headers.macaroon; + } + break; + case 'ECL': + if (err.options && err.options.headers && err.options.headers.authorization) { + delete err.options.headers.authorization; + } + if (err.response && err.response.request && err.response.request.headers && err.response.request.headers.authorization) { + delete err.response.request.headers.authorization; + } + break; + default: + if (err.options && err.options.headers) { + delete err.options.headers; + } + break; + } + this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') }); + let newErrorObj = { statusCode: 500, message: '', error: '' }; + if (err.code && err.code === 'ENOENT') { + newErrorObj = { + statusCode: 500, + message: 'No such file or directory ' + (err.path ? err.path : ''), + error: 'No such file or directory ' + (err.path ? err.path : '') + }; + } + else { + newErrorObj = { + statusCode: err.statusCode ? err.statusCode : err.status ? err.status : (err.error && err.error.code && err.error.code === 'ECONNREFUSED') ? 503 : 500, + message: (err.error && err.error.message) ? err.error.message : err.message ? err.message : errMsg, + error: ((err.error && err.error.error && err.error.error.error && typeof err.error.error.error === 'string') ? err.error.error.error : + (err.error && err.error.error && typeof err.error.error === 'string') ? err.error.error : + (err.error && err.error.error && err.error.error.message && typeof err.error.error.message === 'string') ? err.error.error.message : + (err.error && err.error.message && typeof err.error.message === 'string') ? err.error.message : + (err.error && typeof err.error === 'string') ? err.error : + (err.message && typeof err.message === 'string') ? err.message : (typeof err === 'string') ? err : 'Unknown Error') + }; + } + if (selectedNode.ln_implementation === 'ECL' && err.message && err.message.indexOf('Authentication Error') < 0 && err.name && err.name === 'StatusCodeError') { + newErrorObj.statusCode = 500; + } + return newErrorObj; + }; + this.getRequestIP = (req) => ((typeof req.headers['x-forwarded-for'] === 'string' && req.headers['x-forwarded-for'].split(',').shift()) || + req.ip || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null)); + this.getDummyData = (dataKey, lnImplementation) => { + const dummyDataFile = this.rtl_conf_file_path + sep + 'ECLDummyData.log'; + return new Promise((resolve, reject) => { + if (this.dummy_data_array_from_file.length === 0) { + fs.readFile(dummyDataFile, 'utf8', (err, data) => { + if (err) { + if (err.code === 'ENOENT') { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Dummy data file does not exist' }); + } + else { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Getting dummy data failed' }); + } + } + else { + this.dummy_data_array_from_file = data.split('\n'); + resolve(this.filterData(dataKey, lnImplementation)); + } + }); + } + else { + resolve(this.filterData(dataKey, lnImplementation)); + } + }); + }; + this.readCookie = () => { + const exists = fs.existsSync(this.rtl_cookie_path); + if (exists) { + try { + this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8'); + } + catch (err) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while reading cookie: \n' + err }); + throw new Error(err); + } + } + else { + try { + const directoryName = dirname(this.rtl_cookie_path); + this.createDirectory(directoryName); + fs.writeFileSync(this.rtl_cookie_path, crypto.randomBytes(64).toString('hex')); + this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8'); + } + catch (err) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while reading the cookie: \n' + err }); + throw new Error(err); + } + } + }; + this.refreshCookie = () => { + try { + fs.writeFileSync(this.rtl_cookie_path, crypto.randomBytes(64).toString('hex')); + this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8'); + } + catch (err) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Something went wrong while refreshing cookie', error: err }); + throw new Error(err); + } + }; + this.createDirectory = (directoryName) => { + const initDir = isAbsolute(directoryName) ? sep : ''; + directoryName.split(sep)?.reduce((parentDir, childDir) => { + const curDir = resolve(parentDir, childDir); + try { + if (!fs.existsSync(curDir)) { + fs.mkdirSync(curDir); + } + } + catch (err) { + if (err.code !== 'EEXIST') { + if (err.code === 'ENOENT') { + throw new Error(`ENOENT: No such file or directory, mkdir '${directoryName}'. Ensure that the path separator is '${sep}'`); + } + else { + throw err; + } + } + } + return curDir; + }, initDir); + }; + this.replacePasswordWithHash = (multiPassHashed) => { + this.rtl_conf_file_path = process.env.RTL_CONFIG_PATH ? process.env.RTL_CONFIG_PATH : join(dirname(fileURLToPath(import.meta.url)), '../..'); + try { + const RTLConfFile = this.rtl_conf_file_path + sep + 'RTL-Config.json'; + const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); + config.multiPassHashed = multiPassHashed; + delete config.multiPass; + fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8'); + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Please note that, RTL has encrypted the plaintext password into its corresponding hash' }); + return config.multiPassHashed; + } + catch (err) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Password hashing failed', error: err }); + } + }; + this.getAllNodeAllChannelBackup = (node) => { + const channel_backup_file = node.channel_backup_path + sep + 'channel-all.bak'; + const options = { + url: node.ln_server_url + '/v1/channels/backup', + rejectUnauthorized: false, + json: true, + headers: { 'Grpc-Metadata-macaroon': fs.readFileSync(node.macaroon_path + '/admin.macaroon').toString('hex') } + }; + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Getting Channel Backup for Node ' + node.ln_node + '..' }); + request(options).then((body) => { + fs.writeFile(channel_backup_file, JSON.stringify(body), (err) => { + if (err) { + if (node.ln_node) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Error in Channel Backup for Node ' + node.ln_node, error: err }); + } + else { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Error in Channel Backup for File ' + channel_backup_file, error: err }); + } + } + else { + if (node.ln_node) { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Successful in Channel Backup for Node ' + node.ln_node, data: body }); + } + else { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'INFO', fileName: 'Common', msg: 'Successful in Channel Backup for File ' + channel_backup_file, data: body }); + } + } + }); + }, (err) => { + this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Error in Channel Backup for Node ' + node.ln_node, error: err }); + fs.writeFile(channel_backup_file, '', () => { }); + }); + }; + this.isVersionCompatible = (currentVersion, checkVersion) => { + if (currentVersion) { + const versionsArr = currentVersion.trim()?.replace('v', '').split('-')[0].split('.') || []; + const checkVersionsArr = checkVersion.split('.'); + return (+versionsArr[0] > +checkVersionsArr[0]) || + (+versionsArr[0] === +checkVersionsArr[0] && +versionsArr[1] > +checkVersionsArr[1]) || + (+versionsArr[0] === +checkVersionsArr[0] && +versionsArr[1] === +checkVersionsArr[1] && +versionsArr[2] >= +checkVersionsArr[2]); + } + return false; + }; + this.getMonthDays = (selMonth, selYear) => ((selMonth === 1 && selYear % 4 === 0) ? (this.MONTHS[selMonth].days + 1) : this.MONTHS[selMonth].days); + this.logEnvVariables = (req) => { + const selNode = req.session.selectedNode; + if (selNode && selNode.index) { + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'PORT: ' + this.port }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'HOST: ' + this.host }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'SSO: ' + this.rtl_sso }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'DEFAULT NODE INDEX: ' + selNode.index }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'INDEX: ' + selNode.index }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'LN NODE: ' + selNode.ln_node }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'LN IMPLEMENTATION: ' + selNode.ln_implementation }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'FIAT CONVERSION: ' + selNode.fiat_conversion }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'CURRENCY UNIT: ' + selNode.currency_unit }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'LN SERVER URL: ' + selNode.ln_server_url }); + this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'LOGOUT REDIRECT LINK: ' + this.logout_redirect_link + '\r\n' }); + } + }; + this.filterData = (dataKey, lnImplementation) => { + let search_string = ''; + if (lnImplementation === 'ECL') { + switch (dataKey) { + case 'GetInfo': + search_string = 'INFO: GetInfo => Get Info Response: '; + break; + case 'Fees': + search_string = 'INFO: Fees => Fee Response: '; + break; + case 'Payments': + search_string = 'INFO: Fees => Payments Response: '; + break; + case 'Invoices': + search_string = 'INFO: Invoice => Invoices List Received: '; + break; + case 'OnChainBalance': + search_string = 'INFO: Onchain => Balance Received: '; + break; + case 'Peers': + search_string = 'INFO: Peers => Peers with Alias: '; + break; + case 'Channels': + search_string = 'INFO: Channels => Simplified Channels with Alias: '; + break; + default: + search_string = 'Random Line'; + break; + } + } + else if (lnImplementation === 'CLN') { + switch (dataKey) { + case 'GetInfo': + search_string = 'DEBUG: GetInfo => Node Information. '; + break; + case 'Fees': + search_string = 'DEBUG: Fees => Fee Received. '; + break; + case 'Payments': + search_string = 'DEBUG: Payments => Payment List Received: '; + break; + case 'Invoices': + search_string = 'DEBUG: Invoice => Invoices List Received. '; + break; + case 'ChannelBalance': + search_string = 'DEBUG: Channels => Local Remote Balance. '; + break; + case 'Peers': + search_string = 'DEBUG: Peers => Peers with Alias: '; + break; + case 'Channels': + search_string = 'DEBUG: Channels => List Channels: '; + break; + case 'Balance': + search_string = 'DEBUG: Balance => Balance Received. '; + break; + case 'ForwardingHistory': + search_string = 'DEBUG: Channels => Forwarding History Received: '; + break; + case 'UTXOs': + search_string = 'DEBUG: OnChain => List Funds Received. '; + break; + case 'FeeRateperkb': + search_string = 'DEBUG: Network => Network Fee Rates Received for perkb. '; + break; + case 'FeeRateperkw': + search_string = 'DEBUG: Network => Network Fee Rates Received for perkw. '; + break; + default: + search_string = 'Random Line'; + break; + } + } + const foundDataLine = this.dummy_data_array_from_file.find((dataItem) => dataItem.includes(search_string)); + const dataStr = foundDataLine ? foundDataLine.substring((foundDataLine.indexOf(search_string)) + search_string.length) : '{}'; + return JSON.parse(dataStr); + }; + } +} +export const Common = new CommonService(); diff --git a/backend/utils/config.js b/backend/utils/config.js new file mode 100644 index 00000000..a5704c92 --- /dev/null +++ b/backend/utils/config.js @@ -0,0 +1,352 @@ +import * as os from 'os'; +import * as fs from 'fs'; +import { join, dirname, sep } from 'path'; +import { fileURLToPath } from 'url'; +import * as crypto from 'crypto'; +import ini from 'ini'; +import parseHocon from 'hocon-parser'; +import { Common } from './common.js'; +import { Logger } from './logger.js'; +export class ConfigService { + constructor() { + this.platform = os.platform(); + this.hash = crypto.createHash('sha256'); + this.errMsg = ''; + this.directoryName = dirname(fileURLToPath(import.meta.url)); + this.common = Common; + this.logger = Logger; + this.setDefaultConfig = () => { + const homeDir = os.userInfo().homedir; + let macaroonPath = ''; + let configPath = ''; + let channelBackupPath = ''; + switch (this.platform) { + case 'win32': + macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet'; + configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf'; + channelBackupPath = homeDir + '\\backup\\node-1'; + break; + case 'darwin': + macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet'; + configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf'; + channelBackupPath = homeDir + '/backup/node-1'; + break; + case 'linux': + macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet'; + configPath = homeDir + '/.lnd/lnd.conf'; + channelBackupPath = homeDir + '/backup/node-1'; + break; + default: + macaroonPath = ''; + configPath = ''; + channelBackupPath = ''; + break; + } + const configData = { + port: '3000', + defaultNodeIndex: 1, + SSO: { + rtlSSO: 0, + rtlCookiePath: '', + logoutRedirectLink: '' + }, + nodes: [ + { + index: 1, + lnNode: 'Node 1', + lnImplementation: 'LND', + Authentication: { + macaroonPath: macaroonPath, + configPath: configPath + }, + Settings: { + userPersona: 'MERCHANT', + themeMode: 'DAY', + themeColor: 'PURPLE', + channelBackupPath: channelBackupPath, + logLevel: 'ERROR', + lnServerUrl: 'https://127.0.0.1:8080', + fiatConversion: false, + unannouncedChannels: false + } + } + ] + }; + if ((process?.env?.RTL_SSO && +process?.env?.RTL_SSO === 0) || configData.SSO.rtlSSO === 0) { + configData['multiPass'] = 'password'; + } + return configData; + }; + this.normalizePort = (val) => { + const port = parseInt(val, 10); + if (isNaN(port)) { + return val; + } + if (port >= 0) { + return port; + } + return false; + }; + this.updateLogByLevel = () => { + let updateLogFlag = false; + this.common.rtl_conf_file_path = process?.env?.RTL_CONFIG_PATH ? process?.env?.RTL_CONFIG_PATH : join(this.directoryName, '../..'); + try { + const RTLConfFile = this.common.rtl_conf_file_path + sep + 'RTL-Config.json'; + const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); + config.nodes.forEach((node) => { + if (node.Settings.hasOwnProperty('enableLogging')) { + updateLogFlag = true; + node.Settings.logLevel = node.Settings.enableLogging ? 'INFO' : 'ERROR'; + delete node.Settings.enableLogging; + } + }); + if (updateLogFlag) { + fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8'); + } + } + catch (err) { + this.errMsg = this.errMsg + '\nLog level update failed!'; + } + }; + this.validateNodeConfig = (config) => { + if ((process?.env?.RTL_SSO && +process?.env?.RTL_SSO === 0) || (typeof process?.env?.RTL_SSO === 'undefined' && +config.SSO.rtlSSO === 0)) { + if (process?.env?.APP_PASSWORD && process?.env?.APP_PASSWORD.trim() !== '') { + this.common.rtl_pass = this.hash.update(process?.env?.APP_PASSWORD).digest('hex'); + this.common.flg_allow_password_update = false; + } + else if (config.multiPassHashed && config.multiPassHashed !== '') { + this.common.rtl_pass = config.multiPassHashed; + } + else if (config.multiPass && config.multiPass !== '') { + this.common.rtl_pass = this.common.replacePasswordWithHash(this.hash.update(config.multiPass).digest('hex')); + } + else { + this.errMsg = this.errMsg + '\nNode Authentication can be set with multiPass only. Please set multiPass in RTL-Config.json'; + } + this.common.rtl_secret2fa = config.secret2fa; + } + else { + if (process?.env?.APP_PASSWORD && process?.env?.APP_PASSWORD.trim() !== '') { + this.errMsg = this.errMsg + '\nRTL Password cannot be set with SSO. Please set SSO as 0 or remove password.'; + } + } + this.common.port = (process?.env?.PORT) ? this.normalizePort(process?.env?.PORT) : (config.port) ? this.normalizePort(config.port) : 3000; + this.common.host = (process?.env?.HOST) ? process?.env?.HOST : (config.host) ? config.host : null; + if (config.nodes && config.nodes.length > 0) { + config.nodes.forEach((node, idx) => { + this.common.nodes[idx] = {}; + this.common.nodes[idx].index = node.index; + this.common.nodes[idx].ln_node = node.lnNode; + this.common.nodes[idx].ln_implementation = (process?.env?.LN_IMPLEMENTATION) ? process?.env?.LN_IMPLEMENTATION : node.lnImplementation ? node.lnImplementation : 'LND'; + if (this.common.nodes[idx].ln_implementation === 'CLT') { + this.common.nodes[idx].ln_implementation = 'CLN'; + } + if (this.common.nodes[idx].ln_implementation !== 'ECL' && process?.env?.MACAROON_PATH && process?.env?.MACAROON_PATH.trim() !== '') { + this.common.nodes[idx].macaroon_path = process?.env?.MACAROON_PATH; + } + else if (this.common.nodes[idx].ln_implementation !== 'ECL' && node.Authentication && node.Authentication.macaroonPath && node.Authentication.macaroonPath.trim() !== '') { + this.common.nodes[idx].macaroon_path = node.Authentication.macaroonPath; + } + else if (this.common.nodes[idx].ln_implementation !== 'ECL') { + this.errMsg = 'Please set macaroon path for node index ' + node.index + ' in RTL-Config.json!'; + } + if (this.common.nodes[idx].ln_implementation === 'ECL') { + if (process?.env?.LN_API_PASSWORD) { + this.common.nodes[idx].ln_api_password = process?.env?.LN_API_PASSWORD; + } + else if (node.Authentication && node.Authentication.lnApiPassword) { + this.common.nodes[idx].ln_api_password = node.Authentication.lnApiPassword; + } + else { + this.common.nodes[idx].ln_api_password = ''; + } + } + if (process?.env?.CONFIG_PATH) { + this.common.nodes[idx].config_path = process?.env?.CONFIG_PATH; + } + else if (node.Authentication && node.Authentication.configPath) { + this.common.nodes[idx].config_path = node.Authentication.configPath; + } + else { + this.common.nodes[idx].config_path = ''; + } + if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '' && this.common.nodes[idx].config_path !== '') { + try { + const exists = fs.existsSync(this.common.nodes[idx].config_path || ''); + if (exists) { + try { + const configFile = fs.readFileSync((this.common.nodes[idx].config_path || ''), 'utf-8'); + const iniParsed = ini.parse(configFile); + this.common.nodes[idx].ln_api_password = iniParsed['eclair.api.password'] ? iniParsed['eclair.api.password'] : parseHocon(configFile).eclair.api.password; + } + catch (err) { + this.errMsg = this.errMsg + '\nSomething went wrong while reading config file: \n' + err; + } + } + else { + this.errMsg = this.errMsg + '\nInvalid config path: ' + this.common.nodes[idx].config_path; + } + } + catch (err) { + this.errMsg = this.errMsg + '\nUnable to read config file: \n' + err; + } + } + if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '') { + this.errMsg = this.errMsg + '\nPlease set config path Or api password for node index ' + node.index + ' in RTL-Config.json! It is mandatory for Eclair authentication!'; + } + if (process?.env?.LN_SERVER_URL && process?.env?.LN_SERVER_URL.trim() !== '') { + this.common.nodes[idx].ln_server_url = process?.env?.LN_SERVER_URL.endsWith('/v1') ? process?.env?.LN_SERVER_URL.slice(0, -3) : process?.env?.LN_SERVER_URL; + } + else if (process?.env?.LND_SERVER_URL && process?.env?.LND_SERVER_URL.trim() !== '') { + this.common.nodes[idx].ln_server_url = process?.env?.LND_SERVER_URL.endsWith('/v1') ? process?.env?.LND_SERVER_URL.slice(0, -3) : process?.env?.LND_SERVER_URL; + } + else if (node.Settings.lnServerUrl && node.Settings.lnServerUrl.trim() !== '') { + this.common.nodes[idx].ln_server_url = node.Settings.lnServerUrl.endsWith('/v1') ? node.Settings.lnServerUrl.slice(0, -3) : node.Settings.lnServerUrl; + } + else if (node.Settings.lndServerUrl && node.Settings.lndServerUrl.trim() !== '') { + this.common.nodes[idx].ln_server_url = node.Settings.lndServerUrl.endsWith('/v1') ? node.Settings.lndServerUrl.slice(0, -3) : node.Settings.lndServerUrl; + } + else { + this.errMsg = this.errMsg + '\nPlease set LN Server URL for node index ' + node.index + ' in RTL-Config.json!'; + } + this.common.nodes[idx].user_persona = node.Settings.userPersona ? node.Settings.userPersona : 'MERCHANT'; + this.common.nodes[idx].theme_mode = node.Settings.themeMode ? node.Settings.themeMode : 'DAY'; + this.common.nodes[idx].theme_color = node.Settings.themeColor ? node.Settings.themeColor : 'PURPLE'; + this.common.nodes[idx].unannounced_channels = node.Settings.unannouncedChannels ? !!node.Settings.unannouncedChannels : false; + this.common.nodes[idx].log_level = node.Settings.logLevel ? node.Settings.logLevel : 'ERROR'; + this.common.nodes[idx].fiat_conversion = node.Settings.fiatConversion ? !!node.Settings.fiatConversion : false; + if (this.common.nodes[idx].fiat_conversion) { + this.common.nodes[idx].currency_unit = node.Settings.currencyUnit ? node.Settings.currencyUnit : 'USD'; + } + if (process?.env?.SWAP_SERVER_URL && process?.env?.SWAP_SERVER_URL.trim() !== '') { + this.common.nodes[idx].swap_server_url = process?.env?.SWAP_SERVER_URL.endsWith('/v1') ? process?.env?.SWAP_SERVER_URL.slice(0, -3) : process?.env?.SWAP_SERVER_URL; + this.common.nodes[idx].swap_macaroon_path = process?.env?.SWAP_MACAROON_PATH; + } + else if (node.Settings.swapServerUrl && node.Settings.swapServerUrl.trim() !== '') { + this.common.nodes[idx].swap_server_url = node.Settings.swapServerUrl.endsWith('/v1') ? node.Settings.swapServerUrl.slice(0, -3) : node.Settings.swapServerUrl; + this.common.nodes[idx].swap_macaroon_path = node.Authentication.swapMacaroonPath ? node.Authentication.swapMacaroonPath : ''; + } + else { + this.common.nodes[idx].swap_server_url = ''; + this.common.nodes[idx].swap_macaroon_path = ''; + } + if (process?.env?.BOLTZ_SERVER_URL && process?.env?.BOLTZ_SERVER_URL.trim() !== '') { + this.common.nodes[idx].boltz_server_url = process?.env?.BOLTZ_SERVER_URL.endsWith('/v1') ? process?.env?.BOLTZ_SERVER_URL.slice(0, -3) : process?.env?.BOLTZ_SERVER_URL; + this.common.nodes[idx].boltz_macaroon_path = process?.env?.BOLTZ_MACAROON_PATH; + } + else if (node.Settings.boltzServerUrl && node.Settings.boltzServerUrl.trim() !== '') { + this.common.nodes[idx].boltz_server_url = node.Settings.boltzServerUrl.endsWith('/v1') ? node.Settings.boltzServerUrl.slice(0, -3) : node.Settings.boltzServerUrl; + this.common.nodes[idx].boltz_macaroon_path = node.Authentication.boltzMacaroonPath ? node.Authentication.boltzMacaroonPath : ''; + } + else { + this.common.nodes[idx].boltz_server_url = ''; + this.common.nodes[idx].boltz_macaroon_path = ''; + } + this.common.nodes[idx].enable_offers = process?.env?.ENABLE_OFFERS ? process?.env?.ENABLE_OFFERS : (node.Settings.enableOffers) ? node.Settings.enableOffers : false; + this.common.nodes[idx].enable_peerswap = process?.env?.ENABLE_PEERSWAP ? process?.env?.ENABLE_PEERSWAP : (node.Settings.enablePeerswap) ? node.Settings.enablePeerswap : false; + this.common.nodes[idx].bitcoind_config_path = process?.env?.BITCOIND_CONFIG_PATH ? process?.env?.BITCOIND_CONFIG_PATH : (node.Settings.bitcoindConfigPath) ? node.Settings.bitcoindConfigPath : ''; + this.common.nodes[idx].channel_backup_path = process?.env?.CHANNEL_BACKUP_PATH ? process?.env?.CHANNEL_BACKUP_PATH : (node.Settings.channelBackupPath) ? node.Settings.channelBackupPath : this.common.rtl_conf_file_path + sep + 'channels-backup' + sep + 'node-' + node.index; + try { + this.common.createDirectory(this.common.nodes[idx].channel_backup_path); + const exists = fs.existsSync(this.common.nodes[idx].channel_backup_path + sep + 'channel-all.bak'); + if (!exists) { + try { + if (this.common.nodes[idx].ln_implementation === 'LND') { + this.common.getAllNodeAllChannelBackup(this.common.nodes[idx]); + } + else { + const createStream = fs.createWriteStream(this.common.nodes[idx].channel_backup_path + sep + 'channel-all.bak'); + createStream.end(); + } + } + catch (err) { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while creating backup file: \n' + err }); + } + } + } + catch (err) { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while creating the backup directory: \n' + err }); + } + this.common.nodes[idx].log_file = this.common.rtl_conf_file_path + '/logs/RTL-Node-' + node.index + '.log'; + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'Config', msg: 'Node Config: ' + JSON.stringify(this.common.nodes[idx]) }); + const log_file = this.common.nodes[idx].log_file; + if (fs.existsSync(log_file || '')) { + fs.writeFile((log_file || ''), '', () => { }); + } + else { + try { + const directoryName = dirname(log_file || ''); + this.common.createDirectory(directoryName); + const createStream = fs.createWriteStream(log_file || ''); + createStream.end(); + } + catch (err) { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while creating log file ' + log_file + ': \n' + err }); + } + } + }); + } + this.setSSOParams(config); + if (this.errMsg && this.errMsg.trim() !== '') { + throw new Error(this.errMsg); + } + }; + this.setSSOParams = (config) => { + if (process?.env?.RTL_SSO) { + this.common.rtl_sso = +process?.env?.RTL_SSO; + } + else if (config.SSO && config.SSO.rtlSSO) { + this.common.rtl_sso = config.SSO.rtlSSO; + } + if (process?.env?.RTL_COOKIE_PATH) { + this.common.rtl_cookie_path = process?.env?.RTL_COOKIE_PATH; + } + else if (config.SSO && config.SSO.rtlCookiePath) { + this.common.rtl_cookie_path = config.SSO.rtlCookiePath; + } + else { + this.common.rtl_cookie_path = ''; + } + if (process?.env?.LOGOUT_REDIRECT_LINK) { + this.common.logout_redirect_link = process?.env?.LOGOUT_REDIRECT_LINK; + } + else if (config.SSO && config.SSO.logoutRedirectLink) { + this.common.logout_redirect_link = config.SSO.logoutRedirectLink; + } + if (+this.common.rtl_sso) { + if (!this.common.rtl_cookie_path || this.common.rtl_cookie_path.trim() === '') { + this.errMsg = 'Please set rtlCookiePath value for single sign on option!'; + } + else { + this.common.readCookie(); + } + } + }; + this.setSelectedNode = (config) => { + if (config.defaultNodeIndex) { + this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex) || {}; + } + else { + this.common.initSelectedNode = this.common.findNode(this.common.nodes[0].index) || {}; + } + }; + this.setServerConfiguration = () => { + try { + this.common.rtl_conf_file_path = (process?.env?.RTL_CONFIG_PATH) ? process?.env?.RTL_CONFIG_PATH : join(this.directoryName, '../..'); + const confFileFullPath = this.common.rtl_conf_file_path + sep + 'RTL-Config.json'; + if (!fs.existsSync(confFileFullPath)) { + fs.writeFileSync(confFileFullPath, JSON.stringify(this.setDefaultConfig())); + } + const config = JSON.parse(fs.readFileSync(confFileFullPath, 'utf-8')); + this.updateLogByLevel(); + this.validateNodeConfig(config); + this.setSelectedNode(config); + } + catch (err) { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while configuring the node server: \n' + err }); + throw new Error(err); + } + }; + } +} +export const Config = new ConfigService(); diff --git a/backend/utils/cors.js b/backend/utils/cors.js new file mode 100644 index 00000000..4137b13d --- /dev/null +++ b/backend/utils/cors.js @@ -0,0 +1,25 @@ +import { Logger } from './logger.js'; +import { Common } from './common.js'; +class CORS { + constructor() { + this.logger = Logger; + this.common = Common; + } + mount(app) { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'CORS', msg: 'Setting up CORS..' }); + app.use((req, res, next) => { + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, filePath'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS'); + if (process.env.NODE_ENV === 'development') { + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Origin', req.headers.origin ? req.headers.origin : req.headers.host ? req.headers.host : ''); + } + next(); + }); + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'CORS', msg: 'CORS Set' }); + return app; + } + ; +} +export default new CORS; diff --git a/backend/utils/csrf.js b/backend/utils/csrf.js new file mode 100644 index 00000000..bca5ab20 --- /dev/null +++ b/backend/utils/csrf.js @@ -0,0 +1,20 @@ +import csurf from 'csurf/index.js'; +import { Logger } from './logger.js'; +import { Common } from './common.js'; +class CSRF { + constructor() { + this.csrfProtection = csurf({ cookie: true }); + this.logger = Logger; + this.common = Common; + } + mount(app) { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'CSRF', msg: 'Setting up CSRF..' }); + if (process.env.NODE_ENV !== 'development') { + app.use((req, res, next) => this.csrfProtection(req, res, next)); + } + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'CSRF', msg: 'CSRF Set' }); + return app; + } + ; +} +export default new CSRF; diff --git a/backend/utils/database.js b/backend/utils/database.js new file mode 100644 index 00000000..be1ddd00 --- /dev/null +++ b/backend/utils/database.js @@ -0,0 +1,296 @@ +import * as fs from 'fs'; +import { join, dirname, sep } from 'path'; +import { fileURLToPath } from 'url'; +import { Common } from '../utils/common.js'; +import { Logger } from '../utils/logger.js'; +import { validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js'; +export class DatabaseService { + constructor() { + this.common = Common; + this.logger = Logger; + this.dbDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'database'); + this.nodeDatabase = {}; + } + loadDatabase(session) { + const { id, selectedNode } = session; + try { + if (!this.nodeDatabase[selectedNode.index]) { + this.nodeDatabase[selectedNode.index] = { adapter: null, data: {} }; + this.nodeDatabase[selectedNode.index].adapter = new DatabaseAdapter(this.dbDirectory, selectedNode, id); + this.fetchNodeData(selectedNode); + this.logger.log({ selectedNode: selectedNode, level: 'DEBUG', fileName: 'Database', msg: 'Database Loaded', data: this.nodeDatabase[selectedNode.index].data }); + } + else { + this.nodeDatabase[selectedNode.index].adapter.insertSession(id); + } + } + catch (err) { + this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: 'Database', msg: 'Database Load Error', error: err }); + } + } + fetchNodeData(selectedNode) { + switch (selectedNode.ln_implementation) { + case 'CLN': + for (const collectionName in CLNCollection) { + if (CLNCollection.hasOwnProperty(collectionName)) { + this.nodeDatabase[selectedNode.index].data[CLNCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(CLNCollection[collectionName]); + } + } + break; + case 'ECL': + for (const collectionName in ECLCollection) { + if (ECLCollection.hasOwnProperty(collectionName)) { + this.nodeDatabase[selectedNode.index].data[ECLCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(ECLCollection[collectionName]); + } + } + break; + default: + for (const collectionName in LNDCollection) { + if (LNDCollection.hasOwnProperty(collectionName)) { + this.nodeDatabase[selectedNode.index].data[LNDCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(LNDCollection[collectionName]); + } + } + break; + } + } + validateDocument(collectionName, newDocument) { + return new Promise((resolve, reject) => { + const validationRes = validateDocument(collectionName, newDocument); + if (!validationRes.isValid) { + reject(validationRes.error); + } + else { + resolve(true); + } + }); + } + insert(selectedNode, collectionName, newCollection) { + return new Promise((resolve, reject) => { + try { + if (!selectedNode || !selectedNode.index) { + reject(new Error('Selected Node Config Not Found.')); + } + this.nodeDatabase[selectedNode.index].data[collectionName] = newCollection; + this.saveDatabase(selectedNode, collectionName); + resolve(this.nodeDatabase[selectedNode.index].data[collectionName]); + } + catch (errRes) { + reject(errRes); + } + }); + } + update(selectedNode, collectionName, updatedDocument, documentFieldName, documentFieldValue) { + return new Promise((resolve, reject) => { + try { + if (!selectedNode || !selectedNode.index) { + reject(new Error('Selected Node Config Not Found.')); + } + let foundDocIdx = -1; + let foundDoc = null; + if (this.nodeDatabase[selectedNode.index].data[collectionName]) { + foundDocIdx = this.nodeDatabase[selectedNode.index].data[collectionName].findIndex((document) => document[documentFieldName] === documentFieldValue); + foundDoc = foundDocIdx > -1 ? JSON.parse(JSON.stringify(this.nodeDatabase[selectedNode.index].data[collectionName][foundDocIdx])) : null; + } + if (foundDocIdx > -1 && foundDoc) { + for (const docKey in updatedDocument) { + if (Object.prototype.hasOwnProperty.call(updatedDocument, docKey)) { + foundDoc[docKey] = updatedDocument[docKey]; + } + } + updatedDocument = foundDoc; + } + if (foundDocIdx > -1) { + this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument); + } + else { + if (!this.nodeDatabase[selectedNode.index].data[collectionName]) { + this.nodeDatabase[selectedNode.index].data[collectionName] = []; + } + this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument); + } + this.saveDatabase(selectedNode, collectionName); + resolve(updatedDocument); + } + catch (errRes) { + reject(errRes); + } + }); + } + find(selectedNode, collectionName, documentFieldName, documentFieldValue) { + return new Promise((resolve, reject) => { + try { + if (!selectedNode || !selectedNode.index) { + reject(new Error('Selected Node Config Not Found.')); + } + if (documentFieldName && documentFieldValue) { + resolve(this.nodeDatabase[selectedNode.index].data[collectionName].find((document) => document[documentFieldName] === documentFieldValue)); + } + else { + resolve(this.nodeDatabase[selectedNode.index].data[collectionName]); + } + } + catch (errRes) { + reject(errRes); + } + }); + } + remove(selectedNode, collectionName, documentFieldName, documentFieldValue) { + return new Promise((resolve, reject) => { + try { + if (!selectedNode || !selectedNode.index) { + reject(new Error('Selected Node Config Not Found.')); + } + const removeDocIdx = this.nodeDatabase[selectedNode.index].data[collectionName].findIndex((document) => document[documentFieldName] === documentFieldValue); + if (removeDocIdx > -1) { + this.nodeDatabase[selectedNode.index].data[collectionName].splice(removeDocIdx, 1); + } + else { + reject(new Error('Unable to delete, document not found.')); + } + this.saveDatabase(selectedNode, collectionName); + resolve(documentFieldValue); + } + catch (errRes) { + reject(errRes); + } + }); + } + saveDatabase(selectedNode, collectionName) { + const nodeIndex = +selectedNode.index; + try { + if (nodeIndex < 1) { + return true; + } + const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null; + if (!this.nodeDatabase[nodeIndex]) { + this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error: Selected Node Setup Not Found.' }); + throw new Error('Database Save Error: Selected Node Setup Not Found.'); + } + this.nodeDatabase[nodeIndex].adapter.saveData(collectionName, this.nodeDatabase[selectedNode.index].data[collectionName]); + this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database Collection ' + collectionName + ' Saved' }); + return true; + } + catch (err) { + const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null; + this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error', error: err }); + throw err; + } + } + unloadDatabase(nodeIndex, sessionID) { + if (nodeIndex > 0) { + if (this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter) { + this.nodeDatabase[nodeIndex].adapter.removeSession(sessionID); + if (this.nodeDatabase[nodeIndex].adapter.userSessions && this.nodeDatabase[nodeIndex].adapter.userSessions.length <= 0) { + delete this.nodeDatabase[nodeIndex]; + } + } + } + } +} +export class DatabaseAdapter { + constructor(dbDirectoryPath, selNode = null, id = '') { + this.dbDirectoryPath = dbDirectoryPath; + this.selNode = selNode; + this.id = id; + this.logger = Logger; + this.common = Common; + this.dbFilePath = ''; + this.userSessions = []; + this.dbFilePath = dbDirectoryPath + sep + 'node-' + selNode.index; + // For backward compatibility Start + const oldFilePath = dbDirectoryPath + sep + 'rtldb-node-' + selNode.index + '.json'; + if (selNode.ln_implementation === 'CLN' && fs.existsSync(oldFilePath)) { + this.renameOldDB(oldFilePath, selNode); + } + // For backward compatibility End + this.insertSession(id); + } + renameOldDB(oldFilePath, selNode = null) { + const newFilePath = this.dbFilePath + sep + 'rtldb-' + selNode.ln_implementation + '-Offers.json'; + try { + this.common.createDirectory(this.dbFilePath); + const oldOffers = JSON.parse(fs.readFileSync(oldFilePath, 'utf-8')); + fs.writeFileSync(oldFilePath, JSON.stringify(oldOffers.Offers ? oldOffers.Offers : [], null, 2)); + fs.renameSync(oldFilePath, newFilePath); + } + catch (err) { + this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Rename Old Database Error', error: err }); + } + } + fetchData(collectionName) { + try { + if (!fs.existsSync(this.dbFilePath)) { + this.common.createDirectory(this.dbFilePath); + } + } + catch (err) { + throw new Error(err); + } + const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json'; + try { + if (!fs.existsSync(collectionFilePath)) { + fs.writeFileSync(collectionFilePath, '[]'); + } + } + catch (err) { + throw new Error(err); + } + try { + const otherFiles = fs.readdirSync(this.dbFilePath); + otherFiles.forEach((oFileName) => { + let collectionValid = false; + switch (this.selNode.ln_implementation) { + case 'CLN': + collectionValid = CLNCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false); + break; + case 'ECL': + collectionValid = ECLCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false); + break; + default: + collectionValid = LNDCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false); + break; + } + if (oFileName.endsWith('.json') && !collectionValid) { + fs.renameSync(this.dbFilePath + sep + oFileName, this.dbFilePath + sep + oFileName + '.tmp'); + } + }); + } + catch (err) { + this.logger.log({ selectedNode: this.selNode, level: 'ERROR', fileName: 'Database', msg: 'Rename Other Implementation DB Error', error: err }); + } + try { + const dataFromFile = fs.readFileSync(collectionFilePath, 'utf-8'); + const dataObj = !dataFromFile ? null : JSON.parse(dataFromFile); + return dataObj; + } + catch (err) { + throw new Error(err); + } + } + getSelNode() { + return this.selNode; + } + saveData(collectionName, collectionData) { + try { + if (collectionData) { + const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json'; + const tempFile = collectionFilePath + '.tmp'; + fs.writeFileSync(tempFile, JSON.stringify(collectionData, null, 2)); + fs.renameSync(tempFile, collectionFilePath); + } + return true; + } + catch (err) { + throw err; + } + } + insertSession(id = '') { + if (!this.userSessions.includes(id)) { + this.userSessions.push(id); + } + } + removeSession(sessionID = '') { + this.userSessions.splice(this.userSessions.findIndex((sId) => sId === sessionID), 1); + } +} +export const Database = new DatabaseService(); diff --git a/backend/utils/logger.js b/backend/utils/logger.js new file mode 100644 index 00000000..e2fc12c6 --- /dev/null +++ b/backend/utils/logger.js @@ -0,0 +1,81 @@ +/* eslint-disable no-console */ +import * as fs from 'fs'; +export class LoggerService { + constructor() { + this.log = (msgJSON) => { + let msgStr = '[' + new Date().toLocaleString() + '] ' + msgJSON.level + ': ' + msgJSON.fileName + ' => ' + msgJSON.msg; + switch (msgJSON.level) { + case 'ERROR': + if (msgJSON.error) { + msgStr = msgStr + ': ' + ((msgJSON.error.error && msgJSON.error.error.message && typeof msgJSON.error.error.message === 'string') ? + msgJSON.error.error.message : (typeof msgJSON.error === 'object' && msgJSON.error.message && typeof msgJSON.error.message === 'string') ? msgJSON.error.message : (typeof msgJSON.error === 'object' && msgJSON.error.stack && typeof msgJSON.error.stack === 'string') ? + msgJSON.error.stack : (typeof msgJSON.error === 'object') ? JSON.stringify(msgJSON.error) : (typeof msgJSON.error === 'string') ? + msgJSON.error : '') + '\r\n'; + } + else { + msgStr = msgStr + '.\r\n'; + } + console.error(msgStr); + if (msgJSON.selectedNode && msgJSON.selectedNode.log_file) { + fs.appendFile(msgJSON.selectedNode.log_file, msgStr, () => { }); + } + break; + case 'WARN': + msgStr = prepMsgData(msgJSON, msgStr); + if (!msgJSON.selectedNode || msgJSON.selectedNode.log_level === 'WARN' || msgJSON.selectedNode.log_level === 'INFO' || msgJSON.selectedNode.log_level === 'DEBUG') { + if (msgJSON.selectedNode && msgJSON.selectedNode.log_file) { + fs.appendFile(msgJSON.selectedNode.log_file, msgStr, () => { }); + } + } + break; + case 'INFO': + if (!msgJSON.selectedNode && msgJSON.fileName === 'RTL') { + console.log(msgStr + '.\r\n'); + } + else if (msgJSON.selectedNode && msgJSON.selectedNode.log_level === 'INFO') { + msgStr = msgStr + '.\r\n'; + console.log(msgStr); + if (msgJSON.selectedNode.log_file) { + fs.appendFile(msgJSON.selectedNode.log_file, msgStr, () => { }); + } + } + else if (msgJSON.selectedNode && msgJSON.selectedNode.log_level === 'DEBUG') { + msgStr = prepMsgData(msgJSON, msgStr); + console.log(msgStr); + if (msgJSON.selectedNode.log_file) { + fs.appendFile(msgJSON.selectedNode.log_file, msgStr, () => { }); + } + } + break; + case 'DEBUG': + if (!msgJSON.selectedNode) { + console.log(msgStr + '.\r\n'); + } + else if (msgJSON.selectedNode && msgJSON.selectedNode.log_level === 'DEBUG') { + msgStr = prepMsgData(msgJSON, msgStr); + console.log(msgStr); + if (msgJSON.selectedNode.log_file) { + fs.appendFile(msgJSON.selectedNode.log_file, msgStr, () => { }); + } + } + break; + default: + console.log(msgStr); + break; + } + }; + } +} +; +const prepMsgData = (msgJSON, msgStr) => { + if (msgJSON.data) { + msgStr = msgStr + ': ' + (typeof msgJSON.data === 'object' ? (msgJSON.data.message && typeof msgJSON.data.message === 'string') ? + msgJSON.data.message : (msgJSON.data.stack && typeof msgJSON.data.stack === 'string') ? + msgJSON.data.stack : JSON.stringify(msgJSON.data) : (typeof msgJSON.data === 'string') ? msgJSON.data : '') + '\r\n'; + } + else { + msgStr = msgStr + '.\r\n'; + } + return msgStr; +}; +export const Logger = new LoggerService(); diff --git a/backend/utils/webSocketServer.js b/backend/utils/webSocketServer.js new file mode 100644 index 00000000..3fbecab1 --- /dev/null +++ b/backend/utils/webSocketServer.js @@ -0,0 +1,179 @@ +import { parse } from 'cookie'; +import * as cookieParser from 'cookie-parser'; +import * as crypto from 'crypto'; +import { WebSocketServer } from 'ws'; +import { Logger } from './logger.js'; +import { Common } from './common.js'; +import { verifyWSUser } from './authCheck.js'; +import { EventEmitter } from 'events'; +export class RTLWebSocketServer { + constructor() { + this.logger = Logger; + this.common = Common; + this.clientDetails = []; + this.eventEmitterCLN = new EventEmitter(); + this.eventEmitterECL = new EventEmitter(); + this.eventEmitterLND = new EventEmitter(); + this.webSocketServer = null; + this.pingInterval = setInterval(() => { + if (this.webSocketServer.clients.size && this.webSocketServer.clients.size > 0) { + this.webSocketServer.clients.forEach((client) => { + if (client.isAlive === false) { + this.updateLNWSClientDetails(client.sessionId, -1, client.clientNodeIndex); + return client.terminate(); + } + client.isAlive = false; + client.ping(); + }); + } + }, 1000 * 60 * 60); // Terminate broken connections every hour + this.mount = (httpServer) => { + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'WebSocketServer', msg: 'Connecting Websocket Server..' }); + this.webSocketServer = new WebSocketServer({ noServer: true, path: this.common.baseHref + '/api/ws', verifyClient: (process.env.NODE_ENV === 'development') ? null : verifyWSUser }); + httpServer.on('upgrade', (request, socket, head) => { + if (request.headers['upgrade'] !== 'websocket') { + socket.end('HTTP/1.1 400 Bad Request'); + return; + } + const acceptKey = request.headers['sec-websocket-key']; + const hash = this.generateAcceptValue(acceptKey); + const responseHeaders = ['HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + hash]; + const protocols = !request.headers['sec-websocket-protocol'] ? [] : request.headers['sec-websocket-protocol'].split(',')?.map((s) => s.trim()); + if (protocols.includes('json')) { + responseHeaders.push('Sec-WebSocket-Protocol: json'); + } + this.webSocketServer.handleUpgrade(request, socket, head, this.upgradeCallback); + }); + this.webSocketServer.on('connection', this.mountEventsOnConnection); + this.webSocketServer.on('close', () => clearInterval(this.pingInterval)); + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'WebSocketServer', msg: 'Websocket Server Connected' }); + }; + this.upgradeCallback = (websocket, request) => { + this.webSocketServer.emit('connection', websocket, request); + }; + this.mountEventsOnConnection = (websocket, request) => { + const protocols = !request.headers['sec-websocket-protocol'] ? [] : request.headers['sec-websocket-protocol'].split(',')?.map((s) => s.trim()); + const cookies = request.headers.cookie ? parse(request.headers.cookie) : null; + websocket.clientId = Date.now(); + websocket.isAlive = true; + websocket.sessionId = cookies && cookies['connect.sid'] ? cookieParser.signedCookie(cookies['connect.sid'], this.common.secret_key) : null; + websocket.clientNodeIndex = +protocols[1]; + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'WebSocketServer', msg: 'Connected: ' + websocket.clientId + ', Total WS clients: ' + this.webSocketServer.clients.size }); + websocket.on('error', this.sendErrorToAllLNClients); + websocket.on('message', this.sendEventsToAllLNClients); + websocket.on('pong', () => { websocket.isAlive = true; }); + websocket.on('close', (code, reason) => { + this.updateLNWSClientDetails(websocket.sessionId, -1, websocket.clientNodeIndex); + this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'WebSocketServer', msg: 'Disconnected due to ' + code + ' : ' + websocket.clientId + ', Total WS clients: ' + this.webSocketServer.clients.size }); + }); + }; + this.updateLNWSClientDetails = (sessionId, currNodeIndex, prevNodeIndex) => { + if (prevNodeIndex >= 0 && currNodeIndex >= 0) { + this.webSocketServer.clients.forEach((client) => { + if (client.sessionId === sessionId) { + client.clientNodeIndex = currNodeIndex; + } + }); + this.disconnectFromNodeClient(sessionId, prevNodeIndex); + this.connectToNodeClient(sessionId, currNodeIndex); + } + else if (prevNodeIndex >= 0 && currNodeIndex < 0) { + this.disconnectFromNodeClient(sessionId, prevNodeIndex); + } + else if (prevNodeIndex < 0 && currNodeIndex >= 0) { + this.connectToNodeClient(sessionId, currNodeIndex); + } + else { + const selectedNode = this.common.findNode(currNodeIndex); + this.logger.log({ selectedNode: !selectedNode ? this.common.initSelectedNode : selectedNode, level: 'ERROR', fileName: 'WebSocketServer', msg: 'Invalid Node Selection. Previous and current node indices can not be less than zero.' }); + } + }; + this.disconnectFromNodeClient = (sessionId, prevNodeIndex) => { + const foundClient = this.clientDetails.find((clientDetail) => clientDetail.index === +prevNodeIndex); + if (foundClient) { + const foundSessionIdx = foundClient.sessionIds.findIndex((sid) => sid === sessionId); + if (foundSessionIdx > -1) { + foundClient.sessionIds.splice(foundSessionIdx, 1); + } + if (foundClient.sessionIds.length === 0) { + const foundClientIdx = this.clientDetails.findIndex((clientDetail) => clientDetail.index === +prevNodeIndex); + this.clientDetails.splice(foundClientIdx, 1); + const prevSelectedNode = this.common.findNode(prevNodeIndex); + if (prevSelectedNode && prevSelectedNode.ln_implementation) { + switch (prevSelectedNode.ln_implementation) { + case 'LND': + this.eventEmitterLND.emit('DISCONNECT', prevNodeIndex); + break; + case 'CLN': + this.eventEmitterCLN.emit('DISCONNECT', prevNodeIndex); + break; + case 'ECL': + this.eventEmitterECL.emit('DISCONNECT', prevNodeIndex); + break; + default: + break; + } + } + } + } + }; + this.connectToNodeClient = (sessionId, currNodeIndex) => { + let foundClient = this.clientDetails.find((clientDetail) => clientDetail.index === +currNodeIndex); + if (foundClient) { + const foundSessionIdx = foundClient.sessionIds.findIndex((sid) => sid === sessionId); + if (foundSessionIdx < 0) { + foundClient.sessionIds.push(sessionId); + } + } + else { + const currSelectedNode = this.common.findNode(currNodeIndex); + foundClient = { index: currNodeIndex, sessionIds: [sessionId] }; + this.clientDetails.push(foundClient); + if (currSelectedNode && currSelectedNode.ln_implementation) { + switch (currSelectedNode.ln_implementation) { + case 'LND': + this.eventEmitterLND.emit('CONNECT', currNodeIndex); + break; + case 'CLN': + this.eventEmitterCLN.emit('CONNECT', currNodeIndex); + break; + case 'ECL': + this.eventEmitterECL.emit('CONNECT', currNodeIndex); + break; + default: + break; + } + } + } + }; + this.sendErrorToAllLNClients = (serverError, selectedNode) => { + try { + this.webSocketServer.clients.forEach((client) => { + this.logger.log({ selectedNode: !selectedNode ? this.common.initSelectedNode : selectedNode, level: 'ERROR', fileName: 'WebSocketServer', msg: 'Broadcasting error to clients...: ' + serverError }); + if (+client.clientNodeIndex === +selectedNode.index) { + client.send(serverError); + } + }); + } + catch (err) { + this.logger.log({ selectedNode: !selectedNode ? this.common.initSelectedNode : selectedNode, level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while broadcasting message: ' + JSON.stringify(err) }); + } + }; + this.sendEventsToAllLNClients = (newMessage, selectedNode) => { + try { + this.webSocketServer.clients.forEach((client) => { + if (+client.clientNodeIndex === +selectedNode.index) { + this.logger.log({ selectedNode: !selectedNode ? this.common.initSelectedNode : selectedNode, level: 'DEBUG', fileName: 'WebSocketServer', msg: 'Broadcasting message to client...: ' + client.clientId }); + client.send(newMessage); + } + }); + } + catch (err) { + this.logger.log({ selectedNode: !selectedNode ? this.common.initSelectedNode : selectedNode, level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while broadcasting message: ' + JSON.stringify(err) }); + } + }; + this.generateAcceptValue = (acceptKey) => crypto.createHash('sha1').update(acceptKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64'); + this.getClients = () => this.webSocketServer.clients; + } +} +export const WSServer = new RTLWebSocketServer();