text: /**
  * @file Manages cryptocurrency accounts using remote, local, or in-memory database(s),
  * and provides live blockchain interaction functionality.
  *
  * @version 0.5.1
  */
  async function CP_Account (sessionObj) {
     if ((namespace.wss == null) || (namespace.wss == undefined)) {
        sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "No WebSocket Session server defined.", sessionObj);
        return (false);
     }
     var requestData = sessionObj.requestObj;
     var requestParams = requestData.params;
     if ((requestParams.server_token == undefined) || (requestParams.server_token == null) || (requestParams.server_token == "")) {
        sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid server token.", sessionObj);
        return(false);
     }
     if ((requestParams.user_token == undefined) || (requestParams.user_token == null) || (requestParams.user_token == "")) {
        sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid user token.", sessionObj);
        return(false);
     }
     if (typeof(requestParams.action) != "string") {
        sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid action parameter.", sessionObj);
        return(false);
     }
     if (typeof(requestParams.password) != "string") {
        sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid password parameter.", sessionObj);
        return(false);
     }
     if (typeof(requestParams.type) != "string") {
        sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid account type parameter.", sessionObj);
        return(false);
     }
     if (typeof(requestParams.network) != "string") {
        sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid network parameter.", sessionObj);
        return(false);
     }
     var responseObj = new Object();
     //var connectionID = namespace.wss.makeConnectionID(sessionObj); //makeConnectionID defined in WSS_Handshake.js
     var privateID = namespace.wss.getPrivateID(sessionObj); //getPrivateID defined in WSS_Handshake.js
     if (privateID == null) {
        //must have active WSS session!
        sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Session not established.", sessionObj);
        return(false);
     }
     var resultObj = new Object(); //result to send in response
     resultObj.fees = new Object(); //include fee(s) information
     var fees = config.CP.API[requestParams.type].default[requestParams.network];
     var depositFee = bigInt(fees.depositFee);
     var minerFee = bigInt(fees.minerFee);
     //note that reported deposit fee includes the one defined in the configuration plus the dynamic miner fee:
     resultObj.fees.deposit = depositFee.plus(minerFee).toString(10);
     resultObj.fees.cashout = minerFee.toString(10);
     try {
        switch (requestParams.action) {
           case "new":
              //create new account
              var ccHandler = getHandler("cryptocurrency", requestParams.type);
              if (ccHandler == null) {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Cryptocurrency \""+requestParams.type+"\" not supported.", sessionObj);
                 return(false);
              }
              var accountObj = new Object(); //returned to user
              var fullAccountObj = new Object(); //stored internally
              var newWalletResult = await ccHandler.makeNewWallet(requestParams.type, requestParams.network);
              let hash = crypto.createHash("sha256");
              hash.update(requestParams.password);
              var pwHash = hash.digest("hex");
              fullAccountObj.type = requestParams.type;
              fullAccountObj.network = requestParams.network;
              var walletType = null;
              switch (requestParams.type) {
                 case "bitcoin":
                    if (requestParams.network == "main") {
                       walletType = "bitcoin";
                    } else {
                       walletType = "test3";
                    }
                    break;
                 case "bitcoincash":
                    if (requestParams.network == "main") {
                       walletType = "bitcoincash";
                    } else {
                       walletType = "bchtest";
                    }
                    break;
                 default:
                    sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Cryptocurrency \""+requestParams.type+"\" not supported.", sessionObj);
                    return(false);
                    break;
              }
              fullAccountObj.chain = config.CP.API.wallets[walletType].startChain;
              fullAccountObj.addressIndex = config.CP.API.wallets[walletType].startIndex;
              if ((requestParams.type == "bitcoin") || (requestParams.type == "bitcoincash")) {
                 //use BIP44 derivation path for Bitcoin related addresses
                 var derivationPath = "m/"+String(fullAccountObj.chain)+"/"+String(fullAccountObj.addressIndex);
              } else {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Cryptocurrency \""+requestParams.type+"\" not supported.", sessionObj);
                 return(false);
              }
              fullAccountObj.address = ccHandler.getDerivedWallet(derivationPath, requestParams.network, true, true);
              fullAccountObj.pwhash = pwHash;
              fullAccountObj.balance = "0";
              fullAccountObj.updated = MySQLDateTime(new Date()); //make sure to store local date/time (db may differ)
              accountObj.type = requestParams.type;
              accountObj.network = requestParams.network;
              accountObj.chain = config.CP.API.wallets[walletType].startChain;
              accountObj.addressIndex = config.CP.API.wallets[walletType].startIndex;
              accountObj.address = fullAccountObj.address;
              accountObj.pwhash = pwHash;
              accountObj.balance = bigInt("0");
              var feesObj = resultObj.fees; //save reference to previously created fees object
              resultObj = accountObj;
              resultObj.fees = feesObj; //copy previously created fees object
              var saved = await namespace.cp.saveAccount(fullAccountObj);
              if (saved == false) {
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Couldn't save account information.", sessionObj);
                 return(false);
              }
              break;
           case "info":
              //retrieve account information
              if (typeof(requestParams.password) != "string") {
                 sendError(JSONRPC_ERRORS.AUTH_FAILED, "Authentication failed.", sessionObj);
                 return (false);
              }
              var searchObj = new Object();
              searchObj.address = requestParams.address;
              searchObj.type = requestParams.type;
              searchObj.network = requestParams.network;
              try {
                 var accountResults = await namespace.cp.getAccount(searchObj);
              } catch (err) {
                 console.dir(err);
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Database error.", sessionObj);
                 return (false);
              }
              if (accountResults.length < 1) {
                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Account does not exist.", sessionObj);
                 return (false);
              }
              if (checkPassword(requestParams.password, accountResults[0].pwhash) == false) {
                 sendError(JSONRPC_ERRORS.AUTH_FAILED, "Authentication failed.", sessionObj);
                 return (false);
              }
              ccHandler = getHandler("cryptocurrency", requestParams.type);
              var balanceConfirmed = true;
              if ((accountResults.length == 1) &&
                 ((accountResults[0].balance=="0") || (accountResults[0].balance=="NULL") ||
                  (accountResults[0].balance=="") || (accountResults[0].balance==null))) {
                 //latest balance not yet confirmed
                 var balanceConfirmed = false;
                 var lastUpdateCheck = new Date(accountResults[0].updated); //this date/time must be relative to local date/time
                 var currentDateTime = new Date();
                 var delta = currentDateTime.valueOf()-lastUpdateCheck.valueOf();
                 if (delta < 0) {
                    //this may indicate a local clock discrepency (may have been reset or updated)
                    delta = 0;
                 }
                 var updateLimitSeconds = config.CP.API[requestParams.type].default.updateLimitSeconds * 1000; //convert to milliseconds
                 if (delta >= updateLimitSeconds) {
                    //time limit elapsed for checking live balance (allowed)
                    try {
                       var balanceResult = await ccHandler.getBlockchainBalance(requestParams.address, requestParams.type, requestParams.network);
                       resultObj.address = searchObj.address;
                       resultObj.type = searchObj.type;
                       resultObj.network = searchObj.network;
                       if ((balanceResult.balance == null) || (balanceResult.balance == undefined) || (balanceResult.balance == "")) {
                          balanceResult.balance = 0;
                       }
                       resultObj.balance = String(balanceResult.balance);
                       if (resultObj.balance != accountResults[0].balance) {
                          //new confirmed deposit detected; forward new account balance to cashout wallet
                          var fromAddressPath = "m/" + String(accountResults[0].chain) + "/" + String(accountResults[0].addressIndex);
                          var cashoutPath = config.CP.API[requestParams.type].default[requestParams.network].cashOutAddrPath;
                          var cashoutAddress = ccHandler.getDerivedWallet(cashoutPath, requestParams.network, true);
                          var transferAmount = bigInt(balanceResult.balance);
                          var minerFee = bigInt(config.CP.API[requestParams.type].default[requestParams.network].minerFee);
                          var depositFee = bigInt(config.CP.API[requestParams.type].default[requestParams.network].depositFee);
                          transferAmount = transferAmount.minus(minerFee);
                          try {
                             var txResult = await ccHandler.sendTransaction(fromAddressPath, cashoutAddress, transferAmount, minerFee, requestParams.type, requestParams.network);
                          } catch (err) {
                             sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Unable to forward transaction to cashout wallet.", sessionObj);
                             return(false);
                          }
                          if (txResult != null) {
                             if ((txResult.tx != undefined) && (txResult.tx != null)) {
                                if ((txResult.tx.hash != undefined) && (txResult.tx.hash != null) && (txResult.tx.hash != "")) {
                                   //Successfully forwarded new confirmed deposit:
                                   //Sender: ccHandler.getDerivedWallet(fromWallet).address or ccHandler.getDerivedWallet(fromWallet).address
                                   //Receiver: cashoutAddress
                                   //Transaction hash: txResult.tx.hash
                                   //Amount: transferAmount
                                   //Miner fee: minerFee
                                   //Deposit fee: depositFee
                                }
                             }
                          } else {
                             console.log ("Couldn't forward new account balance:");
                             console.dir (txResult);
                             //manual transfer may be required
                          }
                          //store updated account information
                          resultObj.confirmed = true;
                          transferAmount = transferAmount.minus(depositFee); //reduce amount by deposit fee
                          resultObj.balance = transferAmount.toString(10); //amount transferred (minus fees) is the new account balance
                          accountResults[0].balance = transferAmount.toString(10); //update database query object
                          accountResults[0].updated = MySQLDateTime(new Date());
                          var saved = await namespace.cp.saveAccount(accountResults[0]); //save to database
                          if (saved == false) {
                             sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Couldn't save account information.", sessionObj);
                             return(false);
                          }
                       } else {
                          //update updated date/time
                          resultObj.confirmed = false;
                          accountResults[0].updated = MySQLDateTime(new Date());
                          var updated = await namespace.cp.updateAccount(accountResults[0]); //use search result since it contains primary_key (required)
                          if (updated == false) {
                             sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Couldn't update account information.", sessionObj);
                             return(false);
                          }
                       }
                    } catch (err) {
                       console.error(err);
                       sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Live balance unavailable. Try again later.", sessionObj);
                    }
                 } else {
                    //within time limit for checking live balance (not allowed)
                    resultObj.address = searchObj.address;
                    resultObj.type = searchObj.type;
                    resultObj.network = searchObj.network;
                    resultObj.balance = accountResults[0].balance;
                    resultObj.confirmed = false;
                 }
              } else {
                 //latest balance already confirmed, just return it
                 resultObj.address = searchObj.address;
                 resultObj.type = searchObj.type;
                 resultObj.network = searchObj.network;
                 resultObj.balance = accountResults[0].balance;
                 resultObj.confirmed = true;
              }
              break;
           case "cashout":
              //cashout an account to a provided address
              ccHandler = getHandler("cryptocurrency", requestParams.type);
              if (typeof(requestParams.password) != "string") {
                 sendError(JSONRPC_ERRORS.AUTH_FAILED, "Authentication failed.", sessionObj);
                 return (false);
              }
              if (typeof(requestParams.amount) != "string") {
                 if (typeof(requestParams.amount) == "number") {
                    requestParams.amount = String(requestParams.amount);
                 } else {
                    sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Amount not specified.", sessionObj);
                    return (false);
                 }
              }
              if (typeof(requestParams.toAddress) != "string") {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Receiving address not specified.", sessionObj);
                 return (false);
              }
              if (requestParams.toAddress == requestParams.address) {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Sending and receiving addresses can't be the same.", sessionObj);
                 return (false);
              }
              if (cashoutIsPending(requestParams.address, requestParams.type, requestParams.network) == true) {
                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "A cashout request is currently pending. Only one request may be pending at a time.", sessionObj);
                 return (false);
              }
              if (typeof(requestParams.feeAmount) != "string") {
                 if (typeof(requestParams.feeAmount) == "number") {
                    requestParams.feeAmount = String(requestParams.feeAmount);
                 } else {
                    //use config default
                    requestParams.feeAmount = config.CP.API[requestParams.type].default[requestParams.network].minerFee;
                 }
              }
              var searchObj = new Object();
              searchObj.address = requestParams.address;
              searchObj.type = requestParams.type;
              searchObj.network = requestParams.network;
              try {
                 accountResults = await namespace.cp.getAccount(searchObj);
              } catch (err) {
                 console.dir(err);
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Database error.", sessionObj);
                 return (false);
              }
              if (accountResults.length < 1) {
                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Account does not exist.", sessionObj);
                 return (false);
              }
              if (checkPassword(requestParams.password, accountResults[0].pwhash) == false) {
                 sendError(JSONRPC_ERRORS.AUTH_FAILED, "Authentication failed.", sessionObj);
                 return (false);
              }
              var fees = bigInt(requestParams.feeAmount);
              var cashoutAmount = bigInt(requestParams.amount);
              var totalCashoutAmount = cashoutAmount.plus(fees);
              var availableAmount = bigInt(accountResults[0].balance);
              if (totalCashoutAmount.greater(availableAmount)) {
                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Insufficient balance.", sessionObj);
                 return (false);
              }
              var newBalance = availableAmount.minus(totalCashoutAmount);
              addPendingCashout(requestParams.address, requestParams.toAddress, requestParams.type, requestParams.network, cashoutAmount.toString(10), fees.toString(10));
              var txResult = await ccHandler.cashoutToAddress(requestParams.toAddress, cashoutAmount.toString(10), fees.toString(10), requestParams.type, requestParams.network);
              removePendingCashout(requestParams.address, requestParams.type, requestParams.network);
              if (txResult != null) {
                 if ((txResult.tx != undefined) && (txResult.tx != null)) {
                    if ((txResult.tx.hash != undefined) && (txResult.tx.hash != null) && (txResult.tx.hash != "")) {
                       accountResults[0].balance = newBalance.toString(10);
                       var saved = await namespace.cp.saveAccount(accountResults[0]);
                       if (saved == false) {
                          console.error("Account \""+accountResults[0]+"\" couldn't be updated with new balance: "+accountResults[0].balance);
                          sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Couldn't save account information.", sessionObj, {"txHash":txResult.tx.hash});
                          return(false);
                       }
                       resultObj.txHash = txResult.tx.hash;
                       resultObj.toAddress = requestParams.toAddress;
                       resultObj.amount = cashoutAmount.toString(10);
                       resultObj.fees = fees.toString(10);
                       resultObj.balance = newBalance.toString(10);
                    }
                 }
              } else {
                 console.error("Couldn't process cashout transaction:");
                 console.dir(txResult);
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Couldn't process transaction.", sessionObj);
                 return(false);
              }
              break;
           case "transfer":
              //transfer account balance to another account
              if (typeof(requestParams.password) != "string") {
                 sendError(JSONRPC_ERRORS.AUTH_FAILED, "Authentication failed.", sessionObj);
                 return (false);
              }
              if (typeof(requestParams.amount) != "string") {
                 if (typeof(requestParams.amount) == "number") {
                    requestParams.amount = String(requestParams.amount);
                 } else {
                    sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Amount not specified.", sessionObj);
                    return (false);
                 }
              }
              if (typeof(requestParams.toAccount) != "string") {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Receiving account not specified.", sessionObj);
                 return (false);
              }
              var sourceSearchObj = new Object();
              sourceSearchObj.address = requestParams.address;
              sourceSearchObj.type = requestParams.type;
              sourceSearchObj.network = requestParams.network;
              var targetSearchObj = new Object();
              targetSearchObj.address = requestParams.toAccount;
              targetSearchObj.type = requestParams.type;
              targetSearchObj.network = requestParams.network;
              try {
                 var sourceAccountRes = await namespace.cp.getAccount(sourceSearchObj);
              } catch (err) {
                 console.dir(err);
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Database error.", sessionObj);
                 return (false);
              }
              if (sourceAccountRes.length < 1) {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Source account not found.", sessionObj);
                 return (false);
              }
              if (checkPassword(requestParams.password, sourceAccountRes[0].pwhash) == false) {
                 sendError(JSONRPC_ERRORS.AUTH_FAILED, "Authentication failed.", sessionObj);
                 return (false);
              }
              try {
                 var targetAccountRes = await namespace.cp.getAccount(targetSearchObj);
              } catch (err) {
                 console.dir(err);
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Database error.", sessionObj);
                 return (false);
              }
              if (targetAccountRes.length < 1) {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Target account not found.", sessionObj);
                 return (false);
              }
              if (sourceAccountRes[0].type != targetAccountRes[0].type) {
                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Incompatible currencies: \""+sourceAccountRes[0].type+"\" and \""+targetAccountRes[0].type+"\"", sessionObj);
                 return (false);
              }
              if (sourceAccountRes[0].network != targetAccountRes[0].network) {
                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Incompatible currency networks: \""+sourceAccountRes[0].network+"\" and \""+targetAccountRes[0].network+"\"", sessionObj);
                 return (false);
              }
              try {
                 var transferAmount = bigInt(requestParams.amount);
              } catch (err) {
                 sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid transfer amount.", sessionObj);
                 return (false);
              }
              var sourceBalance = bigInt(sourceAccountRes[0].balance);
              var targetBalance = bigInt(targetAccountRes[0].balance);
              if (transferAmount.greater(sourceBalance)) {
                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Insufficient account balance.", sessionObj);
                 return (false);
              }
              sourceBalance = sourceBalance.minus(transferAmount);
              targetBalance = targetBalance.plus(transferAmount);
              sourceAccountRes[0].balance = sourceBalance.toString(10);
              sourceAccountRes[0].updated = MySQLDateTime(new Date());
              targetAccountRes[0].balance = targetBalance.toString(10);
              targetAccountRes[0].updated = MySQLDateTime(new Date());
              var saved = await namespace.cp.saveAccount(sourceAccountRes[0]);
              if (saved == false) {
                 console.error("Account \""+sourceAccountRes[0]+"\" couldn't be updated with new balance: "+sourceAccountRes[0].balance);
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Couldn't save account information.", sessionObj);
                 return(false);
              }
              saved = await namespace.cp.saveAccount(targetAccountRes[0]);
              if (saved == false) {
                 console.error("Account \""+targetAccountRes[0]+"\" couldn't be updated with new balance: "+targetAccountRes[0].balance);
                 sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Couldn't save account information.", sessionObj);
                 return(false);
              }
              resultObj.address = requestParams.address;
              resultObj.type = requestParams.type;
              resultObj.network = requestParams.network;
              resultObj.balance = sourceBalance.toString(10);
              resultObj.confirmed = true;
              break;
           default:
              sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Unrecognized action.", sessionObj);
              return(false);
              break;
        }
     } catch (err) {
        console.error(err);
        sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Error processing request.", sessionObj);
        return (false);
     }
     sendResult(resultObj, sessionObj);
     return(true);
  }
  
  /**
  * An object containing individual account properties. A player may have multiple
  * accounts.
  *
  * @typedef {Object} AccountObject
  * @property {String} type The address type. Supported values include: "bitcoin
  * @property {Number} chain The HD wallet chain value used in the derivation path.
  * @property {Number} addressIndex The HD wallet address index used in the derivation path.
  * @property {String} address The generated cryptocurrency address for the account.
  * @property {String} network The sub-network, if applicable, to which the address belongs.
  * For example "main", "test3", etc.
  * @property {String} pwhash The SHA256 hash of the password associated with the account.
  * @property {BigInteger} balance The current account balance in the lowest denomination of
  * the associated cryptocurrency (e.g. satoshis if <code>type="bitcoin"</code>).
  * @property {String} updated The [ISO8601 date/time]{@link https://en.wikipedia.org/wiki/ISO_8601} that this account object was last updated.
  * @property {Object} wallet The HD wallet containig the private and public keys as well as the WIF.
  *
  * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
  */
  
  /**
  * Generates a MySQL-compatible date/timestamp.
  *
  * @param {Date} dateObj The native JavaScript Date object from which to
  * generate the timestamp.
  *
  * @return {String} A MySQL "TIMESTAMP" field type compatible date/time string.
  */
  function MySQLDateTime(dateObj) {
  	if ((dateObj == undefined) || (dateObj == null) || (dateObj == "")) {
  		var now = new Date();
  	} else {
  		now = dateObj;
  	}
  	var returnStr = new String();
  	returnStr += String(now.getFullYear())+"-";
  	returnStr += String(now.getMonth()+1)+"-";
  	returnStr += String(now.getDate())+" ";
  	if (now.getHours() < 10) {
  		returnStr += "0";
  	}
  	returnStr += String(now.getHours())+":";
  	if (now.getMinutes() < 10) {
  		returnStr += "0";
  	}
  	returnStr += String(now.getMinutes())+":";
  	if (now.getSeconds() < 10) {
  		returnStr += "0";
  	}
  	returnStr += String(now.getSeconds());
  	return (returnStr);
  }
  
  /**
  * Compares a password (or any string) against its SHA256 hash.
  *
  * @param {String} password The plaintext password (or any string) to compare.
  * @param {String} pwhash The hex-encoded, SHA256 hash to compare <code>password</code>
  * against.
  *
  * @return {Boolean} True if the password matched the supplied hash, false otherwise.
  */
  function checkPassword(password, pwhash) {
     let hash = crypto.createHash("sha256");
     hash.update(password);
     var hashDigest = hash.digest("hex");
     if (hashDigest == pwhash)  {
        return (true);
     } else {
        return (false);
     }
  }
  
  /**
  * Searches for and returns the latest details about a specific account.
  *
  * @param {Object} searchObj An object specifiying the search parameters. All
  * included criteria must be met in order for a match.
  * @param {String} [searchObj.address] The cryptocurrency address of the account. This
  * is also the main account identifier.
  * @param {String} [searchObj.type] The cryptocurrency account type.
  * @param {String} [searchObj.network] The cryptocurrency sub-network type.
  *
  * @return {Promise} The resolved promise contains an array of up to two {@link AccountObject}
  * instances containing the latest activity for the account, index 0 being the most recent, or <code>null</code>
  * if no matching account exists. A rejected promise contains an <code>Error</code> object.
  * @async
  */
  async function getAccount(searchObj) {
     if ((searchObj == null) || (searchObj == undefined)) {
        return (null);
     }
     if (config.CP.API.database.enabled == true) {
        var result = await callAccountDatabase("getrecord", searchObj);
        if (result.error == undefined) {
           return (result.result);
        } else {
           if (result.error.code == -32602) {
              //no matching account, return empty result set
              return (new Array());
           } else {
              throw (new Error(result.error));
           }
        }
     } else {
        //use in-memory data instead
        var resultArr = new Array();
        if (namespace.cp.accounts == undefined) {
           namespace.cp.accounts = new Array();
           return (resultArr);
        }
        for (var count=(namespace.cp.accounts.length-1); count >= 0 ; count--) {
          var currentAccount = namespace.cp.accounts[count];
          var criteriaCount = 0;
          var matchedCount = 0;
          for (var item in searchObj) {
             var searchItem = searchObj[item];
             if (typeof(searchItem) != "function") {
               criteriaCount++;
               if (searchItem == currentAccount[item]) {
                  //note that data types must match exactly
                  matchedCount++;
               }
            }
          }
          if (criteriaCount == matchedCount) {
            currentAccount.primary_key = count;
            resultArr.push (currentAccount);
          }
          //increase here if we want more than 2 recent records for account
          if (resultArr.length == 2) {
            break;
          }
        }
     }
     return (resultArr);
  }
  
  /**
  * Saves a new or existing (updated) account.
  *
  * @param {AccountObject} accountObj Contains the account information to save.
  *
  * @return {Promise} The resolved promise returns <code>true</code> if the account
  * was successfully saved, <code>false</code> false otherwise.
  * @async
  */
  async function saveAccount(accountObj) {
     if (config.CP.API.database.enabled == true) {
        //save to remote database if available
        var saved = false;
        var result = await callAccountDatabase("putrecord", accountObj);
        if (result.error == undefined) {
           saved = true;
        } else {
           throw (new Error(result.error));
        }
     } else {
        //use in-memory array database
        if (namespace.cp.accounts == undefined) {
           namespace.cp.accounts = new Array();
        }
        var currentDate = new Date();
        accountObj.updated = MySQLDateTime(currentDate);
        namespace.cp.accounts.push(accountObj);
     }
     return (true);
  }
  
  /**
  * Updates the "updated" (date/time) property of an  existing account. Any other changes to
  * account information should use {@link saveAccount} in order to maintain the account's history.
  *
  * @param {AccountObject} accountObj Contains the account information to update. This object
  * must contain the <code>primary_key</code> value of the row to update.
  *
  * @return {Promise} The resolved promise returns <code>true</code> if the account
  * was successfully updated, <code>false</code> false otherwise.
  * @async
  */
  async function updateAccount(accountObj) {
     if (config.CP.API.database.enabled == true) {
        //save to remote database if available
        var result = await callAccountDatabase("updaterecord", accountObj);
        if (result.error == undefined) {
           updated = true;
        } else {
           throw (new Error(result.error));
        }
     } else {
        //use in-memory array database
        if (namespace.cp.accounts == undefined) {
           namespace.cp.accounts = new Array();
           return (false);
        }
        var currentDate = new Date();
        namespace.cp.accounts[accountObj.primary_key].updated = MySQLDateTime(currentDate);
     }
     return (true);
  }
  
  /**
  * Calls the an account database interface with a method, message, and optional HMAC
  * signature.
  *
  * @param {String} method The adapter or remote (RPC) method to invoke.
  * @param {Object} message The accompanying data to stringify, (if applicable) sign with
  * <code>config.CP.API.database.accessKey</code>, and include with
  * the remote (RPC) or local request.
  *
  * @return {Promise} Resolves with the response object returned from the server or
  * database adapter, or rejects with an error.
  */
  function callAccountDatabase(method, message) {
     var promise = new Promise(function(resolve, reject) {
        if (config.CP.API.database.enabled) {
           var url = config.CP.API.database.url;
           var transport = url.split("://")[0];
           if (transport == "https") {
              transport = "http";
           }
           switch (transport) {
              case "http":
                 //standard remote database call:
                 var host = config.CP.API.database.host;
                 var accessKey = config.CP.API.database.accessKey;
                 var txObject = new Object();
                 txObject.jsonrpc = "2.0";
                 txObject.id = String(Math.random()).split(".")[1];
                 txObject.method = method;
                 txObject.params = new Object();
                 //create HMAC using access key:
                 var hmac = crypto.createHmac('SHA256', accessKey);
                 //update with stringified message:
                 hmac.update(JSON.stringify(message));
                 //create signature:
                 var signature = hmac.digest('hex');
                 txObject.params.signature = signature;
                 txObject.params.message = message;
                 var headersObj = new Object();
                 headersObj = {
                    "Content-Type":"application/json-rpc",
                    "accept":"application/json-rpc",
                    "Host":host
                 };
                 request({
                    url: url,
                    method: "POST",
                    body: txObject,
                    headers: headersObj,
                    json: true
                 }, (error, response, body) => {
                    if (error) {
                       console.error ("Account database error:");
                       console.error (error);
                       reject(error);
                    } else {
                       resolve(body);
                    }
                 });
                 break;
              default:
                 //process call using adapter
                 var adapterConfigPath = "CP.API.database.adapters."+transport;
                 var adapterConfig = getConfigByPath(adapterConfigPath);
                 var adapter = adapterConfig.instance;
                 var requestObj = new Object();
                 requestObj.jsonrpc = "2.0";
                 requestObj.id = String(Math.random()).split(".")[1];
                 requestObj.method = method;
                 requestObj.params = new Object();
                 requestObj.params.message = message;
                 adapter.invoke(requestObj).then(result => {
                    resolve(result); //any JSON-RPC response is valid
                 }).catch (err => {
                    reject(err);
                 });
                 break;
           }
        } else {
           reject(new Error("Database interactions are disabled."));
        }
     });
     return (promise);
  }
  
  /**
  * Adds a pending cashout transaction to the <code>namespace.cp.pendingCashouts</code>
  * array.
  *
  * @param {String} fromAccount The account address that is cashing out.
  * @param {String} toAddress The address to which the funds are being sent.
  * @param {String} type The cryptocurrency type associated with the transaction (e.g. "bitcoin").
  * @param {String} network The cryptocurrency sub-network associated with the transaction (e.g. "main" or "test3").
  * @param {String} amount The amount of the transaction in the smallest denomination for the associated
  * cryptocurrency.
  * @param {String} fees Any miner fee(s) for the transaction in the smallest denomination for the associated
  * cryptocurrency.
  */
  function addPendingCashout(fromAccount, toAddress, type, network, amount, fees) {
     if (namespace.cp.pendingCashouts == undefined) {
        namespace.cp.pendingCashouts = new Array();
     }
     var cashoutObject = new Object();
     cashoutObject.from = fromAccount;
     cashoutObject.to = toAddress;
     cashoutObject.type = type;
     cashoutObject.network = network;
     cashoutObject.amount = amount;
     cashoutObject.fees = fees;
     var now = new Date();
     cashoutObject.timestamp = now.toISOString();
     namespace.cp.pendingCashouts.push(cashoutObject);
     //should array be saved in case server quits?
  }
  
  /**
  * Checks if a cashout transaction for a specific account is pending (appears in the
  * <code>namespace.cp.pendingCashouts</code> array). Only one pending cashout transaction
  * per account, cryptocurrency type, and network, is assumed to exist.
  *
  * @param {String} fromAccount The account address to check.
  * @param {String} type The cryptocurrency type associated with the pending transaction (e.g. "bitcoin").
  * @param {String} network The cryptocurrency sub-network associated with the pending transaction (e.g. "main" or "test3").
  *
  * @return {Boolean} True if the specified account has a transaction pending, false otherwise.
  */
  function cashoutIsPending(fromAccount, type, network) {
     if (namespace.cp.pendingCashouts == undefined) {
        namespace.cp.pendingCashouts = new Array();
        return (false);
     }
     if (namespace.cp.pendingCashouts.length == 0) {
        return (false);
     }
     for (var count=0; count < namespace.cp.pendingCashouts.length; count++) {
        var currentPendingCashout = namespace.cp.pendingCashouts[count];
        if ((currentPendingCashout.from == fromAccount) &&
           (currentPendingCashout.type == type) &&
           (currentPendingCashout.network == network)) {
              return (true);
           }
     }
     return (false);
  }
  
  /**
  * Removes a pending cashout transaction from the <code>namespace.cp.pendingCashouts</code>
  * array.
  *
  * @param {String} fromAccount The account address that is cashing out.
  * @param {String} type The cryptocurrency type associated with the transaction (e.g. "bitcoin").
  * @param {String} network The cryptocurrency sub-network associated with the transaction (e.g. "main" or "test3").
  *
  * @return {Object} The pending cashout transaction removed from the internal <code>namespace.cp.pendingCashouts</code>
  * array, of <code>null</code> if no matching transaction exists.
  */
  function removePendingCashout(fromAccount, type, network) {
     if (namespace.cp.pendingCashouts == undefined) {
        namespace.cp.pendingCashouts = new Array();
        return (null);
     }
     for (var count=0; count < namespace.cp.pendingCashouts.length; count++) {
        var currentPendingCashout = namespace.cp.pendingCashouts[count];
        if ((currentPendingCashout.from == fromAccount) &&
           (currentPendingCashout.type == type) &&
           (currentPendingCashout.network == network)) {
              var txObj = namespace.cp.pendingCashouts.splice(count, 1);
              return (txObj);
           }
     }
     return (null);
  }
  
  
  /**
  * Returns an estimated miner fee for a transaction. The fee estimation may either be based on an
  * external service or an internal calculation.
  *
  * @param {*} [txData=null] The transaction for which to estimate the fee. The format
  * for this parameter differs based on the <code>APIType</code> and possibly <code>network</code>.<br/>
  * If omitted, a typical (average) transaction is assumed.<br/>
  * For a "bitcoin" <code>APIType</code>, this parameter is expected to be a string of hex-encoded data
  * comprising the binary transaction. If omitted, the transaction is assumed to be 250 bytes.
  * @param {Number} [priority=1] The priority with which the transaction is to be posted. A higher-priority
  * transaction (closer to or equal to 0), is expected to be posted faster than a lower priority one (> 0).
  * This paramater is dependent on the <code>APIType</code> and possibly the <code>network</code> type.<br/>
  * When <code>APIType</code> is "bitcoin", a priority of 0 is the highest priority (to be included in the next 1-2 blocks),
  * a priority of 1 is a medium priority (3 to 6 blocks), and 2 is a low priority (> 6 blocks).
  * @param {String} [APIType="bitcoin"] The main cryptocurrency API type.
  * @param {String} [network=null] The cryptocurrency sub-network, if applicable, for the
  * transaction. Current <code>network</code> types include: "main" and "test3". If <code>null</code>,
  * the default network specified in <code>config.CP.API[APIType].default.network</code> is used.
  *
  * @return {String} The estimated transaction fee, as a numeric string in the lowest denomination of the associated
  * cryptocurrency (e.g. satoshis if <code>APIType="bitcoin"</code>), based on the supplied <code>txData</code>,
  * <code>priority</code>, <code>APIType</code>, and <code>network</code>. If any parameter is invalid or unrecognized,
  * <code>null</code> is returned.
  * @async
  */
  async function estimateTxFee (txData=null, priority=1, APIType="bitcoin", network=null) {
     try {
        switch (APIType) {
           case "bitcoin":
              var txSize = 250; //bytes
              if (txData != null) {
                 txSize = txData.length / 2; //hex-encoded binary data
              }
              //TODO: complete this!
              break;
           default:
              return (null);
              break;
        }
     } catch (err) {
        return (null);
     }
  }
  
  /**
  * Updates the internal transaction fees for all cryptocurrencies and sub-networks defined
  * in the global <code>config</code> object using the cryptocurrency handlers' <code>updateTxFees</code>
  * functions (i.e. some updates may be omitted if the update time limits have not elapsed).
  *
  * @param {Boolean} [startAutoUpdate=true] If true, the automatic update interval defined in the
  * global <code>config</code> object for each cryptocurrency/network is (independently) started.
  * If false, transaction fees must be updated manually ir required.
  * @param {Boolean} [sequential=true] If true, each new update is started when the previous one is
  * completed, otherwise they're all executed simultaneously.
  *
  * @async
  */
  async function updateAllTxFees(startAutoUpdate=true, sequential=true) {
     var APIType = "bitcoin"; //currently only bitcoin is updated
     var btcAPI = config.CP.API[APIType];
     var btcNetworks = btcAPI.networks;
     for (var networkName in btcNetworks) {
        var network = btcNetworks[networkName];
        if (sequential) {
           try {
              var ccHandler = getHandler("cryptocurrency", APIType);
              if (ccHandler == null) {
                 console.error(`Currency ${APIType} network ${networkName} has no registered handler.`);
              } else {
                 var result = await ccHandler.updateTxFees(APIType, network);
              }
           } catch (err) {
              //failed or disabled
              console.error (err);
           }
           if (startAutoUpdate) {
              if (btcAPI.default[network].feeUpdateEnabled == false) {
                 console.log ("Transaction fee updates for \""+APIType+"/"+network+"\" disabled.");
              } else {
                 if (btcAPI.default[network].feeUpdateSeconds < 30) {
                    console.warn ("*WARNING* A transaction fee updates interval of at least 30 seconds is advised in order to deal with possible network latency.");
                 }
                 var updateSeconds = btcAPI.default[network].feeUpdateSeconds;
                 console.log("Updating "+APIType+"/"+network+" transaction fees every "+updateSeconds+" seconds / "+(updateSeconds / 60)+" minutes.");
                 var updateInterval = updateSeconds * 1000;
                 btcAPI.default[network].timeout = setInterval((APIType, network) => {
                    ccHandler.updateTxFees(APIType, network, true).then(result => {
                    }).catch(err => {
                       //failed or disabled
                    })
                 }, updateInterval, APIType, network);
              }
           }
        } else {
           if (btcAPI.default[network].feeUpdateEnabled == false) {
              console.log ("Transaction fee updates for \""+APIType+"/"+network+"\" disabled.");
           } else {
              var ccHandler = getHandler("cryptocurrency", APIType);
              if (ccHandler == null) {
                 console.error(`Currency ${APIType} network ${networkName} has no registered handler.`);
              } else {
                 ccHandler.updateTxFees(APIType, network).then(result => {
                    //updated
                 }).catch(err => {
                    //failed or disabled
                    console.error (err);
                 });
                 if (btcAPI.default[network].feeUpdateSeconds < 30) {
                    console.warn ("*WARNING* A transaction fee updates interval of at least 30 seconds is advised in order to deal with possible network latency.");
                 }
                 updateSeconds = btcAPI.default[network].feeUpdateSeconds;
                 console.log("Updating "+APIType+"/"+network+" transaction fees every "+updateSeconds+" seconds / "+(updateSeconds / 60)+" minutes.");
                 var updateInterval = updateSeconds * 1000;
                 btcAPI.default[network].timeout = setInterval((APIType, network) => {
                    ccHandler.updateTxFees(APIType, network, true).then(result => {
                       //updated
                    }).catch(err => {
                       //failed or disabled
                    })
                 }, updateInterval, APIType, network);
              }
           }
        }
     }
  }
  
  /**
  * Builds a valid CypherPoker.JS message object (usually included as the
  * <code>data</code> property of a JSON-RPC 2.0 result object).
  *
  * @param {String} messageType The notification message type to build.
  *
  * @return {Object} A valid CypherPoker.JS message object. Additional properties
  * to include with the message may be appended directly to this object.
  */
  function buildCPMessage(messageType) {
     var JSONObj = new Object();
     JSONObj.cpMsg = messageType;
     return (JSONObj);
  }
  
  if (namespace.cp == undefined) {
     namespace.cp = new Object();
  }
  
  namespace.cp.getAccount = getAccount;
  namespace.cp.saveAccount = saveAccount;
  namespace.cp.updateAccount = updateAccount;
  namespace.cp.checkPassword = checkPassword;
  namespace.cp.callAccountDatabase = callAccountDatabase;
  namespace.cp.cashoutIsPending = cashoutIsPending;
  namespace.cp.MySQLDateTime = MySQLDateTime;
  namespace.cp.buildCPMessage = buildCPMessage;
  namespace.cp.updateAllTxFees = updateAllTxFees;
  if (namespace.cp.wallets == undefined) {
     namespace.cp.wallets = new Object();
  }
  if (namespace.cp.wallets.bitcoin == undefined) {
     namespace.cp.wallets.bitcoin = new Object();
  }
  if (namespace.cp.wallets.bitcoin.main == undefined) {
     namespace.cp.wallets.bitcoin.main = null;
  }
  if (namespace.cp.wallets.bitcoin.test3 == undefined) {
     namespace.cp.wallets.bitcoin.test3 = null;
  }
  if (namespace.cp.wallets.bitcoincash == undefined) {
     namespace.cp.wallets.bitcoincash = new Object();
  }
  if (namespace.cp.wallets.bitcoincash.main == undefined) {
     namespace.cp.wallets.bitcoincash.main = null;
  }
  if (namespace.cp.wallets.bitcoincash.test == undefined) {
     namespace.cp.wallets.bitcoincash.test = null;
  }
  