Source: channelState.js

/*
* @Author: amitshah
* @Date:   2018-04-13 15:16:52
* @Last Modified by:   amitshah
* @Last Modified time: 2018-04-28 22:44:44
*/

const merkletree = require('./merkletree');
const util = require('ethereumjs-util')
const sjcl = require('sjcl');
const rlp = require('rlp');
const abi = require("ethereumjs-abi");
const message = require('./message');



/** @class channel state endpoint; each Channel is composed of two channel state represent both actors
* @property {message.Proof} proof-the proof snapshot of this endpoint.  If this channel state represents the peer, this proof is submitted during
* channel closing. 
* @property {Object.<string,message.Lock>} pendingLocks - the pending locks that have not had their secrets revealed.  The key is the hashLock value
* @property {Object.<string,message.OpenLock>} openLocks - the opened locks that have had their secrets revealed.  The key is the hashLock value
* @property {merkletree.MerkleTree} merkleTree - the merkleTree based on the pending and open locks
* @property {BN} depositBalance=0 - the amount of funds deposited on-chain by this channel state endpoint
* @property {Buffer} address - the ethereum address of the particpant who's state this endpoint represents
* @see Channel
*/
class ChannelState{
  /** @constructor
  * @param {object} options*/
  constructor(options){
    this.proof = options.proof || new message.ProofMessage({});
    //dictionary of locks ordered by hashLock key
    this.pendingLocks = {};
    this.openLocks = {};
    this.merkleTree = options.merkleTree || new merkletree.MerkleTree([]);
    //the amount the user has put into the channel
    this.depositBalance = options.depositBalance || new util.BN(0);
    this.address = options.address || message.EMPTY_20BYTE_BUFFER;
  }

  /** @property {BN} the nonce that ensures transfer ordering as retreived from the proof */
  get nonce(){
    return this.proof.nonce;
  }

  /** @property {BN} the monotonically increasing transferredAmount retrieved from the proof */
  get transferredAmount(){
    return this.proof.transferredAmount;
  }

  /** update the channel state to reflect locked transfer
  * @param {(message.LockedTransfer|message.MediatedTransfer)}
  * @throws "Invalid Message Type: DirectTransfer expected"
  * @throws "Invalid Lock: lock already registered"
  * @throws "Invalid hashLockRoot"
  */
  applyLockedTransfer(lockedTransfer){
    if(!lockedTransfer instanceof message.LockedTransfer){
      throw new Error("Invalid Message Type: DirectTransfer expected");
    }
    var proof = lockedTransfer.toProof();
    var lock = lockedTransfer.lock;
    var hashLockKey = lock.hashLock.toString('hex');
    if(this.pendingLocks.hasOwnProperty(hashLockKey) || this.openLocks.hasOwnProperty(hashLockKey)){
      throw new Error("Invalid Lock: lock already registered");
    }
    var mt = this._computeMerkleTreeWithHashlock(lock);

    if(mt.getRoot().compare(proof.locksRoot)!= 0){
        throw new Error("Invalid hashLockRoot");
    };
    this.pendingLocks[hashLockKey] = lock;
    this.proof = proof;
    this.merkleTree = mt;
  }

  /** update the channel state to reflect direct transfer
  * @param {message.DirectTransfer}
  * @throws "Invalid Message Type: DirectTransfer expected"
  * @throws "Invalid hashLockRoot"
  */
  applyDirectTransfer(directTransfer){
    if(!directTransfer instanceof message.DirectTransfer){
      throw new Error("Invalid Message Type: DirectTransfer expected");
    }
    if(this.merkleTree.getRoot().compare(directTransfer.locksRoot)!==0){
      throw new Error("Invalid hashLockRoot");
    }
    this.proof = directTransfer.toProof();
  }

  /** applies the secret to to unlock a pending lock
  * @param {message.RevealSecret}
  * @throws "Invalid Message Type: RevealSecret expected"
  * @throws "Invalid Lock: uknown lock secret received"
  */
  applyRevealSecret(revealSecret){
    if(!revealSecret instanceof message.RevealSecret){
      throw new Error("Invalid Message Type: RevealSecret expected");
    }
    var hashLock = revealSecret.hashLock;
    var hashLockKey = hashLock.toString('hex');
    var pendingLock = null;
    if(!(this.pendingLocks.hasOwnProperty(hashLockKey) || this.openLocks.hasOwnProperty(hashLockKey))){
      throw new Error("Invalid Lock: uknown lock secret received");
    }
    if(this.pendingLocks.hasOwnProperty(hashLockKey)){
      //TODO this must be atomic operation, you will have to sanity check on restart
      //if we crash here, we will have the same lock twice...
      pendingLock = this.pendingLocks[hashLockKey];
      this.openLocks[hashLockKey] = new message.OpenLock(pendingLock,revealSecret.secret);
      delete this.pendingLocks[hashLockKey];
    }
  }

  /** removes the open lock and applies the locked amount to the transferredAmount allowing indefinte channel lifetime
  * @param {message.SecretToProof}
  * @throws "Invalid Message Type: SecretToProof expected"
  * @throws "Invalid Lock: uknown lock secret received"
  * @throws "Invalid hashLockRoot in SecretToProof"
  */
  applySecretToProof(secretToProof){
    if(!secretToProof instanceof message.SecretToProof){
      throw new Error("Invalid Message Type: SecretToProof expected");
    }
    var proof = secretToProof.toProof();
    var secret = secretToProof.secret;
    var hashLock = secretToProof.hashLock;
    var hashLockKey = hashLock.toString('hex');

    var pendingLock = null;
    if(this.pendingLocks.hasOwnProperty(hashLockKey)){
      pendingLock = this.pendingLocks[hashLockKey];
    }else if(this.openLocks.hasOwnProperty(hashLockKey)){
      pendingLock = this.openLocks[hashLockKey];
    }
    if(!pendingLock){
      throw new Error("Invalid Lock: uknown lock secret received");
    }

    var mt = this._computeMerkleTreeWithoutHashlock(pendingLock);
    if(!mt.getRoot().compare(proof.locksRoot) ==0){
      throw new Error("Invalid hashLockRoot in SecretToProof");
    }

    //we compare this fundamental assumption in the Channel
    // if(!(proof.transferredAmount == pendingLock.amount + this.proof.transferredAmount){
    //   throw new Error("Invalid transferredAmount in SecretToProof");
    // }

    //remove the secret from lock states, always use local copies of variables
    if(this.pendingLocks.hasOwnProperty(pendingLock.hashLock.toString('hex'))){
      delete this.pendingLocks[pendingLock.hashLock.toString('hex')];
    }else if(this.openLocks.hasOwnProperty(pendingLock.hashLock.toString('hex'))){
      delete this.openLocks[pendingLock.hashLock.toString('hex')];
    }
    this.proof = proof;
    this.merkleTree = mt;
  }
   /** Internal computes merkle tree including a new leaf element
   * @param {message.Lock}
   * @returns {merkletree.MerkleTree}
   */
   _computeMerkleTreeWithHashlock(lock){
      var mt = new merkletree.MerkleTree(Object.values(Object.assign({},this.pendingLocks, this.openLocks)).concat(lock).map(
        function (l) {
        return l.getMessageHash();
      }));

      mt.generateHashTree();
      return mt;
    }
    /** Internal computes merkle tree  without a particular leaf element
    * @param {message.Lock}
    * @returns {merkletree.MerkleTree}
    */
    _computeMerkleTreeWithoutHashlock(lock){
      var hashLockKey = lock.hashLock.toString('hex');
      var locks = Object.assign({}, this.pendingLocks, this.openLocks);
      if(!locks.hasOwnProperty(hashLockKey)){
        throw new Error("Unknown Lock: Cannot compute merkletree trying to remove Unknown lock");
      }
      delete locks[hashLockKey];

       var mt = new merkletree.MerkleTree(Object.values(locks).map(
        function (l) {
        return l.getMessageHash();
      }));

      mt.generateHashTree();
      return mt;
    }


    /**retreive the lock corresponding to the keccak256 hash of the secret
   * @param {Buffer} secret
   * @returns {(message.Lock| message.OpenLock | null)}
   */
    getLockFromSecret(secret){
      var hashLock = util.sha3(secret);
      var hashLockKey = hashLock.toString('hex');
      if(this.pendingLocks.hasOwnProperty(hashLockKey)){
        return this.pendingLocks[hashLockKey];
      }
      if(this.openLocks.hasOwnProperty(hashLockKey)){
        return this.openLocks[hashLockKey];
      }
      return null;
    }

    /** determine if this channel has the lock in pending or open state 
    * @param {message.Lock} lock
    * @returns {bool}
    */
    containsLock(lock){
      var hashLockKey = lock.hashLock.toString("hex");
      return this.pendingLocks.hasOwnProperty(hashLockKey) || this.openLocks.hasOwnProperty(hashLockKey);
    }

    /** @property {BN} return the minimum lock expiration time across all open locks. This effectively give an upper bound for how long the channel
    * can remain open unless the lock is converted to a transfer via message.SecretToProof message sent from counterparty.
    */
    get minOpenLockExpiration(){
      return reduce(
      map(Object.values(this.openLocks),function  (lock) {
        return lock.expiration;
      }),function (expiration,lock) {
        if(lock.expiration.lt(expiration)){
          return lock.expiration;
        }
        return expiration;
      },new util.BN(0));
    }

    /** determine the amount of funds that are locked. The safeblock parameter is required if you want to prevent 
    * channel exhaustion due to lock expirations.  
    * @param {BN} safeBlock
    * @returns {BN}
    */
    lockedAmount(safeBlock){
      //we only want lockedAmounts that have not yet expired
      return this._lockAmount(Object.values(this.pendingLocks),safeBlock);
    }

    /** the amount of funds that are unlocked and usable in the netting channel
    * @returns {BN}
    */
    unlockedAmount(){
       //we sort of disregard the expiration, the expiration of unlocked
       //locks forces an onchain settle more then anything
       return this._lockAmount(Object.values(this.openLocks));
    }

    /** the total amount of funds that are availble regarding both locked and unlocked funds
    * @param {message.Lock[]} locksArray
    * @param {BN} safeBlock - safe expiration time
    * @returns {BN}
    */
    _lockAmount(locksArray,safeBlock){

      if(safeBlock){
        safeBlock = message.TO_BN(safeBlock);
       return locksArray.reduce(function(sum,lock){
        if(lock.expiration.gt(safeBlock)){

          return sum.add(lock.amount);
        }
        return sum;
      }, new util.BN(0));
     }else{
      return locksArray.reduce(function(sum,lock){
        return sum.add(lock.amount);
      }, new util.BN(0));
     }
    }

    /** Deprecated */
    balance(peerState){
      throw new Error("not implemented");
    }

    /** Deprecated */
    transferrable(peerState){
      throw new Error("not implemented");
      this.balance(peerState).sub(this.lockedAmount);
    }

    /** create a lock proof that maybe submitted for onchain withdrawal of lock during the settlement period
    * @param {message.OpenLock} lock
    * @returns {Buffer[]}
    */ 
    generateLockProof(lock){
     var lockProof = this.merkleTree.generateProof(lock.getMessageHash());
     var verified = merkletree.checkMerkleProof(lockProof,this.merkleTree.getRoot(),lock.getMessageHash());
     if(!verified){
      throw new Error("Error creating lock proof");
     }
     return lockProof;
    }

}

module.exports= {
  ChannelState
};