Source: CypherPokerContract.js

/**
* @file A virtual smart contract implementation using the API interface.
*
* @version 0.4.1
* @author Patrick Bay
* @copyright MIT License
*/

/**
* @class A virtual smart contract interface for an associated {@link CypherPokerGame}
* instance. Communicates with an API interface directly rather than being routed
* through {@link CypherPokerGame} communication functions.
*
* @extends EventDispatcher
*/
class CypherPokerContract extends EventDispatcher {

   /**
   * The contract timeout timer has been started or restarted.
   *
   * @event CypherPokerContract#timeoutstart
   * @type {Event}
   * @property {CypherPokerContract} contract The instance dispatching the event.
   * @property {Number} seconds The number of seconds that must elapse without
   * player activity before the contract times out.
   * @property {Number} cSeconds The number of courtesy seconds that will
   * be allowed to elapse before the contract's "timeout" function is
   * invoked. Note that the timeout may occur at any time after the contract
   * has timed out (hence "courtesy" seconds).
   */
   /**
   * The contract appears to have timed out due to player inactivity. This is a local event
   * and does not necessarily reflect the state of the actual contract.
   *
   * @event CypherPokerContract#timeout
   * @type {Event}
   * @property {CypherPokerContract} contract The instance dispatching the event.
   * @property {Array} penalized Array of objects containing private ID(s) and
   * the amount tahat the associated player(s) were penalized. Typically
   * only one player will time out (penalized[0]), but under certain
   * conditions more than one player may time out.
   */
   /**
   * The contract timeout is no longer valid (e.g. the game has ended).
   *
   * @event CypherPokerContract#timeoutinvalid
   * @type {Event}
   * @property {CypherPokerContract} contract The instance dispatching the event.
   */

   /**
   * Creates a new proxy contract instance.
   *
   * @param {CypherPokerGame} gameRef The active game instance with which
   * this contract interface is associated.
   */
   constructor(gameRef) {
      super();
      this._game = gameRef;
      this._active = true;
      this.addGameEventListeners();
      this.cypherpoker.api.addEventListener("update", this.handleUpdateMessage, this);
   }

   /**
   * @property {CypherPokerGame} game Reference to the associated game for
   * which to act as contract handler.
   */
   get game() {
      return (this._game);
   }

   /**
   * @property {CypherPoker} cypherpoker A reference to the
   * {@link CypherPokerContract#game}'s <code>cypherpoker</code> instance or
   * <code>null</code> if none exists.
   */
   get cypherpoker() {
      if ((this._game != null) && (this._game != undefined)) {
         return (this._game.cypherpoker);
      }
      return (null);
   }

   /**
   * @property {TableObject} table A copy of the table associated with the {@link CypherPokerContract#game}
   * instance.
   */
   get table() {
      if (this._table == undefined) {
         this._table = this.game.getTable();
      }
      return (this._table);
   }

   /**
   * @property {Array} players Indexed list of {CypherPokerPlayer} instances copied
   * from the associated {@link CypherPokerContract#game} instance.
   */
   get players() {
      if (this._players == undefined) {
         this.refreshPlayers();
      }
      return (this._players);
   }

   /**
   * Refreshes the {@link CypherPokerContract#players} array with data from
   * the associated {@link CypherPokerContract#game}.
   *
   * @private
   */
   refreshPlayers() {
      this._players = new Array();
      for (var count=0; count < this.game.players.length; count++) {
         this._players.push(this.game.players[count].copy());
      }
   }

   /**
   * @property {Boolean} ready=false Indicates whether or not the contract is
   * ready (has been successfully remotely created).
   */
   get ready() {
      if (this.contractID == null) {
         return (false);
      }
      //other validity tests can be done here
      return (true);
   }

   /**
   * @property {Boolean} active=false Indicates whether or not the contract is currently
   * active (recording or resolving a game).
   */
   get active() {
      if (this._active == undefined) {
         this._active = false;
      }
      return (this._active);
   }

   /**
   * @property {String} contractID=null The ID of the contract instance, usually
   * as returned by the remote contract host. If this is <code>null</code>, this
   * instance should not be considered valid.
   */
   get contractID() {
      if (this._contractID == undefined) {
         this._contractID = null;
      }
      return (this._contractID);
   }

   /**
   * @property {Array} history=null Indexed array of external contract snapshots
   * with index 0 being the most recent.
   */
   get history() {
      if (this._history == undefined) {
         this._history = new Array();
      }
      return (this._history);
   }

   /**
   * @property {Array} deferredActions Indexed list of objects containing
   * game state <code>snapshot</code>, <code>invoke</code>, <code>promise</code>,
   * parent <code>contract</code>, and boolean <code>complete</code> properties.
   */
   get deferredActions() {
      if (this._deferredActions == undefined) {
         this._deferredActions = new Array();
      }
      return (this._deferredActions);
   }

   /**
   * @property {Array} deferredPromises Complete indexed list of Promise instances currently
   * in the {@link CypherPokerContract#deferredActions} array.
   */
   get deferredPromises() {
      var returnArr = new Array();
      for (var count=0; count < this.deferredActions.length; count++) {
         returnArr.push(this.deferredActions[count].promise);
      }
      return (returnArr);
   }

   /**
   * @property {Array} deferredActivePromises Indexed list of Promise instances currently
   * in the {@link CypherPokerContract#deferredActions} array which have the property:
   * <code>complete == false</code>
   */
   get deferredActivePromises() {
      var returnArr = new Array();
      for (var count=0; count < this.deferredActions.length; count++) {
         if (this.deferredActions[count].complete == false) {
            returnArr.push(this.deferredActions[count].promise);
         }
      }
      return (returnArr);
   }

   /**
   * Adds event listeners required by the contract handler to the associated
   * {@link CypherPokerContract#game} instance.
   *
   * @private
   */
   addGameEventListeners() {
      this.game.addEventListener("gamekeypair", this.onGameKeypair, this);
      this.game.addEventListener("gamedeck", this.onNewGameDeck, this);
      this.game.addEventListener("gamecardsencrypt", this.onEncryptCards, this);
      this.game.addEventListener("gamedealprivate", this.onSelectCards, this);
      this.game.addEventListener("gamedealpublic", this.onSelectCards, this);
      this.game.addEventListener("gamedealmsg", this.onGameDeal, this);
      this.game.addEventListener("gamebetplaced", this.onGameBetPlaced, this);
      this.game.addEventListener("gamedecrypt", this.onGameDecrypt, this);
      this.game.addEventListener("gameend", this.onGameEnd, this);
   }

   /**
   * Removes event listeners required by the contract handler from the associated
   * {@link CypherPokerContract#game} instance.
   *
   * @private
   */
   removeGameEventListeners() {
      this.game.removeEventListener("gamekeypair", this.onGameKeypair, this);
      this.game.removeEventListener("gamedeck", this.onNewGameDeck, this);
      this.game.removeEventListener("gamecardsencrypt", this.onEncryptCards, this);
      this.game.removeEventListener("gamedealprivate", this.onSelectCards, this);
      this.game.removeEventListener("gamedealpublic", this.onSelectCards, this);
      this.game.removeEventListener("gamedealmsg", this.onGameDeal, this);
      this.game.removeEventListener("gamebetplaced", this.onGameBetPlaced, this);
      this.game.removeEventListener("gamedecrypt", this.onGameDecrypt, this);
      this.game.removeEventListener("gameend", this.onGameEnd, this);
   }

   /**
   * Removes peer-to-peer network event listener(s).
   *
   * @private
   */
   removeNetworkEventListeners() {
      this.cypherpoker.api.removeEventListener("update", this.handleUpdateMessage, this);
   }

   /**
   * Returns a {@link CypherPokerPlayer} instance associated with this game
   * instance.
   *
   * @param {String} privateID The private ID of the player.
   *
   * @return {CypherPokerPlayer} The {@link CypherPokerPlayer} for the private ID
   * associated with this game. <code>null</code> is returned if no matching
   * player private ID can be found.
   */
   getPlayer(privateID) {
      for (var count=0; count < this.players.length; count++) {
         if (this.players[count].privateID == privateID) {
            return (this.players[count]);
         }
      }
      return (null);
   }

   /**
   * Returns a condensed array containing the copied properties of the
   * {@link CypherPokerContract#players} array. Use the object returned by
   * this function with <code>JSON.stringify</code> instead of using
   * {@link CypherPokerContract#players} directly in order to prevent circular
   * reference errors.
   *
   * @param {Boolean} [includeKeychains=false] If true, the {@link CypherPokerPlayer#keychain}
   * array of each player will be included in the returned object.
   * @param {Boolean} [includePasswords=false] If true, the {@link CypherPokerAccount#password}
   * property of each {@link CypherPokerPlayer#account} reference will be included
   * with the returned object.
   *
   * @return {Object} The condensed players array associated with this game instance.
   */
   getPlayers(includeKeychains=false, includePasswords=false) {
      var returnArr = new Array();
      for (var count=0; count < this.players.length; count++) {
         var playerObj = this.players[count].toObject(includeKeychains, includePasswords);
         returnArr.push(playerObj);
      }
      return (returnArr);
   }

   /**
   * Returns the {@link CypherPokerPlayer} that is currently flagged as the dealer
   * in the {@link CypherPokerContract#players} array.
   *
   * @return {CypherPokerPlayer} The {@link CypherPokerPlayer} instance that
   * is flagged as a dealer. <code>null</code> is returned if no dealer is flagged.
   */
   getDealer() {
      for (var count=0; count < this.players.length; count++) {
         if (this.players[count].isDealer) {
            return (this.players[count]);
         }
      }
      return (null);
   }

   /**
   * Stops any current game timeout timer.
   * @private
   */
   stopContractTimeout() {
      try {
         clearTimeout(this._contractTimeoutID);
      } catch (err) {}
   }

   /**
   * Cancels and resets the current contract timeout, signalling any listeners
   * to ignore future timeout events from this instance.
   *
   * @fires CypherPokerContract#event:timeoutinvalid
   * @private
   */
   resetContractTimeout() {
      this.stopContractTimeout();
      var event = new Event("timeoutinvalid");
      event.contract = this;
      this.dispatchEvent(event);
   }

   /**
   * Stops any current game timeout timer and starts a new one based on the
   * most recent contract (<code>history[0]</code>).
   *
   * @fires CypherPokerContract#timeoutstart
   * @throws {Error} If the contract timeout could not be started.
   * @private
   */
   startContractTimeout() {
      this.stopContractTimeout();
      if ((this.history[0] == undefined) || (this.history[0] == null)) {
         return;
      }
      if (typeof(this.history[0].table.tableInfo.timeout) == "number") {
         //add 5 seconds to timeout to make sure we don't accidentally clock in early
         var timeout = (this.history[0].table.tableInfo.timeout + 5) * 1000;
         var event = new Event("timeoutstart");
         event.contract = this;
         event.seconds = this.history[0].table.tableInfo.timeout;
         event.cSeconds = 5;
         this.dispatchEvent(event);
         this._contractTimeoutID = setTimeout(this.onContractTimeout, timeout, this);
      } else {
         console.error ("Contract \""+this._contractID+"\" does not define a timeout. It may never complete!")
      }
   }

   /**
   * Called on a timer when a contract times out (one or more players
   * have failed to take an action in time).
   *
   * @private
   * @fires CypherPokerContract#timeout
   */
   onContractTimeout(contractInstance) {
      try {
         clearTimeout(contractInstance._contractTimeoutID);
         contractInstance._contractTimeoutID = null;
         delete contractInstance._contractTimeoutID;
      } catch (err) {
      } finally {
         var paramsObj = new Object();
         paramsObj.contract = contractInstance.history[0];
         paramsObj.contractID = paramsObj.contract.contractID;
         paramsObj.ownerPID = contractInstance.getDealer().privateID;
         var snapshot = contractInstance.gameSnapshot();
         console.error ("Contract \""+contractInstance.contractID+"\" has timed out");
         //call "timeout" penalty
         contractInstance.callContractAPI("timeout", paramsObj).then(JSONResult => {
            if (JSONResult.error != undefined) {
               //probably already timed out
               //console.error(JSONResult.error);
               return;
            }
            if (contractInstance.contractID != JSONResult.result.contract.contractID) {
               contractInstance.removeGameEventListeners();
               contractInstance.stopContractTimeout();
               return (false);
            }
            contractInstance.history.unshift(JSONResult.result.contract);
            try {
               contractInstance.updateBalances(JSONResult.result.contract);
            } catch (err) {
               console.error(err);
            }
            //the game can either end here or start a re-key operation
         }).catch (err => {
            contractInstance.removeGameEventListeners();
            contractInstance.stopContractTimeout();
         });
      }
   }

   /**
   * Creates a snapshot (copy) of the associated {@link CypherPokerContract#game} instance's
   * current data in the format of a contract data object for use as a condition in subsequent
   * contract actions.
   */
   gameSnapshot() {
      var snapshot = new Object();
      snapshot.players = this.getPlayers(false, false);
      snapshot.table = this.table;
      try {
         snapshot.prime = this.getPlayer(this.game.ownPID).keychain[0].prime;
      } catch (err) {
         snapshot.prime = null;
      }
      snapshot.cardDecks = this.game.getCardDecks();
      return (snapshot);
   }

   /**
   * Recursively compares two objects for matching properties.
   *
   * @param {Object} obj1 The first object to compare.
   * @param {Object} obj2 The second object to compare.
   *
   * @return {Boolean} True if all properties found in <code>obj1</code> appear
   * in <code>obj2</code>, false otherwise.
   * @private
   */
   compareObjects (obj1, obj2) {
      if ((obj1 == null) && (obj2 == null)) {
         return (true);
      }
      var obj1Entries = Object.entries(obj1);
      for (var count=0; count < obj1Entries.length; count++) {
         try {
            var key = obj1Entries[count][0];
            if (typeof(obj1[key]) == "object") {
               //recurse compare sub-objects
               if (this.compareObjects(obj1[key], obj2[key]) == false) {
                  return (false);
               }
            } else {
               //compare primitives
               if ((typeof(obj1[key]) != "function") && (typeof(obj2[key]) != "function")) {
                  if (obj1[key] != obj2[key]) {
                     if (key != "timeout") {
                        return (false);
                     }
                  }
               }
            }
         } catch (err) {
            console.error (err);
            return (false);
         }
      }
      return (true);
   }

   /**
   * Compares a contract data object to a game snapshot object.
   *
   * @param {Object} contract The (usually) external contract data object to compare.
   * @param {Object} snapshot The game snapshot object to compare to the <code>contract</code>.
   * This object must have the same structure as the contract data object.
   *
   * @return (Number) If the <code>contract</code> appears to be ahead of the snapshot 1
   * is returned. If the <code>snapshot</code> appears to be ahead of the contract 2 is
   * returned. If the contract appears the same as the snapshot but not in the same order
   * (e.g. facedown cards), then 3 is returned. If both contract and snapshot are identical,
   * 0 is returned.
   * @throws {Error} If the expected structure or data of the parameters does not match.
   * @private
   */
   compare(contract, snapshot) {
      if (this.compareObjects(contract.table, snapshot.table) == false) {
         throw (new Error("Table properties don't match for contract "+this.contractID));
      }
      if ((contract.prime != null) && (snapshot.prime == null)) {
         return (1);
      } else if ((contract.prime == null) && (snapshot.prime != null)) {
         return (2);
      } else if (contract.prime != snapshot.prime) {
         throw (new Error("Prime value mismatch."));
      }
      //compare decks
      result = this.compareDecks(contract.cardDecks.faceup, snapshot.cardDecks.faceup, "_mapping");
      if (result != 3) {
         return (result);
      }
      result = this.compareDecks(contract.cardDecks.facedown, snapshot.cardDecks.facedown);
      if (result != 0) {
         return (result);
      }
      result = this.compareDecks(contract.cardDecks.dealt, snapshot.cardDecks.dealt);
      if (result != 0) {
         return (result);
      }
      result = this.compareDecks(contract.cardDecks.public, snapshot.cardDecks.public, "_mapping");
      if (result != 0) {
         return (result);
      }
      //compare players
      for (var count = 0; count < contract.players.length; count++) {
         var contractPlayer = contract.players[count];
         var snapshotPlayer = snapshot.players[count];
         if (contractPlayer.privateID != snapshotPlayer.privateID) {
            //player order must be the same
            throw (new (Error("Incorrect player private ID at position "+count)));
         }
         for (var prop in contractPlayer.info) {
            if (snapshotPlayer.info[prop] != contractPlayer.info[prop]) {
               throw (new (Error("Incorrect player private info property \""+prop+"\"")));
            }
         }
         //compare player dealt cards
         var result = this.compareDecks(contractPlayer.dealtCards, snapshotPlayer.dealtCards, "_mapping");
         if (result != 0) {
            return (result);
         }
         //compare player selected cards
         result = this.compareDecks(contractPlayer.selectedCards, snapshotPlayer.selectedCards);
         if (result != 0) {
            return (result);
         }
         if (contractPlayer.hasBet && (snapshotPlayer.hasBet == false)) {
            return (1);
         } else if ((contractPlayer.hasBet == false) && snapshotPlayer.hasBet) {
            return (2);
         }
         if (contractPlayer.hasFolded && (snapshotPlayer.hasFolded == false)) {
            return (1);
         } else if ((contractPlayer.hasFolded == false) && snapshotPlayer.hasFolded) {
            return (2);
         }
         if (contractPlayer.isDealer != snapshotPlayer.isDealer) {
            throw (new Error("Player role mismatch on dealer."));
         }
         if (contractPlayer.isSmallBlind != snapshotPlayer.isSmallBlind) {
            throw (new Error("Player role mismatch on small blind."));
         }
         if (contractPlayer.isBigBlind != snapshotPlayer.isBigBlind) {
            throw (new Error("Player role mismatch on big blind."));
         }
         if (contractPlayer.ready != snapshotPlayer.ready) {
            throw (new Error("Player ready mismatch."));
         }
         contractPlayer.totalBet = bigInt(contractPlayer.totalBet);
         snapshotPlayer.totalBet = bigInt(snapshotPlayer.totalBet);
         if (contractPlayer.totalBet.greater(snapshotPlayer.totalBet)) {
            return (1);
         } else if (snapshotPlayer.totalBet.greater(contractPlayer.totalBet)) {
            return (2);
         }
         //balance comparison is the inverse of totalBet comparison
         contractPlayer.balance = bigInt(contractPlayer.balance);
         snapshotPlayer.balance = bigInt(snapshotPlayer.balance);
         if (contractPlayer.balance.greater(snapshotPlayer.balance)) {
            return (2);
         } else if (snapshotPlayer.balance.greater(contractPlayer.balance)) {
            return (1);
         }
      }
      //relevant contract properties match snapshot properties
      return (0);
   }

   /**
   * Compares a contract card deck against a snapshot card deck.
   *
   * @param {Array} contractDeck Indexed list of contract cards to examine.
   * @param {Array} snapshotDeck Indexed list of snapshot cards to examine.
   * @param {String} [valueProp=null] If supplied, each deck's element is assumed
   * to be a complex object and this is its value property (e.g. <code>mapping</code>).
   * If omitted or <code>null</code>, elements are compared directly with each other.
   *
   * @return {Number} A 0 is returned if both decks are identical: same values and array
   * lengths (though their order may be different). A 1 is returned if the contract deck
   * has more elements than than the snapshot deck, and 2 is returned if the snapshot deck
   * has more elements than the contract deck.
   *
   * @throws {Error} Thrown when a deck contains one or more elements that should appear
   * in the other deck but don't, or if a deck contains duplicate elements.
   * @private
   */
   compareDecks(contractDeck, snapshotDeck, valueProp=null) {
      if (this.containsDuplicates(contractDeck, valueProp) == true) {
         throw (new Error("Contract deck contains duplicates."));
      }
      if (this.containsDuplicates(snapshotDeck, valueProp) == true) {
         throw (new Error("Snapshot deck contains duplicates."));
      }
      var numMatches = 0;
      contractDeck.forEach((value, index, arr) => {
         if (valueProp != null) {
            var contractValue = value[valueProp];
         } else {
            contractValue = value;
         }
         for (var count=0; count < snapshotDeck.length; count++) {
            if (valueProp != null) {
               var snapshotValue = snapshotDeck[count][valueProp];
            } else {
               snapshotValue = snapshotDeck[count];
            }
            if (snapshotValue == contractValue) {
               numMatches++;
            }
         }
      }, this);
      if (contractDeck.length > snapshotDeck.length) {
         return (1);
      } else if (contractDeck.length < snapshotDeck.length) {
         return (2);
      } else {
         if (numMatches != contractDeck.length) {
            //decks are same length but not all elements match
            throw (new Error("Mismatched deck elements."));
         }
         //decks are identical (no duplications, same length)
         return (0);
      }
   }

   /**
   * 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
   */
   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);
   }

   /**
   * Event handler invoked a new keypair is generated. This triggers the processing
   * of any incomplete contract actions requiring a valid keypair or prime.
   *
   * @param {CypherPokerGame#event:gamekeypair} event A "gamekeypair" event.
   * @private
   * @async
   */
   async onGameKeypair(event) {
      this.refreshPlayers();
      var actions = this.deferredActions;
      for (var count=0; count < actions.length; count++) {
         var currentAction = actions[count];
         currentAction.snapshot.prime = this.getPlayer(this.game.ownPID).keychain[0].prime;
         currentAction.snapshot.players = this.getPlayers(false, false);
      }
      var result = await this.processDeferredActions(this.history[0]);
      return (true);
   }

   /**
   * Event handler invoked a new game deck is fully generated. This triggers
   * the asynchronous creation and / or initialization of a new contract for
   * the game.
   *
   * @param {CypherPokerGame#event:gamedeck} event A "gamedeck" event.
   * @private
   */
   onNewGameDeck(event) {
      if (this.getDealer().privateID == this.game.ownPID) {
         //dealer creates the new contract; other players only agree to it
         var paramsObj = new Object();
         paramsObj.contract = new Object();
         //is there a better way to create the contract ID?
         this._contractID = String(Math.random()).split(".")[1];
         paramsObj.contract.contractID = this._contractID;
         paramsObj.contract.players = this.getPlayers(false, false);
         paramsObj.contract.table = this.table;
         paramsObj.contract.prime = this.getPlayer(this.game.ownPID).keychain[0].prime; //prime generated by us
         paramsObj.contract.cardDecks = this.game.cardDecks;
         console.log ("Creating new contract:");
         console.dir(paramsObj);
         this.callContractAPI("new", paramsObj).then(JSONResult => {
            if ((JSONResult["error"] != undefined) && (JSONResult["error"] != null)) {
               this.game.killGame(JSONResult.error.message);
                 return;
            }
            if (this.contractID != JSONResult.result.contract.contractID) {
               this.removeGameEventListeners();
               this.stopContractTimeout();
               return (false);
            }
            this.history.unshift(JSONResult.result.contract);
            try {
               this.updateBalances(JSONResult.result.contract);
            } catch (err) {
               console.error(err);
            }
         }).catch (err => {
            this.removeGameEventListeners();
            this.stopContractTimeout();
            throw(err);
         });
      } else {
         //not the dealer; do nothing
      }
   }

   /**
   * Event handler invoked a card encryption cycle happens. If this is ours,
   * it automatically triggers a contract "store" operation.
   *
   *
   * @param {CypherPokerGame#event:gamecardsencrypt} event A "gamecardsencrypt" event.
   *
   * @return {Promise} Resolves to <code>true</code> if the store operation successfully completed. Rejections
   * receive an <code>Error</code> object.
   * @private
   */
   async onEncryptCards(event) {
      if ((this.history.length == 0) && (this.getDealer().privateID != this.game.ownPID)) {
         return (false);
      }
      if (event.player.privateID != this.game.ownPID) {
         return(false);
      }
      this.startContractTimeout();
      var paramsObj = new Object();
      paramsObj.type = "encrypt";
      paramsObj.contract = this.history[0];
      paramsObj.contractID = this._contractID;
      paramsObj.ownerPID = this.getDealer().privateID;
      paramsObj.cards = Array.from(event.selected);
      var snapshot = this.gameSnapshot();
      try {
         var JSONResult = await this.onGameState(snapshot, this.callContractAPI, "store", paramsObj).promise;
         if (JSONResult.error != undefined) {
            console.error(JSONResult.error.message);
            throw (new Error(JSONResult.error.message));
         }
         if (this.contractID != JSONResult.result.contract.contractID) {
            this.removeGameEventListeners();
            this.stopContractTimeout();
            return (false);
         }
         try {
            this.updateBalances(JSONResult.result.contract);
         } catch (err) {
            this.game.debug(err, "err");
         }
      } catch (err) {
         this.removeGameEventListeners();
         this.stopContractTimeout();
         return (false);
      }
      return (true);
   }

   /**
   * Event handler invoked when the associated {@link CypherPokerContract#game}
   * instance{@link CypherPokerGame#event:gamedealprivate} or
   * {@link CypherPokerGame#event:gamedealpublic} event. This
   * it automatically triggers a contract "store" operation.
   *
   * @param {CypherPokerGame#event} event A{@link CypherPokerGame#event:gamedealprivate} or
   * {@link CypherPokerGame#event:gamedealpublic} event object.
   *
   * @async
   * @private
   */
   async onSelectCards(event) {
      if (this.history.length == 0) {
         return (false);
      }
      this.startContractTimeout();
      var paramsObj = new Object();
      paramsObj.type = "select";
      paramsObj.contract = this.history[0];
      paramsObj.contractID = paramsObj.contract.contractID;
      paramsObj.ownerPID = this.getDealer().privateID;
      paramsObj.cards = Array.from(event.selected);
      paramsObj.fromPID = this.game.ownPID;
      if (event.type == "gamedealprivate") {
         paramsObj.private = true;
      } else {
         paramsObj.private = false;
      }
      var snapshot = this.gameSnapshot();
      try {
         var JSONResult = await this.onGameState(snapshot, this.callContractAPI, "store", paramsObj).promise;
         if (JSONResult.error != undefined) {
            console.error(JSON.error.message);
            throw(new Error(JSON.error.message));
         }
         if (this.contractID != JSONResult.result.contract.contractID) {
            this.removeGameEventListeners();
            this.stopContractTimeout();
            return (false);
         }
         try {
            this.updateBalances(JSONResult.result.contract);
         } catch (err) {
            this.game.debug(err, "err");
         }
      } catch (err) {
         this.removeGameEventListeners();
         this.stopContractTimeout();
         return (false);
      }
      return (true);
   }

   /**
   * Event handler invoked when the associated {@link CypherPokerContract#game}
   * instance{@link CypherPokerGame#event:gamedealmsg} event.
   *
   * @param {CypherPokerGame#event:gamedealmsg} event An external deal operation
   * notification event.
   *
   * @private
   */
   onGameDeal(event) {
      if (this.history.length == 0) {
         return (false);
      }
      this.startContractTimeout();
   }

   /**
   * Event handler invoked when the associated {@link CypherPokerContract#game}
   * instance{@link CypherPokerGame#event:gamebetplaced} event. This
   * it automatically triggers a contract "store" operation.
   *
   * @param {CypherPokerGame#event} event A {@link CypherPokerGame#event:gamebetplaced} event object.
   *
   * @async
   * @private
   */
   async onGameBetPlaced(event) {
      if (this.history.length == 0) {
         return (false);
      }
      this.startContractTimeout();
      var paramsObj = new Object();
      paramsObj.contract = this.history[0];
      paramsObj.contractID = paramsObj.contract.contractID;
      paramsObj.ownerPID = this.getDealer().privateID;
      paramsObj.amount = event.amount;
      paramsObj.fromPID = this.game.ownPID;
      var snapshot = this.gameSnapshot();
      try {
         var JSONResult = await this.onGameState(snapshot, this.callContractAPI, "bet", paramsObj).promise;
         if (JSONResult.error != undefined) {
            console.error(JSONResult.error.message);
            throw(new Error(JSONResult.error.message));
         }
         if (this.contractID != JSONResult.result.contract.contractID) {
            this.removeGameEventListeners();
            this.stopContractTimeout();
            return (false);
         }
         try {
            this.updateBalances(JSONResult.result.contract);
         } catch (err) {
            this.game.debug(err, "err");
         }
      } catch (err) {
         this.removeGameEventListeners();
         this.stopContractTimeout();
         return (false);
      }
      return (true);
   }

   /**
   * Event handler invoked when the associated {@link CypherPokerContract#game}
   * instance dispatches a {@link CypherPoker#event:gamedecrypt} event.
   *
   * @param {Event} event A {@link CypherPoker#event:gamedecrypt} event object.
   *
   * @async
   * @private
   */
   async onGameDecrypt(event) {
      if (this.history.length == 0) {
         return (false);
      }
      this.startContractTimeout();
      var paramsObj = new Object();
      paramsObj.type = "decrypt";
      paramsObj.contract = this.history[0];
      paramsObj.contractID = paramsObj.contract.contractID;
      paramsObj.ownerPID = this.getDealer().privateID;
      paramsObj.cards = Array.from(event.selected);
      paramsObj.sourcePID = event.payload.sourcePID;
      paramsObj.fromPID = this.game.ownPID;
      paramsObj.private = event.private;
      var snapshot = this.gameSnapshot();
      try {
         var JSONResult = await this.onGameState(snapshot, this.callContractAPI, "store", paramsObj).promise;
         if (this.contractID != JSONResult.result.contract.contractID) {
            this.removeGameEventListeners();
            this.stopContractTimeout();
            return (false);
         }
         try {
            this.updateBalances(JSONResult.result.contract);
         } catch (err) {
            this.game.debug(err, "err");
         }
      } catch (err) {
         this.removeGameEventListeners();
         this.stopContractTimeout();
         return (false);
      }
   }

   /**
   * Event handler invoked when the associated {@link CypherPokerContract#game}
   * instance dispatches a {@link CypherPoker#event:gameend} event. This
   * currently causes an immediate submission of the keychain to the
   * contract via a "store" message.
   *
   * @param {Event} event A {@link CypherPoker#event:gameend} event object.
   *
   * @async
   * @private
   */
   async onGameEnd(event) {
      if (this.history.length == 0) {
         return (false);
      }
      this.removeGameEventListeners();
      this.startContractTimeout();
      var paramsObj = new Object();
      paramsObj.type = "keychain";
      paramsObj.contract = this.history[0];
      paramsObj.contractID = paramsObj.contract.contractID;
      paramsObj.ownerPID = this.getDealer().privateID;
      paramsObj.keychain = Array.from(this.getPlayer(this.game.ownPID).keychain);
      paramsObj.fromPID = this.game.ownPID;
      //var snapshot = this.gameSnapshot();
      try {
         var JSONResult = await this.callContractAPI("store", paramsObj);
         if (JSONResult.error != undefined) {
            console.error (JSONResult.error);
            return;
         }
         if (this.contractID != JSONResult.result.contract.contractID) {
            this.removeGameEventListeners();
            this.stopContractTimeout();
            return (false);
         }
         try {
            this.updateBalances(JSONResult.result.contract);
         } catch (err) {
            this.game.debug(err, "err");
         }
      } catch (err) {
         this.removeGameEventListeners();
         this.stopContractTimeout();
         return (false);
      }
   }

   /**
   * Creates a deferred invocation action object based on a game snapshot (state).
   *
   * @param {Object} snapshot A game snapshot (state) to match to a contract
   * state in order to invoke <code>functionRef</code>; for example, a snapshot
   * created using {@link CypherPokerContract#gameSnapshot}.
   * @param {Function} functionRef The <b>asynchronous</b> function to invoke when the game
   * <code>snapshot</code> (state) matches the contract state.
   * @param {*} params Any parameters to include with the function invocation.
   *
   * @return {Object} A deferred game state action object containing a game
   * <code>snapshot</code>, <code>invoke</code> object containing a deferred
   * <code>func</code> function to invoke with <code>params</code> parameters,
   * a <code>promise</code> that will resolve when the <code>snapshot</code>
   * matches the reported contract, and a <code>complete</code> property
   * indicating if the action has completed (true) or not (false).
   * @private
   */
   onGameState(snapshot, functionRef, ...params) {
      var action = new Object();
      action.snapshot = snapshot;
      action.invoke = new Object();
      action.contract = this;
      action.invoke.func = functionRef;
      action.invoke.params = params;
      action.promise = new Promise((resolve, reject) => {
         //resolves or rejects in processDeferredActions
         action._resolve = resolve;
         action._reject = reject;
      })
      action.complete = false;
      this.deferredActions.push(action);
      if (this.history.length > 0) {
         this.processDeferredActions(this.history[0]);
      }
      return (action);
   }

   /**
   * Examines {@link CypherPokerContract#deferredActions} and executes the next un-executed action
   * if its game <code>snapshot</code> matches the <code>contract</code> state.
   *
   * @param {ContractObject} contract The (ideally) updated contract object to check against
   * {@link CypherPokerContract#deferredActions} for possible execution.
   *
   * @return {Promise} Resolves with the deferred action objects that were just executed. Will be an empty
   * array if no actions were executed.
   * @private
   */
   async processDeferredActions(contract){
      var actions = this.deferredActions;
      var previousAction = null;
      var completedActions = new Array();
      var incompletedActions = new Array();
      for (var count=0; count < actions.length; count++) {
         var currentAction = actions[count];
         var exec = false;
         if (previousAction == null) {
            exec = true;
         }
         if (previousAction != null) {
            if (previousAction.complete == true) {
               exec = true;
            }
         }
         if ((currentAction.complete == false) && (exec == true)) {
            var snapshot = currentAction.snapshot;
            var result = this.compare (contract, snapshot);
            if ((result == 0) || (result == 2)) {
               var func = currentAction.invoke.func;
               var params = currentAction.invoke.params;
               var context = currentAction.contract;
               try {
                  currentAction.complete = true;
                  var fResult = await func.apply(context, params);
                  completedActions.push(currentAction);
                  currentAction._resolve(fResult);
               } catch (err) {
                  incompletedActions.push(currentAction);
                  this.game.debug(err, "err");
                  currentAction._reject(err);
               }
            }
         } else {
            if (currentAction.complete == false) {
               incompletedActions.push(currentAction);
            }
         }
         previousAction = currentAction;
      }
      return (completedActions);
   }

   /**
   * Deteremines whether a function is asynchronous (async) or synchronous.
   *
   * @param {function} func The function to evaluate.
   *
   * @return {Boolean} True if the function is asynchronous, false if it's a
   * synchronous function or not a function.
   * @private
   */
   isAsync(func) {
      if (typeof(func) != "function") {
         return (false);
      }
      return (String(func).startsWith("async"));
   }

   /**
   * Sends an "agree" message to the contract API signalling our acceptance of
   * the contract rules. At this point the <code>contract</code> parameter
   * should have been carefully examined for accuracy.
   *
   * @param {ContractObject} contract The contract associated with this instance
   * to agree to. The contract's owner is assumed to be the current dealer in the
   * {@link CypherPokerContract#game} instance.
   *
   * @return {ContractObject} The contract object that was agreed to, according to
   * the remote service.
   * @todo Compare input and output of function as a final verification
   * @private
   */
   async agreeToContract(contract) {
      this.game.debug ("CypherPokerContract: Agreeing to contract "+contract.contractID);
      this.game.debug (contract, "dir");
      var paramsObj = new Object();
      paramsObj.ownerPID = this.getDealer().privateID;
      paramsObj.contractID = contract.contractID;
      var JSONResult = await this.callContractAPI("agree", paramsObj);
      if ((JSONResult["error"] != undefined) && (JSONResult["error"] != null)) {
         this.game.killGame(JSONResult.error.message);
         return (null);
      } else {
         this.updateBalances(JSONResult.result.contract);
      }
      this.history.unshift(JSONResult.result.contract);
      return (JSONResult.result.contract);
   }

   /**
   * Updates the balances of the associated {@link CypherPokerContract#game} instance's
   * players from a provided contract data object.
   *
   * @param {Object} contractData The contract data object to use to update
   * player balances.
   * @param {Boolean} fatalFail=false If true, the function will throw an error on an update failure
   * (usually a value can't be converted). If false, errors are ignored.
   * @throws {Error} If a game balance is not of a convertible type or subsequently if an account
   * balance can't be converted. Note that an invalid game balance does not mean that the account
   * balance is valid because it's checked second. Only thrown if <code>fataiFail=true</code>.
   * @private
   */
   updateBalances(contractData, fatalFail=false) {
      var contractPlayers = contractData.players;
      for (var count = 0; count < contractPlayers.length; count++) {
         var contractPlayer = contractPlayers[count]; //player in contract
         var privateID = contractPlayer.privateID;
         var localPlayer = this.getPlayer(privateID); //player in game
         if ((typeof(contractPlayer.balance) == "string") || (typeof(contractPlayer.balance) == "number")) {
            localPlayer.balance = contractPlayer.balance;
         } else {
            try {
                  //balance may be a String-able instance
                  localPlayer.balance = contractPlayer.balance.toString();
            } catch (err) {
               if (fatalFail == true) {
                  throw (new Error("Player game balance for \""+privateID+"\" invalid (type: "+typeof(contractPlayer.balance)+"): "+contractPlayer.balance));
               }
            }
         }
         if ((typeof(contractPlayer.account) == "object") && (contractPlayer.account != null)) {
            if ((typeof(contractPlayer.account.balance) == "string") || (typeof(contractPlayer.account.balance) == "number")) {
               if ((localPlayer.account == null) || (localPlayer.account == undefined)) {
                  localPlayer.account = new CypherPokerAccount(this.cypherpoker, contractPlayer.account);
               } else {
                  localPlayer.account.balance = contractPlayer.account.balance;
               }
            } else {
               try {
                  localPlayer.account.balance = contractPlayer.account.balance.toString();
               } catch (err) {
                  if (fatalFail == true) {
                     throw (new Error("Player account balance for \""+privateID+"\" invalid (type: "+typeof(contractPlayer.account.balance)+"): "+contractPlayer.account.balance));
                  }
               }
            }
         }
      }
   }

   /**
   * Asynchronously calls the contract API and returns the JSON-RPC 2.0 result / error
   * of the call.
   *
   * @param {String} action The contract API action to take. This parameter is appended
   * the <code>params</code> object and will overwrite any <code>action</code> property
   * included.
   * @param {Object} [params=null] The parameters to include with the remote function call.
   * If null, an empty object is created.
   * @param {String} [APIFunc="CP_SmartContract"] The remote API function to invoke.
   *
   * @return {Promise} The promise resolves with the parsed JSON-RPC 2.0 result or
   * error (native object) of the call. Currently there is no rejection state.
   * @private
   */
   async callContractAPI(action, params=null, APIFunc="CP_SmartContract") {
      var sendObj = new Object();
      if (params == null) {
         params = new Object();
      }
      for (var item in params) {
         sendObj[item] = params[item];
      }
      if (this.history.length > 0) {
         if (this.history[0].invalid == true) {
            throw (new Error("Contract is not valid!"));
         }
      }
      sendObj.action = action;
      sendObj.user_token = this.cypherpoker.api.userToken;
      sendObj.server_token = this.cypherpoker.api.serverToken;
      sendObj.account = this.getPlayer(this.game.ownPID).account.toObject(true);
      var requestID = "CP" + String(Math.random()).split(".")[1];
      var rpc_result = await RPC(APIFunc, sendObj, this.cypherpoker.api, false, requestID);
      var result = JSON.parse(rpc_result.data);
      //since raw API messages are asynchronous the next immediate message may not be ours so:
      while (requestID != result.id) {
         rpc_result = await this.cypherpoker.api.rawConnection.onEventPromise("message");
         result = JSON.parse(rpc_result.data);
         //we could include a max wait limit here
      }
      return (result);
   }

   /**
   * Verifies that a result object contains valid contract update data
   * intended for this instance and the associated {@link CypherPokerContract#game}
   * instance.
   *
   * @param {Object} resultObj The object to analyze, usually the <code>result</code>
   * of a JSON-RPC 2.0 message.
   *
   * @return {Boolean} True if the result object has a valid contract update object
   * structure and is intended for this contract instance.
   * @private
   */
   verifyContractMessage(resultObj) {
      if ((typeof(resultObj.data) != "object") || (resultObj.data == null)) {
         console.error("not an object or null");
         return (false);
      }
      var data = resultObj.data;
      var contract = data.contract;
      var table = contract.table;
      if ((this.game == undefined) || (this.game == null)) {
         console.error("no game ref");
         return (false);
      }
      if ((typeof(this.table) != "object") || (this.table == null)) {
         return (false);
      }
      if ((this.table.tableID != table.tableID) ||
         (this.table.tableName != table.tableName) ||
         (this.table.ownerPID != table.ownerPID)) {
            return (false);
      }
      var tableInfo = table.tableInfo;
      var gameTableInfo = this.table.tableInfo;
      if ((gameTableInfo.buyIn != tableInfo.buyIn) ||
         (gameTableInfo.bigBlind != tableInfo.bigBlind) ||
         (gameTableInfo.smallBlind != tableInfo.smallBlind)) {
            return (false);
      }
      var players = contract.players;
      var matchingPlayers = 0;
      for (var count = 0; count < players.length; count++) {
         for (var count2 = 0; count2 < this.players.length; count2++) {
            if (players[count].privateID == this.players[count2].privateID) {
               matchingPlayers++;
            }
         }
      }
      if (matchingPlayers != this.players.length) {
         return (false);
      }
      //other checks can be performed here
      return (true);
   }

   /**
   * Verifies that a result object contains the same contract ID as this
   * instance. This function should only be called after the result object
   * has been verified {@link CypherPokerContract#verifyContractMessage} and
   * the {@link CypherPokerContract#contractID} property has been set.
   *
   * @param {Object} resultObj The object to analyze, usually the <code>result</code>
   * of a JSON-RPC 2.0 message.
   *
   * @return {Boolean} True if the result object's contract ID matches this
   * instances'.
   * @private
   */
   verifyContractID(resultObj) {
      var data = resultObj.data;
      if ((typeof(data.contract) != "object") || (data.contract == null)) {
         return (false);
      }
      var contract = data.contract;
      if (contract.contractID != this.contractID) {
         return (false);
      }
      return (true);
   }

   /**
   * Handles a server update message event dispatched by the communication
   * interface of the associated {@link CypherPokerContract#game} instance.
   *
   * @param {Event} event An "update" event dispatched by the communication interface.
   *
   * @private
   * @async
   */
   async handleUpdateMessage(event) {
      if (this.cypherpoker.isCPMsgEvent(event) == false) {
         //don't process any further
         return;
      }
      var resultObj = event.data.result;
      if (this.verifyContractMessage(resultObj) == false) {
         //either not a contract update message or not for this instance
         return;
      }
      if (resultObj.from != undefined) {
         var fromPID = resultObj.from; //peer-initiated
      } else {
         fromPID = null; //server-initiated
      }
      var contractObj = resultObj.data.contract;
      var messageType = resultObj.data.cpMsg;
      var contract = resultObj.data.contract;
      var table = contract.table;
      var players = contract.players;
      this.history.unshift(contractObj); //make sure to store contract in history!
      this.game.debug("CypherPokerContract.handleUpdateMessage("+event+") => \""+messageType+"\"");
      this.processDeferredActions(contractObj); //respond immediately on game state match
      switch (messageType) {
         case "contractnew":
            if (this.contractID == null) {
               this._contractID = contract.contractID;
               this.updateBalances(contract);
               var snapshot = this.gameSnapshot();
               this.onGameState(snapshot, this.agreeToContract, contract).promise.catch (err => {
                  this._contractID = null; //could not agree / contract is invalid
                  this.removeGameEventListeners();
                  this.game.debug(err, "err");
               });
               if (this.history[0].invalid != true) {
                  this.startContractTimeout();
               } else {
                  this.stopContractTimeout();
               }
            }
            break;
         case "contractnewfail":
            var errorMessage = "Player \""+fromPID+"\" failed to create contract:<br/>";
            errorMessage += resultObj.data.error.message;
            this.game.killGame(errorMessage);
            this.stopContractTimeout();
            break;
         case "contractagree":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               return;
            }
            //note that contract owner (dealer) auto-agrees in addition to...
            this.game.debug ("Player "+fromPID+" has agreed to contract: "+contract.contractID);
            this.updateBalances(contract);
            this.processDeferredActions(contract);
            if (this.history[0].invalid != true) {
               this.startContractTimeout();
            } else {
               this.stopContractTimeout();
            }
            break;
         case "contractagreefail":
            var errorMessage = "Player \""+fromPID+"\" failed to agree to contract:<br/>";
            errorMessage += resultObj.data.error.message;
            this.game.killGame(errorMessage);
            this.stopContractTimeout();
            break;
         case "contractencryptstore":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               return;
            }
            this.game.debug ("Player "+fromPID+" has stored an encryption round to the contract:");
            this.game.debug (contract, "dir");
            this.updateBalances(contract);
            this.processDeferredActions(contract);
            if (this.history[0].invalid != true) {
               this.startContractTimeout();
            } else {
               this.stopContractTimeout();
            }
            break;
         case "contractselectstore":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               return;
            }
            this.game.debug ("Player "+fromPID+" has stored a card(s) selection to the contract:");
            this.game.debug (contract, "dir");
            this.updateBalances(contract);
            this.processDeferredActions(contract);
            if (this.history[0].invalid != true) {
               this.startContractTimeout();
            } else {
               this.stopContractTimeout();
            }
            break;
         case "contractdecryptstore":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               return;
            }
            this.game.debug ("Player "+fromPID+" has stored a decryption round to the contract:");
            this.game.debug (contract, "dir");
            this.updateBalances(contract);
            this.processDeferredActions(contract);
            if (this.history[0].invalid != true) {
               this.startContractTimeout();
            } else {
               this.stopContractTimeout();
            }
            break;
         case "contractbet":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               return;
            }
            this.game.debug ("Player "+fromPID+" has stored a bet or fold action to the contract:");
            this.game.debug (contract, "dir");
            this.updateBalances(contract);
            this.processDeferredActions(contract);
            if (this.history[0].invalid != true) {
               this.startContractTimeout();
            } else {
               this.stopContractTimeout();
            }
            break;
         case "contractkeychainstore":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               return;
            }
            this.game.debug ("Player "+fromPID+" has stored their keychain to the contract:");
            this.game.debug (contract, "dir");
            this.updateBalances(contract);
            this.processDeferredActions(contract);
            this.startContractTimeout();
            break;
         case "contracttimeout":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               return;
            }
            this.updateBalances(contract);
            this.processDeferredActions(contract);
            var event = new Event("timeout");
            event.contract = this;
            event.penalized = contract.penalty.penalized;
            this.dispatchEvent(event);
            this.stopContractTimeout();
            this.resetContractTimeout();
            break;
         case "contractend":
            if (this.verifyContractID(resultObj) == false) {
               //wrong contract ID
               console.error ("Contracts don't match!");
               return;
            }
            this.removeNetworkEventListeners();
            this.removeGameEventListeners();
            this.stopContractTimeout();
            this.resetContractTimeout();
            this._active = false;
            break;
         default:
            //not a recognized CypherPokerContract message type
            break;
      }
   }

}