Source: server.js

/**
* @file A JSON-RPC 2.0 WebSocket and HTTP server. The full specification (<a href="http://www.jsonrpc.org/specification">http://www.jsonrpc.org/specification</a>), including batched requests is supported.
* @version 0.5.1
* @author Patrick Bay
* @copyright MIT License
*
* @todo Separate CypherPoker.JS-specific and generic WebSocket Sessions functionality and publish WSS as an independent project (e.g. NPM + GitHub)
*/

//JSDoc typedefs:
/**
* HTTP response headers used with HTTP / HTTPS endpoints.
* @typedef {Array} HTTP_Headers_Array
* @default [{"Access-Control-Allow-Origin" : "*"},
*  {"Content-Type" : "application/json-rpc"}]
*/
/**
* Server objects exposed to API modules.
* @typedef {Object} Exposed_API_Objects
* @default {
*  namespace:namespace,<br/>
*  config:{@link config},<br/>
*  getConfigByPath:{@link getConfigByPath},<br/>
*  require:require,<br/>
*  process:process,<br/>
*  Buffer:Buffer,<br/>
*  request:request,<br/>
*  console:console,<br/>
*  module:module,<br/>
*  hostEnv:{@link hostEnv},<br/>
*  setInterval:setInterval,<br/>
*  clearInterval:clearInterval,<br/>
*  setTimeout:setTimeout,<br/>
*  clearTimeout:clearTimeout,<br/>
*  crypto:crypto,<br/>
*  bigInt:bigInt,
*  getHandler:{@link getHandler}<br/>
*  bitcoin:bitcoin,<br/>
*  secp256k1:secp256k1,<br/>
*  sendResult:{@link sendResult},<br/>
*  sendError:{@link sendError},<br/>
*  buildJSONRPC:{@link buildJSONRPC},<br/>
*  paramExists:{@link paramExists},<br/>
*  JSONRPC_ERRORS:{@link JSONRPC_ERRORS}
* }
*/
/**
* Server objects exposed to external library, adapter, gateway, and other add-on
* modules. Note that because libraries are not executed in their own virtual machines
* they have access to most top-level objects.
* @typedef {Object} Exposed_Library_Objects
* @default {
*  namespace:namespace,<br/>
*  config:{@link config},<br/>
*  require:require,<br/>
*  Buffer:Buffer,<br/>
*  request:request,<br/>
*  console:console,<br/>
*  module:module,<br/>
*  hostEnv:{@link hostEnv},<br/>
*  setInterval:setInterval,<br/>
*  clearInterval:clearInterval,<br/>
*  setTimeout:setTimeout,<br/>
*  clearTimeout:clearTimeout,<br/>
*  bigInt:bigInt,<br/>
*  crypto:crypto,<br/>
*  getHandler:getHandler,<br/>
*  bitcoin:bitcoin,<br/>
*  secp256k1:secp256k1,<br/>
*  sendResult:{@link sendResult},<br/>
*  sendError:{@link sendError},<br/>
*  buildJSONRPC:{@link buildJSONRPC},<br/>
*  paramExists:{@link paramExists},<br/>
*  getConfigByPath:{@link getConfigByPath},<br/>
*  mkdirSync:{@link mkdirSync},<br/>
*  validateJSONRPC:{@link validateJSONRPC},<br/>
*  handleHTTPRequest:{@link handleHTTPRequest}
,<br/>
*  processRPCRequest:{@link processRPCRequest},<br/>
*  invokeAPIFunction:{@link invokeAPIFunction},<br/>
*  JSONRPC_ERRORS:{@link JSONRPC_ERRORS}<br/>
* }
*/

/**
* @typedef {ws} wsserv A WebSocket endpoint ("ws" module).
*/
/**
* @typedef {http} httpserv A HTTP endpoint ("http" module).
*/
//Required installed modules:
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const http = require("http");
const https = require("https");
var Gateways = null; //loaded dynamically post-config
const websocket = require("ws");
const request = require("request");
const crypto = require("crypto");
const bigInt = require("big-integer");
const bitcoin = require("bitcoinjs-lib");
const bitcoincash = require("bitcoinjs-lib");
const secp256k1 = require("secp256k1"); //required for Bitcoin transaction signing


/**
* @property {Object} namespace A global object containing references and data shared by API scripts.
*/
const namespace = new Object();
/**
* @property {String} configFile="./config.json" Location of JSON data file to load and parse into the
* {@link config} object. May be either a filesystem path or a URL.
*/
const configFile = "./config.json";
/**
* @property {Object} hostEnv An object containing data and references supplied by the host
* environment if the server is running as an embedded component in a
* desktop (Electron) environment. Refer to the embedding (parent) script for details
* on supplied properties and types.
* @property {Boolean} hostEnv.embedded=false True if the server is running as an
* embedded component in a desktop (Electron) environment.
*/
var hostEnv = {
   embedded:false
};
/**
* @property {Object} config Dynamic configuration options loaded at runtime.
*/
var config = new Object();
/**
* @property {httpserv} http_server The default HTTP endpoint.
*/
var http_server;
/**
* @property {wsserv} ws_server The default WebSocket endpoint.
*/
var ws_server;
/**
* @property {Gateways} gateways A reference to the gateways manager instance.
*/
var gateways;
/**
* @const {Number} [ws_ping_interval=15000] The number of milliseconds to ping connected
* WebSockets at in order to keep them alive.
*/
const ws_ping_interval = 15000;
/**
* @property {Number} ws_ping_intervalID The interval ID of the WebSocket server
* ping / keep-alive timer.
*/
var ws_ping_intervalID = null;

/**
* Defines all standard and application-specific JSON-RPC 2.0 error codes. Standard error codes are in the range -32700 to -32603 while application-specific
* error codes are in the range -32001 to -32099. Error codes in the range -32768 to -32000 are reserved for future use.
* @see http://www.jsonrpc.org/specification#error_object
*
* @property {Number} PARSE_ERROR=-32700 Invalid JSON was received (the request cannot be parsed).
* @property {Number} REQUEST_ERROR=-32600 The request is not a valid JSON-RPC 2.0 request.
* @property {Number} METHOD_NOT_FOUND_ERROR=-32601 The requested RPC method does not exist.
* @property {Number} INVALID_PARAMS_ERROR=-32602 The parameters supplied for the requested RPC method are invalid.
* @property {Number} INTERNAL_ERROR=-32603 There was an internal server error when attempting to process the RPC request.
* @property {Number} SESSION_CLOSE=-32001 The session is about to terminate and any current tokens / credentials will no longer be
* accepted.
* @property {Number} WRONG_TRANSPORT=-32002 The wrong transport was used to deliver API request (e.g. used "http" or "https" instead of "ws" or "wss").
* @property {Number} ACTION_DISALLOWED=-32003 The action being requested is not currently allowed.
* @property {Number} PLAYER_ACTION_ERROR=-32005 A player has committed a penalizable wrong or incorrect action.
*/
const JSONRPC_ERRORS = {
	PARSE_ERROR: -32700,
	REQUEST_ERROR: -32600,
	METHOD_NOT_FOUND_ERROR: -32601,
	INVALID_PARAMS_ERROR: -32602,
	INTERNAL_ERROR: -32603,
   SESSION_CLOSE: -32001,
   WRONG_TRANSPORT: -32002,
   ACTION_DISALLOWED: -32003,
   AUTH_FAILED: -32004,
   PLAYER_ACTION_ERROR: -32005
}

/**
* The default options for the RPC server.
*
* @property {String} api_dir="./api" A directory containing all available API methods that may be invoked. Each API method must match
* a filename and the entry function in that file must also have the same name.
* @property {Number} http_port=8080 The default listening port for the HTTP server. This value may be overriden by a {@link config} setting.
* @property {Number} ws_port=8090 The listening port for the WebSocket server. This value may be overriden by a {@link config} setting.
* @property {Number} max_batch_requests=5 The maximum allowable number of batched RPC calls in a single request. If more than this number
* of calls are encountered in a request batch a JSONRPC_INTERNAL_ERROR error is thrown.
* @property {HTTP_Headers_Array} http_headers Default headers to include in HTTP / HTTPS responses. Each array element is an object
* containing a name / value pair.
* @property {Exposed_API_Objects} exposed_api_objects Internal server references to expose to external API functions. Note that each internal
* reference may be assigned an alias instead of its actual name.
* @property {Number} api_timelimit=3000 The time limit, in milliseconds, to allow external API functions to execute.
* @property {Boolean} http_only_handshake=false Defines whether session handshakes are done only through HTTP/HTTPS (true),
* or if they can be done through the WebSocket server (false).
* @property {Number} max_ws_per_ip=10 The maximum number of concurrent WebSocket connections to allow from a single IP.
* @property {Exposed_Library_Objects} exposed_library_objects Internal server references to expose to libraries, adapters, communication gateways functions, and other
* server add-ons. Note that each internal reference may be assigned an alias instead of its actual name.

*/
var rpc_options = {
  api_dir: "./api",
  http_port: 8080,
  ws_port: 8090,
  max_batch_requests: 5,
  http_headers: [
		{"Access-Control-Allow-Origin" : "*"}, //CORS header for global access
		{"Content-Type" : "application/json-rpc"}
  ],
  exposed_api_objects: {
    namespace:namespace,
    config:config,
    getConfigByPath:getConfigByPath,
    require:require,
    Buffer:Buffer,
    request:request,
    console:console,
    module:module,
    hostEnv:hostEnv,
    setInterval:setInterval,
    clearInterval:clearInterval,
    setTimeout:setTimeout,
    clearTimeout:clearTimeout,
    bigInt:bigInt,
    crypto:crypto,
    getHandler:getHandler,
    bitcoin:bitcoin,
    bitcoincash:bitcoincash,
    secp256k1:secp256k1,
    sendResult:sendResult,
    sendError:sendError,
    buildJSONRPC:buildJSONRPC,
    paramExists:paramExists,
    JSONRPC_ERRORS:JSONRPC_ERRORS
  },
  api_timelimit: 3000,
  http_only_handshake: false,
  max_ws_per_ip:10,
  exposed_library_objects: {
    namespace:namespace,
    config:config,
    require:require,
    Buffer:Buffer,
    request:request,
    console:console,
    module:module,
    hostEnv:hostEnv,
    setInterval:setInterval,
    clearInterval:clearInterval,
    setTimeout:setTimeout,
    clearTimeout:clearTimeout,
    bigInt:bigInt,
    crypto:crypto,
    getHandler:getHandler,
    mkdirSync:mkdirSync,
    bitcoin:bitcoin,
    bitcoincash:bitcoincash,
    secp256k1:secp256k1,
    sendResult:sendResult,
    sendError:sendError,
    buildJSONRPC:buildJSONRPC,
    paramExists:paramExists,
    getConfigByPath:getConfigByPath,
    validateJSONRPC:validateJSONRPC,
    handleHTTPRequest:handleHTTPRequest,
    processRPCRequest:processRPCRequest,
    invokeAPIFunction:invokeAPIFunction,
    JSONRPC_ERRORS:JSONRPC_ERRORS
   }
}
rpc_options.exposed_api_objects.rpc_options = rpc_options; // expose the options too (circular reference!)

/**
* @const {Object} _APIFunctions Enumerated functions found in the API directory as specified in {@linkcode rpc_options}.
*/
var _APIFunctions = new Object();

/**
* Handles all uncaught exceptions, allowing the server to keep running and responding to requests.
* Additional error handling / tracking / reporting can be added here.
*
* @listens global -> uncaughtException
*/
process.on('uncaughtException', (err) => {
  console.error(err.stack);
});

/**
* Asynchronously loads and parses an external JSON configuration file, making it available
* as the global {@link config} object.
*
* @param {String} [filePath={@link configFile}] The URL or filesystem path of the
* external configuration file to load. If the <code>RPCOptions.host</code> property
* is included (the request is being made to a shared hosting environment), this URL
* should contain the server IP rather than the domain name.
* @param {Object} [RPCOptions=null] If the <code>filePath</code> points to a
* JSON-RPC 2.0 endpoint, this object contains additional properties to use
* for the call.
* @param {String} RPCOptions.method The RPC method to invoke in order
* to retrieve the configuation data.
* @param {String} [RPCOptions.params] The RPC parameters to include
* when invoking the <code>RPCOptions.method</code>. If not defined, an empty
* <code>params</code> object is included with the request.
* @param {String} [RPCOptions.host] If defined, this property sets the "Host" HTTP
* header in the request when accessing a shared hosting environment. The
* <code>filePath</code> parameter should contain the server's IP address rather
* than a domain name.
*
* @return {Promise} The promise resolves with the loaded and parsed configuration
* object and rejects with the error when the file could not be loaded or parsed.
*/
function loadConfig(filePath=configFile, RPCOptions=null) {
   var promise = new Promise(function(resolve, reject) {
      filePath = filePath.trim();
      var isURL = false;
      if (filePath.toLowerCase().startsWith("http://") || filePath.toLowerCase().startsWith("https://")) {
         isURL = true;
      }
      if (isURL) {
         if (RPCOptions != null) {
            if ((RPCOptions.method == undefined) || (RPCOptions.method == null) || (RPCOptions.method == "")) {
               reject("No JSON-RPC 2.0 \"method\" defined for request.");
               return;
            }
            if (typeof(RPCOptions.params) != "object") {
               RPCOptions.params = new Object();
            }
            var headers = {"Content-Type": "application/json-rpc"};
            if (typeof(RPCOptions.host) == "string") {
               console.log ("Loading external configuration from RPC: "+filePath+" (shared host: "+RPCOptions.host+")");
               headers.Host = RPCOptions.host;
            } else {
               console.log ("Loading external configuration from RPC: "+filePath);
            }
            var requestObj = {
               "jsonrpc":"2.0",
               "id":"0",
               "method":RPCOptions.method,
               "params":RPCOptions.params
            }
            request({
               url: filePath,
               method: "POST",
               body: requestObj,
               headers: headers,
               json:true
            }, (error, response, body) => {
               if (error) {
                  reject(error);
               } else {
                  if ((body.error == undefined)) {
                     config = body.result;
                     resolve(config);
                  } else {
                     reject(body.error);
                  }
               }
            });
         } else {
            console.log ("Loading external configuration from URL: "+filePath);
            request({
               url: filePath,
               method: "GET",
               json: true
            }, (error, response, body) => {
               if (error) {
                  reject(error);
               } else {
                  config = body;
                  resolve(body);
               }
            });
         }
      } else {
         if (hostEnv.embedded == true) {
            filePath = hostEnv.dir.server + filePath;
            filePath = filePath.split("/./").join("/"); //remove extraneous same-folder path
         }
         console.log ("Loading external configuration from filesystem: "+filePath);
         try {
            var JSONData = fs.readFileSync(filePath);
            config = JSON.parse(JSONData);
            resolve (config);
         } catch (err) {
            reject (err);
         }
      }
   });
   return (promise);
}

/**
* Loads and verifies all API functions in the directory specified in {@linkcode rpc_options} and optionally invokes one or more functions when completed.
*
* @param {...Function} [postLoadFunctions] Any function(s) to invoke once the API functions have been loaded and verified.
*/
function loadAPIFunctions(...postLoadFunctions) {
   if (hostEnv.embedded == true) {
      rpc_options.api_dir = hostEnv.dir.server + rpc_options.api_dir;
      rpc_options.api_dir = rpc_options.api_dir.split("/./").join("/"); //remove extraneous same-folder path
   }
  if (fs.existsSync(rpc_options.api_dir) == false) {
    throw (new Error(`API directory "${rpc_options.api_dir}" is not accessible.`));
  }
  if (fs.lstatSync(rpc_options.api_dir).isDirectory() == false) {
    throw (new Error(`"${rpc_options.api_dir}" is not a directory.`));
  }
  //rpc_options.api_dir exists and is a directory
  var fileList = fs.readdirSync(rpc_options.api_dir);
  fileList.forEach(registerAPIFunction);
  for (var count=0; count<postLoadFunctions.length; count++) {
    postLoadFunctions[count]();
  }
}

/**
* Registers an API function handler stored in a specified file in the internal {@linkcode _APIFunctions} array.
*
* @param {String} filename The full path of the file to register as an API handler.
*/
function registerAPIFunction(fileName) {
  var folderPrefix = rpc_options.api_dir;
  if (folderPrefix.endsWith("/") == false) {
    folderPrefix += "/";
  }
  var fullPath = folderPrefix + fileName;
  if ((fs.lstatSync(fullPath).isDirectory() == false) &&
    (fullPath.endsWith(".js") || fullPath.endsWith(".javascript"))) {
    //only process files ending in ".js" or ".javascript"
    var script = fs.readFileSync(fullPath, {encoding:"UTF-8"});
    var vmContext = new Object();
    rpc_options.exposed_api_objects.hostEnv = hostEnv; //overwrite default reference
    vmContext = Object.assign(rpc_options.exposed_api_objects, vmContext);
    var context = vm.createContext(vmContext);
    try {
      vm.runInContext(script, context, {timeout:rpc_options.api_timelimit});
   } catch (err) {
      console.error ("Error registering API function: \n"+err.stack);
      return;
   }
    var functionName = path.basename(fullPath).split(".")[0];
    if (typeof(context[functionName]) == "function") {
      var functionObj = new Object();
      functionObj.script = script;
      _APIFunctions[functionName] = functionObj;
      console.log(`Registered external API function "${functionName}" (${fullPath}).`);
    } else {
      console.log(`External reference "${functionName}" (${fullPath}) is not a function. Not registered.`);
    }
  }
}

/**
* Returns a configuration setting from the configuration data such as the
* global application {@link config} via a do-notation path string (as in
* standard JavaScript dot notation).
*
* @param {String} configPath The path of the object within the configuration object
* to retrieve.
* @param {Object} [configObj=config] The current configuration object being evaluated.
* This reference is passed for recursion but may be used to limit the search to specific
* child objects only.
*
* @return {*} The first matching configuration object or property, or <code>null</code> if
* it can't be found.
*/
function getConfigByPath(configPath, configObj=config) {
   var pathSplit = configPath.split(".")
   var currentItem = pathSplit.shift();
   var found = null;
   for (var configItem in configObj) {
      if (configItem == currentItem) {
         if (pathSplit.length > 0) {
            found = getConfigByPath(pathSplit.join("."), configObj[configItem])
         } else {
            return (configObj[configItem]);
         }
      }
   }
   return (found);
}

/**
* Verifies whether a data object is a valid JSON-RPC 2.0 request.
*
* @param {Object} dataObj The parsed object to check for validity.
*
* @return {String} A description of the validation failure, or null if the validation passed.
* @see http://www.jsonrpc.org/specification
*/
function validateJSONRPC (dataObj) {
  if ((dataObj["jsonrpc"] == null) || (dataObj["jsonrpc"] == undefined)) {
    return (`Missing "jsonrpc" property.`);
  }
  if (dataObj.jsonrpc != "2.0") {
		return (`Expecting JSON-RPC version "2.0" (exactly). Got "${dataObj.jsonrpc}""`);
	}
	if ((dataObj["method"] == undefined) || (dataObj["method"] == null)) {
		return (`Missing "method" property.`);
	}
  try {
  	if (dataObj.method.split(" ").join("") == "") {
  		return (`Property "method" is empty.`);
  	}
  } catch (err) {
    return (`Property "method" must be a string.`);
  }
  return (null);
}

/**
* Main RPC request handler for all endpoints. Successfully parsed requests are passed to {@link invokeAPIFunction} otherwise an
* error is immediately returned.
*
* @param {String} requestData The raw, unparsed request body.
* @param {Object} sessionObj An object containing data for the current sesssion.
* The contents of this object will differ depending on the type of endpoint that received the original request.
* @param {String} sessionObj.endpoint The source / target endpoint type associated with this session object. Valid values include:<br/>
* "ws" - WebSocket<br/>
* "wss" - secure WebSocket<br/>
* "wstunnel" - WebSocket tunnel<br/>
* "http" - HTTP<br/>
* "https" - secure HTTP
* @param {Object} sessionObj.requestObj The full, parsed JSON-RPC 2.0 object as received in the original request.
* @param {String} sessionObj.serverRequest The functional request object, either an <a href="https://nodejs.org/api/http.html#http_class_http_incomingmessage">IncomingMessage</a> instance when
* the endpoint is "http" or "https", or the source WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}.
* @param {String} sessionObj.serverResponse The functional response object either a <a href="https://nodejs.org/api/http.html#http_class_http_serverresponse">ServerResponse</a> instance when
* the endpoint is "http" or "https", or the target WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}
*/
function processRPCRequest(requestData, sessionObj) {
   var parsed = false;
   try {
      sessionObj.requestObj = JSON.parse(requestData);
      if ((sessionObj.requestObj != null) && (sessionObj.requestObj != undefined) && (sessionObj.requestObj != "")) {
         parsed = true;
      }
   } catch (err) {
   } finally {
    if (!parsed) {
      sessionObj.requestObj = new Object();
		sendError(JSONRPC_ERRORS.PARSE_ERROR, "Request empty or malformed.", sessionObj);
		return;
    }
   }
	if (isNaN(sessionObj.requestObj.length)) {
      //single request
      var validationResult = validateJSONRPC(sessionObj.requestObj);
      if (validationResult != null) {
        sendError(JSONRPC_ERRORS.REQUEST_ERROR, validationResult, sessionObj);
        return;
      } else {
        invokeAPIFunction(sessionObj);
      }
	} else {
    //batched requests
		if (sessionObj.requestObj.length > rpc_options.max_batch_requests) {
      var errString = `No more than ${rpc_options.max_batch_requests} batched requests allowed. Request has ${sessionObj.requestObj.length} requests.`;
			sendError(JSONRPC_ERRORS.INTERNAL_ERROR, validationResult, sessionObj);
			return;
		}
		var batchResponses = new Object();
		batchResponses.responses = new Array();
		batchResponses.total = sessionObj.requestObj.length;
		for (var count = 0; count < sessionObj.requestObj.length; count++) {
         var validationResult = validateJSONRPC(sessionObj.requestObj[count]);
         if (validationResult != null) {
           sendError(JSONRPC_ERRORS.REQUEST_ERROR, validationResult, sessionObj);
           return;
         } else {
             invokeAPIFunction(sessionObj, count);
         }
		}
	}
}

/**
* Invokes an external API function as requested by a RPC call.
*
* @param {Object} sessionObj An object containing data for the current sesssion.
* The contents of this object will differ depending on the type of endpoint that received the original request.
* @param {String} sessionObj.endpoint The source / target endpoint type associated with this session object. Valid values include:<br/>
* "ws" - WebSocket<br/>
* "wss" - secure WebSocket<br/>
* "wstunnel" - WebSocket tunnel<br/>
* "http" - HTTP<br/>
* "https" - secure HTTP
* @param {Object} sessionObj.requestObj The full, parsed JSON-RPC 2.0 object as received in the original request.
* @param {String} sessionObj.serverRequest The functional request object, either an <a href="https://nodejs.org/api/http.html#http_class_http_incomingmessage">IncomingMessage</a> instance when
* the endpoint is "http" or "https", or the source WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}.
* @param {String} sessionObj.serverResponse The functional response object either a <a href="https://nodejs.org/api/http.html#http_class_http_serverresponse">ServerResponse</a> instance when
* the endpoint is "http" or "https", or the target WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}.
* @param {Number} [requestNum=null] The index of the batched request within sessionObj.requestObj to process if the request is a batched request. If null or omitted then the requests is processed as
* a single request.
*/
function invokeAPIFunction(sessionObj, requestNum=null) {
  if (requestNum == undefined) {
    requestNum = null;
  }
  if (requestNum != null) {
    var requestMethod = sessionObj.requestObj[requestNum].method;
  } else {
    requestMethod = sessionObj.requestObj.method;
  }
  if (_APIFunctions[requestMethod] == undefined) {
     if (requestMethod.trim().startsWith("rpc:")) {
        //an optional proposal for the JSON-RPC 2.0 spec (system extensions)
        sendError(JSONRPC_ERRORS.REQUEST_ERROR, "System extensions (\"rpc:\") are not implemented.", sessionObj);
     } else {
        sendError(JSONRPC_ERRORS.METHOD_NOT_FOUND_ERROR, "Method \""+requestMethod+"\" does not exist.", sessionObj);
     }
    return;
  } else {
    var script = _APIFunctions[requestMethod].script;
    var vmContext = new Object();
    vmContext = Object.assign(rpc_options.exposed_api_objects, vmContext);
    var context = vm.createContext(vmContext);
    vm.runInContext(script, context, {timeout:rpc_options.api_timelimit});
    context[requestMethod](sessionObj).catch(err => {
       sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "An internal server error occurred while processing your request.", sessionObj);
       console.error ("API invocation error: \n"+err.stack);
    });
  }
}

/**
* Generates a final JSON-RPC 2.0 error object and sends it to the requestor via the source endpoint.
*
* @param {Number} code A RPC error result code to include in the response. This code is non-standard and is defined on a per-application basis.
* @param {String} message A human-readable error message to include in the response.
* @param {Object} sessionObj An object containing data for the current sesssion.
* The contents of this object will differ depending on the type of endpoint that received the original request.
* @param {String} sessionObj.endpoint The source / target endpoint type associated with this session object. Valid values include:<br/>
* "ws" - WebSocket<br/>
* "wss" - secure WebSocket<br/>
* "wstunnel" - WebSocket tunnel<br/>
* "http" - HTTP<br/>
* "https" - secure HTTP
* @param {Object} sessionObj.requestObj The full, parsed JSON-RPC 2.0 object as received in the original request.
* @param {String} sessionObj.serverRequest The functional request object, either an <a href="https://nodejs.org/api/http.html#http_class_http_incomingmessage">IncomingMessage</a> instance when
* the endpoint is "http" or "https", or the source WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}.
* @param {String} sessionObj.serverResponse The functional response object either a <a href="https://nodejs.org/api/http.html#http_class_http_serverresponse">ServerResponse</a> instance when
* the endpoint is "http" or "https", or the target WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}.
* @param {Object} sessionObj.batchResponses An object containing batched responses if the original request was a batched request.
* @param {*} [data] Any additional relevant data to include in the response.
*/
function sendError(code, message, sessionObj, data) {
	var requestData = sessionObj.requestObj;
	var responseData = buildJSONRPC(null, false); //use default version,  create error message (isResult=false)
	if ((requestData["id"] == null) || (requestData["id"] == undefined)) {
		responseData.id = null;
	} else {
		responseData.id = requestData.id;
	}
	responseData.error = new Object();
	responseData.error.code = code;
	responseData.error.message = message;
	if (data != undefined) {
		responseData.error.data = data;
	}
   try {
      if (sessionObj.batchResponses != null) {
         //handle batched responses
   		sessionObj.batchResponses.responses.push(responseData);
   		if (sessionObj.batchResponses.total == sessionObj.batchResponses.responses.length) {
            switch (sessionObj.endpoint) {
              case "http":
                setDefaultHeaders(sessionObj.serverResponse);
        			  sessionObj.serverResponse.end(JSON.stringify(sessionObj.batchResponses.responses));
                break;
              case "https":
                break;
              case "ws":
                sessionObj.serverResponse.send(JSON.stringify(sessionObj.batchResponses.responses));
                break;
              case "wss":
                break;
             case "wstunnel":
               sessionObj.serverResponse.send(JSON.stringify(sessionObj.batchResponses.responses));
               break;
              default:
                throw (new Error(`Unsupported endpoint type ${sessionObj.endpoint}`));
                break;
            }
   		}
   	} else {
       //handle single response
       switch (sessionObj.endpoint) {
         case "http":
           setDefaultHeaders(sessionObj.serverResponse);
           sessionObj.serverResponse.end(JSON.stringify(responseData));
           break;
         case "https":
           setDefaultHeaders(sessionObj.serverResponse);
           sessionObj.serverResponse.end(JSON.stringify(responseData));
           break;
         case "ws":
           sessionObj.serverResponse.send(JSON.stringify(responseData));
           break;
         case "wss":
           break;
        case "wstunnel":
          sessionObj.serverResponse.send(JSON.stringify(responseData));
          break;
         default:
           throw (new Error(`Unsupported endpoint type ${sessionObj.endpoint}`));
           break;
       }
    }
   } catch (err) {
      console.error(err.stack);
   }
}

/**
* Generates a final JSON-RPC 2.0 response (result) object and sends it to the requestor via the source endpoint.
*
* @param {Object} result The result object to include with the JSON-RPC response object, usually the result of the RPC call.
* @param {Object} sessionObj An object containing data for the current sesssion.
* The contents of this object will differ depending on the type of endpoint that received the original request.
* @param {String} sessionObj.endpoint The source / target endpoint type associated with this session object. Valid values include:<br/>
* "ws" - WebSocket<br/>
* "wss" - secure WebSocket<br/>
* "wstunnel" - WebSocket tunnel<br/>
* "http" - HTTP<br/>
* "https" - secure HTTP
* @param {Object} sessionObj.requestObj The full, parsed JSON-RPC 2.0 object as received in the original request.
* @param {String} sessionObj.serverRequest The functional request object, either an <a href="https://nodejs.org/api/http.html#http_class_http_incomingmessage">IncomingMessage</a> instance when
* the endpoint is "http" or "https", or the source WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}.
* @param {String} sessionObj.serverResponse The functional response object either a <a href="https://nodejs.org/api/http.html#http_class_http_serverresponse">ServerResponse</a> instance when
* the endpoint is "http" or "https", or the target WebSocket connection when the endpoint is "ws" or "wss".
* Tunneled endpoints are usually handled by custom [Gateway]{@link libs/Gateway.js}-based classes such as [WSSTunnel]{@link libs/gateways/WSSTunnel.js}.
* @param {Object} sessionObj.batchResponses An object containing batched responses if the original request was a batched request.
*/
function sendResult(result, sessionObj) {;
	var requestData = sessionObj.requestObj;
	var responseData = buildJSONRPC();
  //copy id from request to maintain session continuity
	if ((requestData["id"] == null) || (requestData["id"] == undefined)) {
		responseData.id = null;
	} else {
		responseData.id = requestData.id;
	}
	responseData.result = result;
   try {
   	if (sessionObj.batchResponses != null) {
         //handle batched responses
   		sessionObj.batchResponses.responses.push(responseData);
   		if (sessionObj.batchResponses.total == sessionObj.batchResponses.responses.length) {
            switch (sessionObj.endpoint) {
              case "http":
                 setDefaultHeaders(sessionObj.serverResponse);
        			  sessionObj.serverResponse.end(JSON.stringify(sessionObj.batchResponses.responses));
                break;
              case "https":
                break;
              case "ws":
                sessionObj.serverResponse.send(sessionObj.batchResponses.responses);
                break;
              case "wss":
                break;
             case "wstunnel":
               sessionObj.serverResponse.send(sessionObj.batchResponses.responses);
               break;
              default:
                throw (new Error(`Unsupported endpoint type ${sessionObj.endpoint}`));
                break;
            }
   		}
      } else {
          //handle single response
          switch (sessionObj.endpoint) {
            case "http":
              setDefaultHeaders(sessionObj.serverResponse);
              sessionObj.serverResponse.end(JSON.stringify(responseData));
              break;
            case "https":
              setDefaultHeaders(sessionObj.serverResponse);
              sessionObj.serverResponse.end(JSON.stringify(responseData));
              break;
            case "ws":
              sessionObj.serverResponse.send(JSON.stringify(responseData));
              break;
            case "wss":
              break;
           case "wstunnel":
             sessionObj.serverResponse.send(JSON.stringify(responseData));
             break;
            default:
              throw (new Error(`Unsupported endpoint type ${sessionObj.endpoint}`));
              break;
          }
      }
   } catch (err) {
      console.error(err);
   }
}

/**
* Builds a JSON-RPC message object.
*
* @param {String} [version="2.0"] The JSON-RPC version to designate the object as.
* Currently only JSON-RPC 2.0 message formatting is supported and other versions
* will throw an error. If this parameter is null, the default value is assumed.
* @param {Boolean} [isResult=true] True if this is a result object or
* notification, false if it's an error.
* @param {Boolean} [includeUniqueID=false] A uniquely generated message ID
* will be generated if true otherwise no ID is included (e.g. notification).
*/
function buildJSONRPC(version="2.0", isResult=true, includeUniqueID=false) {
   var jsonObj = new Object();
   if (version == null) {
      version = "2.0";
   }
   version = version.trim();
   if (version != "2.0") {
      throw (new Error("Unsupported JSON-RPC message format version (\"" + version + "\")"));
   }
   jsonObj.jsonrpc = version;
   if (includeUniqueID) {
      jsonObj.id = String(Date.now()).split("0.").join("");
   }
   if (isResult) {
      jsonObj.result = new Object();
   } else {
      jsonObj.error = new Object();
      jsonObj.error.message = "An error occurred.";
      jsonObj.error.code = JSONRPC_ERRORS.INTERNAL_ERROR;
   }
   return (jsonObj);
}

/**
* Adds the default HTTP headers, as defined in rpc_options.http_headers,
* to a HTTP / HTTPS response object.
*
* @param {Object} serverResponse The
* <a href="https://nodejs.org/api/http.html#http_class_http_serverresponse">
* ServerResponse</a> object to add default headers to.
*/
function setDefaultHeaders(serverResponse) {
	for (var count=0; count < rpc_options.http_headers.length; count++) {
		var headerData = rpc_options.http_headers[count];
		for (var headerType in headerData) {
			serverResponse.setHeader(headerType, headerData[headerType]);
		}
	}
}

/**
* Empty (no operation) function provided as a parameter to the WebSocket ping
* function in {@link ping}.
*/
function noop() {}

/**
* Invoked when a WebSocket client pong is received in response to a {@link ping}.
* Note that this function is invoked in the context of the responding WebSocket
* instance.
*/
function heartbeat() {
   //console.log ("Received pong from: "+this._socket.remoteAddress);
   this.isAlive = true;
}

/**
* Pings all connnected WebSocket clients to ensure that they're still alive
* and active. This function is usually invoked through a timer created by the
* {@link startWSServer} function. Any WebSocket connnections that do not respond to
* pings are automatically closed.
*/
function ping() {
   ws_server.clients.forEach(function each(ws) {
    if (ws.isAlive === false) {
      return (ws.terminate());
    }
    ws.isAlive = false;
    ws.ping(noop);
   });
}

/**
* Checks a JSON-RPC 2.0 request object for the existence of a specific parameter.
*
* @param {Object} requestObj The JSON-RPC 2.0 request object to check for a parameter.
* @param {String} param The parameter name to check for.
* @param {Boolean} [nullAllowed=true] If true, null values are considered as existing, otherwise they are considered as non-existent.
*
* @return {Boolean} True if the specified parameter exists, false otherwise.
*/
function paramExists(requestObj, param, nullAllowed = true) {
   if ((requestObj == null) || (requestObj == undefined)) {
      return (false);
   }
	if ((requestObj["params"] == null) || (requestObj["params"] == undefined)) {
  	   return (false);
	}
	if (requestObj.params[param] == undefined) {
    return (false);
	}
   if ((requestObj.params[param] == null) && (nullAllowed == false)) {
    return (false);
   }
  return (true);
}

/**
* Attempts to start the JSON-RPC 2.0 HTTP server using the default {@link rpc_options}. The {@link onStartHTTPServer} function is invoked when
* the server is successfully started. The {@link handleHTTPRequest} function handles requests.
*
* @async
*/
async function startHTTPServer() {
   if (rpc_options.http_port < 1) {
      console.log ("HTTP server disabled.")
   } else {
   	console.log (`Starting JSON-RPC 2.0 HTTP server on port ${rpc_options.http_port}...`);
   	http_server = http.createServer(handleHTTPRequest);
   	http_server.listen(rpc_options.http_port, onStartHTTPServer);
   }
}


/**
* Attempts to start the JSON-RPC 2.0 WebSocket server using the default {@link rpc_options}. The {@link onStartWSServer} function is invoked when
* the server is successfully started.
*/
function startWSServer() {
   if (rpc_options.ws_port < 1) {
      console.log ("WebSocket server disabled.");
   } else {
   	console.log (`Starting JSON-RPC 2.0 WebSocket server on port ${rpc_options.ws_port}...`);
   	try {
         ws_server = new websocket.Server({ port: rpc_options.ws_port });
         ws_server.on("listening", onStartWSServer);
         ws_server.on("connection", onWSConnection);
         ws_ping_intervalID = setInterval(ping, ws_ping_interval);
   	} catch (err) {
   		console.error (err);
   	}
   }
}

/**
* Invoked when a WebSocket connection is established on the WebSocket server.
*
* @param {WebSocket} wsInstance The WebSocket instance that was just connected.
* @param {Object} request The request object included with the new connection.
* @listens ws_server -> connection
*/
function onWSConnection (wsInstance, request) {
  //console.log (`Established WebSocket connection on -> ${request.connection.remoteAddress}`);
  wsInstance._req = request;
  wsInstance.isAlive = true;
  wsInstance.on('pong', heartbeat);
  wsInstance.on("message", handleWSRequest);
  wsInstance.on("close", handleWSDisconnect);
}


/**
* Function invoked by {@link startHTTPServer} when the HTTP server has been successfully started.
*/
function onStartHTTPServer() {
	console.log (`JSON-RPC 2.0 HTTP server listening on port ${rpc_options.http_port}.`);
}

/**
* Function invoked by {@link startWSServer} when the WebSocket server has been successfully started.
*/
function onStartWSServer () {
	console.log (`JSON-RPC 2.0 WebSocket server listening on port ${rpc_options.ws_port}.`);
}

/**
* Function invoked when a single WebSocket connection is closed / disconnected.
*/
function handleWSDisconnect() {
   //console.log ("WebSocket connection closed");
}

/**
* Handles a WebSocket request by creating a session object and invoking
* {@link processRPCRequest}.
*
* @param {Object} requestData The raw, unparsed request body.
*/
function handleWSRequest(requestData) {
  var sessionObj = new Object();
  sessionObj.endpoint = "ws";
  sessionObj.serverRequest = this;
  sessionObj.serverResponse = this;
  processRPCRequest(requestData, sessionObj);
}

/**
* Handles data received through the HTTP endpoint and invokes {@link processRPCRequest} when a full request is received via a POST
* method. This endpoint may also used to call functionailty through GET requests.
*
* @param {Object} requestObj HTTP request object <a href="https://nodejs.org/api/http.html#http_class_http_incomingmessage">https://nodejs.org/api/http.html#http_class_http_incomingmessage</a>
* @param {Object} responseObj HTTP response object <a href="https://nodejs.org/api/http.html#http_class_http_serverresponse">https://nodejs.org/api/http.html#http_class_http_serverresponse</a>
*/
function handleHTTPRequest(requestObj, responseObj){
	//only headers received at this point so read following POST data in chunks...
	if (requestObj.method == 'POST') {
		var requestData = new String();
		requestObj.on('data', function(chunk) {
			//reading message body...
			if ((chunk != undefined) && (chunk != null)) {
				requestData += chunk;
			}
		});
		requestObj.on('end', function() {
			//message body fully read
         var sessionObj = new Object();
         sessionObj.endpoint = "http";
         sessionObj.serverRequest = requestObj;
         sessionObj.serverResponse = responseObj;
			processRPCRequest(requestData, sessionObj);
		});
   } else if (requestObj.method == 'GET') {
      //process GET request
      //responseObj.send("HTTP response...");
   }
}

/**
* Utility function to recursively create a directory structure. This is a drop-in
* replacement function to use when <code>fs.mkdirSync</code> with the <code>mkdirSync</code>
* option fails (typically when running as an Electron app where the version is < 5.0.0).
* Adapted from [https://stackoverflow.com/a/40686853](https://stackoverflow.com/a/40686853).
*
* @param {String} targetDir The target directory structure to synchronously and recursively create.
*
* @see https://stackoverflow.com/a/40686853
*/
function mkdirSync(targetDir) {
   const sep = path.sep;
   const initDir = path.isAbsolute(targetDir) ? sep : "";
   const baseDir = ".";
   return targetDir.split(sep).reduce((parentDir, childDir) => {
      const curDir = path.resolve(baseDir, parentDir, childDir);
      try {
         fs.mkdirSync(curDir);
      } catch (err) {
         if (err.code === "EEXIST") { // curDir already exists!
            return curDir;
         }
         // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
         if (err.code === "ENOENT") { // Throw the original parentDir error on curDir `ENOENT` failure.
            throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`);
         }
         const caughtErr = ["EACCES", "EPERM", "EISDIR"].indexOf(err.code) > -1;
         if (!caughtErr || caughtErr && curDir === path.resolve(targetDir)) {
            throw err; // Throw if it's just the last created dir.
         }
      }
      return curDir;
   }, initDir);
}

/**
* Adjusts various settings and properties based on the detected runtime environment. For example,
* Zeit (NOW) deployments require only a single WebSocket connection that must be on port 80 (this
* may change).
*/
function adjustEnvironment() {
   //NOW_DC and NOW_REGION are defined for Zeit's Data Centre settings
   if ((typeof(process.env["NOW_DC"]) == "string") && (typeof(process.env["NOW_REGION"]) == "string")) {
      console.log ("Detected Zeit (NOW) runtime environment.")
      rpc_options.http_port = -1; //disable HTTP server altogether
      rpc_options.ws_port = 80; //WebSocket server can only listen on port 80 (forwarded from a secure connection)
      rpc_options.http_only_handshake = false; //enable WebSockets for handshakes (since HTTP server is disabled)
   }
}

/**
* Creates and initializes the account system using either environmental settings,
* command line parameters, or configuration options (in that order).
*
* @param {Function} [onCreateCB=null] An optional callback function to invoke when the
* account system has been successfully created. This callback will not be invoked
* if an error occurs during creation.
*
* @async
* @private
*/
async function createAccountSystem(onCreateCB=null) {
   console.log ("Initializing account system...")
   try {
      if (typeof(process.env["BLOCKCYPHER_TOKEN"]) == "string") {
         config.CP.API.tokens.blockcypher = process.env["BLOCKCYPHER_TOKEN"];
      }
      if (typeof(process.env["DB_URL"]) == "string") {
         config.CP.API.database.url = process.env["DB_URL"];
      }
      if (typeof(process.env["DB_HOST"]) == "string") {
         config.CP.API.database.host = process.env["DB_HOST"];
      }
      if (typeof(process.env["DB_ACCESS_KEY"]) == "string") {
         config.CP.API.database.accessKey = process.env["DB_ACCESS_KEY"];
      }
      if (typeof(process.env["WALLET_XPRV"]) == "string") {
         config.CP.API.wallets.bitcoin.xprv = process.env["WALLET_XPRV"];
      }
      if (typeof(process.env["WALLET_TPRV"]) == "string") {
         config.CP.API.wallets.test3.tprv = process.env["WALLET_TPRV"];
      }
      if (typeof(process.env["BCHWALLET_XPRV"]) == "string") {
         config.CP.API.wallets.bitcoincash.xprv = process.env["BCHWALLET_XPRV"];
      }
      if (typeof(process.env["BCHWALLET_TPRV"]) == "string") {
         config.CP.API.wallets.bchtest.tprv = process.env["BCHWALLET_TPRV"];
      }
      //try updating via command line arguments:
      for (var count = 2; count < process.argv.length; count++) {
         var currentArg = process.argv[count];
         var splitArg = currentArg.split("=");
         if (splitArg.length < 2) {
            throw (new Error("Malformed command line argument: "+currentArg));
         }
         var argName = new String(splitArg[0]);
         var joinedArr = new Array();
         joinedArr.push (splitArg[1]);
         //there may have been more than one "=" in the argument
         for (count2 = 2; count2 < splitArg.length; count2++) {
            joinedArr.push(splitArg[count2]);
         }
         var argValue = joinedArr.join("=");
         switch (argName.toLowerCase()) {
            case "blockcypher_token":
               config.CP.API.tokens.blockcypher = argValue;
               break;
            case "db_url":
               config.CP.API.database.url = argValue;
               break;
            case "db_host":
               config.CP.API.database.host = argValue;
               break;
            case "db_access_key":
               config.CP.API.database.accessKey = argValue;
               break;
            case "wallet_xprv":
               config.CP.API.wallets.bitcoin.xprv = argValue;
               break;
            case "wallet_tprv":
               config.CP.API.wallets.test3.tprv = argValue;
               break;
            case "bchwallet_xprv":
               config.CP.API.wallets.bitcoincash.xprv = argValue;
               break;
            case "bchwallet_tprv":
               config.CP.API.wallets.bchtest.tprv = argValue;
               break;
            default:
               //unrecognized command line parameter
               break;
         }
      }
      //Bitcoin (standard) wallets
      var ccHandler = getHandler("cryptocurrency", "bitcoin");
      namespace.cp.wallets.bitcoin.main = ccHandler.makeHDWallet(config.CP.API.wallets.bitcoin.xprv);
      if (namespace.cp.wallets.bitcoin.main != null) {
         var walletPath = config.CP.API.bitcoin.default.main.cashOutAddrPath;
         var cashoutWallet = namespace.cp.wallets.bitcoin.main.derivePath(walletPath);
         console.log ("Bitcoin HD wallet (\""+walletPath+"\") configured @ "+ccHandler.getAddress(cashoutWallet));
      } else {
         console.log ("Could not configure Bitcoin wallet.");
      }
      namespace.cp.wallets.bitcoin.test3 = ccHandler.makeHDWallet(config.CP.API.wallets.test3.tprv);
      if (namespace.cp.wallets.bitcoin.test3 != null) {
         walletPath = config.CP.API.bitcoin.default.test3.cashOutAddrPath;
         cashoutWallet = namespace.cp.wallets.bitcoin.test3.derivePath(walletPath);
         console.log ("Bitcoin testnet HD wallet (\""+walletPath+"\") configured @ "+ccHandler.getAddress(cashoutWallet, "test3"));
      } else {
         console.log ("Could not configure Bitcoin testnet wallet.");
      }
      //Bitcoin Cash wallets
      ccHandler = getHandler("cryptocurrency", "bitcoincash");
      namespace.cp.wallets.bitcoincash.main = ccHandler.makeHDWallet(config.CP.API.wallets.bitcoincash.xprv);
      if (namespace.cp.wallets.bitcoincash.main != null) {
         var walletPath = config.CP.API.bitcoincash.default.main.cashOutAddrPath;
         var cashoutWallet = namespace.cp.wallets.bitcoincash.main.derivePath(walletPath);
         console.log ("Bitcoin Cash HD wallet (\""+walletPath+"\") configured @ "+ccHandler.getAddress(cashoutWallet, "main", true));
      } else {
         console.log ("Could not configure Bitcoin Cash wallet.");
      }
      namespace.cp.wallets.bitcoincash.test = ccHandler.makeHDWallet(config.CP.API.wallets.bchtest.tprv);
      if (namespace.cp.wallets.bitcoincash.test != null) {
         walletPath = config.CP.API.bitcoincash.default.test.cashOutAddrPath;
         cashoutWallet = namespace.cp.wallets.bitcoincash.test.derivePath(walletPath);
         console.log ("Bitcoin Cash testnet HD wallet (\""+walletPath+"\") configured @ "+ccHandler.getAddress(cashoutWallet, "test", true));
      } else {
         console.log ("Could not configure Bitcoin Cash testnet wallet.");
      }
      var wallets = config.CP.API.wallets;
      if (config.CP.API.database.enabled == true) {
         console.log ("Database functionality is ENABLED.");
         //the second parameter is there to provide a value for the HMAC
         try {
            var walletStatusObj = await namespace.cp.callAccountDatabase("walletstatus", {"random":String(Math.random())});
         } catch (err) {
            console.error ("Could not get current wallet status.");
            console.error (err);
            console.error ("Trying again in 5 seconds...");
            setTimeout(5000, createAccountSystem, onCreateCB);
            return (false);
         }
         var resultObj = walletStatusObj.result;         
         //force-convert values in case the database returned them as strings
         var btcStartChain = Number(String(resultObj.bitcoin.main.startChain));
         var btcStartIndex = Number(String(resultObj.bitcoin.main.startIndex));
         var test3StartChain = Number(String(resultObj.bitcoin.test3.startChain));
         var test3StartIndex = Number(String(resultObj.bitcoin.test3.startIndex));
         if (btcStartChain > wallets.bitcoin.startChain) {
            wallets.bitcoin.startChain = Number(String(resultObj.bitcoin.main.startChain));
         }
         if (btcStartIndex > wallets.bitcoin.startIndex) {
            wallets.bitcoin.startIndex = Number(String(resultObj.bitcoin.main.startIndex));
         }
         if (test3StartChain > wallets.test3.startChain) {
            wallets.test3.startChain = Number(String(resultObj.bitcoin.test3.startChain));
         }
         if (test3StartIndex > wallets.test3.startIndex) {
            wallets.test3.startIndex = Number(String(resultObj.bitcoin.test3.startIndex));
         }
         //force-convert values in case the database returned them as strings
         var bchStartChain = Number(String(resultObj.bitcoincash.main.startChain));
         var bchStartIndex = Number(String(resultObj.bitcoincash.main.startIndex));
         var bchTestStartChain = Number(String(resultObj.bitcoincash.test.startChain));
         var bchTestStartIndex = Number(String(resultObj.bitcoincash.test.startIndex));
         if (bchStartChain > wallets.bitcoincash.startChain) {
            wallets.bitcoincash.startChain = Number(String(resultObj.bitcoincash.main.startChain));
         }
         if (btcStartIndex > wallets.bitcoin.startIndex) {
            wallets.bitcoincash.startIndex = Number(String(resultObj.bitcoincash.main.startIndex));
         }
         if (bchTestStartChain > wallets.bchtest.startChain) {
            wallets.bchtest.startChain = Number(String(resultObj.bitcoincash.test.startChain));
         }
         if (bchTestStartIndex > wallets.bchtest.startIndex) {
            wallets.bchtest.startIndex = Number(String(resultObj.bitcoincash.test.startIndex));
         }
      } else {
         console.log ("Database functionality is DISABLED.");
      }
      console.log ("Initial (next) Bitcoin account derivation path: m/"+wallets.bitcoin.startChain+"/"+(wallets.bitcoin.startIndex+1));
      console.log ("Initial (next) Bitcoin testnet account derivation path: m/"+wallets.test3.startChain+"/"+(wallets.test3.startIndex+1));
      console.log ("Initial (next) Bitcoin Cash account derivation path: m/"+wallets.bitcoincash.startChain+"/"+(wallets.bitcoincash.startIndex+1));
      console.log ("Initial (next) Bitcoin Cash testnet account derivation path: m/"+wallets.bchtest.startChain+"/"+(wallets.bchtest.startIndex+1));
      if (config.CP.API.database.enabled == true) {
         if (hostEnv.embedded == true) {
            console.log ("Local database size: "+resultObj.db.sizeMB+" megabytes");
            console.log ("Local database limit: "+resultObj.db.maxMB+" megabytes");
         } else {
            console.log ("Remote database size: "+resultObj.db.sizeMB+" megabytes");
            console.log ("Remote database limit: "+resultObj.db.maxMB+" megabytes");
         }
         console.log ("Database last updated "+resultObj.db.elapsedUpdateSeconds+" seconds ago");
      } else {
         console.log ("Using in-memory storage instead of database.");
      }
   } catch (err) {
      console.error (err);
      return (false);
   }
   console.log ("... account system initialized.");
   if (onCreateCB != null) {
      onCreateCB();
   }
   return (true);
}

/**
* Starts a database adapter and makes it available to the application.
*
* @param {String} [dbAdapter=null] The database adapter to start. The name must match one of those defined
* in the {@link config}.CP.API.database.adapters objects. If null or not supplied, the adapter (if applicable),
is determined from the {@link config}CP.API.database.url property.
*
* @return {Promise} The returned promise resolves with <code>true</code> if the database could be
* successfully started. An <code>Error</code> object is included with a rejection on any failure.
*/
async function startDatabase(dbAdapter=null) {
   if (dbAdapter == null) {
      var url = config.CP.API.database.url;
      var dbAdapter = url.split("://")[0];
      if ((dbAdapter == "https") || (dbAdapter == "http")) {
         //use remote adapter (nothing to do)
         return (true);
      }
      var adapters = config.CP.API.database.adapters;
   }
   var found = false;
   for (var adapter in adapters) {
      if (adapter == dbAdapter) {
         found = true;
         break;
      }
   }
   if (found == false) {
      throw (new Error("Database adapter definition for \""+dbAdapter+"\" not found in configuration."));
   }
   console.log ("Starting database adapter: "+dbAdapter);
   var adapterConfig = config.CP.API.database.adapters[dbAdapter];
   var scriptPath = adapterConfig.script;
   var binPath = adapterConfig.bin;
   var dbFilePath = config.CP.API.database.url.split(dbAdapter+"://")[1];
   if (hostEnv.embedded == true) {
      scriptPath = path.resolve(hostEnv.dir.server + scriptPath);
      adapterConfig.bin = path.resolve(hostEnv.dir.server + adapterConfig.bin);
      dbFilePath = path.resolve(hostEnv.dir.server + dbFilePath);
      dbFilePath = dbFilePath.split("\\").join("\\\\"); //fix windows path
      var dbFileName = path.basename(dbFilePath);
      var desktopDBPath = path.join(hostEnv.dir.data, "sqlite3");
      var desktopDBFile = path.join(desktopDBPath, dbFileName);
      console.log ("Using desktop mode database file location: "+desktopDBFile);
      if (fs.existsSync(desktopDBPath) == false) {
         console.log ("Creating target directory: "+desktopDBPath);
         fs.mkdirSync(desktopDBPath);
      }
      if (fs.existsSync(desktopDBFile) == false) {
         console.log ("Copying default database file from: "+dbFilePath);
         console.log ("                                To: "+desktopDBFile);
         try {
            fs.copyFileSync(dbFilePath, desktopDBFile);
         } catch (err) {
            console.error (err);
         }
      } else {
         console.log ("Desktop database file exists in target location.")
      }
   }
   var DatabaseAdapter = require(scriptPath);
   var adapter = new DatabaseAdapter(rpc_options.exposed_library_objects);
   adapterConfig.instance = adapter;
   try {
      var result = await adapter.initialize(adapterConfig);
      if (result == true) {
         if (hostEnv.embedded == true) {
            var opened = await adapter.openDBFile(desktopDBFile);
         } else {
            opened = await adapter.openDBFile(dbFilePath);
         }
      }
      console.log ("Database adapter successfully started.");
      return (true);
   } catch (err) {
      console.error ("Couldn't initialize \""+dbAdapter+"\" database adapter: \n"+err.stack);
      return (false);
   }
}

/**
* Loads and starts any handlers defined in the {@link config}
* object's <code>CP.RPC.handlers</code> array. Each affected element
* will have a new <code>handler</code> property addedd to it with the
* defined handler instance. Any existing handlers will be <code>destroy</code>ed
* and removed.
*
* @param {String} [type="all"] The handlers or a specific <code>type</code>
* to load. If "all", all handlers found in <code>CP.RPC.handlers</code> are
* created.
*/
async function loadHandlers(type="all") {
   var handlers = config.CP.API.handlers;
   console.log ("Now loading \""+type+"\" handlers ("+String(handlers.length)+"): ");
   for (var count = 0; count < handlers.length; count++) {
      try {
         var currentHandler = handlers[count];
         var handlerType = currentHandler.type;
         var handlerName = currentHandler.name;
         var handlerEnabled = currentHandler.enabled;
         if ((handlerEnabled == undefined) || (handlerEnabled == null)) {
            handlerEnabled = true;
         }
         if (handlerEnabled == true) {
            if (type == "all") {
               if (typeof (currentHandler.handler) == "object") {
                  try {
                     currentHandler.handler.destroy();
                  } catch (err) {
                     //probably no "destroy" function
                  }
                  delete currentHandler.handler;
               }
               var handlerClassFile = currentHandler.handlerClass;
               if (hostEnv.embedded == true) {
                  handlerClassFile = path.resolve(hostEnv.dir.server + handlerClassFile);
               }
               var handlerClass = require(handlerClassFile);
               var handler = new handlerClass(rpc_options.exposed_library_objects, currentHandler);
               console.log ("   Loaded handler: "+handlerName);
               if (typeof(handler.initialize) == "function") {
                  try {
                     var initResult = await handler.initialize();
                  } catch (err) {
                     console.error(err);
                  }
               }
               currentHandler.handler = handler;
            } else {
               if (handlerType == type) {
                  if (typeof (currentHandler.handler) == "object") {
                     try {
                        currentHandler.handler.destroy();
                     } catch (err) {
                        //probably no "destroy" function
                     }
                     delete currentHandler.handler;
                  }
                  var handlerClass = require(currentHandler.handlerClass);
                  var handler = new handlerClass(rpc_options.exposed_library_objects);
                  if (typeof(handler.initialize) == "function") {
                     try {
                        var initResult = await handler.initialize();
                     } catch (err) {
                        console.error(err);
                     }
                  }
                  console.log ("   Loaded handler: "+handlerName);
                  currentHandler.handler = handler;
               }
            }
         } else {
               console.log ("   Skipped disabled handler: "+handlerName);
         }
      } catch (err) {
         console.error (err);
      }
   }
}

/**
* Retrieves the first availablle handler of a certain type and sub-type from
* the {@link config} object's <code>CP.RPC.handlers</code> array.
*
* @param {String} handlerType The main handler category matching a handler's
* <code>type</code> definition. E.g. "cryptocurrency"
* @param {String} subType The secondary handler category matching one of the handler's
* <code>types</code> elements. If this is "any" or "*" then the first match for the
* <code>handlerType</code> os returned.
* @param {Boolean} [caseSensitive=false] If true, the sub-type match is done with
* case sensititivity.
*
* @return {Object} Either the class instance of the found handler, or <code>null</code>
* if no such handler can be found.
*/
function getHandler(handlerType, subType, caseSensitive=false) {
   var handlers = config.CP.API.handlers;
   for (var count = 0; count < handlers.length; count++) {
      var currentHandler = handlers[count];
      var handlerTypes = currentHandler.types;
      for (var count2=0; count2 < handlerTypes.length; count2++) {
         var currentType = handlerTypes[count2];
         if (caseSensitive == false) {
            currentType = currentType.toLowerCase();
            subType = subType.toLowerCase();
         }
         if ((currentType == subType) || (subType == "*") || (subType == "any")) {
            if ((typeof(currentHandler.handler) == "object") && (currentHandler.enabled == true)) {
               return (currentHandler.handler);
            }
         }
      }
   }
   return (null);
}

/**
* Invokes the "onInit" function in the host desktop (Electron) environment when
* all initialization routines have completed. If the server is not running as
* an embedded component this function does nothing.
*
* @private
*/
function invokeHostOnInit() {
   if (hostEnv.embedded == true) {
      try {
         hostEnv.server.onInit();
      } catch (err) {
         console.error("Couldn't invoke host environment \"onInit\" function: "+err.stack);
      }
   }
}

/**
* Function invoked after the main configuration data is loaded and parsed but
* before any database adapter is started, API functions are loaded, the account system initialized,
* or any server gateway(s) are started.
*
* @async
* @private
*/
async function postLoadConfig() {
   rpc_options.exposed_library_objects.hostEnv = hostEnv;
   rpc_options.exposed_library_objects.config = config;
   var result = await startDatabase();
   loadAPIFunctions(startHTTPServer, startWSServer); //load available API functions and then start servers
   try {
      var result = await loadHandlers("all");
      result = await createAccountSystem();
      result = checkBalances();
   } catch(error) {
      return (false);
   }
   return (true);
}

//Application entry point:

if ((this["electronEnv"] != undefined) && (this["electronEnv"] != null)) {
   hostEnv = this.electronEnv;
   hostEnv.embedded = true;
   console.log ("Launching in desktop embedded (Electron) mode.");
   console.log ("Server path prefix: "+hostEnv.dir.server);
   console.log ("Client path prefix: "+hostEnv.dir.client);
} else {
   console.log ("Launching in standalone mode.");
}

//load external configuration data from default location
loadConfig().then (configObj => {
   console.log ("Configuration data successfully loaded and parsed.");
   rpc_options.exposed_api_objects.config = config;
   if (config.CP.API.RPC.http.enabled == true) {
      rpc_options.http_port = config.CP.API.RPC.http.port;
   } else {
      rpc_options.http_port = -1;
   }
   if (config.CP.API.RPC.wss.enabled == true) {
      rpc_options.ws_port = config.CP.API.RPC.wss.port;
   } else {
      rpc_options.ws_port = -1;
   }
   adjustEnvironment(); //adjust for local runtime environment
   postLoadConfig().then(success => {
      if (hostEnv.embedded == true) {
         var gatewaysClass = path.resolve(hostEnv.dir.server + "./libs/Gateways.js");
         Gateways = require(gatewaysClass);
      } else {
         Gateways = require("./libs/Gateways.js");
      }
      gateways = new Gateways(rpc_options.exposed_library_objects, config.CP.API.gateways);
      gateways.initialize();
      try {
         //start updating transaction fees, if enabled
         namespace.cp.updateAllTxFees().then(result => {
            //update syccessfully started
         }).catch (err => {
            console.error (err);
         })
      } catch (err) {
         // updateAllTxFees may not exist or be registered in the global namespace;
         // usually defined in CP_Account API endpoint
      } finally {
         invokeHostOnInit();
      }
   }).catch (error => {
      console.error ("Couldn't complete configuration post-load initialization.");
      console.error (error);
   })
}).catch (err => {
   console.error ("Couldn't load or parse configuration data.");
   console.error (err);
})