contract/src/deployer.js

'use strict'
const path   = require('path')
const assert = require('chai').assert
const { Map, List, Seq, OrderedMap }
             = require('immutable')
const ethjsABI = require('ethjs-abi')

const { DEPLOYS_DIR, getConfig, getNetwork, Logger, getImmutableKey, setImmutableKey, toJS }
             = require('demo-utils')
const LOGGER = new Logger('Deployer')
const { awaitOutputter, isLink, isForkedDeploy }
             = require('./utils')
const { BuildsManager }
             = require('./buildsManager')
const { isValidAddress, toChecksumAddress, keccak }
             = require('ethereumjs-util')
const { getKeys } = require('ethjs-util')
const tx = require('demo-tx')

const deploys = {}

/**
 * Class encapsulating a deployer for a given network (chain ID)
 * and inputter / outputter (possibly a remote data store)
 * @memberof module:contract
 * @param inputter {Function} a getter function, possibly async,
 *   that takes a key and returns the associated value if it exists.
 * @param outputter {Function} a setter function, possibly async,
 *   that takes a key and a value as an Immutable Map, and returns
 *   the written value, also as an Immutable Map.
 * @param bm {Object} a BuildsManager, null if we want to auto-create one.
 * @param eth {Object} an Ethereum network object
 * @param chainId {String} the chain ID of the given eth, must match.
 * @param address {String} the `0x`-prefixed Ethereum address to deploy from
 */
deploys.Deployer = class {

  constructor({inputter, outputter, bm, eth, chainId, address}) {
    assert(chainId, `chainId param is empty.`)
    this.bm        = bm || new BuildsManager(...arguments)
    this.eth       = eth || getNetwork()
    this.chainId   = chainId
    assert(isValidAddress(address), `${address} not a valid ethereum address`)
    this.address   = address 
  }

  getBuildsManager() {
    return this.bm
  }

  /**
   * Create the raw tx data for deploying a new contract
   * @param args {Array} list of arguments matching constructor
   * @param abi {Object} ABI of contract
   * @param contractBytecode {String} `0x`-prefixed string of deployedBytecode
   */
  getNewContractTxData(args, abi, contractBytecode) {
		const constructorMethod = abi.filter((x) => x.type === 'constructor')[0]
		const assembleTxObject = {}

		// set contract deploy bytecode
		if (contractBytecode) {
			assembleTxObject.data = contractBytecode;
		}   
    LOGGER.debug('args', args)

		// append encoded constructor arguments
		if (constructorMethod) {
      LOGGER.debug('constructorMethod.inputs', constructorMethod.inputs)
      const keys = getKeys(constructorMethod.inputs, 'type')
      LOGGER.debug('keys', keys)
			const constructorBytecode = ethjsABI.encodeParams(keys, args).substring(2)
			assembleTxObject.data = `${assembleTxObject.data}${constructorBytecode}`
		}   

		return assembleTxObject.data
  }

  /**
   * Validate dependencies then deploy the given contract output to a network.
   * @param eth network object connected to a local provider
   * @param contractName {String} of source contract
   * @param linkId {String} ID of previous link to instantiate and deploy
   * @param deployId {String} ID of previous deploy
   * @param ctorArgs {Object} Immutable Map of constructor arguments, can be empty Map or null
   * @param fork {boolean} whether to fork the given deploy at the current timestamp
   *   can be left null for false
   */
  async deploy(contractName, linkId, deployId, ctorArgs, fork) {
    const linkName   = `${contractName}-${linkId}`
    const link       = await this.bm.getLink(linkName)
    assert( isLink(link), `Link ${linkName} not valid: ${JSON.stringify(link.toJS())}` )
    const code       = link.get('code')
    const abi        = link.get('abi')
    const _deployId  = deployId || 'deploy'
    const deployName = `${contractName}-${_deployId}`
   
    assert.equal(this.chainId, await this.eth.net_version())

    const now = new Date()
    const deployMap = await this.bm.getDeploys()
    LOGGER.debug('deployMap', List(deployMap.keys()))

    const inputHash = keccak(JSON.stringify(link.toJS())).toString('hex')
    // Warn with multiple deploys with the same ID
    const deploy = deployMap.get(deployName)
    if (Map.isMap(deploy)) {
      LOGGER.debug(`previous input hash ${deploy.get('inputHash')}`)
      LOGGER.debug(`current input hash ${inputHash}`)
      if (deploy.get('inputHash') === inputHash) {
        if (fork) {
          LOGGER.info(`Forking at time ${now.getTime()}`)
        } else {
          LOGGER.info(`${deployName} has already been deployed`,
                      `on chain ID ${this.chainId} at address ${deploy.get('deployAddress')}`)
          return deploy
        }
      }
    }
    LOGGER.info(`Deploy ${deployName} is out-of-date, re-deploying...`)

    const ctorArgList = Map.isMap(ctorArgs) ? List(ctorArgs.values()).toJS() : []
    LOGGER.debug(ctorArgList)

    const Contract = this.eth.contract(toJS( abi ), code)

    const gasPrice = getConfig()[ 'GAS_PRICE' ]
    const gasLimit = getConfig()[ 'GAS_LIMIT' ]
    LOGGER.debug(`gasPrice`, gasPrice)
    LOGGER.debug(`gasLimit`, gasLimit)

    const txData = this.getNewContractTxData(ctorArgList, toJS( abi ), code)
    LOGGER.debug('newContractTxData', txData)

    const rawTx = await tx.createRawTx({
      from: this.address,
      data: txData,
    })
    const deployPromise = tx.sendSignedTx({
      rawTx: rawTx, signerEth: this.eth
    }).then((txHash) => this.eth.getTransactionReceipt(txHash))

    const minedContract = await deployPromise.then((receipt) => { return receipt })
    LOGGER.debug('MINED', minedContract)
    const instance = Contract.at(minedContract.contractAddress)

    const preHash = new OrderedMap({
      type         : 'deploy',
      name         : contractName,
      chainId      : this.chainId,
      deployId     : deployId,
      linkId       : link.get('linkId'),
      abi          : abi,
      code         : code,
      inputHash    : inputHash,
      ctorArgList  : ctorArgList,
    })

    const deployOutput = preHash.
      set('contentHash', keccak(JSON.stringify(preHash)).toString('hex'))
      .merge(OrderedMap({
        deployTx     : new Map(minedContract),
        deployAddress: toChecksumAddress(minedContract.contractAddress),
        deployDate   : now.toLocaleString(),
        deployTime   : now.getTime(),
      }))

    // This is an updated deploy, overwrite it
    return await this.bm.setDeploy(deployName, deployOutput, true)
  }

}

module.exports = deploys