Empty Offer Description Save bug fix

Empty Offer Description Save bug fix
Release-0.12.0
Shahana Farooqui 2 years ago
parent dec7ea7dfd
commit dccfc76655

@ -1,10 +1,9 @@
name: Artifact
on:
create:
tags: [ 'v*' ]
push:
branches: [ master, 'Release-*' ]
tags: [ 'v*' ]
release:
types: [released]
# Triggers the workflow only when merging pull request to the branches.
@ -83,5 +82,5 @@ jobs:
- uses: actions/upload-artifact@v2
with:
name: rtlbuild${{ github.event.release.tag_name }}
name: rtl-build-${{ github.event.release.tag_name }}
path: /tmp/rtlbuild.tar.gz

3
.gitignore vendored

@ -1,10 +1,8 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/frontend
/dist
/tmp
/out-tsc
/backend
# dependencies
/node_modules
@ -50,7 +48,6 @@ Thumbs.db
/logs/*
/cookies/*
/backup/*
/database/rtl-db.sqlite
cookies
.env
RTL-Config.json

@ -0,0 +1,31 @@
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Balance', msg: 'Balance Received', data: 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' });
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 });
});
};

@ -0,0 +1,129 @@
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'List Channels', data: 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 Received' });
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: 'DEBUG', fileName: 'Channels', msg: 'Open Channel Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Opened' });
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: 'DEBUG', fileName: 'Channels', msg: 'Update Channel Policy', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Fee Set' });
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: 'DEBUG', fileName: 'Channels', msg: 'Close Channel Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Closed' });
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Local Remote Balance', data: 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 Balances Received' });
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;
request.get(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Forwarding History Response For Status ' + req.query.status, data: body });
if (body && body.length > 0) {
body = common.sortDescByKey(body, 'received_time');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Forwarding History Received For Status' + req.query.status, data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel List Forwards Received For Status ' + req.query.status });
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 });
});
};

@ -0,0 +1,25 @@
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Fee Received', data: body });
if (!body.feeCollected) {
body.feeCollected = 0;
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Fees Received' });
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 });
});
};

@ -0,0 +1,81 @@
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 CLightning 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: 'DEBUG', fileName: 'GetInfo', msg: 'Selected Node', data: req.session.selectedNode.ln_node });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Calling Info from C-Lightning server url', data: options.url });
if (!options.headers || !options.headers.macaroon) {
const errMsg = 'C-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 {
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Node Information', data: 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 {
body.lnImplementation = 'C-Lightning';
const chainObj = { chain: '', network: '' };
if (body.network === 'testnet') {
chainObj.chain = 'Bitcoin';
chainObj.network = 'Testnet';
}
else if (body.network === 'bitcoin') {
chainObj.chain = 'Bitcoin';
chainObj.network = 'Mainnet';
}
else if (body.network === 'signet') {
chainObj.chain = 'Bitcoin';
chainObj.network = 'Signet';
}
else if (body.network === 'litecoin') {
chainObj.chain = 'Litecoin';
chainObj.network = 'Mainnet';
}
else if (body.network === 'litecoin-testnet') {
chainObj.chain = 'Litecoin';
chainObj.network = 'Testnet';
}
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 || '';
clWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'CLightning Node Information Received' });
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 });
});
}
};

@ -0,0 +1,62 @@
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: 'DEBUG', fileName: 'Invoice', msg: 'Invoices Deleted', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Expired Invoices Deleted' });
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 });
if (body.invoices && body.invoices.length > 0) {
body.invoices = common.sortDescByKey(body.invoices, 'expires_at');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Invoices Received' });
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: 'DEBUG', fileName: 'Invoice', msg: 'Add Invoice Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Invoice Created' });
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 });
});
};

@ -0,0 +1,39 @@
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/utility/signMessage';
options.form = { message: req.body.message };
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Messages', msg: 'Message Signed', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Signed' });
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: 'DEBUG', fileName: 'Messages', msg: 'Message Verified', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Verified' });
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 });
});
};

@ -0,0 +1,69 @@
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: 'DEBUG', fileName: 'Network', msg: 'Query Routes Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Network Routes Received' });
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: 'DEBUG', fileName: 'Network', msg: 'Node Lookup', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Node Lookup Finished' });
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: 'DEBUG', fileName: 'Network', msg: 'Channel Lookup', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Channel Lookup Finished' });
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: 'DEBUG', 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 });
});
};

@ -0,0 +1,105 @@
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: 'DEBUG', fileName: 'Offers', msg: 'Offer Bookmarks Received', data: offers });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmarks Received' });
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.destroy(req.session.selectedNode, CollectionsEnum.OFFERS, CollectionFieldsEnum.BOLT12, req.params.offerStr).then((deleteRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Offers', msg: 'Offer Bookmark Deleted', data: deleteRes });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmark Deleted' });
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: 'DEBUG', fileName: 'Offers', msg: 'Offers List Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offers Received' });
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: 'DEBUG', fileName: 'Offer', msg: 'Add Offer Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Created' });
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: 'DEBUG', fileName: 'Offers', msg: 'Offer Invoice Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Invoice Received' });
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: 'DEBUG', fileName: 'Offers', msg: 'Offer Disabled', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Disabled' });
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 });
});
};

@ -0,0 +1,57 @@
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' });
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: 'DEBUG', fileName: 'OnChain', msg: 'OnChain Withdraw Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Withdraw Finished' });
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: 'List 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) => {
if (body.outputs) {
body.outputs = common.sortDescByStrKey(body.outputs, 'status');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'OnChain', msg: 'List Funds 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 });
});
};

@ -0,0 +1,146 @@
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;
}
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
};
}
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 });
if (body && body.payments && body.payments.length > 0) {
body.payments = common.sortDescByKey(body.payments, 'created_at');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'List Payments Received' });
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 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: 'DEBUG', fileName: 'Payments', msg: 'Payment Decode Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Decoded' });
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) => {
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: 'DEBUG', fileName: 'Payments', msg: 'Send Payment Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent' });
if (req.body.paymentType === 'OFFER') {
if (req.body.saveToDB) {
if (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;
}
return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, req.body.bolt12).then((updatedOffer) => {
logger.log({ level: 'DEBUG', fileName: 'Offer', 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 });
});
}
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 });
});
};

@ -0,0 +1,72 @@
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);
}
});
const peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers with Alias', data: peers });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers Received' });
res.status(200).json(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/peer/connect';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Added', data: body });
options.url = req.session.selectedNode.ln_server_url + '/v1/peer/listPeers';
request(options).then((body) => {
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
peers = common.newestOnTop(peers, 'id', req.body.id);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer with Newest On Top', data: peers });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Added Successfully' });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Connected' });
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: 'DEBUG', fileName: 'Peers', msg: 'Detach Peer Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Detached', data: req.params.peerId });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconnected' });
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 });
});
};

@ -0,0 +1,113 @@
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 CLightning\'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 CLightning\'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 CLightning\'s Websocket Server..' });
this.waitTime = 0.5;
};
clWsClt.webSocketClient.onclose = (e) => {
if (clWsClt && clWsClt.selectedNode && clWsClt.selectedNode.ln_implementation === 'CLT') {
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: 'INFO', fileName: 'CLWebSocket', msg: 'Received message from the server..', data: msg.data });
msg = (typeof msg.data === 'string') ? JSON.parse(msg.data) : msg.data;
msg['source'] = 'CLT';
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 CLightning\'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);
const newClient = this.webSocketClients[clientIdx];
newClient.selectedNode = JSON.parse(JSON.stringify(newSelectedNode));
this.webSocketClients[clientIdx] = newClient;
};
this.wsServer.eventEmitterCLT.on('CONNECT', (nodeIndex) => {
this.connect(this.common.findNode(+nodeIndex));
});
this.wsServer.eventEmitterCLT.on('DISCONNECT', (nodeIndex) => {
this.disconnect(this.common.findNode(+nodeIndex));
});
}
}
export const CLWSClient = new CLWebSocketClient();

@ -0,0 +1,156 @@
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 = (lnServerUrl, 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 : '',
channelFlags: channel.data && channel.data.commitments && channel.data.commitments.channelFlags ? channel.data.commitments.channelFlags : 0,
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.shortChannelId ? channel.data.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 = lnServerUrl + '/nodes';
options.form = { nodeIds: channelNodeIds };
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Channels', msg: 'Node Ids to find alias', data: channelNodeIds });
return request.post(options).then((nodes) => {
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Channels', msg: 'Filtered Nodes', 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: 'All Channels', data: body });
if (body && body.length) {
return simplifyAllChannels(req.session.selectedNode.ln_server_url, body).then((simplifiedChannels) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Simplified Channels with Alias', data: simplifiedChannels });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channels List Received' });
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({ activeChannels: [], pendingChannels: [], inactiveChannels: [], lightningBalances: { localBalance: 0, remoteBalance: 0 }, channelStatus: { active: { channels: 0, capacity: 0 }, inactive: { channels: 0, capacity: 0 }, pending: { channels: 0, capacity: 0 } } });
}
}).
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';
options.form = {};
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channel Stats Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel States Received' });
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: 'DEBUG', fileName: 'Channels', msg: 'Open Channel Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Opened' });
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: 'DEBUG', fileName: 'Channels', msg: 'Update Relay Fee Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Relay Fee Updated' });
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, Close Params]', data: [options.url, options.form] });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Close Channel Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Closed' });
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 });
});
};

@ -0,0 +1,125 @@
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 = (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 >= 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: null, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Fee', data: fees });
return fees;
};
export const arrangePayments = (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);
}
if (sentEle.parts && sentEle.parts.length > 0) {
sentEle.firstPartTimestamp = sentEle.parts[0].timestamp;
}
sentEle.parts.forEach((part) => {
if (part.amount) {
part.amount = Math.round(part.amount / 1000);
}
if (part.feesPaid) {
part.feesPaid = Math.round(part.feesPaid / 1000);
}
});
});
payments.received.forEach((receivedEle) => {
if (receivedEle.parts && receivedEle.parts.length > 0) {
receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp;
}
receivedEle.parts.forEach((part) => {
if (part.amount) {
part.amount = Math.round(part.amount / 1000);
}
});
});
payments.relayed.forEach((relayedEle) => {
if (relayedEle.amountIn) {
relayedEle.amountIn = Math.round(relayedEle.amountIn / 1000);
}
if (relayedEle.amountOut) {
relayedEle.amountOut = Math.round(relayedEle.amountOut / 1000);
}
});
payments.sent = common.sortDescByKey(payments.sent, 'firstPartTimestamp');
payments.received = common.sortDescByKey(payments.received, 'firstPartTimestamp');
payments.relayed = common.sortDescByKey(payments.relayed, 'timestamp');
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Payments', 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(data, Math.round((new Date().getTime())))); });
}
else {
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Fee Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Fee Received' });
res.status(200).json(arrangeFees(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';
options.form = null;
if (common.read_dummy_data) {
common.getDummyData('Payments', req.session.selectedNode.ln_implementation).then((data) => { res.status(200).json(arrangePayments(data)); });
}
else {
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Fees', msg: 'Payments Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Fees', msg: 'Payments Received' });
res.status(200).json(arrangePayments(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 });
});
}
};

@ -0,0 +1,51 @@
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: 'DEBUG', fileName: 'GetInfo', msg: 'Selected Node', data: req.session.selectedNode.ln_node });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Calling Info from Eclair server url', data: options.url });
if (common.read_dummy_data) {
common.getDummyData('GetInfo', req.session.selectedNode.ln_implementation).then((data) => {
data.lnImplementation = 'Eclair';
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 {
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Eclair\'s Websocket Server.' });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Get Info Response', data: body });
body.lnImplementation = 'Eclair';
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Eclair Node Information Received' });
req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
eclWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'GetInfo', 'Get Info Error', req);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
}
}
};

@ -0,0 +1,129 @@
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: 'DEBUG', 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 });
}
options.form = {};
const options1 = JSON.parse(JSON.stringify(options));
options1.url = req.session.selectedNode.ln_server_url + '/listinvoices';
options1.form = {};
const options2 = JSON.parse(JSON.stringify(options));
options2.url = req.session.selectedNode.ln_server_url + '/listpendinginvoices';
options2.form = {};
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) => {
body = common.sortDescByKey(invoices, 'expiresAt');
return 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) => {
body = common.sortDescByKey(invoices, 'expiresAt');
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Final Invoices List', data: invoices });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'List Invoices Received' });
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: 'DEBUG', fileName: 'Invoice', msg: 'Create Invoice Response', data: body });
if (body.amount) {
body.amount = Math.round(body.amount / 1000);
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Invoice Created' });
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 });
});
};

@ -0,0 +1,23 @@
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: 'DEBUG', fileName: 'Network', msg: 'Node Lookup', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Node Lookup Finished' });
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 });
});
};

@ -0,0 +1,103 @@
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: 'DEBUG', fileName: 'Onchain', msg: 'New Address Generated', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'New Address Generated' });
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: 'DEBUG', fileName: 'Onchain', msg: 'Balance Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'On Chain Balance Received' });
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: 'DEBUG', fileName: 'OnChain', msg: 'Getting On Chain Transactions Options', data: options.form });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'OnChain', msg: 'Transaction Received', data: body });
if (body && body.length > 0) {
body = common.sortDescByKey(body, 'timestamp');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'On Chain Transaction Received' });
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: 'DEBUG', fileName: 'Onchain', msg: 'Send Funds Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'On Chain Fund Sent' });
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 });
});
};

@ -0,0 +1,129 @@
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 = (lnServerUrl, payment) => {
options.url = lnServerUrl + '/getsentinfo';
options.form = { paymentHash: payment };
return request.post(options).then((body) => {
logger.log({ selectedNode: null, 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 = (lnServerUrl, nodeIds) => {
options.url = lnServerUrl + '/nodes';
options.form = { nodeIds: nodeIds };
return request.post(options).then((nodes) => {
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Payments', msg: 'Query Nodes', 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: 'DEBUG', fileName: 'Payments', msg: 'Payment Decode Received', data: body });
if (body.amount) {
body.amount = Math.round(body.amount / 1000);
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Decoded' });
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: 'DEBUG', fileName: 'Payments', msg: 'Send Payment Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Invoice Paid' });
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.length) {
const queryRoutes = [];
return getQueryNodes(req.session.selectedNode.ln_server_url, body).then((hopsWithAlias) => {
let foundPeer = null;
body.map((hop) => {
foundPeer = hopsWithAlias.find((hopWithAlias) => hop === hopWithAlias.nodeId);
queryRoutes.push({ nodeId: hop, alias: foundPeer ? foundPeer.alias : '' });
return hop;
});
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Query Routes with Alias', data: queryRoutes });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Route Information Received' });
res.status(200).json(queryRoutes);
});
}
else {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Empty Payment Route Information Received' });
res.status(200).json([]);
}
}).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.ln_server_url, payment))).
then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment Sent Informations', data: values });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sent Payment Information Received' });
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([]);
}
};

@ -0,0 +1,135 @@
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 = (lnServerUrl, peersNodeIds) => {
options.url = lnServerUrl + '/nodes';
options.form = { nodeIds: peersNodeIds };
return request.post(options).then((nodes) => {
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Peers', msg: 'Filtered Nodes', 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 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.ln_server_url, 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;
});
body = common.sortDescByStrKey(body, 'alias');
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers with Alias', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers Received' });
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: 'Add Peer Response', 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 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.ln_server_url, 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;
});
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
peers = common.newestOnTop(peers, '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: 'DEBUG', fileName: 'Peers', msg: 'Peer with Newest On Top', data: peers });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Added Successfully' });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Connected' });
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: 'DEBUG', fileName: 'Peers', msg: 'Disconnect Peer Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Disconnected: ' + req.params.nodeId });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconnected' });
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 });
});
};

@ -0,0 +1,112 @@
import WebSocket from 'ws';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { WSServer } from '../../utils/webSocketServer.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;
};
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: 'INFO', fileName: 'ECLWebSocket', msg: 'Received message from the server..', data: msg.data });
msg = (typeof msg.data === 'string') ? JSON.parse(msg.data) : msg.data;
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);
const newClient = this.webSocketClients[clientIdx];
newClient.selectedNode = JSON.parse(JSON.stringify(newSelectedNode));
this.webSocketClients[clientIdx] = newClient;
};
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();

@ -0,0 +1,34 @@
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;
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Balance', msg: '[Request params, Request Query, Balance Received]', data: [req.params, req.query, 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' });
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 });
});
};

@ -0,0 +1,262 @@
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 = (lnServerUrl, channel) => {
const pubkey = (channel.remote_pubkey) ? channel.remote_pubkey : (channel.remote_node_pub) ? channel.remote_node_pub : '';
options.url = lnServerUrl + '/v1/graph/node/' + pubkey;
return request(options).then((aliasBody) => {
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Channels', msg: 'Alias', data: aliasBody.node.alias });
channel.remote_alias = aliasBody.node.alias;
return aliasBody.node.alias;
}).catch((err) => {
channel.remote_alias = pubkey.slice(0, 10) + '...' + pubkey.slice(-10);
return pubkey;
});
};
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: 'All Channels 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.ln_server_url, channel);
})).then((values) => {
body.channels = common.sortDescByKey(body.channels, 'balancedness');
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'All Channels with Alias', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channels Received' });
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 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.ln_server_url, 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.ln_server_url, 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.ln_server_url, 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.ln_server_url, channel.channel)));
}
return Promise.all(promises).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Pending Channels', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Pending Channels Received' });
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.ln_server_url, channel);
})).then((values) => {
body.channels = common.sortDescByKey(body.channels, 'close_height');
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Closed Channels', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels Received' });
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: 'DEBUG', fileName: 'Channels', msg: 'Channel Open Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channels Opened' });
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/transactions';
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Response', data: 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' });
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);
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);
options.form = JSON.stringify({
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) }
});
}
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: 'DEBUG', fileName: 'Channels', msg: 'Update Channel Policy', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Policy Updated' });
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 });
});
};

@ -0,0 +1,240 @@
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', 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 Backup Finished' });
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: 'DEBUG', fileName: 'ChannelBackup', msg: 'Channel Backup Verify', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'ChannelBackup', msg: 'Channel Backup Verified' });
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 Backup Restore', 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' });
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);
}
});
};

@ -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: 'DEBUG', 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, (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' });
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 });
});
};

@ -0,0 +1,57 @@
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: 'DEBUG', fileName: 'GetInfo', msg: 'Selected Node', data: req.session.selectedNode.ln_node });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Calling Info from LND server url', data: 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;
});
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Node Information', data: 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: 'INFO', fileName: 'GetInfo', msg: 'LND Node Information Received' });
req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
lndWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
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 });
});
}
};

@ -0,0 +1,169 @@
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 = (lnServerUrl, pubkey) => {
options.url = lnServerUrl + '/v1/graph/node/' + pubkey;
return request(options).then((res) => {
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Graph', msg: 'Alias', data: res.node.alias });
return res.node.alias;
}).
catch((err) => pubkey.substring(0, 17) + '...');
};
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: 'DEBUG', fileName: 'Graph', msg: 'Describe Graph Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Network Graph Received' });
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: 'DEBUG', fileName: 'Graph', msg: 'Network Information After Rounding and Conversion', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Information Received' });
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: 'DEBUG', fileName: 'Graph', msg: 'Node Info Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Node Information Received' });
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: 'DEBUG', fileName: 'Graph', msg: 'Edge Info Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Edge Information Received' });
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.ln_server_url, 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: 'DEBUG', fileName: 'Graph', msg: 'Hops with Alias', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Routes Received' });
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' });
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' });
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.ln_server_url, pubkey))).
then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', 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([]);
}
};

@ -0,0 +1,93 @@
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 getInvoice = (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 + '/v1/invoice/' + req.params.rHashStr;
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoice Info Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Information Received' });
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(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: '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;
});
body.invoices = common.sortDescByKey(body.invoices, 'creation_date');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoices List Received' });
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 = {
memo: req.body.memo,
private: req.body.private,
expiry: req.body.expiry
};
if (req.body.amount > 0 && req.body.amount < 1) {
options.form.value_msat = req.body.amount * 1000;
}
else {
options.form.value = req.body.amount;
}
options.form = JSON.stringify(options.form);
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Add Invoice Responce', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Added' });
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 });
});
};

@ -0,0 +1,45 @@
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: 'DEBUG', fileName: 'Messages', msg: 'Message Signed', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Signed' });
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: 'DEBUG', fileName: 'Messages', msg: 'Message Verified', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Message', msg: 'Message Verified' });
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 });
});
};

@ -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 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: 'DEBUG', fileName: 'NewAddress', msg: 'New Address Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'NewAddress', msg: 'New Address Received' });
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 });
});
};

@ -0,0 +1,89 @@
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 = (lnServerUrl, payment) => {
options.url = lnServerUrl + '/v1/payreq/' + payment;
return request(options).then((res) => {
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'PayReq', msg: 'Description', 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: 'DEBUG', fileName: 'PayReq', msg: 'Payment Decode Received', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'PayRequest', msg: 'Payment Decoded' });
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.ln_server_url, payment))).
then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'PayReq', msg: 'Decoded Payments', data: values });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'PayRequest', msg: 'Payment List Decoded' });
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 });
if (body.payments && body.payments.length > 0) {
body.payments = common.sortDescByKey(body.payments, 'creation_date');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payments After Sort', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payments List Received' });
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payments & Invoices Received' });
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 });
});
};

@ -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 getAliasForPeers = (lnServerUrl, peer) => {
options.url = lnServerUrl + '/v1/graph/node/' + peer.pub_key;
return request(options).then((aliasBody) => {
logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'Peers', msg: 'Alias', data: aliasBody.node.alias });
peer.alias = aliasBody.node.alias;
return aliasBody.node.alias;
}).catch((err) => {
peer.alias = peer.pub_key.slice(0, 10) + '...' + peer.pub_key.slice(-10);
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 Received', data: body });
const peers = !body.peers ? [] : body.peers;
return Promise.all(peers.map((peer) => getAliasForPeers(req.session.selectedNode.ln_server_url, peer))).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers with Alias before Sort', data: body });
if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers with Alias after Sort', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers Received' });
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 Added', 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.ln_server_url, peer))).then((values) => {
if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer with Alias', data: body });
body.peers = common.newestOnTop(body.peers, 'pub_key', req.body.pubkey);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer with Newest On Top', data: body });
}
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Added Successfully' });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Connected' });
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;
request.delete(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Detach Peer Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Detached', data: req.params.peerPubKey });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconnected' });
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 });
});
};

@ -0,0 +1,61 @@
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 responseData = { 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, (eventsResponse) => {
if (eventsResponse.error) {
res.status(eventsResponse.error.statusCode).json(eventsResponse);
}
else {
res.status(201).json(eventsResponse);
}
});
};
export const getAllForwardingEvents = (req, start, end, offset, callback) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Switch', msg: 'Getting Forwarding Events..' });
if (offset === 0) {
responseData = { 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 History Params', data: options.form });
return request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Switch', msg: 'Forwarding History', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Switch', msg: 'Forwarding Events Received' });
if (body.forwarding_events) {
responseData.forwarding_events.push(...body.forwarding_events);
}
if (!body.last_offset_index || body.last_offset_index < offset + num_max_events) {
responseData.last_offset_index = body.last_offset_index ? body.last_offset_index : 0;
if (responseData.forwarding_events) {
responseData.forwarding_events = common.sortDescByKey(responseData.forwarding_events, 'timestamp');
}
return callback(responseData);
}
else {
return getAllForwardingEvents(req, start, end, offset + num_max_events, 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 });
});
};

@ -0,0 +1,51 @@
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: 'Transaction Received', data: body });
if (body.transactions && body.transactions.length > 0) {
body.transactions = common.sortDescByKey(body.transactions, 'time_stamp');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Transactions', msg: 'Transactions Received' });
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: 'DEBUG', fileName: 'Transactions', msg: 'Transaction Post Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Transactions', msg: 'Transaction Sent' });
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 });
});
};

@ -0,0 +1,219 @@
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' });
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Wallet', msg: 'Wallet Response', data: 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' });
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: 'DEBUG', fileName: 'Wallet', msg: 'UTXO List Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'UTXOs Received' });
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: 'DEBUG', fileName: 'Wallet', msg: 'Bump Fee Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Fee Bumped' });
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: 'DEBUG', fileName: 'Wallet', msg: 'Label Transaction Post Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'Transaction Labelled' });
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: 'DEBUG', fileName: 'Wallet', msg: 'UTXO Lease Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'UTXO Leased' });
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: 'DEBUG', fileName: 'Wallet', msg: 'UTXO Release Response', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Wallet', msg: 'UTXO Released' });
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 });
});
};

@ -0,0 +1,103 @@
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: 'DEBUG', fileName: 'WebSocketClient', msg: 'Pending 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: 'DEBUG', 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.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);
const newClient = this.webSocketClients[clientIdx];
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();

@ -0,0 +1,374 @@
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.body.currNodeIndex ? req.body.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.body.prevNodeIndex);
if (req.body.prevNodeIndex !== -1) {
databaseService.unloadDatabase(req.body.prevNodeIndex);
}
}
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', data: 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 = {};
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.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: {}
});
});
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Initial RTL Configuration Received' });
res.status(200).json({ 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 });
}
});
};
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 = {};
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.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.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
});
});
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'RTL Configuration Received' });
res.status(200).json({ 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 });
}
});
};
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.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.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: 'DEBUG', fileName: 'RTLConf', msg: 'Updating Node Settings Succesful!' });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'UI Settings Updated' });
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'));
config.secret2fa = req.body.secret2fa;
const message = req.body.secret2fa.trim() === '' ? 'Two factor authentication disabled sucessfully.' : 'Two factor authentication enabled sucessfully.';
try {
fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8');
common.rtl_secret2fa = config.secret2fa;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'RTLConf', msg: message });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: '2FA Updated' });
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: 'DEBUG', fileName: 'RTLConf', msg: 'Updating Default Node Succesful!' });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Default Node Updated' });
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, File Path]', data: [req.params.nodeType, 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';
jsonConfig = ini.parse(data);
if (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);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Configuration Data Received' });
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, File Path]', data: [req.query.channel, 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: 'DEBUG', fileName: 'RTLConf', msg: 'File Data', data: data });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'File Data Received' });
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' });
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: 'DEBUG', fileName: 'RTLConf', msg: 'Updating SSO Succesful!' });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'SSO Setting Updated' });
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.find((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;
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: 'DEBUG', fileName: 'RTLConf', msg: 'Updating Service Settings Succesful!' });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Service Settings Updated' });
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'))) {
obj[keys[i]] = '********************';
}
}
}
return obj;
};

@ -0,0 +1,132 @@
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') {
const cookieValue = common.readCookie();
if (cookieValue.trim().length >= 32 && crypto.timingSafeEqual(Buffer.from(crypto.createHash('sha256').update(cookieValue).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.destroy();
res.status(200).json({ loggedout: true });
};

@ -0,0 +1,171 @@
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' });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Boltz Get Info', 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: 'Service Information Received' });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Boltz Get Service Info', 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: 'DEBUG', fileName: 'Boltz', msg: 'Boltz List Swaps Info', 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();
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'List Swaps Received' });
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..' });
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: 'DEBUG', fileName: 'Boltz', msg: 'Boltz Swap Info', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Swap Received' });
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 Body', data: options.body });
request.post(options).then(createSwapRes => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Create Swap Response', data: createSwapRes });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Swap Created' });
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: 'DEBUG', fileName: 'Boltz', msg: 'Create Reverse Swap Response', data: createReverseSwapRes });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Reverse Swap Created' });
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 Body', data: options.body });
request.post(options).then(createChannelRes => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Create Channel Response', data: createChannelRes });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz Channel Created' });
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: 'DEBUG', fileName: 'Boltz', msg: 'Deposit Response', data: depositRes });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Boltz Deposit Finished' });
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 });
});
};

@ -0,0 +1,255 @@
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: 'DEBUG', fileName: 'Loop', msg: 'Loop Out', data: loopOutRes });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Out Finished' });
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: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Terms', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Out Terms Received' });
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Quote', data: 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 Quotes Received' });
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', 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: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Quotes 1', data: values[0] });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Out Quotes 2', data: values[1] });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop Out Terms & Quotes Received' });
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: 'DEBUG', fileName: 'Loop', msg: 'Loop In Body', data: options.body });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop In', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Finished' });
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: 'DEBUG', fileName: 'Loop', msg: 'Loop In Terms', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Terms Received' });
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop In Quote', data: 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' });
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', 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: 'DEBUG', fileName: 'Loop', msg: 'Loop In Quotes 1', data: values[0] });
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop In Quotes 2', data: values[1] });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Loop In Terms & Qoutes Received' });
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', data: body });
if (body.swaps && body.swaps.length > 0) {
body.swaps = common.sortDescByKey(body.swaps, 'initiation_time');
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Swaps after Sort', data: body });
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'List Swaps Received' });
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: 'DEBUG', fileName: 'Loop', msg: 'Loop Swap', data: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Swap Information Received' });
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 });
});
};

@ -0,0 +1,62 @@
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, fiat_conversion, currency_unit, ln_version, api_version, enable_offers) {
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.fiat_conversion = fiat_conversion;
this.currency_unit = currency_unit;
this.ln_version = ln_version;
this.api_version = api_version;
this.enable_offers = enable_offers;
}
}
export class AuthenticationConfiguration {
constructor(configPath, swapMacaroonPath, boltzMacaroonPath) {
this.configPath = configPath;
this.swapMacaroonPath = swapMacaroonPath;
this.boltzMacaroonPath = boltzMacaroonPath;
}
}
export class NodeSettingsConfiguration {
constructor(userPersona, themeMode, themeColor, fiatConversion, currencyUnit, bitcoindConfigPath, logLevel, lnServerUrl, swapServerUrl, boltzServerUrl, channelBackupPath, enableOffers) {
this.userPersona = userPersona;
this.themeMode = themeMode;
this.themeColor = themeColor;
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;
}
}
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;
}
}

@ -0,0 +1,38 @@
export var CollectionsEnum;
(function (CollectionsEnum) {
CollectionsEnum["OFFERS"] = "Offers";
})(CollectionsEnum || (CollectionsEnum = {}));
export var OfferFieldsEnum;
(function (OfferFieldsEnum) {
OfferFieldsEnum["BOLT12"] = "bolt12";
OfferFieldsEnum["AMOUNTMSAT"] = "amountmSat";
OfferFieldsEnum["TITLE"] = "title";
OfferFieldsEnum["VENDOR"] = "vendor";
OfferFieldsEnum["DESCRIPTION"] = "description";
})(OfferFieldsEnum || (OfferFieldsEnum = {}));
export const CollectionFieldsEnum = Object.assign({}, 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 validateOffer = (documentToValidate) => {
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.BOLT12)) {
return ({ isValid: false, error: CollectionFieldsEnum.BOLT12 + 'is mandatory.' });
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.AMOUNTMSAT)) {
return ({ isValid: false, error: CollectionFieldsEnum.AMOUNTMSAT + 'is mandatory.' });
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.TITLE)) {
return ({ isValid: false, error: CollectionFieldsEnum.TITLE + 'is mandatory.' });
}
if ((typeof documentToValidate[CollectionFieldsEnum.AMOUNTMSAT] !== 'number')) {
return ({ isValid: false, error: CollectionFieldsEnum.AMOUNTMSAT + 'should be a number.' });
}
return ({ isValid: true });
};

@ -0,0 +1,7 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getBalance } from '../../controllers/c-lightning/balance.js';
const router = Router();
router.get('/', isAuthenticated, getBalance);
export default router;

@ -0,0 +1,12 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards } from '../../controllers/c-lightning/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);
export default router;

@ -0,0 +1,7 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getFees } from '../../controllers/c-lightning/fees.js';
const router = Router();
router.get('/', isAuthenticated, getFees);
export default router;

@ -0,0 +1,7 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getInfo } from '../../controllers/c-lightning/getInfo.js';
const router = Router();
router.get('/', isAuthenticated, getInfo);
export default router;

@ -0,0 +1,31 @@
import 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 messageCLRoutes from './message.js';
import offersCLRoutes from './offers.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: '/message', route: messageCLRoutes },
{ path: '/offers', route: offersCLRoutes }
];
clRoutes.forEach((route) => {
router.use(route.path, route.route);
});
export default router;

@ -0,0 +1,9 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { listInvoices, addInvoice, deleteExpiredInvoice } from '../../controllers/c-lightning/invoices.js';
const router = Router();
router.get('/', isAuthenticated, listInvoices);
router.post('/', isAuthenticated, addInvoice);
router.delete('/', isAuthenticated, deleteExpiredInvoice);
export default router;

@ -0,0 +1,8 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { signMessage, verifyMessage } from '../../controllers/c-lightning/message.js';
const router = Router();
router.post('/sign', isAuthenticated, signMessage);
router.post('/verify', isAuthenticated, verifyMessage);
export default router;

@ -0,0 +1,10 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getRoute, listNode, listChannel, feeRates } from '../../controllers/c-lightning/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);
export default router;

@ -0,0 +1,12 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { listOfferBookmarks, deleteOfferBookmark, listOffers, disableOffer, createOffer, fetchOfferInvoice } from '../../controllers/c-lightning/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;

@ -0,0 +1,9 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getNewAddress, onChainWithdraw, getUTXOs } from '../../controllers/c-lightning/onchain.js';
const router = Router();
router.get('/', isAuthenticated, getNewAddress);
router.post('/', isAuthenticated, onChainWithdraw);
router.get('/utxos/', isAuthenticated, getUTXOs);
export default router;

@ -0,0 +1,9 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { listPayments, decodePayment, postPayment } from '../../controllers/c-lightning/payments.js';
const router = Router();
router.get('/', isAuthenticated, listPayments);
router.get('/decode/:payReq', isAuthenticated, decodePayment);
router.post('/', isAuthenticated, postPayment);
export default router;

@ -0,0 +1,9 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getPeers, postPeer, deletePeer } from '../../controllers/c-lightning/peers.js';
const router = Router();
router.get('/', isAuthenticated, getPeers);
router.post('/', isAuthenticated, postPeer);
router.delete('/:peerId', isAuthenticated, deletePeer);
export default router;

@ -0,0 +1,11 @@
import 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;

@ -0,0 +1,8 @@
import 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;

@ -0,0 +1,7 @@
import 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;

@ -0,0 +1,25 @@
import 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;

@ -0,0 +1,9 @@
import 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;

@ -0,0 +1,7 @@
import 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;

@ -0,0 +1,10 @@
import 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;

@ -0,0 +1,10 @@
import 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;

@ -0,0 +1,9 @@
import 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;

@ -0,0 +1,7 @@
import 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;

@ -0,0 +1,13 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getAllChannels, getPendingChannels, getClosedChannels, postChannel, postTransactions, closeChannel, postChanPolicy } from '../../controllers/lnd/channels.js';
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;

@ -0,0 +1,10 @@
import 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;

@ -0,0 +1,7 @@
import 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;

@ -0,0 +1,7 @@
import 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;

@ -0,0 +1,13 @@
import 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;

@ -0,0 +1,37 @@
import 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;

@ -0,0 +1,9 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { listInvoices, getInvoice, addInvoice } from '../../controllers/lnd/invoices.js';
const router = Router();
router.get('/', isAuthenticated, listInvoices);
router.get('/:rHashStr', isAuthenticated, getInvoice);
router.post('/', isAuthenticated, addInvoice);
export default router;

@ -0,0 +1,8 @@
import 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;

@ -0,0 +1,7 @@
import 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;

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

@ -0,0 +1,9 @@
import 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;

@ -0,0 +1,7 @@
import 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;

@ -0,0 +1,8 @@
import 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;

@ -0,0 +1,14 @@
import 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;

@ -0,0 +1,17 @@
import 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.post('/updateSelNode', updateSelectedNode);
router.post('/updateDefaultNode', updateDefaultNode);
router.post('/updateServiceSettings', updateServiceSettings);
router.post('/updateSSO', updateSSO);
router.get('/rates', getCurrencyRates);
export default router;

@ -0,0 +1,9 @@
import 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;

@ -0,0 +1,14 @@
import 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;

@ -0,0 +1,17 @@
import 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';
const router = Router();
const sharedRoutes = [
{ path: '/authenticate', route: authenticateRoutes },
{ path: '/boltz', route: boltzRoutes },
{ path: '/loop', route: loopRoutes },
{ path: '/conf', route: RTLConfRoutes }
];
sharedRoutes.forEach((route) => {
router.use(route.path, route.route);
});
export default router;

@ -0,0 +1,16 @@
import 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;

@ -0,0 +1,85 @@
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 clRoutes from '../routes/c-lightning/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/c-lightning/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: 'DEBUG', 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/cl', clRoutes);
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() : '');
res.sendFile(join(this.directoryName, '../..', 'frontend', 'index.html'));
});
this.app.use((err, req, res, next) => this.handleApplicationErrors(err, res));
};
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: 'DEBUG', 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;

@ -0,0 +1,45 @@
import jwt from 'jsonwebtoken';
import csurf from 'csurf/index.js';
import { Common } from './common.js';
const common = Common;
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 {
const updatedReq = JSON.parse(JSON.stringify(info.req));
updatedReq['cookies'] = !headers || !headers.cookie ? {} : '{"' + headers.cookie.replace(/ /g, '').replace(/;/g, '","').trim().replace(/[=]/g, '":"') + '"}';
updatedReq['cookies'] = JSON.parse(updatedReq['cookies']);
csurfProtection(updatedReq, null, (err) => {
if (err) {
next(false, 403, 'Invalid CSRF token!');
}
else {
next(true);
}
});
}
});
}
};

@ -0,0 +1,499 @@
/* eslint-disable no-console */
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.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) {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Loop macaroon Error: ' + JSON.stringify(err));
}
}
console.log('\r\n[' + new Date().toLocaleString() + '] INFO: Common => Swap Options: ' + JSON.stringify(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) {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Boltz macaroon Error: ' + JSON.stringify(err));
}
}
console.log('\r\n[' + new Date().toLocaleString() + '] INFO: Common => Boltz Options: ' + JSON.stringify(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 'CLT':
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) {
console.log('\r\n[' + new Date().toLocaleString() + '] INFO: Common => Updated Node Options: ' + JSON.stringify(req.session.selectedNode.options));
}
return { status: 200, message: 'Updated Successfully!' };
}
catch (err) {
req.session.selectedNode.options = {
url: '',
rejectUnauthorized: false,
json: true,
form: null
};
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Common Update Selected Node Options Error:' + JSON.stringify(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 'CLT':
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) {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Common Set Options Error:' + JSON.stringify(err));
node.options = {
url: '',
rejectUnauthorized: false,
json: true,
form: ''
};
}
console.log('\r\n[' + new Date().toLocaleString() + '] INFO: Common => Set Node Options: ' + JSON.stringify(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.handleError = (errRes, fileName, errMsg, selectedNode) => {
const err = JSON.parse(JSON.stringify(errRes));
if (!selectedNode) {
selectedNode = { ln_implementation: '' };
}
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 'CLT':
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;
}
const msgStr = '\r\n[' + new Date().toLocaleString() + '] ERROR: ' + fileName + ' => ' + errMsg + ': ' + (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error');
console.error(msgStr);
if (selectedNode.log_file && selectedNode.log_file !== '') {
fs.appendFile(selectedNode.log_file, msgStr, () => { });
}
const 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')
};
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') {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Dummy data file does not exist!');
}
else {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => 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 {
return 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'));
return 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'));
}
catch (err) {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Something went wrong while refreshing cookie: \n' + 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');
console.log('\r\n[' + new Date().toLocaleString() + '] INFO: Common => Please note that, RTL has encrypted the plaintext password into its corresponding hash.');
return config.multiPassHashed;
}
catch (err) {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Password hashing failed!');
}
};
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') }
};
request(options).then((body) => {
fs.writeFile(channel_backup_file, JSON.stringify(body), (err) => {
if (err) {
if (node.ln_node) {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Channel Backup Failed for Node ' + node.ln_node + ': ' + JSON.stringify(err));
}
else {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Channel Backup Error: ' + JSON.stringify(err));
}
}
else {
if (node.ln_node) {
console.log('\r\n[' + new Date().toLocaleString() + '] INFO: Common => Channel Backup Successful for Node: ' + JSON.stringify(node.ln_node));
}
else {
console.log('\r\n[' + new Date().toLocaleString() + '] INFO: Common => Channel Backup Successful');
}
}
});
}, (err) => {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Channel Backup Response Error: ' + JSON.stringify(err));
fs.writeFile(channel_backup_file, '', (writeErr) => {
console.error('\r\n[' + new Date().toLocaleString() + '] ERROR: Common => Channel Backup Response Empty File Write Error: ' + JSON.stringify(writeErr));
});
});
};
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) {
if (fs.existsSync(selNode.log_file)) {
fs.writeFile(selNode.log_file, '', () => { });
}
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'PORT: ' + this.port });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'HOST: ' + this.host });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'SSO: ' + this.rtl_sso });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'DEFAULT NODE INDEX: ' + selNode.index });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'INDEX: ' + selNode.index });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'LN NODE: ' + selNode.ln_node });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'LN IMPLEMENTATION: ' + selNode.ln_implementation });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'FIAT CONVERSION: ' + selNode.fiat_conversion });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'CURRENCY UNIT: ' + selNode.currency_unit });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Config Setup Variable', msg: 'LN SERVER URL: ' + selNode.ln_server_url });
this.logger.log({ selectedNode: selNode, level: 'DEBUG', 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 === 'CLT') {
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();

@ -0,0 +1,338 @@
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;
}
return {
multiPass: 'password',
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://localhost:8080',
fiatConversion: false
}
}
]
};
};
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 === 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() !== '') || (config.multiPass && config.multiPass.trim() !== '') || (config.multiPassHashed && config.multiPassHashed.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 !== '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].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].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: 'DEBUG', fileName: 'Config', msg: 'Node Information', data: 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 && (!this.common.rtl_cookie_path || this.common.rtl_cookie_path.trim() === '')) {
this.errMsg = 'Please set rtlCookiePath value for single sign on option!';
}
};
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();

@ -0,0 +1,22 @@
import { Logger } from './logger.js';
class CORS {
constructor() {
this.logger = Logger;
}
mount(app) {
this.logger.log({ selectedNode: null, level: 'DEBUG', 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();
});
return app;
}
;
}
export default new CORS;

@ -0,0 +1,17 @@
import csurf from 'csurf/index.js';
import { Logger } from './logger.js';
class CSRF {
constructor() {
this.csrfProtection = csurf({ cookie: true });
this.logger = Logger;
}
mount(app) {
this.logger.log({ selectedNode: null, level: 'DEBUG', fileName: 'CSRF', msg: 'Setting up CSRF.' });
if (process.env.NODE_ENV !== 'development') {
app.use((req, res, next) => this.csrfProtection(req, res, next));
}
return app;
}
;
}
export default new CSRF;

@ -0,0 +1,209 @@
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 { CollectionsEnum, validateOffer } 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(selectedNode) {
try {
if (!this.nodeDatabase[selectedNode.index]) {
this.nodeDatabase[selectedNode.index] = { adapter: null, data: null };
}
this.nodeDatabase[selectedNode.index].adapter = new DatabaseAdapter(this.dbDirectory, 'rtldb', selectedNode);
this.nodeDatabase[selectedNode.index].data = this.nodeDatabase[selectedNode.index].adapter.fetchData();
}
catch (err) {
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: 'Database', msg: 'Database Load Error', error: err });
}
}
create(selectedNode, collectionName, newDocument) {
return new Promise((resolve, reject) => {
try {
if (!selectedNode || !selectedNode.index) {
reject(new Error('Selected Node Config Not Found.'));
}
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, newDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
}
else {
this.nodeDatabase[selectedNode.index].data[collectionName].push(newDocument);
this.saveDatabase(+selectedNode.index);
resolve(newDocument);
}
}
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;
}
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, updatedDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
}
else {
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.index);
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);
}
});
}
destroy(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.index);
resolve(documentFieldValue);
}
catch (errRes) {
reject(errRes);
}
});
}
validateDocument(collectionName, documentToValidate) {
switch (collectionName) {
case CollectionsEnum.OFFERS:
return validateOffer(documentToValidate);
default:
return ({ isValid: false, error: 'Collection does not exist' });
}
}
saveDatabase(nodeIndex) {
try {
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(this.nodeDatabase[nodeIndex].data);
this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database 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 });
return new Error(err);
}
}
unloadDatabase(nodeIndex) {
this.saveDatabase(nodeIndex);
this.nodeDatabase[nodeIndex] = null;
}
}
export class DatabaseAdapter {
constructor(dbDirectoryPath, fileName, selNode = null) {
this.dbDirectoryPath = dbDirectoryPath;
this.fileName = fileName;
this.selNode = selNode;
this.dbFile = '';
this.dbFile = dbDirectoryPath + sep + fileName + '-node-' + selNode.index + '.json';
}
fetchData() {
try {
if (!fs.existsSync(this.dbDirectoryPath)) {
fs.mkdirSync(this.dbDirectoryPath);
}
}
catch (err) {
return new Error('Unable to Create Directory Error ' + JSON.stringify(err));
}
try {
if (!fs.existsSync(this.dbFile)) {
fs.writeFileSync(this.dbFile, '{}');
}
}
catch (err) {
return new Error('Unable to Create Database File Error ' + JSON.stringify(err));
}
try {
const dataFromFile = fs.readFileSync(this.dbFile, 'utf-8');
return !dataFromFile ? null : JSON.parse(dataFromFile);
}
catch (err) {
return new Error('Database Read Error ' + JSON.stringify(err));
}
}
getSelNode() {
return this.selNode;
}
saveData(data) {
try {
if (data) {
const tempFile = this.dbFile + '.tmp';
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2));
fs.renameSync(tempFile, this.dbFile);
}
return true;
}
catch (err) {
return new Error('Database Write Error ' + JSON.stringify(err));
}
}
}
export const Database = new DatabaseService();

@ -0,0 +1,68 @@
/* eslint-disable no-console */
import * as fs from 'fs';
export class LoggerService {
constructor() {
this.log = (msgJSON) => {
let msgStr = '\r\n[' + 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.stack) ? JSON.stringify(msgJSON.error.stack) : (typeof msgJSON.error === 'object') ? JSON.stringify(msgJSON.error) : (typeof msgJSON.error === 'string') ? msgJSON.error : '');
}
else {
msgStr = msgStr + '.';
}
console.error(msgStr);
if (msgJSON.selectedNode && msgJSON.selectedNode.log_file) {
fs.appendFile(msgJSON.selectedNode.log_file, msgStr, () => { });
}
break;
case 'WARN':
if (!msgJSON.selectedNode) {
console.warn(prepMsgData(msgJSON, msgStr));
}
else if (msgJSON.selectedNode && (msgJSON.selectedNode.log_level === 'INFO' || msgJSON.selectedNode.log_level === 'WARN' || msgJSON.selectedNode.log_level === 'DEBUG')) {
msgStr = prepMsgData(msgJSON, msgStr);
console.warn(msgStr);
if (msgJSON.selectedNode && msgJSON.selectedNode.log_file) {
fs.appendFile(msgJSON.selectedNode.log_file, msgStr, () => { });
}
}
break;
case 'DEBUG':
case 'INFO':
if (!msgJSON.selectedNode) {
console.log(prepMsgData(msgJSON, msgStr));
}
else if (msgJSON.selectedNode && (msgJSON.selectedNode.log_level === 'DEBUG')) {
if (typeof msgJSON.data !== 'string' && msgJSON.data && msgJSON.data.length && msgJSON.data.length > 0) {
msgStr = msgJSON.data.reduce((accumulator, dataEle) => accumulator + (typeof dataEle === 'object' ? JSON.stringify(dataEle) : (typeof dataEle === 'string') ? dataEle : '') + ', ', msgStr + ': [');
msgStr = msgStr.slice(0, -2) + ']';
}
else {
msgStr = prepMsgData(msgJSON, msgStr);
}
console.log(msgStr);
if (msgJSON.selectedNode && 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' ? JSON.stringify(msgJSON.data) : (typeof msgJSON.data === 'string') ? msgJSON.data : '');
}
else {
msgStr = msgStr + '.';
}
return msgStr;
};
export const Logger = new LoggerService();

@ -0,0 +1,178 @@
import { parse } from 'cookie';
import * as cookieParser from 'cookie-parser';
import * as crypto from 'crypto';
import WebSocket from 'ws';
import { Logger } from './logger.js';
import { Common } from './common.js';
import { verifyWSUser } from './authCheck.js';
import { EventEmitter } from 'events';
export class WebSocketServer {
constructor() {
this.logger = Logger;
this.common = Common;
this.clientDetails = [];
this.eventEmitterCLT = 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: 'DEBUG', fileName: 'WebSocketServer', msg: 'Connecting Websocket Server.' });
this.webSocketServer = new WebSocket.Server({ 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.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 = parse(request.headers.cookie);
websocket.clientId = Date.now();
websocket.isAlive = true;
websocket.sessionId = cookieParser.signedCookie(cookies['connect.sid'], this.common.secret_key);
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 'CLT':
this.eventEmitterCLT.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 'CLT':
this.eventEmitterCLT.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: 'INFO', 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 + crypto.randomBytes(64).toString('hex')).digest('base64');
this.getClients = () => this.webSocketServer.clients;
}
}
export const WSServer = new WebSocketServer();

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save