Files
purple-explorer/app.js
T
Joe Uhren 3a2f679201 Improved block sync speed
-A number of functions have been rewritten to be more optimized and faster: calculate_total, is_unique, convert_to_satoshi, get_input_addresses, processVoutAddresses, prepare_vout, prepare_vin
-Txes are now written to database via bulk writes which helps improve the sync speed and also controls memory usage with batching to write data once a certain threshold is reached
-update_address function changed to update_addresses since it now bulk writes the addresses in batches to improve sync speed and also controls memory usage with batching to write data once a certain threshold is reached
-The syncLoop function has been completely removed from the project and replaced with async library loops or even normal "for" loops in some cases which greatly improves sync speeds over large batches of data
-Fixed an issue with the flattened count of txes that is saved to the coinstats collection which could save incorrectly when using more than 1 thread
-Fixed an issue with the block sync which caused an unwanted delay when syncing less blocks than the amount of threads used to sync the data
-Fixed an issue with vout data processing that could sometimes populate data out of order
-Added a new sync.batch_size setting used to determine how many records (txes, addresses, addresstxes) should be saved in a single database transaction
-Added a new wait_for_bulk_database_save setting used to increase the block sync speed at the cost of not returning any error msgs for data that failed to save
-get_input_addresses function no longer returns in the exports section of the explorer.js file since it is only referenced in that file
-Updated explorerspec tests to use the newest function changes for any tests that needed to be updated

Special thanks to Karzo from Pepecoin for help with the bulkwrite code changes!
2025-02-02 19:10:17 -07:00

1259 lines
58 KiB
JavaScript

var express = require('express'),
path = require('path'),
nodeapi = require('./lib/nodeapi'),
favicon = require('serve-favicon'),
logger = require('morgan'),
cookieParser = require('cookie-parser'),
bodyParser = require('body-parser'),
settings = require('./lib/settings'),
routes = require('./routes/index'),
lib = require('./lib/explorer'),
db = require('./lib/database'),
package_metadata = require('./package.json');
var app = express();
var apiAccessList = [];
var viewPaths = [path.join(__dirname, 'views')]
var pluginRoutes = [];
const { exec } = require('child_process');
// pass wallet rpc connection info to nodeapi
nodeapi.setWalletDetails(settings.wallet);
// dynamically build the nodeapi cmd access list by adding all non-blockchain-specific api cmds that have a value
Object.keys(settings.api_cmds).forEach(function(key, index, map) {
if (key != 'use_rpc' && key != 'rpc_concurrent_tasks' && settings.api_cmds[key] != null && settings.api_cmds[key] != '')
apiAccessList.push(key);
});
// dynamically find and add additional blockchain_specific api cmds
Object.keys(settings.blockchain_specific).forEach(function(key, index, map) {
// check if this feature is enabled and has api cmds
if (settings.blockchain_specific[key].enabled == true && Object.keys(settings.blockchain_specific[key]).indexOf('api_cmds') > -1) {
// add all blockchain specific api cmds that have a value
Object.keys(settings.blockchain_specific[key]['api_cmds']).forEach(function(key2, index, map) {
if (settings.blockchain_specific[key]['api_cmds'][key2] != null && settings.blockchain_specific[key]['api_cmds'][key2] != '')
apiAccessList.push(key2);
});
}
});
// whitelist the cmds in the nodeapi access list
nodeapi.setAccess('only', apiAccessList);
// determine if http traffic should be forwarded to https
if (settings.webserver.tls.enabled == true && settings.webserver.tls.always_redirect == true) {
app.use(function(req, res, next) {
if (req.secure) {
// continue without redirecting
next();
} else {
// add webserver port to the host value if it does not already exist
const host = req.headers.host + (req.headers.host.indexOf(':') > -1 ? '' : ':' + settings.webserver.port.toString());
// redirect to the correct https page
res.redirect(301, 'https://' + host.replace(':' + settings.webserver.port.toString(), (settings.webserver.tls.port != 443 ? ':' + settings.webserver.tls.port.toString() : '')) + req.url);
}
});
}
// determine if cors should be enabled
if (settings.webserver.cors.enabled == true) {
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", settings.webserver.cors.corsorigin);
res.header('Access-Control-Allow-Methods', 'DELETE, PUT, GET, POST');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
}
// loop through all plugins defined in the settings
settings.plugins.allowed_plugins.forEach(function (plugin) {
// check if this plugin is enabled
if (plugin.enabled) {
const pluginName = (plugin.plugin_name == null ? '' : plugin.plugin_name);
// check if the plugin exists in the plugins directory
if (db.fs.existsSync(`./plugins/${pluginName}`)) {
// check if the plugin's local_plugin_settings file exists
if (db.fs.existsSync(`./plugins/${pluginName}/lib/local_plugin_settings.js`)) {
// load the local_plugin_settings.js file from the plugin
let localPluginSettings = require(`./plugins/${pluginName}/lib/local_plugin_settings`);
// loop through all local plugin settings
Object.keys(localPluginSettings).forEach(function(key, index, map) {
// check if this is a known setting type that should be brought into the main settings
if (key.endsWith('_page') && typeof localPluginSettings[key] === 'object' && localPluginSettings[key]['enabled'] == true) {
// this is a page setting
// add the page_id to the page setting
localPluginSettings[key].page_id = key;
// add the menu item title to the page setting
localPluginSettings[key].menu_title = localPluginSettings['localization'][`${key}_menu_title`];
// check if there is already a page for this plugin
if (plugin.pages == null) {
// initialize the pages array
plugin.pages = [];
}
// add this page setting to the main plugin data
plugin['pages'].push(localPluginSettings[key]);
} else if (key == 'public_apis') {
// this is a collection of new apis
// check if there is an ext section
if (localPluginSettings[key]['ext'] != null) {
// loop through all ext apis for this plugin
Object.keys(localPluginSettings[key]['ext']).forEach(function(extKey, extIndex, extMap) {
// add the name of the api into the object
localPluginSettings[key]['ext'][extKey]['api_name'] = extKey;
// loop through all parameters for this api and replace them in the description string if applicable
for (let p = 0; p < localPluginSettings[key]['ext'][extKey]['api_parameters'].length; p++)
localPluginSettings['localization'][`${extKey}_description`] = localPluginSettings['localization'][`${extKey}_description`].replace(new RegExp(`\\{${(p + 1)}}`, 'g'), localPluginSettings[key]['ext'][extKey]['api_parameters'][p]['parameter_name']);
// add the localized api description into the object
localPluginSettings[key]['ext'][extKey]['api_desc'] = localPluginSettings['localization'][`${extKey}_description`];
});
}
// copy the entire public_apis section from the plugin into the main settings
plugin.public_apis = localPluginSettings[key];
}
});
}
// check if the plugin's routes/index.js file exists
if (db.fs.existsSync(`./plugins/${pluginName}/routes/index.js`)) {
// get the plugin routes and save them to an array
pluginRoutes.push(require(`./plugins/${pluginName}/routes/index`));
// check if the plugin has a views directory
if (db.fs.existsSync(`./plugins/${pluginName}/views`)) {
// get the list of files in the views directory
const files = db.fs.readdirSync(`./plugins/${pluginName}/views`);
// filter the list of files to check if any have the .pug extension
const pugFiles = files.filter(file => path.extname(file) === '.pug');
// check if the plugin has 1 or more views
if (pugFiles.length > 0) {
// add this plugins view path to the list of view paths
viewPaths.push(path.resolve(`./plugins/${pluginName}/views`));
}
}
}
}
}
});
// view engine setup
app.set('views', viewPaths);
app.set('view engine', 'pug');
var default_favicon = '';
// loop through the favicons
Object.keys(settings.shared_pages.favicons).forEach(function(key, index, map) {
// remove the public directory from the path if exists
if (settings.shared_pages.favicons[key] != null && settings.shared_pages.favicons[key].indexOf('public/') > -1)
settings.shared_pages.favicons[key] = settings.shared_pages.favicons[key].replace(/public\//g, '');
// check if the favicon file exists
if (!db.fs.existsSync(path.join('./public', settings.shared_pages.favicons[key])))
settings.shared_pages.favicons[key] = '';
else if (default_favicon == '')
default_favicon = settings.shared_pages.favicons[key];
});
if (default_favicon != '')
app.use(favicon(path.join('./public', default_favicon)));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// routes
app.use('/api', nodeapi.app);
app.use('/', routes);
// loop through all plugin routes and add them to the app
pluginRoutes.forEach(function (r) {
app.use('/', r);
});
// post method to claim an address using verifymessage functionality
app.post('/claim', function(req, res) {
// validate captcha if applicable
validate_captcha(settings.claim_address_page.enable_captcha, req.body, function(captcha_error) {
// check if there was a problem with captcha
if (captcha_error) {
// show the captcha error
res.json({'status': 'failed', 'error': true, 'message': 'The captcha validation failed'});
} else {
// filter bad words if enabled
filter_bad_words((req.body.message == null || req.body.message == '' ? '' : req.body.message), function(claim_error, message) {
// check if there was an error or if the message was filtered
if (claim_error != null) {
// an error occurred with loading the bad-words filter
res.json({'status': 'failed', 'error': true, 'message': 'Error loading the bad-words filter: ' + claim_error});
} else if (message == req.body.message) {
// call the verifymessage api
lib.verify_message(req.body.address, req.body.signature, req.body.message, function(body) {
if (body == false)
res.json({'status': 'failed', 'error': true, 'message': 'Invalid signature'});
else if (body == true) {
db.update_claim_name(req.body.address, req.body.message, function(val) {
// check if the update was successful
if (val == '')
res.json({'status': 'success'});
else if (val == 'no_address')
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address ' + req.body.address + ' is not valid or does not have any transactions'});
else
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
});
} else
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
});
} else {
// message was filtered which would change the signature
res.json({'status': 'failed', 'error': true, 'message': 'Display name contains bad words and cannot be saved: ' + message});
}
});
}
});
});
function validate_captcha(captcha_enabled, data, cb) {
// check if captcha is enabled for the requested feature
if (captcha_enabled == true) {
// determine the captcha type
if (settings.captcha.google_recaptcha3.enabled == true) {
if (data.google_recaptcha3 != null) {
const request = require('postman-request');
request({uri: 'https://www.google.com/recaptcha/api/siteverify?secret=' + settings.captcha.google_recaptcha3.secret_key + '&response=' + data.google_recaptcha3, json: true}, function (error, response, body) {
if (error) {
// an error occurred while trying to validate the captcha
return cb(true);
} else if (body == null || body == '' || typeof body !== 'object') {
// return data is invalid
return cb(true);
} else if (body.score == null || body.score < settings.captcha.google_recaptcha3.pass_score) {
// captcha challenge failed
return cb(true);
} else {
// captcha challenge passed
return cb(false);
}
});
} else {
// a captcha response wasn't received
return cb(true);
}
} else if (settings.captcha.google_recaptcha2.enabled == true) {
if (data.google_recaptcha2 != null) {
const request = require('postman-request');
request({uri: 'https://www.google.com/recaptcha/api/siteverify?secret=' + settings.captcha.google_recaptcha2.secret_key + '&response=' + data.google_recaptcha2, json: true}, function (error, response, body) {
if (error) {
// an error occurred while trying to validate the captcha
return cb(true);
} else if (body == null || body == '' || typeof body !== 'object') {
// return data is invalid
return cb(true);
} else if (body.success == null || body.success == false) {
// captcha challenge failed
return cb(true);
} else {
// captcha challenge passed
return cb(false);
}
});
} else {
// a captcha response wasn't received
return cb(true);
}
} else if (settings.captcha.hcaptcha.enabled == true) {
if (data.hcaptcha != null) {
const request = require('postman-request');
request({uri: 'https://hcaptcha.com/siteverify?secret=' + settings.captcha.hcaptcha.secret_key + '&response=' + data.hcaptcha, json: true}, function (error, response, body) {
if (error) {
// an error occurred while trying to validate the captcha
return cb(true);
} else if (body == null || body == '' || typeof body !== 'object') {
// return data is invalid
return cb(true);
} else if (body.success == null || body.success == false) {
// captcha challenge failed
return cb(true);
} else {
// captcha challenge passed
return cb(false);
}
});
} else {
// a captcha response wasn't received
return cb(true);
}
} else {
// no captcha options are enabled
return cb(false);
}
} else {
// captcha is not enabled for this feature
return cb(false);
}
}
function filter_bad_words(msg, cb) {
// check if the bad-words filter is enabled
if (settings.claim_address_page.enable_bad_word_filter == true) {
// import the bad-words dependency
import('bad-words').then(function(module) {
// load the bad-words filter
const bad_word_lib = module.Filter;
const bad_word_filter = new bad_word_lib();
// return the filtered msg
return cb(null, bad_word_filter.clean(msg));
})
.catch(function(err) {
return cb(err, null);
});
} else {
// return the msg without filtering for bad words
return cb(null, msg);
}
}
// post method to receive data from a plugin
app.post('/plugin-request', function(req, res) {
const pluginLockName = 'plugin';
// check if another plugin request is already running
if (lib.is_locked([pluginLockName], true) == true)
res.json({'status': 'failed', 'error': true, 'message': `Another plugin request is already running..`});
else {
// create a new plugin lock before checking the rest of the locks to minimize problems with running scripts at the same time
lib.create_lock(pluginLockName);
// check the backup, restore and delete locks since those functions would be problematic when updating data
if (lib.is_locked(['backup', 'restore', 'delete'], true) == true) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Another script has locked the database..`});
} else {
// all lock tests passed. OK to run plugin request
let dataObject = {};
try {
// attempt to parse the POST data field into a JSON object
dataObject = JSON.parse(req.body.data);
} catch {
// do nothing. errors will be handled below
}
// check if the dataObject was populated
if (dataObject == null || JSON.stringify(dataObject) === '{}') {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': 'POST data is missing or not in the correct format'});
} else {
// check if the plugin secret code is correct and if the coin name was specified
if (dataObject.plugin_data == null || settings.plugins.plugin_secret_code != dataObject.plugin_data.secret_code) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': 'Secret code is missing or incorrect'});
} else if (dataObject.plugin_data.coin_name == null || dataObject.plugin_data.coin_name == '') {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': 'Coin name is missing'});
} else {
const tableData = dataObject.table_data;
// check if the table_data seems valid
if (tableData == null || !Array.isArray(tableData)) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `table_data from POST data is missing or empty`});
} else {
const pluginName = (dataObject.plugin_data.plugin_name == null ? '' : dataObject.plugin_data.plugin_name);
const pluginObj = settings.plugins.allowed_plugins.find(item => item.plugin_name === pluginName && pluginName != '');
// check if the requested plugin was found in the settings
if (pluginObj == null) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not defined in settings`});
} else {
// check if the requested plugin is enabled
if (!pluginObj.enabled) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not enabled`});
} else {
// check if the plugin exists in the plugins directory
if (!db.fs.existsSync(`./plugins/${pluginName}`)) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not installed in the plugins directory`});
} else {
// check if the plugin's server_functions file exists
if (!db.fs.existsSync(`./plugins/${pluginName}/lib/server_functions.js`)) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is missing the /lib/server_functions.js file`});
} else {
// load the server_functions.js file from the plugin
const serverFunctions = require(`./plugins/${pluginName}/lib/server_functions`);
// check if the process_plugin_request function exists
if (typeof serverFunctions.process_plugin_request !== 'function') {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is missing the process_plugin_request function`});
} else {
// call the process_plugin_request function to process the new table data
serverFunctions.process_plugin_request(dataObject.plugin_data.coin_name, tableData, settings.sync.update_timeout, function(response) {
lib.remove_lock(pluginLockName);
res.json(response);
});
}
}
}
}
}
}
}
}
}
}
});
// extended apis
app.use('/ext/getmoneysupply', function(req, res) {
// check if the getmoneysupply api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmoneysupply.enabled == true) {
// lookup stats
db.get_stats(settings.coin.name, function (stats) {
res.setHeader('content-type', 'text/plain');
res.end((stats && stats.supply ? stats.supply.toString() : '0'));
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getaddress/:hash', function(req, res) {
// check if the getaddress api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getaddress.enabled == true) {
db.get_address(req.params.hash, false, function(address) {
db.get_address_txs_ajax(req.params.hash, 0, settings.api_page.public_apis.ext.getaddresstxs.max_items_per_query, function(txs, count) {
if (address) {
var last_txs = [];
for (i = 0; i < txs.length; i++) {
if (typeof txs[i].txid !== "undefined") {
var out = 0,
vin = 0,
tx_type = 'vout',
row = {};
txs[i].vout.forEach(function (r) {
if (r.addresses == req.params.hash)
out += r.amount;
});
txs[i].vin.forEach(function (s) {
if (s.addresses == req.params.hash)
vin += s.amount;
});
if (vin > out)
tx_type = 'vin';
row['addresses'] = txs[i].txid;
row['type'] = tx_type;
last_txs.push(row);
}
}
var a_ext = {
address: address.a_id,
sent: (address.sent / 100000000),
received: (address.received / 100000000),
balance: (address.balance / 100000000).toString().replace(/(^-+)/mg, ''),
last_txs: last_txs
};
res.send(a_ext);
} else
res.send({ error: 'address not found.', hash: req.params.hash});
});
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/gettx/:txid', function(req, res) {
// check if the gettx api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.gettx.enabled == true) {
var txid = req.params.txid;
db.get_tx(txid, function(tx) {
if (tx) {
lib.get_blockcount(function(blockcount) {
res.send({ active: 'tx', tx: tx, confirmations: (blockcount - tx.blockindex + 1), blockcount: (blockcount ? blockcount : 0)});
});
} else {
lib.get_rawtransaction(txid, function(rtx) {
if (rtx && rtx.txid) {
lib.prepare_vin(rtx, function(vin, tx_type_vin) {
lib.prepare_vout(rtx.vout, rtx.txid, vin, ((typeof rtx.vjoinsplit === 'undefined' || rtx.vjoinsplit == null) ? [] : rtx.vjoinsplit), function(rvout, rvin, tx_type_vout) {
const total = lib.calculate_total(rvout);
if (!rtx.confirmations > 0) {
var utx = {
txid: rtx.txid,
vin: rvin,
vout: rvout,
total: total.toFixed(8),
timestamp: rtx.time,
blockhash: '-',
blockindex: -1
};
res.send({ active: 'tx', tx: utx, confirmations: rtx.confirmations, blockcount:-1});
} else {
var utx = {
txid: rtx.txid,
vin: rvin,
vout: rvout,
total: total.toFixed(8),
timestamp: rtx.time,
blockhash: rtx.blockhash,
blockindex: rtx.blockheight
};
lib.get_blockcount(function(blockcount) {
res.send({ active: 'tx', tx: utx, confirmations: rtx.confirmations, blockcount: (blockcount ? blockcount : 0)});
});
}
});
});
} else
res.send({ error: 'tx not found.', hash: txid});
});
}
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getbalance/:hash', function(req, res) {
// check if the getbalance api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getbalance.enabled == true) {
db.get_address(req.params.hash, false, function(address) {
if (address) {
res.setHeader('content-type', 'text/plain');
res.end((address.balance / 100000000).toString().replace(/(^-+)/mg, ''));
} else
res.send({ error: 'address not found.', hash: req.params.hash });
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getdistribution', function(req, res) {
// check if the getdistribution api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getdistribution.enabled == true) {
db.get_richlist(settings.coin.name, function(richlist) {
db.get_stats(settings.coin.name, function(stats) {
db.get_distribution(richlist, stats, function(dist) {
res.send(dist);
});
});
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getcurrentprice', function(req, res) {
// check if the getcurrentprice api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getcurrentprice.enabled == true) {
db.get_stats(settings.coin.name, function (stats) {
const currency = lib.get_market_currency_code();
eval('var p_ext = { "last_price_' + currency.toLowerCase() + '": stats.last_price, "last_price_usd": stats.last_usd_price, }');
res.send(p_ext);
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getbasicstats', function(req, res) {
// check if the getbasicstats api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getbasicstats.enabled == true) {
// lookup stats
db.get_stats(settings.coin.name, function (stats) {
const currency = lib.get_market_currency_code();
// check if the masternode count api is enabled
if (settings.api_page.public_apis.rpc.getmasternodecount.enabled == true && settings.api_cmds['getmasternodecount'] != null && settings.api_cmds['getmasternodecount'] != '') {
// masternode count api is available
lib.get_masternodecount(function(masternodestotal) {
eval('var p_ext = { "block_count": (stats.count ? stats.count : 0), "money_supply": (stats.supply ? stats.supply : 0), "last_price_' + currency.toLowerCase() + '": stats.last_price, "last_price_usd": stats.last_usd_price, "masternode_count": masternodestotal.total }');
res.send(p_ext);
});
} else {
// masternode count api is not available
eval('var p_ext = { "block_count": (stats.count ? stats.count : 0), "money_supply": (stats.supply ? stats.supply : 0), "last_price_' + currency.toLowerCase() + '": stats.last_price, "last_price_usd": stats.last_usd_price }');
res.send(p_ext);
}
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getlasttxs/:min', function(req, res) {
// check if the getlasttxs api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getlasttxs.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
var min = req.params.min, start, length, internal = false;
// split url suffix by forward slash and remove blank entries
var split = req.url.split('/').filter(function(v) { return v; });
// determine how many parameters were passed
switch (split.length) {
case 2:
// capture start and length
start = split[0];
length = split[1];
break;
default:
if (split.length == 1) {
// capture start
start = split[0];
} else if (split.length >= 2) {
// capture start and length
start = split[0];
length = split[1];
// check if this is an internal request
if (split.length > 2 && split[2] == 'internal')
internal = true;
}
break;
}
// fix parameters
if (typeof length === 'undefined' || isNaN(length) || length > settings.api_page.public_apis.ext.getlasttxs.max_items_per_query)
length = settings.api_page.public_apis.ext.getlasttxs.max_items_per_query;
if (typeof start === 'undefined' || isNaN(start) || start < 0)
start = 0;
if (typeof min === 'undefined' || isNaN(min) || min < 0)
min = 0;
else
min = (min * 100000000);
db.get_last_txs(start, length, min, internal, function(data, count) {
// check if this is an internal request
if (internal) {
// display data formatted for internal datatable
res.json({"data": data, "recordsTotal": count, "recordsFiltered": count});
} else {
// display data in more readable format for public api
res.json(data);
}
});
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getaddresstxs/:address/:start/:length', function(req, res) {
// check if the getaddresstxs api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getaddresstxs.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
var internal = false;
// split url suffix by forward slash and remove blank entries
var split = req.url.split('/').filter(function(v) { return v; });
// check if this is an internal request
if (split.length > 0 && split[0] == 'internal')
internal = true;
// fix parameters
if (typeof req.params.length === 'undefined' || isNaN(req.params.length) || req.params.length > settings.api_page.public_apis.ext.getaddresstxs.max_items_per_query)
req.params.length = settings.api_page.public_apis.ext.getaddresstxs.max_items_per_query;
if (typeof req.params.start === 'undefined' || isNaN(req.params.start) || req.params.start < 0)
req.params.start = 0;
if (typeof req.params.min === 'undefined' || isNaN(req.params.min) || req.params.min < 0)
req.params.min = 0;
else
req.params.min = (req.params.min * 100000000);
db.get_address_txs_ajax(req.params.address, req.params.start, req.params.length, function(txs, count) {
var data = [];
for (i = 0; i < txs.length; i++) {
if (typeof txs[i].txid !== "undefined") {
var out = 0;
var vin = 0;
txs[i].vout.forEach(function(r) {
if (r.addresses == req.params.address)
out += r.amount;
});
txs[i].vin.forEach(function(s) {
if (s.addresses == req.params.address)
vin += s.amount;
});
if (internal) {
var row = [];
row.push(txs[i].timestamp);
row.push(txs[i].txid);
row.push(Number(out / 100000000));
row.push(Number(vin / 100000000));
row.push(Number(txs[i].balance / 100000000));
data.push(row);
} else {
data.push({
timestamp: txs[i].timestamp,
txid: txs[i].txid,
sent: Number(out / 100000000),
received: Number(vin / 100000000),
balance: Number(txs[i].balance / 100000000)
});
}
}
}
// check if this is an internal request
if (internal) {
// display data formatted for internal datatable
res.json({"data": data, "recordsTotal": count, "recordsFiltered": count});
} else {
// display data in more readable format for public api
res.json(data);
}
});
} else
res.end(settings.localization.method_disabled);
});
function get_connection_and_block_counts(get_data, cb) {
// check if the connection and block counts should be returned
if (get_data) {
lib.get_connectioncount(function(connections) {
lib.get_blockcount(function(blockcount) {
return cb(connections, blockcount);
});
});
} else
return cb(null, null);
}
app.use('/ext/getsummary', function(req, res) {
const isInternal = (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1);
// check if the getsummary api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getsummary.enabled == true) || isInternal) {
// check if this is a footer-only method that should only return the connection count and block count
if (req.headers['footer-only'] != null && req.headers['footer-only'] == 'true') {
// only return the connection count and block count
get_connection_and_block_counts(true, function(connections, blockcount) {
res.send({
connections: (connections ? connections : '-'),
blockcount: (blockcount ? blockcount : '-')
});
});
} else {
// get the connection and block counts only if this is NOT an internal call
get_connection_and_block_counts(!isInternal, function(connections, blockcount) {
lib.get_hashrate(function(hashrate) {
db.get_stats(settings.coin.name, function (stats) {
lib.get_masternodecount(function(masternodestotal) {
lib.get_difficulty(function(difficulty) {
let difficultyHybrid = '';
if (difficulty && difficulty['proof-of-work']) {
if (settings.shared_pages.difficulty == 'Hybrid') {
difficultyHybrid = 'POS: ' + difficulty['proof-of-stake'];
difficulty = 'POW: ' + difficulty['proof-of-work'];
} else if (settings.shared_pages.difficulty == 'POW')
difficulty = difficulty['proof-of-work'];
else
difficulty = difficulty['proof-of-stake'];
}
if (hashrate == `${settings.localization.ex_error}: ${settings.localization.check_console}`)
hashrate = 0;
let mn_total = 0;
let mn_enabled = 0;
// check if the masternode count api is enabled
if (settings.api_page.public_apis.rpc.getmasternodecount.enabled == true && settings.api_cmds['getmasternodecount'] != null && settings.api_cmds['getmasternodecount'] != '') {
// masternode count api is available
if (masternodestotal) {
if (masternodestotal.total)
mn_total = masternodestotal.total;
if (masternodestotal.enabled)
mn_enabled = masternodestotal.enabled;
}
}
res.send({
difficulty: (difficulty ? difficulty : '-'),
difficultyHybrid: difficultyHybrid,
supply: (stats == null || stats.supply == null ? 0 : stats.supply),
hashrate: hashrate,
lastPrice: (stats == null || stats.last_price == null ? 0 : stats.last_price),
lastUSDPrice: (stats == null || stats.last_usd_price == null ? 0 : stats.last_usd_price),
connections: (connections ? connections : '-'),
blockcount: (blockcount ? blockcount : '-'),
masternodeCountOnline: (masternodestotal && mn_enabled != 0 ? mn_enabled : '-'),
masternodeCountOffline: (masternodestotal && mn_total != 0 ? Math.floor(mn_total - mn_enabled) : '-')
});
});
});
});
});
});
}
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getnetworkpeers', function(req, res) {
// check if the getnetworkpeers api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getnetworkpeers.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
// get list of peers
db.get_peers(function(peers) {
// loop through peers list and remove the mongo _id and __v keys
for (i = 0; i < peers.length; i++) {
delete peers[i]['_doc']['_id'];
delete peers[i]['_doc']['__v'];
}
// sort ip6 addresses to the bottom
peers.sort(function(a, b) {
var address1 = a.address.indexOf(':') > -1;
var address2 = b.address.indexOf(':') > -1;
if (address1 < address2)
return -1;
else if (address1 > address2)
return 1;
else
return 0;
});
// return peer data
res.json(peers);
});
} else
res.end(settings.localization.method_disabled);
});
// get the list of masternodes from local collection
app.use('/ext/getmasternodelist', function(req, res) {
// check if the getmasternodelist api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmasternodelist.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
// get the masternode list from local collection
db.get_masternodes(function(masternodes) {
// loop through masternode list and remove the mongo _id and __v keys
for (i = 0; i < masternodes.length; i++) {
delete masternodes[i]['_doc']['_id'];
delete masternodes[i]['_doc']['__v'];
}
// return masternode list
res.send(masternodes);
});
} else
res.end(settings.localization.method_disabled);
});
// returns a list of masternode reward txs for a single masternode address from a specific block height
app.use('/ext/getmasternoderewards/:hash/:since', function(req, res) {
// check if the getmasternoderewards api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmasternoderewards.enabled == true) {
db.get_masternode_rewards(req.params.hash, req.params.since, function(rewards) {
if (rewards != null) {
// loop through the tx list to fix vout values and remove unnecessary data such as the always empty vin array and the mongo _id and __v keys
for (i = 0; i < rewards.length; i++) {
// remove unnecessary data keys
delete rewards[i]['vin'];
delete rewards[i]['_id'];
delete rewards[i]['__v'];
// convert amounts from satoshis
rewards[i]['total'] = rewards[i]['total'] / 100000000;
rewards[i]['vout']['amount'] = rewards[i]['vout']['amount'] / 100000000;
}
// return list of masternode rewards
res.json(rewards);
} else
res.send({error: "failed to retrieve masternode rewards", hash: req.params.hash, since: req.params.since});
});
} else
res.end(settings.localization.method_disabled);
});
// returns the total masternode rewards received for a single masternode address from a specific block height
app.use('/ext/getmasternoderewardstotal/:hash/:since', function(req, res) {
// check if the getmasternoderewardstotal api is enabled
if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmasternoderewardstotal.enabled == true) {
db.get_masternode_rewards_totals(req.params.hash, req.params.since, function(total_rewards) {
if (total_rewards != null) {
// return the total of masternode rewards
res.json(total_rewards);
} else
res.send({error: "failed to retrieve masternode rewards", hash: req.params.hash, since: req.params.since});
});
} else
res.end(settings.localization.method_disabled);
});
// get the list of orphans from local collection
app.use('/ext/getorphanlist/:start/:length', function(req, res) {
// check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1) {
// fix parameters
if (typeof req.params.start === 'undefined' || isNaN(req.params.start) || req.params.start < 0)
req.params.start = 0;
if (typeof req.params.length === 'undefined' || isNaN(req.params.length))
req.params.length = 10;
// get the orphan list from local collection
db.get_orphans(req.params.start, req.params.length, function(orphans, count) {
var data = [];
for (i = 0; i < orphans.length; i++) {
var row = [];
row.push(orphans[i].blockindex);
row.push(orphans[i].orphan_blockhash);
row.push(orphans[i].good_blockhash);
row.push(orphans[i].prev_blockhash);
row.push(orphans[i].next_blockhash);
data.push(row);
}
// display data formatted for internal datatable
res.json({"data": data, "recordsTotal": count, "recordsFiltered": count});
});
} else
res.end(settings.localization.method_disabled);
});
// get the last updated date for a particular section
app.use('/ext/getlastupdated/:section', function(req, res) {
// check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1) {
// fix parameters
if (req.params.section == null)
req.params.section = '';
switch (req.params.section.toLowerCase()) {
case 'blockchain':
case 'movement':
// lookup last updated date
db.get_stats(settings.coin.name, function (stats) {
res.json({'last_updated_date': stats.blockchain_last_updated});
});
break;
default:
res.send({error: 'Cannot find last updated date'});
}
} else
res.end(settings.localization.method_disabled);
});
app.use('/ext/getnetworkchartdata', function(req, res) {
db.get_network_chart_data(function(data) {
if (data)
res.send(data);
else
res.send();
});
});
app.use('/system/restartexplorer', function(req, res, next) {
// check to ensure this special cmd is only executed by the local server
if (req._remoteAddress != null && req._remoteAddress.indexOf('127.0.0.1') > -1) {
// send a msg to the cluster process telling it to restart
process.send('restart');
res.end();
} else {
// show the error page
var err = new Error(settings.localization.error_not_found);
err.status = 404;
next(err);
}
});
var market_data = [];
var market_count = 0;
// check if markets are enabled
if (settings.markets_page.enabled == true) {
// dynamically populate market data
Object.keys(settings.markets_page.exchanges).forEach(function (key, index, map) {
// check if market is enabled via settings
if (settings.markets_page.exchanges[key].enabled == true) {
// check if market is installed/supported
if (db.fs.existsSync('./lib/markets/' + key + '.js')) {
// load market file
var exMarket = require('./lib/markets/' + key);
// save market_name and market_logo from market file to settings
eval('market_data.push({id: "' + key + '", name: "' + (exMarket.market_name == null ? '' : exMarket.market_name) + '", alt_name: "' + (exMarket.market_name_alt == null ? '' : exMarket.market_name_alt) + '", logo: "' + (exMarket.market_logo == null ? '' : exMarket.market_logo) + '", alt_logo: "' + (exMarket.market_logo_alt == null ? '' : exMarket.market_logo_alt) + '", trading_pairs: []});');
// loop through all trading pairs for this market
for (var i = 0; i < settings.markets_page.exchanges[key].trading_pairs.length; i++) {
var isAlt = false;
var pair = settings.markets_page.exchanges[key].trading_pairs[i].toUpperCase(); // ensure trading pair setting is always uppercase
var coin_symbol = pair.split('/')[0];
var pair_symbol = pair.split('/')[1];
// determine if using the alt name + logo
if (exMarket.market_url_template != null && exMarket.market_url_template != '') {
switch ((exMarket.market_url_case == null || exMarket.market_url_case == '' ? 'l' : exMarket.market_url_case.toLowerCase())) {
case 'l':
case 'lower':
isAlt = (exMarket.isAlt != null ? exMarket.isAlt({coin: coin_symbol.toLowerCase(), exchange: pair_symbol.toLowerCase()}) : false);
break;
case 'u':
case 'upper':
isAlt = (exMarket.isAlt != null ? exMarket.isAlt({coin: coin_symbol.toUpperCase(), exchange: pair_symbol.toUpperCase()}) : false);
break;
default:
}
}
// add trading pair to market_data
market_data[market_data.length - 1].trading_pairs.push({
pair: pair,
isAlt: isAlt
});
// increment the market count
market_count++;
}
// sort trading pairs by alt status
market_data[market_data.length - 1].trading_pairs.sort(function(a, b) {
if (a.isAlt < b.isAlt)
return -1;
else if (a.isAlt > b.isAlt)
return 1;
else
return 0;
});
}
}
});
// sort market data by market name
market_data.sort(function(a, b) {
var name1 = a.name.toLowerCase();
var name2 = b.name.toLowerCase();
if (name1 < name2)
return -1;
else if (name1 > name2)
return 1;
else
return 0;
});
// fix default exchange name case
if (settings.markets_page.default_exchange.exchange_name != null)
settings.markets_page.default_exchange.exchange_name = settings.markets_page.default_exchange.exchange_name.toLowerCase();
else
settings.markets_page.default_exchange.exchange_name = '';
// fix default exchange trading pair case
if (settings.markets_page.default_exchange.trading_pair != null)
settings.markets_page.default_exchange.trading_pair = settings.markets_page.default_exchange.trading_pair.toUpperCase();
else
settings.markets_page.default_exchange.trading_pair = '';
var ex = settings.markets_page.exchanges;
var ex_name = settings.markets_page.default_exchange.exchange_name;
var ex_pair = settings.markets_page.default_exchange.trading_pair;
var ex_keys = Object.keys(ex);
var ex_error = '';
// check to ensure default market and trading pair exist and are enabled
if (ex[ex_name] == null) {
// exchange name does not exist in exchanges list
ex_error = 'Default exchange name is not valid' + ': ' + ex_name;
} else if (!ex[ex_name].enabled) {
// exchange is not enabled
ex_error = 'Default exchange is disabled in settings' + ': ' + ex_name;
} else if (ex[ex_name].trading_pairs.findIndex(p => p.toUpperCase() == ex_pair.toUpperCase()) == -1) {
// invalid default exchange trading pair
ex_error = 'Default exchange trading pair is not valid' + ': ' + ex_pair;
}
// check if there was an error msg
if (ex_error != '') {
// there was an error, so find the next available market from settings.json
var new_default_index = -1;
// find the first enabled exchange with at least one trading pair
for (var i = 0; i < ex_keys.length; i++) {
if (ex[ex_keys[i]]['enabled'] === true && ex[ex_keys[i]]['trading_pairs'].length > 0) {
// found a match so save the index
new_default_index = i;
// stop looking for more matches
break;
}
}
// check if a valid and enabled market was found
if (new_default_index == -1) {
// no valid markets found
console.log('WARNING: ' + ex_error + '. ' + 'No valid or enabled markets found in settings.json. The markets feature will be temporarily disabled. To restore markets functionality, please enable at least 1 market and ensure at least 1 valid trading pair is added. Finally, restart the explorer to resolve the problem');
// disable the markets feature for this session
settings.markets_page.enabled = false;
} else {
// a valid and enabled market was found to replace the default
console.log('WARNING: ' + ex_error + '. ' + 'Default exchange will be set to' + ': ' + ex_keys[new_default_index] + '[' + ex[ex_keys[new_default_index]].trading_pairs[0].toUpperCase() + ']');
// set new default exchange data
settings.markets_page.default_exchange.exchange_name = ex_keys[new_default_index];
settings.markets_page.default_exchange.trading_pair = ex[ex_keys[new_default_index]].trading_pairs[0].toUpperCase();
}
}
}
// check if home_link_logo file exists
if (!db.fs.existsSync(path.join('./public', settings.shared_pages.page_header.home_link_logo)))
settings.shared_pages.page_header.home_link_logo = '';
// always disable the rpc masternode list cmd from public apis
settings.api_page.public_apis.rpc.getmasternodelist = { "enabled": false };
// locals
app.set('explorer_version', package_metadata.version);
app.set('localization', settings.localization);
app.set('coin', settings.coin);
app.set('network_history', settings.network_history);
app.set('shared_pages', settings.shared_pages);
app.set('index_page', settings.index_page);
app.set('block_page', settings.block_page);
app.set('transaction_page', settings.transaction_page);
app.set('address_page', settings.address_page);
app.set('error_page', settings.error_page);
app.set('masternodes_page', settings.masternodes_page);
app.set('movement_page', settings.movement_page);
app.set('network_page', settings.network_page);
app.set('richlist_page', settings.richlist_page);
app.set('markets_page', settings.markets_page);
app.set('api_page', settings.api_page);
app.set('claim_address_page', settings.claim_address_page);
app.set('orphans_page', settings.orphans_page);
app.set('captcha', settings.captcha);
app.set('labels', settings.labels);
app.set('default_coingecko_ids', settings.default_coingecko_ids);
app.set('api_cmds', settings.api_cmds);
app.set('blockchain_specific', settings.blockchain_specific);
app.set('plugins', settings.plugins);
// determine panel offset based on which panels are enabled
var paneltotal = 5;
var panelcount = (settings.shared_pages.page_header.panels.network_panel.enabled == true && settings.shared_pages.page_header.panels.network_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.difficulty_panel.enabled == true && settings.shared_pages.page_header.panels.difficulty_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.masternodes_panel.enabled == true && settings.shared_pages.page_header.panels.masternodes_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.coin_supply_panel.enabled == true && settings.shared_pages.page_header.panels.coin_supply_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.price_panel.enabled == true && settings.shared_pages.page_header.panels.price_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.usd_price_panel.enabled == true && settings.shared_pages.page_header.panels.usd_price_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.market_cap_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.usd_market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.usd_market_cap_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.logo_panel.enabled == true && settings.shared_pages.page_header.panels.logo_panel.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.spacer_panel_1.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_1.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.spacer_panel_2.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_2.display_order > 0 ? 1 : 0) +
(settings.shared_pages.page_header.panels.spacer_panel_3.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_3.display_order > 0 ? 1 : 0);
app.set('paneloffset', paneltotal + 1 - panelcount);
// determine panel order
var panel_order = new Array();
if (settings.shared_pages.page_header.panels.network_panel.enabled == true && settings.shared_pages.page_header.panels.network_panel.display_order > 0) panel_order.push({name: 'network_panel', val: settings.shared_pages.page_header.panels.network_panel.display_order});
if (settings.shared_pages.page_header.panels.difficulty_panel.enabled == true && settings.shared_pages.page_header.panels.difficulty_panel.display_order > 0) panel_order.push({name: 'difficulty_panel', val: settings.shared_pages.page_header.panels.difficulty_panel.display_order});
if (settings.shared_pages.page_header.panels.masternodes_panel.enabled == true && settings.shared_pages.page_header.panels.masternodes_panel.display_order > 0) panel_order.push({name: 'masternodes_panel', val: settings.shared_pages.page_header.panels.masternodes_panel.display_order});
if (settings.shared_pages.page_header.panels.coin_supply_panel.enabled == true && settings.shared_pages.page_header.panels.coin_supply_panel.display_order > 0) panel_order.push({name: 'coin_supply_panel', val: settings.shared_pages.page_header.panels.coin_supply_panel.display_order});
if (settings.shared_pages.page_header.panels.price_panel.enabled == true && settings.shared_pages.page_header.panels.price_panel.display_order > 0) panel_order.push({name: 'price_panel', val: settings.shared_pages.page_header.panels.price_panel.display_order});
if (settings.shared_pages.page_header.panels.usd_price_panel.enabled == true && settings.shared_pages.page_header.panels.usd_price_panel.display_order > 0) panel_order.push({name: 'usd_price_panel', val: settings.shared_pages.page_header.panels.usd_price_panel.display_order});
if (settings.shared_pages.page_header.panels.market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.market_cap_panel.display_order > 0) panel_order.push({name: 'market_cap_panel', val: settings.shared_pages.page_header.panels.market_cap_panel.display_order});
if (settings.shared_pages.page_header.panels.usd_market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.usd_market_cap_panel.display_order > 0) panel_order.push({name: 'usd_market_cap_panel', val: settings.shared_pages.page_header.panels.usd_market_cap_panel.display_order});
if (settings.shared_pages.page_header.panels.logo_panel.enabled == true && settings.shared_pages.page_header.panels.logo_panel.display_order > 0) panel_order.push({name: 'logo_panel', val: settings.shared_pages.page_header.panels.logo_panel.display_order});
if (settings.shared_pages.page_header.panels.spacer_panel_1.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_1.display_order > 0) panel_order.push({name: 'spacer_panel_1', val: settings.shared_pages.page_header.panels.spacer_panel_1.display_order});
if (settings.shared_pages.page_header.panels.spacer_panel_2.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_2.display_order > 0) panel_order.push({name: 'spacer_panel_2', val: settings.shared_pages.page_header.panels.spacer_panel_2.display_order});
if (settings.shared_pages.page_header.panels.spacer_panel_3.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_3.display_order > 0) panel_order.push({name: 'spacer_panel_3', val: settings.shared_pages.page_header.panels.spacer_panel_3.display_order});
panel_order.sort(function(a,b) { return a.val - b.val; });
for (var i = 1; i < 6; i++)
app.set('panel'+i.toString(), ((panel_order.length >= i) ? panel_order[i-1].name : ''));
app.set('market_data', market_data);
app.set('market_count', market_count);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler - will print stacktrace when in development mode, otherwise no stacktraces will be leaked to the user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: (app.get('env') === 'development' ? err : {})
});
});
// determine if tls features should be enabled
if (settings.webserver.tls.enabled == true) {
function readCertsSync() {
var tls_options = {};
try {
tls_options = {
key: db.fs.readFileSync(settings.webserver.tls.key_file),
cert: db.fs.readFileSync(settings.webserver.tls.cert_file),
ca: db.fs.readFileSync(settings.webserver.tls.chain_file)
};
} catch(e) {
console.warn('There was a problem reading tls certificates. Check that the certificate, chain and key paths are correct.');
}
return tls_options;
}
const https = require('https');
let httpd = https.createServer(readCertsSync(), app).listen(settings.webserver.tls.port);
try {
let waitForCertsToRefresh;
// watch for changes to the certificate directory
db.fs.watch(path.dirname(settings.webserver.tls.key_file), () => {
clearTimeout(waitForCertsToRefresh);
// refresh certificates as they are changed on disk
waitForCertsToRefresh = setTimeout(() => {
httpd.setSecureContext(readCertsSync());
}, 1000);
});
} catch(e) {
console.warn('There was a problem reading tls certificates. Check that the certificate, chain and key paths are correct.');
}
}
// get the latest git commit id (if exists)
exec('git rev-parse HEAD', (err, stdout, stderr) => {
// check if the commit id was returned
if (stdout != null && stdout != '') {
// set the explorer revision code based on the git commit id
app.set('revision', stdout.substring(0, 7));
}
});
module.exports = app;