Source: api/CP_SmartContract.js

/**
* @file Manages proxy CypherPoker smart contracts and defines a number of related utility functions.
*
* @version 0.5.1
*/

/**
* An object containing player account information of an associated smart contract, usually
* as part of a player's <code>account</code> property.
* @typedef {Object} AccountObject
* @property {String} address The cryptocurrency address associated with the account (and used as the
* primary identifier).
* @property {String} type The cryptocurrency type of the <code>address</code>. Valid values include: "bitcoin"
* @property {String} network The sub-network of the <code>address</code>, if applicable. Valid values include:
* "main", "test3"
* @property {String} password The password associated with the account.
* @property {String} [balance] Optional balance for the account (typically this value is ignored in favour
* of the database <code>balance</code> for the account).
*/
/**
* A CypherPoker.JS table object associated with a game.
* @typedef {Object} TableObject
* @property {String} ownerPID The private ID of the owner / creator of the table.
* @property {String} tableID The pseudo-randomly generated, unique table ID of the table.
* @property {String} tableName The name given to the table by the owner.
* @property {Array} requiredPID Indexed array of private IDs of peers required to join this room before it's
* considered full or ready.
* @property {Array} joinedPID Indexed array of private IDs that have been accepted by the owner. When a contract
* is first created, this array should only have one element: the owner's PID.
* @property {Array} restorePID Copy of the original private IDs in the <code>requiredPID</code> array
* used to restore it if members of the <code>joinePID</code> array leave the table.
* @property {Object} tableInfo Additional information to be included with the table.
*/
/**
* A CypherPoker.JS proxy smart contract object.
* @typedef {Object} ContractObject
* @property {String} contractID The ID of the contract.
* @property {TableObject} table The table associated with the contract.
* @property {Array} players Indexed array of player object instances associated with the contract.
* @property {String} prime The root prime number value associated with the contract.
* @property {String} pot="0" A numeric string representing the value currently held by the contract "pot"
* (the total subtracted from players' initial balances), to be awarded to the hand's winner(s).
* @property {Object} cardDecks Contains the currently active card decks associated with the contract.
* @property {Array} cardDecks.faceup Indexed array of card objects representing the faceup or unencrypted deck.
* @property {Array} cardDecks.facedown Indexed array of strings representing the facedown or encrypted deck. This
* array will change as cards are drawn during game play.
* @property {Array} cardDecks.dealt Indexed array of strings representing the dealt face or encrypted cards. This
* array will change as cards are drawn during game play.
* @property {Array} cardDecks.public Indexed array of card objects representing the dealt public / community cards. This
* array will change as cards are drawn during game play.
* @property {Object} history Contains a history of card generation, encryption, and decryption operations for correctness analysis.
*/
async function CP_SmartContract (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 (typeof(requestParams.server_token) != "string") {
      sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid server token.", sessionObj);
      return(false);
   }
   if (typeof(requestParams.user_token) != "string") {
      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.", 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, "No active session.", sessionObj);
      return(false);
   }
   var resultObj = new Object(); //result to send in response);
   switch (requestParams.action) {
      case "new":
         var gameContracts = namespace.cp.getContractsByPID(privateID);
         if (gameContracts.length > 10) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Too many open game contracts.", sessionObj);
            return(false);
         }
         if (validContractObject(requestParams.contract, privateID, requestParams.account) == false) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Invalid contract.", sessionObj);
            return(false);
         }
         try {
            var playerAccount = await validAccount(requestParams.account);
         } catch (err) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, err.message, sessionObj);
            return(false);
         }
         var newContract = new Object();
         newContract = requestParams.contract;
         newContract.ownerPID = privateID;
         //overwrite currency settings for contract in case user mis-reported them
         newContract.table.tableInfo.currency.type = requestParams.account.type;
         newContract.table.tableInfo.currency.network = requestParams.account.network;
         //sanitize private tokens!
         newContract.user_token = "";
         newContract.server_token = "";
         delete newContract.user_token;
         delete newContract.server_token;
         newContract.history = new Object(); //sanitize history
         newContract.history.keychains = new Object(); //sanitize submitted player keychains object
         newContract.pot = "0"; //sanitize hand pot
         newContract.invalid = false;
         var player = getPlayer(newContract, privateID);
         if (player == null) {
            sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Owner's player object not found in players array.",  sessionObj);
            return(false);
         }
         if (typeof(newContract.table.tableInfo.timeout) != "number") {
            //use config-defined timeout
            newContract.table.tableInfo.timeout = config.CP.API.contract.timeoutDefault;
         }
         resetPlayerBalances(newContract); //reset all players' balances
         setPlayerBalance(newContract, privateID, newContract.table.tableInfo.buyIn); //set deposit balance for dealer/current user
         //subtract buy-in from account and deposit to contract
         var buyIn = "-"+String(newContract.table.tableInfo.buyIn);
         try {
            var result = await addToAccountBalance(playerAccount[0], buyIn, newContract);
            resultObj.contract = newContract;
            gameContracts.push(newContract);
            //create contract history
            newContract.history = new Object();
            newContract.history.deck = new Array();
            newContract.history.keychains = new Object();
            var historyObj = new Object();
            historyObj.fromPID = privateID;
            historyObj.cards = new Array();
            for (var count = 0; count < newContract.cardDecks.faceup.length; count++) {
               if (newContract.cardDecks.faceup[count].mapping != undefined) {
                  historyObj.cards.push(newContract.cardDecks.faceup[count].mapping);
               } else {
                  historyObj.cards.push(newContract.cardDecks.faceup[count]._mapping);
               }
            }
            newContract.history.deck.push(historyObj); //initial history item is new faceup deck
            //save new game contract here
            sendContractMessage("contractnew", newContract, privateID);
         } catch (err) {
            setPlayerBalance(newContract, privateID, "0"); //revert buy-in
            var payloadObj = new Object();
            payloadObj.error = new Object();
            payloadObj.error.message = err.message;
            //notify other contract players of the failure
            sendContractMessage("contractnewfail", newContract, privateID, null, payloadObj);
            cancelContract(newContract);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, err.message, sessionObj);
            return(false);
         }
         break;
      case "agree":
         var contractOwnerPID = requestParams.ownerPID;
         var contractID = requestParams.contractID;
         var gameContract = getContractByID(contractOwnerPID, contractID);
         if (gameContract == null) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "No such contract.", sessionObj);
            return(false);
         }
         if (gameContract.invalid) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Contract is invalid.", sessionObj);
            return(false);
         }
         try {
            var playerAccount = await validAccount(requestParams.account);
         } catch (err) {
            console.error(err);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, err.message, sessionObj);
            return(false);
         }
         var player = getPlayer(gameContract, privateID);
         if (player == null) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Not registered with contract.", sessionObj);
            return(false);
         }
         var playerBalance = bigInt(player.balance);
         if (playerBalance.greater(0)) {
            //this is similar to re-depositing into contract
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Can't agree to contract more than once.", sessionObj);
            return(false);
         }
         var contractCurrencyType = gameContract.table.tableInfo.currency.type;
         var contractCurrencyNetwork = gameContract.table.tableInfo.currency.network;
         if (playerAccount[0].type != contractCurrencyType) {
            var payloadObj = new Object();
            payloadObj.error = new Object();
            payloadObj.error.message = "Attempt to agree using an incompatible currency (\""+playerAccount[0].type+"\").";
            //notify other contract players of the failure
            sendContractMessage("contractagreefail", gameContract, privateID, null, payloadObj);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Can't use currency \""+playerAccount[0].type+"\" to agree to contract using currency \""+contractCurrencyType+"\".", sessionObj);
            return(false);
         }
         if (playerAccount[0].network != contractCurrencyNetwork) {
            payloadObj = new Object();
            payloadObj.error = new Object();
            payloadObj.error.message = "Attempt to agree using an incompatible currency network (\""+playerAccount[0].network+"\").";
            //notify other contract players of the failure
            sendContractMessage("contractagreefail", gameContract, privateID, null, payloadObj);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Can't use currency network \""+playerAccount[0].network+"\" to agree to contract using currency network \""+contractCurrencyNetwork+"\".", sessionObj);
            return(false);
         }
         player.account = new Object();
         player.account.address = requestParams.account.address;
         player.account.type = requestParams.account.type;
         player.account.network = requestParams.account.network;
         player.account.balance = String(playerAccount[0].balance);
         setPlayerBalance(gameContract, privateID, gameContract.table.tableInfo.buyIn);
         //subtract buy-in from account and deposit to contract
         var buyIn = "-"+String(gameContract.table.tableInfo.buyIn);
         try {
            var result = await addToAccountBalance(playerAccount[0], buyIn, gameContract);
            //save game contract here
            resultObj.contract = gameContract;
            sendContractMessage("contractagree", gameContract, privateID);
         } catch (err) {
            setPlayerBalance(gameContract, privateID, "0"); //revert buy-in
            var payloadObj = new Object();
            payloadObj.error = new Object();
            payloadObj.error.message = err.message;
            //notify other contract players of the failure
            sendContractMessage("contractagreefail", gameContract, privateID, null, payloadObj);
            cancelContract(gameContract);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, err.message, sessionObj);
            return(false);
         }
         break;
      case "store":
         if (typeof(requestParams.type) != "string") {
            sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "\"type\" parameter must be a string.", sessionObj);
            return(false);
         }
         if ((typeof(requestParams.contract) != "object") && (requestParams.contract != null)) {
            sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "\"contract\" parameter must be an object.", sessionObj);
            return(false);
         }
         contractOwnerPID = requestParams.ownerPID;
         contractID = requestParams.contractID;
         gameContract = getContractByID(contractOwnerPID, contractID);
         if (gameContract == null) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "No such contract.", sessionObj);
            return(false);
         }
         if (gameContract.invalid) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Contract is invalid.", sessionObj);
            return(false);
         }
         try {
            var playerAccount = await validAccount(requestParams.account);
         } catch (err) {
            console.error(err);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, err.message, sessionObj);
            return(false);
         }
         var player = getPlayer(gameContract, privateID);
         if (player == null) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Not registered with contract.", sessionObj);
            return(false);
         }
         updateDate = new Date();
         if ((gameContract.history == undefined) || (gameContract.history == null)) {
            gameContract.history = new Object();
         }
         if ((gameContract.history.deck == undefined) || (gameContract.history.deck == null)) {
            gameContract.history.deck = new Array();
         }
         //this should be examined:
         try {
            gameContract.cardDecks = requestParams.contract.cardDecks;
         } catch (err) {}
         switch (requestParams.type) {
            case "encrypt":
               if ((typeof(requestParams.cards) == "object") || (requestParams.cards != null)) {
                  if ((typeof(requestParams.cards.length) == "number")) {
                     infoObj = new Object();
                     infoObj.fromPID = privateID;
                     infoObj.cards = Array.from(requestParams.cards);
                     gameContract.history.deck.push (infoObj);
                     //save game contract here
                     resultObj.contract = gameContract;
                     updatePlayersTimeout(privateID, getDealer(gameContract).privateID, gameContract, "store", "encrypt", gameContract.history.deck);
                     try {
                        sendContractMessage("contractencryptstore", gameContract, privateID);
                     } catch (err) {
                        console.error(err.stack);
                        sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not store encryption round.", sessionObj);
                        return(false);
                     }
                  } else {
                     sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"cards\" array.", sessionObj);
                     return(false);
                  }
               } else {
                  sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"cards\" array.", sessionObj);
                  return(false);
               }
               break;
            case "select":
               if ((typeof(requestParams.cards) == "object") || (requestParams.cards != null)) {
                  if ((typeof(requestParams.cards.length) == "number")) {
                     infoObj = new Object();
                     //if infoObj.fromPID != privateID here, they're trying to spoof a PID. But it doesn't matter since:
                     infoObj.fromPID = privateID;
                     infoObj.type = "select";
                     infoObj.private = requestParams.private; //probably doesn't need to be checked
                     infoObj.cards = Array.from(requestParams.cards);
                     if ((gameContract.history.deals == undefined) || (gameContract.history.deals == null)) {
                        gameContract.history.deals = new Object();
                     }
                     if ((gameContract.history.deals[privateID] == undefined) || (gameContract.history.deals[privateID] == null)) {
                        gameContract.history.deals[privateID] = new Array();
                     }
                     gameContract.history.deals[privateID].push (infoObj);
                     //save game contract here
                     resultObj.contract = gameContract;
                     updatePlayersTimeout(privateID, privateID, gameContract, "store", "select", gameContract.history.deals[privateID]);
                     try {
                        sendContractMessage("contractselectstore", gameContract, privateID);
                     } catch (err) {
                        console.error(err.stack);
                        sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not store decryption round.", sessionObj);
                        return(false);
                     }
                  } else {
                     sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"cards\" array.", sessionObj);
                     return(false);
                  }
               } else {
                  sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"cards\" array.", sessionObj);
                  return(false);
               }
               break;
            case "decrypt":
               if ((typeof(requestParams.cards) == "object") || (requestParams.cards != null)) {
                  if ((typeof(requestParams.cards.length) == "number")) {
                     var sourcePID = requestParams.sourcePID; //the deal initiator
                     infoObj = new Object();
                     infoObj.fromPID = privateID; //the last decryptor
                     infoObj.type = "decrypt";
                     infoObj.private = requestParams.private;
                     infoObj.cards = Array.from(requestParams.cards);
                     if ((gameContract.history.deals == undefined) || (gameContract.history.deals == null)) {
                        gameContract.history.deals = new Object();
                     }
                     if ((gameContract.history.deals[sourcePID] == undefined) || (gameContract.history.deals[sourcePID] == null)) {
                        gameContract.history.deals[sourcePID] = new Array();
                     }
                     gameContract.history.deals[sourcePID].push (infoObj);
                     updatePlayersTimeout(privateID, sourcePID, gameContract, "store", "decrypt", gameContract.history.deals[sourcePID]);
                     //save game contract here
                     resultObj.contract = gameContract;
                     try {
                        sendContractMessage("contractdecryptstore", gameContract, privateID);
                     } catch (err) {
                        console.error(err.stack);
                        sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not store decryption round.", sessionObj);
                        return(false);
                     }
                  } else {
                     sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"cards\" array.", sessionObj);
                     return(false);
                  }
               } else {
                  sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"cards\" array.", sessionObj);
                  return(false);
               }
               break;
            case "keychain":
               if ((typeof(requestParams.keychain) == "object") || (requestParams.keychain != null)) {
                  if ((typeof(requestParams.keychain.length) == "number")) {
                     if ((gameContract.history.keychains[privateID] != undefined) && (gameContract.history.keychains[privateID] != null)) {
                        console.error(privateID+" has attempted to re-submit keychain for contract: "+contractID);
                        sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Keychain can only be stored once.", sessionObj);
                        return(false);
                     }
                     gameContract.history.keychains[privateID] = requestParams.keychain;
                     updatePlayersTimeout(privateID, privateID, gameContract, "store", "keychain", gameContract.history.keychains[privateID]);
                     //save game contract here
                     resultObj.contract = gameContract;
                     try {
                        sendContractMessage("contractkeychainstore", gameContract, privateID);
                     } catch (err) {
                        console.error(err.stack);
                        sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not store keychain.", sessionObj);
                        return(false);
                     }
                     var keychainsFound = 0;
                     for (var items in gameContract.history.keychains) {
                        keychainsFound++;
                     }
                     if (keychainsFound == gameContract.players.length) {
                        try {
                           var nonFoldedPlayers = new Array();
                           for (count = 0; count < gameContract.players.length; count++) {
                              if ((gameContract.players[count]._hasFolded == false) || (gameContract.players[count].hasFolded == false)) {
                                 nonFoldedPlayers.push(gameContract.players[count]);
                              }
                           }
                           if (nonFoldedPlayers.length > 1) {
                              try {
                                 var analyzeResult = await analyzeCards(gameContract);
                              } catch (err) {
                                 console.error(err);
                                 //currently everyone gets a refund (until contract is more stable)
                                 err.failedPIDs = new Array();
                                 try {
                                    var penaltyResult = await applyPenalty(gameContract, err.failedPIDs, "validate");
                                    gameContract.penalty = penaltyResult;
                                    gameContract.invalid = true;
                                 } catch (err) {
                                    console.error(err.stack);
                                    gameContract.penalty = null;
                                    sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Could not apply validation penalty.", sessionObj);
                                    return(false);
                                 }
                                 sendError(JSONRPC_ERRORS.PLAYER_ACTION_ERROR, "Contract validation failed.", sessionObj);
                                 return (false);
                              }
                              var scoreResult = await scoreHands(gameContract);
                              //Additional information can be gathered from:
                              //   scoreResult.winningPlayers
                              //   scoreResult.winningHands
                              //console.log ("Contract "+contractID+" completed.");
                           } else {
                              //all but player nonFoldedPlayers[0].privateID have folded
                              //console.log ("Contract "+contractID+" played to end.");
                              //console.log ("All but one player have folded: "+nonFoldedPlayers[0].privateID);
                              scoreResult = new Object();
                              scoreResult.winningPlayers = new Array();
                              scoreResult.winningPlayers.push(nonFoldedPlayers[0]);
                              scoreResult.winningHands = new Array();
                           }
                           var winnings = bigInt(gameContract.pot);
                           winnings = winnings.divide(scoreResult.winningPlayers.length); //this may produce rounding errors
                           for (count = 0; count < scoreResult.winningPlayers.length; count++) {
                              var winningPlayer = scoreResult.winningPlayers[count];
                              try {
                                 var accountResult = await namespace.cp.getAccount(winningPlayer.account, false);
                                 var result = await addToAccountBalance(accountResult[0], winnings.toString(10), gameContract);
                              } catch (err) {
                                 console.error(err.stack);
                                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not update account balance.", sessionObj);
                                 return(false);
                              }
                           }
                           for (count = 0; count < gameContract.players.length; count++) {
                              var currentPlayer = gameContract.players[count];
                              try {
                                 var accountResult = await namespace.cp.getAccount(currentPlayer.account, false);
                                 var result = await addToAccountBalance(accountResult[0], currentPlayer.balance, gameContract);
                              } catch (err) {
                                 console.error(err.stack);
                                 sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not update account balance.", sessionObj);
                                 return(false);
                              }
                           }
                           gameContract.invalid = true;
                           //save game contract here
                           sendContractMessage("contractend", gameContract);
                        } catch (err) {
                           console.error(err);
                        }
                        //analyze here
                     } else {
                        //contract is waiting for additional keychains
                     }
                  } else {
                     sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"keychain\" array.", sessionObj);
                     return(false);
                  }
               } else {
                  sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"keychain\" array.", sessionObj);
                  return(false);
               }
               return (true);
               break;
            default:
               sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Unrecognized store \"type\"", sessionObj);
               return(false);
               break;
         }
         break;
      case "bet":
         var contractOwnerPID = requestParams.ownerPID;
         var contractID = requestParams.contractID;
         var gameContract = getContractByID(contractOwnerPID, contractID);
         if (typeof(requestParams.amount) != "string")
         if (gameContract == null) {
            console.error ("Attempt to access non-existent contract: "+contractID);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "No such contract.", sessionObj);
            return(false);
         }
         if (gameContract.invalid) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Contract is invalid.", sessionObj);
            return(false);
         }
         try {
            var playerAccount = await validAccount(requestParams.account);
         } catch (err) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, err.message, sessionObj);
            return(false);
         }
         var player = getPlayer(gameContract, privateID);
         if (player == null) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Not registered with contract.", sessionObj);
            return(false);
         }
         var playerBalance = bigInt(player.balance);
         var totalBet = bigInt(player.totalBet);
         var betAmount = bigInt(requestParams.amount);
         if (betAmount.greater(0)) {
            totalBet = totalBet.plus(betAmount);
         }
         player.totalBet = totalBet.toString(10);
         player.numActions++;
         player.hasBet = true;
         player._hasBet = true;
         try {
            if (betAmount.lesser(0)) {
               //folding
               player.hasFolded = true;
               player._hasFolded = true;
               player.hasBet = true;
               player._hasBet = true;
               player.totalBet = "0";
               updatePlayersTimeout(privateID, privateID, gameContract, "bet");
               if (bettingDone(gameContract) == true) {
                  for (var count = 0; count < gameContract.players.length; count++) {
                     gameContract.players[count].hasBet = false;
                  }
                  updatePlayersTimeout(privateID, privateID, gameContract, "deal"); //do this after resetting everyone!
               }
            } else {
               //betting or raising (betAmount.greater(0)) / checking (betAmount.equals(0))
               gameContract.pot = bigInt(gameContract.pot).plus(betAmount).toString(10);
               var biggestBet = largestBet(gameContract);
               var totalCurrentBet = bigInt(player.totalBet);
               if (totalCurrentBet.equals(biggestBet)) {
                  //matched bet / checking / calling
                  if (bettingDone(gameContract) == true) {
                     for (var count = 0; count < gameContract.players.length; count++) {
                        gameContract.players[count].hasBet = false;
                     }
                     updatePlayersTimeout(privateID, privateID, gameContract, "deal"); //do this after resetting everyone!
                  } else {
                     updatePlayersTimeout(privateID, privateID, gameContract, "bet");
                  }
               } else if (totalCurrentBet.greater(biggestBet)) {
                  //raising
                  updatePlayersTimeout(privateID, privateID, gameContract, "bet"); //do this before resetting everyone!
                  for (var count = 0; count < gameContract.players.length; count++) {
                     if (gameContract.players[count].privateID != privateID) {
                        gameContract.players[count].hasBet = false;
                     }
                  }
               }
               setPlayerBalance(gameContract, privateID, playerBalance.minus(betAmount).toString(10));
            }
            //save game contract here
            resultObj.contract = gameContract;
            sendContractMessage("contractbet", gameContract, privateID);
         } catch (err) {
            //attempt to reverse the bet
            if (betAmount.greater(0)) {
               gameContract.pot = bigInt(gameContract.pot).minus(betAmount).toString(10);
            }
            console.error(err.stack);
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not update account balance.", sessionObj);
            return(false);
         }
         break;
      case "timeout":
         contractOwnerPID = requestParams.ownerPID;
         contractID = requestParams.contractID;
         gameContract = getContractByID(contractOwnerPID, contractID);
         if (gameContract == null) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "No such contract.", sessionObj);
            return(false);
         }
         if (gameContract.invalid) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Contract is invalid.", sessionObj);
            return(false);
         }
         try {
            var playerAccount = await validAccount(requestParams.account);
         } catch (err) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, err.message, sessionObj);
            return(false);
         }
         var player = getPlayer(gameContract, privateID);
         if (player == null) {
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Not registered with contract.", sessionObj);
            return(false);
         }
         if (typeof(gameContract.table.tableInfo.timeout) == "number") {
            //use contract-defined timeout
            var timeout = gameContract.table.tableInfo.timeout;
         } else {
            //use config-defined timeout
            timeout = config.CP.API.contract.timeoutDefault;
         }
         var timedoutPlayers = checkContractTimeout(gameContract, timeout);
         if (timedoutPlayers.length > 0) {
            var timedoutPIDs = new Array();
            for (count = 0; count < timedoutPlayers.length; count++) {
               timedoutPIDs.push(timedoutPlayers[count].privateID);
            }
            try {
               var penaltyResult = await applyPenalty(gameContract, timedoutPIDs, "timeout");
               gameContract.penalty = penaltyResult;
            } catch (err) {
               console.error(err.stack);
               gameContract.penalty = null;
               sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Could not apply timeout penalty.", sessionObj);
               return(false);
            }
         }
         gameContract.invalid = true;
         resultObj.contract = gameContract;
         //save game contract here
         sendContractMessage("contracttimeout", gameContract);
         break;
      default:
         sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Unrecognized action.", sessionObj);
         return(false);
         break;
   }
   sendResult(resultObj, sessionObj);
   return(true);
}

/**
* Analyzes a contract's history for cryptographic correctness and returns
* the verified, decrypted cards (as {@link CypherPokerCard} instances),
* for each player along with the public / community cards. This function should
* only be called when the game has completed and all keychains received.
*
* @param {ContractObject} contract The contract containing the <code>history</code>
* to analyze.
*
* @return {Promise} The promise resolves with an object containing a <code>players</code>
* object containing name/value pairs with each name matching a player private ID and containing
* an array of {@link CypherPokerCard} instances, and a <code>public</code>
* property containing an array of the public / community {@link CypherPokerCard} instances.
* If the analysis fails it is rejected with an <code>Error</code> which includes a
* <code>message</code> and numeric <code>code</code> identifying the analysis failure.
*
* @async
* @private
*/
async function analyzeCards(contract) {
   var history = contract.history;
   if ((history.analysis == undefined) || (history.analysis == null)) {
      history.analysis = new Object();
   }
   //step 1: analyze the full deck (creation & encryption)
   if (history.deck.length == 0) {
      return (null);
   }
   //todo: check to ensure that all values are quadratic residues
   var cardsObj = history.analysis;
   cardsObj.private = new Object();
   cardsObj.public = new Array();
   var faceUpMappings = Array.from(history.deck[0].cards); //generated plaintext (quadratic residues) values
   var previousDeck = Array.from(faceUpMappings);
   for (var count = 1; count < history.deck.length; count++) {
      var currentDeck = Array.from(history.deck[count].cards);
      var keychain = history.keychains[history.deck[count].fromPID];
      var resultDeck = new Array();
      try {
         for (var count2=0; count2 < previousDeck.length; count2++) {
            resultDeck.push(SRAEncrypt(keychain[0], previousDeck[count2]));
         }
      } catch (err) {
         var error = new Error("Likely problem with keychain for PID: "+history.deck[count].fromPID);
         error.code = 1;
         error.failedPIDs = new Array();
         error.failedPIDs.push (history.deck[count].fromPID);
         history.analysis.error = err;
         history.analysis.complete = true;
         throw (error);
      }
      if (compareDecks(currentDeck, resultDeck) == false) {
         var error = new Error("Deck encryption at stage "+count+" by \""+history.deck[count].fromPID+"\" failed.");
         error.code = 1;
         error.failedPIDs = new Array();
         error.failedPIDs.push (history.deck[count].fromPID);
         history.analysis.error = error;
         history.analysis.complete = true;
         throw (error);
      }
      previousDeck = currentDeck;
   }
   //previousDeck should now contain the fully encrypted deck
   var encryptedDeck = previousDeck;
   //step 1: passed
   //step 2: analyze private / public card selections and decryptions
   history.deals = fixDealsOrder(contract);
   for (var privateID in history.deals) {
      var dealArray = history.deals[privateID];
      var decrypting = false; //currently decrypting cards?
      var previousType = "select"; //should match dealArray[0].type
      for (count = 0; count < dealArray.length; count++) {
         var currentDeal = dealArray[count];
         if ((currentDeal == undefined) || (currentDeal == null)) {
            //not a deal history object (probably inherited onEventPromise)
            break;
         }
         if (count > 0) {
            var previousDeal = dealArray[count-1];
            var previousCards = previousDeal.cards;
            var previousPID = previousDeal.fromPID;
            var previousPrivate = previousDeal.private;
            previousType = previousDeal.type;
         }
         var sourcePID = privateID; //card dealer / selector
         var fromPID = currentDeal.fromPID; //private ID of "cards" (result) sender
         var type = currentDeal.type; //"select" or "decrypt"
         var privateDeal = currentDeal.private; //private / hole cards?
         var cards = currentDeal.cards; //numeric card value strings, encrypted or plaintext;
         if (cardsObj.private[sourcePID] == undefined) {
            cardsObj.private[sourcePID] = new Array();
         };
         if ((previousType == "select") && (type == "select")) {
            //probably the first entry but...
            if (count > 0) {
               var error = new Error("Multiple sequential \"select\" sequences in deal.");
               error.code = 2;
               error.failedPIDs = new Array();
               error.failedPIDs.push (fromPID);
               history.analysis.error = error;
               history.analysis.complete = true;
               throw (error);
            }
            if (removeFromDeck(cards, encryptedDeck) == false) {
               var error = new Error("Duplicates found in \"select\" deal index "+count+" for \""+fromPID+"\".");
               error.code = 2;
               error.failedPIDs = new Array();
               error.failedPIDs.push (fromPID);
               history.analysis.error = error;
               history.analysis.complete = true;
               throw (error);
            }
         } else if ((previousType == "select") && (type == "decrypt") && (count < (dealArray.length - 1))) {
            //starting a new decryption operation (deal or select cards)
            decrypting = true;
         } else if ((previousType == "decrypt") && (type == "select")) {
            //ending decryption operation (final decryption outstanding)
            keychain = history.keychains[sourcePID];
            promises = new Array();
            promiseResults = new Array();
            try {
               for (count2=0; count2 < previousCards.length; count2++) {
                  promiseResults.push(SRADecrypt(keychain[0], previousCards[count2]));
               }
            } catch (err) {
               var error = new Error("Likely problem with keychain for PID: "+sourcePID);
               error.code = 1;
               error.failedPIDs = new Array();
               error.failedPIDs.push (sourcePID);
               history.analysis.error = err;
               history.analysis.complete = true;
               throw (error);
            }
            var dealtCards = new Array();
            for (count2 = 0; count2 < promiseResults.length; count2++) {
               var card = getMappedCard(contract, promiseResults[count2]);
               if (card == null) {
                  var error = new Error("Final decryption (deal "+count+") by \""+fromPID+"\" does not map: "+promiseResults[count2]);
                  error.code = 2;
                  error.failedPIDs = new Array();
                  error.failedPIDs.push (fromPID);
                  history.analysis.error = error;
                  history.analysis.complete = true;
                  throw (error);
               }
               if (previousPrivate) {
                  cardsObj.private[sourcePID].push(card);
               } else {
                  cardsObj.public.push(card);
               }
            }
            if (removeFromDeck(cards, encryptedDeck) == false) {
               var error = new Error("Duplicates found in \"select\" deal index "+count+" for \""+fromPID+"\".");
               error.code = 2;
               error.failedPIDs = new Array();
               error.failedPIDs.push (fromPID);
               history.analysis.error = error;
               history.analysis.complete = true;
               throw (error);
            }
         } else {
            //decryption in progress
            if (count == (dealArray.length - 1)) {
               //final decryption for source
               keychain = history.keychains[sourcePID];
               promises = new Array();
               promiseResults = new Array();
               try {
                  for (count2=0; count2 < cards.length; count2++) {
                     promiseResults.push(SRADecrypt(keychain[0], cards[count2]));
                  }
               } catch (err) {
                  var error = new Error("Likely problem with keychain for PID: "+sourcePID);
                  error.code = 1;
                  error.failedPIDs = new Array();
                  error.failedPIDs.push (sourcePID);
                  history.analysis.error = err;
                  history.analysis.complete = true;
                  throw (error);
               }
               for (count2 = 0; count2 < promiseResults.length; count2++) {
                  var card = getMappedCard(contract, promiseResults[count2]);
                  if (card == null) {
                     var error = new Error("Final decryption (deal "+count+") by \""+fromPID+"\" does not map: "+promiseResults[count2]);
                     error.code = 2;
                     error.failedPIDs = new Array();
                     error.failedPIDs.push (fromPID);
                     history.analysis.error = error;
                     history.analysis.complete = true;
                     throw (error);
                  }
                  if (privateDeal) {
                     cardsObj.private[sourcePID].push(card);
                  } else {
                     cardsObj.public.push(card);
                  }
               }
            } else {
               //continuing decryption from another player
               keychain = history.keychains[fromPID];
               compareDeck = new Array();
               promises = new Array();
               promiseResults = new Array();
               try {
                  //decrypt current cards to compare to what was sent by current player...
                  for (count2=0; count2 < previousCards.length; count2++) {
                     promises.push(SRADecrypt(keychain[0], previousCards[count2]));
                  }
               } catch (err) {
                  var error = new Error("Likely problem with keychain for PID: "+fromPID);
                  error.code = 1;
                  error.failedPIDs = new Array();
                  error.failedPIDs.push (fromPID);
                  history.analysis.error = err;
                  history.analysis.complete = true;
                  throw (error);
               }
               promiseResults = await Promise.all(promises);
               for (count2 = 0; count2 < promiseResults.length; count2++) {
                  compareDeck.push(promiseResults[count2]);
               }
               if (compareDecks(compareDeck, cards) == false) {
                  var error = new Error("Previous round ("+count+") of decryption by \""+fromPID+"\" for \""+sourcePID+"\" does not match computed results.");
                  error.code = 2;
                  error.failedPIDs = new Array();
                  error.failedPIDs.push (fromPID);
                  history.analysis.error = error;
                  history.analysis.complete = true;
                  throw (error);
               }
            }
         }
      }
   }
   return (cardsObj);
}

/**
* Generates player card permutations for analysis and scores the hands.
*
* @param {ContractObject} contract The analyzed and validated (using {@link analyzeCards}),
* contract object to use for scoring.
*
* @return {Object} Contains the arrays <code>winningPlayers</code> containing the
* winning player object(s) for the <code>contract</code>, and <code>winningHands</code>
* which contains the associated winning hand(s) for the player(s).
* @private
*/
async function scoreHands(contract) {
   var cardsObj = contract.history.analysis;
   var analysis = contract.history.analysis;
   var playersObj = cardsObj.private;
   cardsObj.hands = new Object();
   var playerHands = new Object();
   var highestScore = -1;
   var highestHand = new Array;
   var winningPlayers = new Array();
   var winningHands = new Array();
   for (var privateID in playersObj) {
      var player = getPlayer(contract, privateID);
      //private ID may actually be some other object property (e.g. onEventPromise)
      if (player != null) {
         if (player.hasFolded == false) {
            var fullCards = playersObj[privateID].concat(cardsObj.public);
            cardsObj.hands[privateID] = new Array();
            var perms = createCardPermutations(fullCards);
            for (var count = 0; count < perms.length; count++) {
               var handObj = new Object();
               handObj.hand = perms[count];
               handObj.score = -1; //default (not scored)
               scoreHand (handObj);
               cardsObj.hands[privateID].push(handObj);
               if (handObj.score == highestScore) {
                  //this may be a split pot; see below
                  winningPlayers.push(player);
                  winningHands.push(handObj);
               } else if (handObj.score > highestScore) {
                  //new best hand
                  winningPlayers = new Array();
                  winningHands = new Array();
                  winningPlayers.push(player);
                  winningHands.push(handObj);
                  highestScore = handObj.score;
               }
            }
         }
      }
   }
   if (winningPlayers.length > 1) {
      //need to look at both private cards since we currently have a potential split pot
      var newWinningPlayers = new Array();
      var newWinningHands = new Array();
      var highestScore = 0;
      for (count = 0; count < winningPlayers.length; count++) {
         var winningHand = winningHands[count]; //indexes match with winningPlayers
         var hand = winningHand.hand;
         var player = winningPlayers[count];
         var playerPID = player.privateID;
         var privateCard1 = analysis.private[playerPID][0];
         var privateCard2 = analysis.private[playerPID][1];
         //adjust score for highest card value
         if (privateCard1.highvalue > privateCard2.highvalue) {
            var currentScore = (privateCard1.highvalue * 10) + privateCard2.highvalue;
         } else {
            currentScore = (privateCard2.highvalue * 10) + privateCard1.highvalue;
         }
         winningHand.score = currentScore;
         if (currentScore > highestScore) {
            highestScore = currentScore;
            newWinningPlayers = new Array();
            newWinningHands = new Array();
            newWinningPlayers.push(player);
            newWinningHands.push(winningHand);
         } else if (currentScore == highestScore) {
            //both private card values are the same -- possible split pot
            var playerExists = false;
            for (count2 = 0; count2 < newWinningPlayers.length; count2++) {
               if (newWinningPlayers[count2].privateID == player.privateID) {
                  playerExists = true;
                  break;
               }
            }
            //only add player once (since some hands generate multiple similar results)
            if (playerExists == false) {
               newWinningPlayers.push(player);
               newWinningHands.push(winningHand);
            }
         }
      }
      winningPlayers = newWinningPlayers;
      winningHands = newWinningHands;
   }
   cardsObj.winningPlayers = winningPlayers;
   cardsObj.winningHands = winningHands;
   //console.log ("Winning players:");
   //console.dir (winningPlayers);
   //console.log ("Winning Hands:");
   //console.dir (winningHands);
   return (cardsObj);
}

/**
* Scores a 5 cards (or fewer) poker hand. The higher the score the
* better the hand.
*
* @param {Object} handObj A hand permutation and score object. This object is
* direccly updated with the resulting score.
* @param {Array} handObj.hand Array of {@link CypherPokerCard} instances
* comprising the hand to score.
* @param {Number} handObj.score=-1 Calculated score of final hand. -1 means
* that the hand is not scored.
*
* @private
*/
function scoreHand(handObj) {
   if ((handObj.hand == undefined) || (handObj.hand == null)) {
      return;
   }
   handObj.score = -1;
   //create groups sorted by suits and values
   var suitGroups = new Object();
   var valueGroups = new Object();
   for (count = 0; count < handObj.hand.length; count++) {
      var currentCard = handObj.hand[count];
      var suit = currentCard.suit;
      var value = currentCard.value;
      if (suitGroups[suit] == undefined) {
         suitGroups[suit] = new Array();
      }
      if (valueGroups[value] == undefined) {
         valueGroups[value] = new Array();
      }
      suitGroups[suit].push(currentCard);
      valueGroups[value].push(currentCard);
   }
   //convert group objects to arrays of arrays
   suitGroups = Object.entries(suitGroups);
   valueGroups = Object.entries(valueGroups);
   var flush = false;
   //evaluate for flush (only 1 suit and 5 cards):
   if ((suitGroups.length == 1) && (handObj.hand.length == 5)) {
      flush = true;
   }
   //evaluate straight:
   var straight = false;
   var royalflush = false;
   var acesHigh = true;
   var valuesArr = new Array();
   var handValue = 0; //the base numeric value of the hand
   var valueMultiplier = 1; //the multiplier applied to handValue to determine the score
   var valueAdjust = 0; //the amount to adjust the hand value in the final calculation
   for (var count = 0; count < handObj.hand.length; count++) {
      valuesArr.push(handObj.hand[count].value);
   };
   var straightVal = straightType(valuesArr);
   if ((straightVal == 10) && flush) {
      straight = true;
      royalflush = true;
   } else if (straightVal > 0) {
      straight = true;
   }
   if (royalflush) {
      valueMultiplier = 1000000000;
      handObj.name = "Royal Flush";
   } else if (straight && flush) {
      if (straightVal == 1) {
         //this is a straight starting with an ace
         acesHigh = false;
      }
      valueMultiplier = 100000000;
      handObj.name = "Straight Flush";
   } else if ((valueGroups.length == 2) && (handObj.hand.length >= 5)) {
      if ((valueGroups[0][1].length == 4) || (valueGroups[1][1].length == 4)) {
         valueMultiplier = 10000000;
         for (count = 0; count < valueGroups.length; count++) {
            if (valueGroups[count][1].length != 4) {
               var cardSum = getCardSum(valueGroups[count][1]);
               //remove multiplied values for cards that are not in the hand
               //otherwise they can cause significant scoring problems
               valueAdjust = (cardSum * valueMultiplier * -1) + cardSum;
            }
         }
         handObj.name = "Four of a Kind";
      }
      if ((valueGroups[0][1].length == 3) || (valueGroups[1][1].length == 3)) {
         valueMultiplier = 1000000;
         handObj.name = "Full House";
      }
   } else if (flush && (straight==false)) {
      valueMultiplier = 100000;
      handObj.name = "Flush";
   } else if (straight && (flush == false)) {
      if (straightVal == 1) {
         //this is a straight starting with an ace
         acesHigh = false;
      }
      valueMultiplier = 10000;
      handObj.name = "Straight";
   } else if (valueGroups.length == 3) {
      if ((valueGroups[0][1].length == 3) || (valueGroups[1][1].length == 3) || (valueGroups[2][1].length == 3)) {
         valueMultiplier = 1000;
         for (count = 0; count < valueGroups.length; count++) {
            if (valueGroups[count][1].length != 3) {
               var cardSum = getCardSum(valueGroups[count][1]);
               valueAdjust = (cardSum * valueMultiplier * -1) + cardSum;
            }
         }
         handObj.name = "Three of a Kind";
      } else {
         valueMultiplier = 100;
         handObj.name = "Two Pairs";
         for (count = 0; count < valueGroups.length; count++) {
            if (valueGroups[count][1].length != 2) {
               var cardSum = getCardSum(valueGroups[count][1]);
               valueAdjust = (cardSum * valueMultiplier * -1) + cardSum;
            }
         }
      }
   } else if ((valueGroups.length == 4) || (valueGroups.length == 1)) {
      //valueGroups.length == 1 on flop
      valueMultiplier = 15;
      for (count = 0; count < valueGroups.length; count++) {
         if (valueGroups[count][1].length != 2) {
            var cardSum = getCardSum(valueGroups[count][1]);
            valueAdjust = (cardSum * valueMultiplier * -1) + cardSum;
         }
      }
      handObj.name = "One Pair";
   } else {
      handObj.name = "High Card";
   }
   if (valueMultiplier > 1) {
      for (count = 0; count < handObj.hand.length; count++) {
         if (acesHigh) {
            handValue += handObj.hand[count].highvalue;
         } else {
            handValue += handObj.hand[count].value;
         }
      }
   } else {
      //high card (secondary scan is required for additional card)
      handValue = 0;
      for (count = 0; count < handObj.hand.length; count++) {
         if (handValue < handObj.hand[count].highvalue) {
            handValue = handObj.hand[count].highvalue;
         }
      }
   }
   handObj.score = (handValue * valueMultiplier) + valueAdjust;
}

/**
* Evaluates an unordered series of card values to determine what type of
* straight they comprise.
*
* @param {Array} cardValues Unordered sequence of card values to analyze.
*
* @return {Number} A 0 is returned if the input is not a straight, otherwise
* the lowest value in the straight is returned (e.g. if <code>cardValues=[4,3,5,6,7]</code>
* then 3 is returned).
* @private
*/
function straightType(cardValues) {
   if (cardValues.length < 5) {
      //need 5 cards for a straight
      return (0);
   }
   //check for ace through 9
   for (var count = 1; count < 10; count++) {
      if (compareDecks(cardValues, [count, count+1, count+2, count+3, count+4])) {
         return (count);
      }
   }
   //check for high ace with a 10 (is there a more elegant way to do this in the "for" loop?)
   if (compareDecks(cardValues, [10,11,12,13,1])) {
      return (10);
   }
   //not a straight
   return (0);
}

/**
*  Sum the {@link CypherPokerCard} instances provided.
*
* @param {Array} cards Indexed array of {@link CypherPokerCard} instances to sum.
* @param {Boolean} [high=true] If true, the card's <code>highvalue</code> is used to
* calculate the sum otherwise its <code>value</code> is used.
* @return {Number} The numeric sum of the card values.
* @private
*/
function getCardSum(cards, high=true) {
   var sum = new Number(0);
   for (var count = 0; count < cards.length; count++) {
      if (high) {
         sum += cards[count].highvalue;
      } else {
         sum += cards[count].value;
      }
   }
   return (sum);
}

/**
* Fixes potential deal order problems within a {@link ContractObject}.
*
* @param {ContractObject} contract The contract conntaining a <code>history.deals</code>
* object to fix.
*
* @return {Object} A structure similar to the <code>contract.history.deals</code> but with
* deal orders fixed according to the betting order in the <code>contract.players</code> array.
* @private
*/
function fixDealsOrder(contract) {
   var deals = contract.history.deals;
   var returnDeals = new Object();
   for (var privateID in deals) {
      var playerDeals = deals[privateID];
      returnDeals[privateID] = new Array();
      var dealIndex = 0;
      var startingPlayerIndex = 0;
      for (var count = 0; count < contract.players.length; count++) {
         if (contract.players[count].privateID == privateID) {
            startingPlayerIndex = (count + 1) % contract.players.length;
            break;
         }
      }
      var playerIndex = startingPlayerIndex;
      for (count = 0; count < playerDeals.length; count++) {
         if (playerDeals[count].type == "select") {
            returnDeals[privateID].push(playerDeals[count]); //store select
            if (count > 0) {
               //if not first "select"
               dealIndex++;
               playerIndex = startingPlayerIndex;
            }
         } else {
            var nextFromPID = contract.players[playerIndex].privateID;
            var nextDeal = getNextDealAction (playerDeals, nextFromPID, dealIndex);
            returnDeals[privateID].push(nextDeal);
            playerIndex = (playerIndex + 1) % contract.players.length;
         }
      }
   }
   return (returnDeals);
}

/**
* Checks to see if any of a contract's players have timed out and returns the list
* of those that have.
*
* @param {ContractObject} contract The contract to analyze.
* @param {Number} [timeoutThreshold=20] The number of seconds to elapse before a
* player is considered timed out.
*
* @return {Array} A list of all players that have timed out. This will usually only
* be a single player who's <code>updated</code> property is the oldest but may be more
* than one if they're exactly the same.
* @private
*/
function checkContractTimeout(contract, timeoutThreshold=20) {
   var currentTimestamp = new Date();
   var timedoutPlayers = new Array();
   for (var count=0; count < contract.players.length; count++) {
      if (typeof(contract.players[count].updated) == "string") {
         var playerTimestamp = new Date(contract.players[count].updated);
         //we assume that player timestamp is always in the past or at the most present
         //with respect to the system time:
         if ((currentTimestamp.valueOf() - playerTimestamp.valueOf()) >= (timeoutThreshold * 1000)) {
            timedoutPlayers.push(contract.players[count]);
         }
      }
   }
   var returnPlayers = new Array();
   if (timedoutPlayers.length > 0) {
      var oldestPlayer = timedoutPlayers[0];
      var oldestTimestamp = new Date(oldestPlayer.updated);
      for (count=0; count < timedoutPlayers.length; count++) {
         var playerTimestamp = new Date(timedoutPlayers[count].updated);
         if (playerTimestamp.valueOf() < oldestTimestamp.valueOf()) {
            //we have a new oldest timestamp
            returnPlayers = new Array();
            returnPlayers.push(timedoutPlayers[count]);
            oldestPlayer = timedoutPlayers[count];
            oldestTimestamp = new Date(oldestPlayer.updated);
         } else if (oldestTimestamp.valueOf() == playerTimestamp.valueOf()) {
            //it's the same
            returnPlayers.push(timedoutPlayers[count]);
         } else {
            //it's newer
         }
      }
   }
   return (returnPlayers);
}

/**
* Immediately applies a specific penalty to the supplied player(s)
* associated with a contract.
*
* @param {ContractObject} contract The contract to use as the authority on how to apply
* the penalty.
* @param {Array} playerPIDs The player(s) to be penalized.
* @param {String} penaltyType The type of infraction that the player committed, to
* be correlated to the penalty.
*
* @return {Promise} Resolves with an object containing details about the penalty applied.
* Rejects with an {@link Error} object.
* @private
* @async
*/
async function applyPenalty (contract, playerPIDs, penaltyType) {
   var penaltyReport = new Object();
   switch (penaltyType) {
      case "timeout":
         //distributes all penalized players' funds (bets and balance) to other players
         penaltyReport.penalized = new Array();
         penaltyReport.awarded = new Array();
         var distributionAmount = bigInt(contract.pot);
         var penalizedPIDs = new Array();
         for (var count = 0; count < playerPIDs.length; count++) {
            var player = getPlayer(contract, playerPIDs[count]);
            if (player != null) {
               var playerBalance = bigInt(player.balance);
               distributionAmount = distributionAmount.plus(playerBalance);
               player.balance = "0"; //entire balance is lost
               var penalizationObj = new Object();
               penalizationObj.privateID = playerPIDs[count];
               penalizationObj.amount = player.balance;
               penaltyReport.penalized.push (penalizationObj);
               penalizedPIDs.push(player.privateID);
            }
         }
         //console.log ("Player(s) \""+penalizedPIDs+"\" has/have timed out contract: "+contract.contractID);
         var distributionPIDs = new Array();
         for (count = 0; count < contract.players.length; count++) {
            var currentPlayer = contract.players[count];
            var penaltyPlayer = playerPIDs.find(penaltyPrivateID => {
               return (currentPlayer.privateID == penaltyPrivateID);
            });
            if (penaltyPlayer == undefined) {
               distributionPIDs.push (currentPlayer.privateID);
            }
         }
         if (distributionPIDs.length == 0) {
            //everyone timed out equally at some critical step / contract is simply refunded
            distributionPIDs = penalizedPIDs;
         }
         var perPlayerAmount = distributionAmount.divide(distributionPIDs.length);
         for (count = 0; count < distributionPIDs.length; count++) {
            var currentPlayer = getPlayer(contract, distributionPIDs[count]);
            if (currentPlayer != null) {
               var currentPlayerBalance = bigInt(currentPlayer.balance);
               currentPlayerBalance = currentPlayerBalance.plus(perPlayerAmount);
               currentPlayer.balance = currentPlayerBalance.toString(10);
               var searchObj = new Object();
               searchObj.address = currentPlayer.account.address;
               searchObj.type = currentPlayer.account.type;
               searchObj.network = currentPlayer.account.network;
               var accountResult = await namespace.cp.getAccount(searchObj);
               //subtract buy-in from account and deposit to contract
               try {
                  var result = await addToAccountBalance(accountResult[0], currentPlayer.balance, contract);
                  //save game contract here
                  var awardObj = new Object();
                  awardObj.privateID = currentPlayer.privateID;
                  awardObj.amount = perPlayerAmount.toString(10);
                  penaltyReport.awarded.push (awardObj);
               } catch (err) {
                  console.error(err.stack);
                  sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not update account balance.", sessionObj);
                  return(false);
               }
            }
         }
         break;
      case "validate":
         //distributes all penalized players' funds (bets and balance) to other players
         penaltyReport.penalized = new Array();
         penaltyReport.awarded = new Array();
         var distributionAmount = bigInt(contract.pot);
         for (var count = 0; count < playerPIDs.length; count++) {
            var player = getPlayer(contract, playerPIDs[count]);
            var playerBalance = bigInt(player.balance);
            distributionAmount = distributionAmount.plus(playerBalance);
            player.balance = "0"; //entire balance is lost
            var penalizationObj = new Object();
            penalizationObj.privateID = playerPIDs[count];
            penalizationObj.amount = player.balance;
            penaltyReport.penalized.push (penalizationObj);
         }
         var distributionPIDs = new Array();
         for (count = 0; count < contract.players.length; count++) {
            var currentPlayer = contract.players[count];
            var penaltyPlayer = playerPIDs.find(penaltyPrivateID => {
               return (currentPlayer.privateID == penaltyPrivateID);
            });
            if (penaltyPlayer == undefined) {
               distributionPIDs.push (currentPlayer);
            }
         }
         if (distributionPIDs.length == 0) {
            //no one is being penalized (everyone is refunded)
            distributionPIDs = new Array();
            for (count = 0; count < contract.players.length; count++) {
               distributionPIDs.push(contract.players[count].privateID);
            }
         }
         var perPlayerAmount = distributionAmount.divide(distributionPIDs.length); //per non-penalilzed player
         for (count = 0; count < distributionPIDs.length; count++) {
            var currentPlayer = distributionPIDs[count];
            var currentPlayerBalance = bigInt(currentPlayer.balance);
            currentPlayerBalance = currentPlayerBalance.plus(perPlayerAmount);
            currentPlayer.balance = currentPlayerBalance.toString(10);
            var searchObj = new Object();
            searchObj.address = currentPlayer.account.address;
            searchObj.type = currentPlayer.account.type;
            searchObj.network = currentPlayer.account.network;
            var accountResult = await namespace.cp.getAccount(searchObj);
            //subtract buy-in from account and deposit to contract
            try {
               var result = await addToAccountBalance(accountResult[0], currentPlayer.balance, contract);
               //save game contract here
               var awardObj = new Object();
               awardObj.privateID = currentPlayer.privateID;
               awardObj.amount = perPlayerAmount.toString(10);
               penaltyReport.awarded.push (awardObj);
            } catch (err) {
               console.error(err.stack);
               sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Could not update account balance.", sessionObj);
               return(false);
            }
         }
         break;
      default:
         throw (new Error("Unrecognized penalty type \""+penaltyType+"\"."));
         break;
   }
   return (penaltyReport);
}

/**
* Returns the next deal object / action for a specified player from a list of
* deals.
*
* @param {Array} dealsArray An indexed array of deal objects for a specific player
* (usually the "select" initiator).
* @param {String} fromPID The private ID of the player for which to get the next action
* within the <code>dealsArray</code>.
* @param {Number} actionIndex The numeric index (0-based), of the action to retrieve
* for <code>fromPID</code> from within the <code>dealsArray</code>.
*
* @return {Object} The deal by <code>fromPID</code> matching the parameters
* or <code>null</code> if none can be found.
* @private
*/
function getNextDealAction(dealsArray, fromPID, actionIndex) {
   var actionCounter = 0;
   for (var count=0; count < dealsArray.length; count++) {
      var currentDeal = dealsArray[count];
      if (currentDeal.fromPID == fromPID) {
         if (actionIndex == actionCounter) {
            return (currentDeal);
         } else {
            actionCounter++;
         }
      }
   }
   return (null);
}

/**
* Compares two card decks of either plaintext mappings or encrypted
* card values, regardless of their order.
*
* @param {Array} deckArr1 First array of numeric strings to compare.
* @param {Array} deckArr2 Second array of numeric strings to compare.
*
* @return {Boolean} True if both decks have exactly the same elements (regardless of order),
* false if there's a difference.
* @private
*/
function compareDecks(deck1Arr, deck2Arr) {
   if (deck1Arr.length != deck2Arr.length)  {
      return (false);
   }
   var deck1 = Array.from(deck1Arr);
   var deck2 = Array.from(deck2Arr);
   while (deck1.length > 0) {
      var currentCard = deck1.splice(0, 1);
      var index = 0;
      while (index < deck2.length) {
         var compareCard = deck2[index];
         if (compareCard == currentCard) {
            deck2.splice(index, 1);
            break;
         }
         index++;
      }
   }
   if (deck2.length == 0) {
      //all unique matching elements removed from secondary array (all match)
      return (true);
   }
   return (false);
}

/**
* Checks a card deck array for duplicate values.
*
* @param {Array} cardDeck Indexed array of values to examine.
* @param {String} [valueProp=null] The value property of each array
* element to examine (e.g. <code>mapping</code>). If omitted or <code>null</code>,
* each element is examined directly.
*
* @return {Boolean} True if the <code>cardDeck</code> contains duplicate values,
* otherwise false.
* @private
*/
function containsDuplicates(cardDeck, valueProp=null) {
   var compareDeck = Array.from(cardDeck);
   var dupFound = false;
   cardDeck.forEach((value,index,arr) => {
      if (valueProp != null) {
         var cardValue = value[valueProp];
      } else {
         cardValue = value;
      }
      var matchCount = 0;
      compareDeck.forEach((cValue,cIndex,cArr) => {
         if (valueProp != null) {
            var compareValue = cValue[valueProp];
         } else {
            compareValue = cValue;
         }
         if (compareValue == cardValue) {
            matchCount++;
         }
         if (matchCount > 1) {
            dupFound = true;
         }
      });
   });
   return (dupFound);
}

/**
* Remove a set of items from a deck.
*
* @param {Array} removeItems Array of strings matching card values to remove
* from <code>deckArr</code>
* @param {Array} deckArr Array of strings matching card values and representing
* a deck. Items found in <code>removeItems</code> will be removed directly
* from this array.
*
* @return {Boolean} True if the correct number of items were removed from
* <code>deckArr</code> (i.e. only one unique match for each value existed).
* False is returned if the removed items don't match the expected set but
* <code>deckArr</code> may still be modified.
* @private
*/
function removeFromDeck(removeItems, deckArr) {
   var itemsToRemove = removeItems.length;
   var removedItems = new Array();
   for (var count=0; count < removeItems.length; count++) {
      var count2=0;
      while (count2 < deckArr.length) {
         if (removeItems[count] == deckArr[count2]) {
            removedItems.push(deckArr.splice(count2, 1)[0]);
            //keep going in case there are duplicates
         } else {
            count2++;
         }
      }
   }
   if (removedItems.length == itemsToRemove) {
      return (true);
   }
   return (false);
}

/**
* Returns all of the available, unordered 5-hand permutations for a set of supplied cards.
*
* @param {Array} cardsArr Values or {@link CypherPokerCard} instance for
* which to produce permutatuions, up to a maximum of 7 elements.
*
* @return {Array} Each array element contains a unique 5-card permutation
* from the input set. If there are less than 6 cards provided, only one
* permutation is returned.
* @private
*/
function createCardPermutations(cardsArr) {
   var permArray = new Array();
   if (cardsArr.length <= 5) {
      //only one hand permutation available
      permArray.push (cardsArr);
   } else if (cardsArr.length == 6) {
      //only private card 2 (index 1)
      permArray.push ([cardsArr[1], cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[5]]);
      //only private card 1 (index 0)
      permArray.push ([cardsArr[0], cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[5]]);
      //both private cards
      permArray.push ([cardsArr[1], cardsArr[0], cardsArr[3], cardsArr[4], cardsArr[5]]);
      permArray.push ([cardsArr[1], cardsArr[2], cardsArr[0], cardsArr[4], cardsArr[5]]);
      permArray.push ([cardsArr[1], cardsArr[2], cardsArr[3], cardsArr[0], cardsArr[5]]);
      permArray.push ([cardsArr[1], cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[0]]);
   } else {
      //no private cards
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[5], cardsArr[6]]);
      //private card 1 (index 0)
      permArray.push ([cardsArr[0], cardsArr[3], cardsArr[4], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[0], cardsArr[4], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[0], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[0], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[5], cardsArr[0]]);
      //private card 2 (index 1)
      permArray.push ([cardsArr[1], cardsArr[3], cardsArr[4], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[1], cardsArr[4], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[1], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[1], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[5], cardsArr[1]]);
      //both private cards
      permArray.push ([cardsArr[0], cardsArr[1], cardsArr[4], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[0], cardsArr[3], cardsArr[1], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[0], cardsArr[3], cardsArr[4], cardsArr[1], cardsArr[6]]);
      permArray.push ([cardsArr[0], cardsArr[3], cardsArr[4], cardsArr[5], cardsArr[1]]);
      permArray.push ([cardsArr[2], cardsArr[0], cardsArr[1], cardsArr[5], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[0], cardsArr[4], cardsArr[1], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[0], cardsArr[4], cardsArr[5], cardsArr[1]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[0], cardsArr[1], cardsArr[6]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[0], cardsArr[5], cardsArr[1]]);
      permArray.push ([cardsArr[2], cardsArr[3], cardsArr[4], cardsArr[0], cardsArr[1]]);
   }
   return (permArray);
}

/**
* Returns a matching player object from a contract.
*
* @param {ContractObject} contract The contract within which to find the
* player matching the <code>privateID</code>.
* @param {String} privateID The private ID of the player to find within the
* <code>contract</code>.
*
* @return {Object} The player object matching the parameters or <code>null</code>
* if none can be found.
* @private
*/
function getPlayer(contract, privateID) {
   for (var count=0; count < contract.players.length; count++) {
      if (contract.players[count].privateID == privateID) {
         return (contract.players[count]);
      }
   }
   return (null);
}

/**
* Retrieves the next player after a specified one in a contract.
*
* @param {ContractObject} contract The contract to use to determine the next
* player.
* @param {String} privateID The private ID of the player preceding
* the player to retrieve.
* @param {Boolean} [allowFolded=true] If false, return only the next non-folded
* player, otherwise return any next player.
*
* @return {Object} A player object or <code>null</code> if no matching
* player private ID can be found in the contract.
* @private
*/
function getNextPlayer(contract, privateID, allowFolded=true) {
   for (var count=0; count < contract.players.length; count++) {
      if (contract.players[count].privateID == privateID) {
         var nextPlayer = contract.players[(count+1) % contract.players.length];
         if (contract.players[count].folded && (allowFolded == false)) {
            nextPlayer = getNextPlayer(contract, nextPlayer.privateID, allowFolded);
         }
         return (nextPlayer);
      }
   }
   return (null);
}

/**
* Retrieves the previous player after a specified one in a contract.
*
* @param {ContractObject} contract The contract to use to determine the previous
* player.
* @param {String} privateID The private ID of the player following
* the player to retrieve.
*
* @return {Object} A player object or <code>null</code> if no matching
* player private ID can be found in the contract.
* @private
*/
function getPreviousPlayer(contract, privateID) {
   for (var count=0; count < contract.players.length; count++) {
      if (contract.players[count].privateID == privateID) {
         if (count == 0) {
            return (contract.players[contract.players.length-1]);
         } else {
            return (contract.players[count-1]);
         }
      }
   }
   return (null);
}

/**
* Returns the player that is currently flagged as the dealer
* in the associated contract's <code>players</code> array.
*
* @return {Object} The player instance that is flagged as a dealer.
* <code>null</code> is returned if no dealer is flagged.
* @private
*/
function getDealer(contract) {
   for (var count=0; count < contract.players.length; count++) {
      if (contract.players[count].isDealer) {
         return (contract.players[count]);
      }
   }
   return (null);
}

/**
* Returns the player object that is currently flagged as the big blind
* in a {@link ContractObject}.
*
* @param {ContractObject} contract The contract from which to extract the
* big blind.
*
* @return {Object} The player object in the contract that
* is flagged as a big blind. <code>null</code> is returned if no big blind
* is flagged.
* @private
*/
function getBigBlind(contract) {
   for (var count=0; count < contract.players.length; count++) {
      if (contract.players[count].isBigBlind) {
         return (contract.players[count]);
      }
   }
   return (null);
}

/**
* Returns the player object that is currently flagged as the small blind
* in a {@link ContractObject}.
*
* @param {ContractObject} contract The contract from which to extract the
* big blind.
*
* @return {Object} The player object in the contract that
* is flagged as a small blind. <code>null</code> is returned if no small blind
* is flagged.
* @private
*/
function getSmallBlind(contract) {
   for (var count=0; count < contract.players.length; count++) {
      if (contract.players[count].isSmallBlind) {
         return (contract.players[count]);
      }
   }
   return (null);
}

/**
* Examines a {@link ContractObject} to determine the next player that should deal
* according to it's deals <code>history</code>.
*
* @param {ContractObject} contract The contract to examine.
*
* @return {Object} A player object representing the player to deal next.
* <code>null</code> is returned if the player can't be determined.
* @private
*/
function getNextDealingPlayer(contract) {
   try {
      var longestDeal = 0;
      var currentDealerPID = "";
      for (var privateID in contract.history.deals) {
         if (contract.history.deals[privateID].length > longestDeal) {
            currentDealerPID = privateID;
            longestDeal = contract.history.deals[privateID].length;
         }
      }
      var nextDealer = getNextPlayer(contract, currentDealerPID);
      return (nextDealer);
   } catch (err) {
      return (null);
   }
}

/**
* Finds players who have not completed a deal action within a specific contract.
*
* @param {ContractObject} contract The contract to look within.
*
* @return {Array} Array of player objects of players who have not completed
* a deal action within the <code>contract</code>.
* @private
*/
function getIncompletePlayers(contract) {
   var incompletePlayers = new Array();
   for (var privateID in contract.history.deals) {
      var deals = contract.history.deals[privateID];
      var numPlayers = contract.players.length;
      var numDeals = deals.length;
      if ((numDeals/numPlayers) != Math.floor(numDeals/numPlayers)) {
         var lastDeal = deals[deals.length-1];
         incompletePlayers.push(getPlayer(contract, lastDeal.fromPID));
      } else {
         //current deal is complete
      }
   }
   return (incompletePlayers);
}

/**
* Returns the next player to bet based on information stored within
* a contract and the private ID of the player who has just bet or folded.
*
* @param {ContractObject} contract The contract within which to look for the
* next betting player.
* @param {String} privateID The private ID of the player that has just bet
* or folded.
*
* @return {Object} A reference to the next betting player object within
* the <code>contract</code>, or <code>null</code> if one can't be determined.
* @private
*/
function getNextBettingPlayer(contract, privateID) {
   var anyBetsPlaced = false; //during this round of betting?
   var largestPlayerBet = largestBet(contract);
   for (var count=0; count < contract.players.length; count++) {
      var player = contract.players[count];
      if ((player.hasBet == true) && (player.hasFolded == false)) {
         anyBetsPlaced = true;
         break;
      }
   }
   var nextPlayer = getNextPlayer(contract, privateID);
   while (nextPlayer.privateID != privateID) {
      var nextTotalBet = bigInt(nextPlayer.totalBet);
      if (nextTotalBet.lesser(largestPlayerBet) && (nextPlayer.hasFolded == false)) {
         if (getBigBlind(contract).numActions > 0) {
            return (nextPlayer);
         }
      }
      nextPlayer = getNextPlayer(contract, nextPlayer.privateID);
   }
   if ((getBigBlind(contract).numActions < 2) && getPreviousPlayer(contract, getBigBlind(contract).privateID).hasBet && (getBigBlind(contract).hasFolded == false)) {
      if ((contract.players.length == 2) && (bigInt(getSmallBlind(contract).totalBet).lesser(bigInt(getBigBlind(contract).totalBet)))) {
         return (getSmallBlind(contract));
      } else {
         return (getBigBlind(contract));
      }
   }
   //starting bets
   if (contract.players.length == 2) {
      //heads-up betting order
      if ((publicCardsDeals(contract).length == 0) && (bettingDone(contract) == false)) {
         //pre-flop
         if (getDealer(contract).hasBet == false) {
            //dealer goes first
            return (getDealer(contract));
         } else {
            return (getNextPlayer(contract, getDealer(contract).privateID));
         }
      } else {
         //post-flop
         if (getNextPlayer(contract, getDealer(contract).privateID).hasBet == false) {
            //player goes first
            return (getNextPlayer(contract, getDealer(contract).privateID));
         } else {
            return (getDealer(contract));
         }
      }
   } else {
      //standard betting order
      var startingPlayer = getSmallBlind(contract);
      var firstNonFoldedPlayer = null;
      if (startingPlayer.hasFolded == false) {
         firstNonFoldedPlayer = startingPlayer;
         if ((startingPlayer.hasBet == false) || bigInt(startingPlayer.totalBet).lesser(largestPlayerBet)) {
            return (startingPlayer);
         }
      }
      var startingID = startingPlayer.privateID;
      startingPlayer = getNextPlayer(contract, startingPlayer.privateID);
      while (startingPlayer.privateID != startingID) {
         if (startingPlayer.hasFolded == false) {
            if (firstNonFoldedPlayer == null) {
               firstNonFoldedPlayer = startingPlayer;
            }
            if ((startingPlayer.hasBet == false) || bigInt(startingPlayer.totalBet).lesser(largestPlayerBet)) {
               return (startingPlayer);
            }
         }
         startingPlayer = getNextPlayer(contract, startingPlayer.privateID);
      }
      return (firstNonFoldedPlayer);
   }
   return (null);
}

/**
* Updates the timeout for player(s) of a contract based on the action currently being
* performed.
*
* @param {String} privateID The private ID of the player currently having just performed the <code>action</code>.
* @param {String} sourcePID The private ID of the <code>action</code> source or origin (player that initiated the action chain).
* @param {ContractObject} contract The contract instance associated with the <code>action</code>.
* Each player's <code>updated</code> time may be updated.
* @param {String} action The type of action being performed by the player. Valid actions are
* "deal", "store" and "bet".
* @param {String} [storeAction=null] The type of store action being performed if <code>action=="store"</code>.
* Valid <code>storeType</code>s are "encrypt", "select", "decrypt", and "keychain".
* If the action is not a "store", this parameter is ignored.
* @param {Array} [storeArray=null] The array of values being stored if <code>action=="store"</code>.
* If the action is not a "store", this parameter is ignored.
*
* @throws {Error} Thrown on an incorrect <code>action</code> or other errors.
* @private
*/
function updatePlayersTimeout(privateID, sourcePID, contract, action, storeAction=null, storeArray=null) {
   var date = new Date();
   var now = new Date();
   var timeout = contract.table.tableInfo.timeout;
   now = now.toISOString();
   var later = new Date();
   later.setSeconds(later.getSeconds()+timeout);
   later = later.toISOString();
   var morelater = new Date();
   morelater.setSeconds(morelater.getSeconds()+timeout);
   morelater = morelater.toISOString();
   if (action == "bet") {
      //reset all players to expire later
      for (var count=0; count < contract.players.length; count++) {
         contract.players[count].updated = morelater;
      }
      var nextBettingPlayer = getNextBettingPlayer(contract, privateID);
      if (nextBettingPlayer != null) {
         nextBettingPlayer.updated = now;
      }
   } else if (action == "deal") {
      //reset all players to expire later
      for (var count=0; count < contract.players.length; count++) {
         contract.players[count].updated = morelater;
      }
      var nextDealingPlayer = getNextDealingPlayer(contract);
      //nextDealingPlayer is dealing next
      nextDealingPlayer.updated = now;
   } else if (action == "store") {
      var incompletePlayers = getIncompletePlayers(contract);
      if (incompletePlayers.length > 0) {
         for (count = 0; count < incompletePlayers.length; count++) {
            incompletePlayers[count].updated = now;
         }
      } else {
         if ((contract.players.length == 2) && (publicCardsDeals(contract).length == 0)){
            nextBettingPlayer = getNextBettingPlayer(contract, getDealer(contract).privateID);
         } else {
            nextBettingPlayer = getNextBettingPlayer(contract, getBigBlind(contract).privateID);
         }
         if (nextBettingPlayer != null) {
            nextBettingPlayer.updated = now;
            //nextBettingPlayer is betting next
         }
      }
   } else if (action == "keychain") {
      getPlayer(contract, privateID).updated = now;
   } else {
      throw (new Error("Unrecognized action \""+action+"\""));
   }
}

/**
* Returns a reference to a {@link CypherPokerCard} based on its mapping, as specified
* in a specific contract.
*
* @param {ContractObject} contract The contract within which to look up the mapping.
* This object's <code>cardDecks.faceup</code> property is used as the lookup reference.
* @param {String} mapping The plaintext or face-up card mapping value to
* find.
*
* @return {CypherPokerCard} The matching card instance or <code>null</code>
* if none exists.
* @private
*/
function getMappedCard(contract, mapping) {
   var referenceDeck = contract.cardDecks.faceup;
   for (var count=0; count < referenceDeck.length; count++) {
      if (referenceDeck[count].mapping != undefined) {
         if (referenceDeck[count].mapping == mapping) {
            return (referenceDeck[count]);
         }
      } else {
         if (referenceDeck[count]._mapping == mapping) {
            return (referenceDeck[count]);
         }
      }
   }
   return (null);
}

/**
* Returns an array of completed public card deals for a specific contract.
*
* @param {ContractObject} contract The contract to analyze for completed public deals.
*
* @return {Array} An array of public / community card deals. Each element contains
* a number representing the number of cards dealt in that deal. Elements should
* be assumed to be out of order (e.g. a turn may appear after the river).
* @private
*/
function publicCardsDeals(contract) {
   var cardsDealt = 0;
   var returnArr = new Array();
   for (var privateID in contract.history.deals) {
      var numActions = 0;
      for (var count=0; count < contract.history.deals[privateID].length; count++) {
         var currentDeal = contract.history.deals[privateID][count];
         if ((currentDeal.private == false) && ((currentDeal.type=="select") || (currentDeal.type=="decrypt"))) {
            numActions++;
            if (numActions >= contract.players.length) {
               returnArr.push(currentDeal.cards.length);
               cardsDealt++;
               numActions = 0;
            }
         }
      }
   }
   return (returnArr);
}

/**
* Checks whether or not the private cards have been completely dealt (selected and decrypted),
* for a specific player.
*
* @param {ContractObject} contract The contract to examine.
* @param {String} privateID The privateID of the player to check for within the <code>contract</code>.
*
* @return {Boolean} True if all of the private cards for the specific player in the contract
* <i>appear</i> to have been correctly dealt (selected and partially decrypted). False
* in all other cases.
* dealt.
* @private
*/
function privateCardsDealt(contract, privateID) {
   if ((contract.history.deals[privateID] == undefined) || (contract.history.deals[privateID] == null)) {
      return (false);
   }
   var returnArr = new Array();
   var numActions = 0;
   //should we check for the length and valid-looking contents of the cards array?
   for (var count=0; count < contract.history.deals[privateID].length; count++) {
      var currentDeal = contract.history.deals[privateID][count];
      if (currentDeal.private && ((currentDeal.type=="select") || (currentDeal.type=="decrypt"))) {
         numActions++;
         if (numActions >= contract.players.length) {
            returnArr.push(currentDeal.cards.length);
            return (returnArr);
         }
      }
   }
   return (returnArr);
}

/**
* Finds the largest bet within a contract by any non-folded player.
*
* @param {ContractObject} contract The contract within which to look for
* the largest bet/
*
* @return {BigInteger} The largest bet currently placed by
* a non-folded player at the table.
* @private
*/
function largestBet(contract) {
   var largestBet = bigInt(0);
   for (var count=0; count < contract.players.length; count++) {
      if ((largestBet.compare(bigInt(contract.players[count].totalBet)) == -1) && (contract.players[count].hasFolded == false)) {
         largestBet = bigInt(contract.players[count].totalBet);
      }
   }
   return (largestBet);
}

/**
* Examines a contract to see if betting has completed.
*
* @param {ContractObject} contract The contract to examine.
*
* @return {Boolean} True if all non-folded players have committed the same
* bet amount, or if all players but one have folded (new cards may be dealt or the game has completed).
* @private
*/
function bettingDone(contract) {
   var foldedPlayers = 0;
   var nonFoldedPlayers = 0;
   var currentBet = "";
   var betGroups = new Object(); //players grouped by bet amount
   if ((getBigBlind(contract).numActions < 2) && ((getBigBlind(contract).hasFolded == false))) {
      return (false);
   }
   for (var count=0; count < contract.players.length; count++) {
      if (contract.players[count].hasFolded) {
         foldedPlayers++;
      } else {
         nonFoldedPlayers++;
         if (contract.players[count].hasBet) {
            currentBet = contract.players[count].totalBet;

            if (betGroups[currentBet] == undefined) {
               betGroups[currentBet] = new Array();
            }
            betGroups[currentBet].push(contract.players[count]);
         }
      }
   }
   if (betGroups[currentBet] != undefined) {
      if (betGroups[currentBet].length == nonFoldedPlayers) {
         return (true);
      }
   }
   return (false);
}

/**
* Retrieves an indexed array of game contract objects for a contract owner.
*
* @param {String} ownerPID The private ID of the owner for which to retrieve currently active
* smart contracts.
*
* @return {Array} An indexed list of {@link ContractObject} instances registered the <code>privateID</code>. If none
* are registered, an empty array is returned.
* @private
*/
function getContractsByPID(ownerPID) {
   if ((namespace.cp.contracts == undefined) || (namespace.cp.contracts == null)) {
      //create contracts container
      namespace.cp.contracts = new Object();
   }
   if ((namespace.cp.contracts[ownerPID] == null) || (namespace.cp.contracts[ownerPID] == undefined) || (namespace.cp.contracts[ownerPID] == "")) {
      //create a new container
      namespace.cp.contracts[ownerPID] = new Array();
   }
   return (namespace.cp.contracts[ownerPID]);
}

/**
* Retrieves a contract by its ID and its owner's private ID.
*
* @param {String} ownerPID The private ID of the contract owner.
* @param {String} contractID The contract ID of the contract to retrieve.
*
* @return {ContractObject} The contract object matching the parameters or <code>null</code>
* if none can be found.
* @private
*/
function getContractByID(ownerPID, contractID) {
   var contractsArr = namespace.cp.getContractsByPID(ownerPID);
   if (contractsArr.length == 0) {
      return (null);
   }
   for (var count=0; count < contractsArr.length; count++) {
      var currentContract = contractsArr[count];
      if (currentContract.contractID == contractID) {
         return (currentContract);
      }
   }
   return (null);
}

/**
* Examines an object for required contract properties.
*
* @param {ContractObject} obj The expected contract object to examine.
* @param {String} privateID The private ID of the contract creator / owner.
* @param {AccountObject} accountObj A valid account object to include
* in the <code>obj.players</code> array as an <code>account</code> property
* for the contract owner / creator. The existence of this object can be used to
* determine if a player has agree to the contract (otherwise it will be
* <code>null</code> or <code>undefined</code>).
*
* @return {Boolean} True if the object appears to be a valid contract object,
* false otherwise.
* @private
*/
function validContractObject(obj, privateID, accountObj) {
   //check to make sure all required contract parameters are there:
   if (typeof(obj) != "object") {
      return(false);
   }
   if ((obj.contractID == undefined) || (obj.contractID == null) || (obj.contractID == "")) {
      return(false);
   }
   if ((obj.contractID == undefined) || (obj.contractID == null) || (obj.contractID == "")) {
      return(false);
   }
   if (validTableObject(obj.table) == false) {
      return(false);
   }
   if ((obj.players == undefined) || (obj.players == null) || (obj.players == "")) {
      return(false);
   }
   if (typeof(obj.players) != "object") {
      return(false);
   }
   if (typeof(obj.players.length) != "number") {
      return(false);
   }
   if (obj.players.length < 2) {
      return(false);
   }
   var numAgreed = 0;
   for (var count = 0; count < obj.players.length; count++) {
      var player = obj.players[count];
      if ((player.privateID == null) || (player.privateID == undefined) || (player.privateID == "")) {
         player.privateID = player._privateID;
      }
      if ((player.privateID == null) || (player.privateID == undefined) || (player.privateID == "")) {
         return(false);
      }
      //sanitize keys if accidentally included
      try {
         player.keychain = new Array();
         delete player.keychain;
      } catch (err) {}
      try {
         player._keychain = new Array();
         delete player._keychain;
      } catch (err) {}
      if (privateID == player.privateID) {
         //player who created this automatically agrees
         numAgreed++;
      }
      if ((player.totalBet == undefined) || (player.totalBet == null) || (player.totalBet == "")) {
         player.totalBet = "0";
      }
      if (typeof(player.hasBet) != "boolean") {
         player.hasBet = false;
      }
      if (typeof(player.hasFolded) != "boolean") {
         player.hasFolded = false;
      }
      if (typeof(player.numActions) != "number") {
         player.numActions = 0;
      }
   }
   if (numAgreed > 1) {
      return (false);
   }
   if (typeof(obj.prime) != "string") {
      return (false);
   }
   if (obj.prime.length == 0) {
      return (false);
   }
   if (typeof(obj.cardDecks) != "object") {
      return (false);
   }
   if (typeof(obj.cardDecks.faceup) != "object") {
      return (false);
   }
   //contract creation includes submission of face-up (generated) cards:
   if (obj.cardDecks.faceup.length < 52) {
      return (false);
   }
   //... but not other values:
   if (typeof(obj.cardDecks.facedown) != "object") {
     return (false);
   }
   if (typeof(obj.cardDecks.public) != "object") {
     return (false);
   }
   if (typeof(obj.cardDecks.facedown.length) != "number") {
     return (false);
   }
   if (typeof(obj.cardDecks.dealt) != "object") {
     return (false);
   }
   if (typeof(obj.cardDecks.dealt.length) != "number") {
     return (false);
   }
   if (typeof(obj.cardDecks.public.length) != "number") {
     return (false);
   }
   return (true);
}

/**
* Evaluates a provided object to determine if it's a valid table object.
*
* @param {TableObject} tableObj The object to examine.
*
* @return {Boolean} True if the supplied parameter has a valid {@link TableObject}
* structure, false otherwise.
* @private
*/
function validTableObject(tableObj) {
   if ((tableObj == undefined) || (tableObj == undefined)) {
      return (false);
   }
   if (typeof(tableObj.ownerPID) != "string") {
      return (false);
   }
   if (tableObj.ownerPID == "") {
      return (false);
   }
   if (typeof(tableObj.tableID) != "string") {
      return (false);
   }
   if (tableObj.tableID == "") {
      return (false);
   }
   if (typeof(tableObj.tableName) != "string") {
      return (false);
   }
   if (tableObj.tableName == "") {
      return (false);
   }
   if (typeof(tableObj.requiredPID) != "object") {
      return (false);
   }
   if (typeof(tableObj.requiredPID.length) != "number") {
      return (false);
   }
   //all players should now have joined the table
   if (tableObj.requiredPID.length > 0) {
      return (false);
   }
   if (typeof(tableObj.joinedPID) != "object") {
      return (false);
   }
   if (typeof(tableObj.joinedPID.length) != "number") {
      return (false);
   }
   if (tableObj.joinedPID.length < 2) {
      return (false);
   }
   if (typeof(tableObj.restorePID) != "object") {
      return (false);
   }
   if (typeof(tableObj.restorePID.length) != "number") {
      return (false);
   }
   if (tableObj.restorePID.length != tableObj.joinedPID.length) {
      return (false);
   }
   for (var count=0; count < tableObj.requiredPID.length; count++) {
      if ((typeof(tableObj.requiredPID[count]) != "string") || (tableObj.requiredPID[count] == "")) {
         return (false);
      }
   }
   if (typeof(tableObj.joinedPID[0]) != "string") {
      return (false);
   }
   if (tableObj.joinedPID[0] != tableObj.ownerPID) {
      return (false);
   }
   if (typeof(tableObj.tableInfo) != "object") {
      return (false);
   }
   try {
      //buyIn, bigBlind, and smallBlind musst be valid positive integer values
      if ((typeof(tableObj.tableInfo.buyIn) != "string") || (tableObj.tableInfo.buyIn == "") || (tableObj.tableInfo.buyIn == "0")) {
         return (false);
      }
      var checkAmount = bigInt(tableObj.tableInfo.buyIn);
      if (checkAmount.lesser(0)) {
         return (false);
      }
      if ((typeof(tableObj.tableInfo.bigBlind) != "string") || (tableObj.tableInfo.bigBlind == "") || (tableObj.tableInfo.bigBlind == "0")) {
         return (false);
      }
      checkAmount = bigInt(tableObj.tableInfo.bigBlind);
      if (checkAmount.lesser(0)) {
         return (false);
      }
      if ((typeof(tableObj.tableInfo.smallBlind) != "string") || (tableObj.tableInfo.smallBlind == "") || (tableObj.tableInfo.smallBlind == "0")) {
         return (false);
      }
      checkAmount = bigInt(tableObj.tableInfo.smallBlind);
      if (checkAmount.lesser(0)) {
         return (false);
      }
   } catch (err) {
      return (false);
   }
   return (true);
}

/**
* Evaluates a provided object to determine if it's a valid account object and
* optionally if the provided password correctly matches the one stored for the account.
*
* @param {AccountObject} accountObj The object to examine.
* @param {Boolean} [checkCredentials=true] If true, the login credentials
* provided are also validated, otherwise only the <code>accountObj</code>
* structure is checked.
*
* @return {Promise} The promise will resolve with an array containing the latest
* database (or otherwise stored) rows for the associated {@link AccountObject}
* parameter if it has a valid structure and optionally if the
* account exists and the password matches. Otherwise the promise will reject with
* an <code>Error</code> object.
* @private
* @async
*/
async function validAccount(accountObj, checkCredentials=true) {
   if ((accountObj == undefined) || (accountObj == null)) {
      return (false);
   }
   if (typeof(accountObj.address) != "string") {
      return (false);
   }
   if (accountObj.address == "") {
      return (false);
   }
   if (typeof(accountObj.type) != "string") {
      return (false);
   }
   if (accountObj.type == "") {
      return (false);
   }
   if (typeof(accountObj.network) != "string") {
      return (false);
   }
   if (accountObj.network == "") {
      return (false);
   }
   if (typeof(accountObj.password) != "string") {
      return (false);
   }
   if (accountObj.password == "") {
      return (false);
   }
   var accountResult = null;
   var searchObj = new Object();
   //don't include (potentially incorrect) balance and (unhashed) password!
   searchObj.address = accountObj.address;
   searchObj.type = accountObj.type;
   searchObj.network = accountObj.network;
   accountResult = await namespace.cp.getAccount(searchObj);
   if (accountResult.length < 1) {
      throw (new Error("No matching account."));
   }
   if (checkCredentials) {
      var pwhash = accountResult[0].pwhash;
      if (namespace.cp.checkPassword(accountObj.password, pwhash) == false) {
         throw (new Error("Wrong password."));
      }
   }
   return (accountResult);
}

/**
* Resets all player (not account) balances to 0 within a specific contract object.
*
* @param {ContractObject} contractObj The contract within which to reset all players'
* balances.
*
* @private
*/
function resetPlayerBalances(contractObj) {
   for (var count = 0; count < contractObj.players.length; count++) {
      var player = contractObj.players[count];
      player.balance = "0";
   }
}

/**
* Sets the balance of a player (not account) within a specific contract object.
*
* @param {ContractObject} contractObj The contract within which to set the player's
* balance.
* @param {String} privateID The private ID of the player to update.
* @param {String|Number} balance The balance amount to set.
*
* @private
*/
function setPlayerBalance(contractObj, privateID, balance) {
   for (var count = 0; count < contractObj.players.length; count++) {
      var player = contractObj.players[count];
      if (player.privateID == privateID) {
         player.balance = String(balance);
         break;
      }
   }
}

/**
* Cancels a contract by immediately refunding the balances of all registered players
* and then removing the contract. And pot balance of the contract is destroyed with
* the contract.
*
* @param {ContractObject} contractObj The contract to cancel.
*
* @private
* @async
*/
async function cancelContract(contractObj) {
   for (var count = 0; count < contractObj.players.length; count++) {
      var player = contractObj.players[count];
      if (player.account != null) {
         var balance = player.balance;
         var searchObj = new Object();
         searchObj.address = player.account.address;
         searchObj.type = player.account.type;
         searchObj.network = player.account.network;
         if (bigInt(balance).greater(0)) {
            try {
               var accountResults = await namespace.cp.getAccount(searchObj);
               var updateResult = await addToAccountBalance(accountResults[0], balance);
            } catch (err) {
               console.error("Couldn't refund cancelled contract.");
               console.error("   Contract ID: "+contractObj.contractID);
               console.error("   Number of players: "+contractObj.players.length);
               console.error("   Account: "+player.account.address);
               console.error("   Balance: "+balance);
            }
        }
      }
   }
}

/**
* Adds to and stores a balance amount for an account row such as one retrieved via
* {@link validAccount}. The account should already have been checked for validity
* and proper credentials prior to calling this function.
*
* @param {Object} accountRow The latest account row (e.g. from the database), to
* use for the update.
* @param {String|Number} balanceInc The amount to increment the account's balance by, in
* the smallest denomination for the associated cryptocurrency type and
* network (e.g. satoshis if <code>type="bitcoin"</code>). A negative <code>balanceInc</code>
* will be subtracted from the account's balance.
* @param {Object} [contract=null] A contract object to also update. This object's
* <code>players</code> array will be searched and the player matching the <code>accountRow</code>
* will have their balance updated with a negative <code>balanceInc</code>. In other words,
* if <code>balanceInc</code> is negative (a withdrawal from the account), then
* the update to the <code>contract.players</code> array will be positive
* (a deposit to the contract).
*
* @return {Promise} The promise will resolve with the new account balance (String) if the account
* was successfully updated. An <code>Error</code> object will be included with a rejection.
* @private
* @async
*/
async function addToAccountBalance(accountRow, balanceInc, contract=null) {
   var currentBalance = bigInt(accountRow.balance);
   var balanceUpdate = bigInt(balanceInc);
   currentBalance = currentBalance.plus(balanceUpdate);
   if (currentBalance.lesser(0)) {
      throw (new Error("Insufficient account balance to continue."));
   }
   //update database
   accountRow.balance = currentBalance.toString(10);
   accountRow.updated = namespace.cp.MySQLDateTime(new Date());
   var result = await namespace.cp.saveAccount(accountRow);
   if (result != true) {
      throw (new Error("Couldn't update account."));
   }
   //update contract
   if (contract != null) {
      for (var count = 0; count < contract.players.length; count++) {
         var currentPlayer = contract.players[count];
         if ((typeof(currentPlayer.account) == "object") && (currentPlayer.account != null)) {
            var account = currentPlayer.account;
            if (account != null) {
               if ((account.address == accountRow.address) &&
                   (account.type == accountRow.type) &&
                   (account.network == accountRow.network)) {
                  account.balance = bigInt(account.balance).minus(balanceUpdate).toString(10);
                  return (accountRow.balance);
               }
            }
         }
      }
      throw (new Error("Couldn't find account in contract."));
   }
   return (currentBalance.toString(10));
}

/**
* SRA encrypts a value using a keypair object.
*
* @param {Object} keypair The keypair object to use for the encryption.
* This object must contain a valid <code>encKey</code> and <code>prime</code>.
* @param {String} encValue A hexadecimal numeric string (staring with "0x"), or
* decimal numeric string representing the value to encrypt.
*
* @return {String} The encrypted value as either a hexadecimal string or
* decimal string, depending on the representation of <code>encValue</code>.
* @private
*/
function SRAEncrypt (keypair, encValue) {
   if (keypair.encKey.startsWith("0x")) {
      var encKey = keypair.encKey.substring(2);
      var prime = keypair.prime.substring(2);
      var keyRadix = 16;
   } else {
      encKey = keypair.encKey;
      prime = keypair.prime;
      keyRadix = 10;
   }
   if (encValue.startsWith("0x")) {
      var value = encValue.substring(2);
      var valueRadix = 16;
   } else {
      value = encValue;
      valueRadix = 10;
   }
   var message = bigInt(value, valueRadix);
   var key = bigInt(encKey, keyRadix);
   var prime = bigInt(prime, keyRadix);
   var result = message.modPow(key, prime); //this is where the encryption happens
   if (valueRadix == 16) {
      return ("0x"+result.toString(valueRadix));
   } else {
      return (result.toString(valueRadix));
   }
}

/**
* SRA decrypts a value using a keypair object.
*
* @param {Object} keypair The keypair object to use for the encryption.
* This object must contain a valid <code>decKey</code> and <code>prime</code>.
* @param {String} decValue A hexadecimal numeric string (staring with "0x"), or
* decimal numeric string representing the value to decrypt.
*
* @return {String} The decrypted value as either a hexadecimal string or
* decimal string, depending on the representation of <code>decValue</code>.
* @private
*/
function SRADecrypt (keypair, decValue) {
   if (keypair.decKey.startsWith("0x")) {
      var decKey = keypair.decKey.substring(2);
      var prime = keypair.prime.substring(2);
      var keyRadix = 16;
   } else {
      decKey = keypair.decKey;
      prime = keypair.prime;
      keyRadix = 10;
   }
   if (decValue.startsWith("0x")) {
      var value = decValue.substring(2);
      var valueRadix = 16;
   } else {
      value = decValue;
      valueRadix = 10;
   }
   var message = bigInt(value, valueRadix);
   var key = bigInt(decKey, keyRadix);
   var prime = bigInt(prime, keyRadix);
   var result = message.modPow(key, prime); //this is where the decryption happens
   if (valueRadix == 16) {
      return ("0x"+result.toString(valueRadix));
   } else {
      return (result.toString(valueRadix));
   }
}

/**
* Notifies the players associated with a contract of a change to the contract.
*
* @param {String} messageType The message type of the notification (e.g. "contractnew")
* @param {Object} contractObj The contract to notify players about.
* @param {Object} fromPID The sender's private ID.
* @param {Array} [excludePIDs=null] The private IDs to exclude from the notification.
* The <code>fromPID</code> parameter is automatically included.
* @param {Object} [payload=null] An object containing properties to include in
* the notification. Standard properties <code>data</code>, <code>from</code>, and
* <code>type</code> will be ignored.
*
* @private
*/
function sendContractMessage(messageType, contractObj, fromPID, excludePIDs=null, payload=null) {
   var recipients = new Array();
   if (excludePIDs == null) {
      excludePIDs = new Array();
   }
   excludePIDs.push(fromPID);
   contractObj.players.forEach((currentPlayer, index, arr) => {
      if (excludePIDs != null) {
         for (count=0; count < excludePIDs.length; count++) {
            if (excludePIDs[count] == currentPlayer.privateID) {
               return;
            }
         }
      }
      recipients.push(currentPlayer.privateID);
   });
   var messageObj = namespace.cp.buildCPMessage(messageType);
   if (payload != null) {
      for (var item in payload) {
         //exclude standard properties
         if ((item != "data") && (item != "from") && (item != "type")) {
            messageObj[item] = payload[item];
         };
      }
   }
   messageObj.contract = contractObj;
   namespace.wss.sendUpdate(recipients, messageObj, fromPID);
}

/**
* Handles a WebSocket close / disconnect event and notifies all active / live
* sessions of the disconnection.
*
* @param {Event} event A standard WebSocket close event.
* @private
*/
function handleWebSocketClose(event) {
   try{
      for (var connectionID in namespace.wss.connections) {
         if ((namespace.wss.connections[connectionID] != undefined) && (namespace.wss.connections[connectionID] != null)) {
            for (var count = 0; count < namespace.wss.connections[connectionID].length; count++) {
               var connectionObj = namespace.wss.connections[connectionID][count];
               // connectionObj.private_id disconnected
            }
         }
      }
   } catch (err) {
      console.error(err.stack);
   }
}

if (namespace.cp == undefined) {
   namespace.cp = new Object();
}

namespace.cp.getContractsByPID = getContractsByPID;