contract/src/contractsManager.js

'use strict'
// Compile with solcjs
const fs         = require('fs')
const path       = require('path')
const assert     = require('chai').assert
const { List, Map, Set }
                 = require('immutable')
const { traverseDirs, COMPILES_DIR, getImmutableKey, setImmutableKey, Logger,
  awaitInputter, awaitOutputter }
                 = require('demo-utils')

const LOGGER = new Logger('ContractsManager')

const cm = {}

/**
 * A ContractsManager which can read and clean compiled contracts.
 * @param _outputter {Function} a (possibly asynchronous) function that
 *        takes (key: string, val: {Map} | {List} | null ) and returns a Promise or
 *        other value that you want returned from `compile` or `clean*` methods.
 *        If missing, _outputter defaults to `setImmutableKey`
 *        to a local file-based DB store.
 */
cm.ContractsManager = class {
  
  constructor({sourcePathList, inputter, outputter}) {
    this.sourcePathSet = Set(sourcePathList)
      .filter((d) => {
        if (!fs.existsSync(d)) {
          LOGGER.warn(`Source path ${d} does not exist, ignoring.`)
          return false
        } else {
          return true
        }
      })
    assert((inputter && outputter) || (!inputter && !outputter))
    this.inputter = inputter || getImmutableKey
    this.outputter = outputter || setImmutableKey
  }

  async getContracts() {
    if (!this.contracts) {
      const contractSources = []
      traverseDirs(
        this.sourcePathSet.toJS(),
        (fnParts) => { return (fnParts.length > 1 && !fnParts[1].startsWith('sol')) },
        function(source, f) {
          const fn = List(f.split(path.sep)).last()
          const fb = path.basename(fn.split('.')[0])
          contractSources.push(fb)
          LOGGER.debug(`Source ${fb}`)
        }
      )

      this.contracts = await awaitInputter(
        this.inputter(COMPILES_DIR, new Map({})),
        (contractOutputs) => {
          return {
            contractSources: List(contractSources),
            contractOutputs: contractOutputs
          }
        }
      )
    }
    return this.contracts
  }

  /**
   * Asynchronously return a contract previously outputted at key `/compiles/`
   * @param contractName name of the compiled contract
   */
  async getContract(contractName) {
    const { contractOutputs } = await this.getContracts()
    return contractOutputs.get(contractName)
  }

  async setContract(contractName, contractOutput) {
    const compileKey = `${COMPILES_DIR}/${contractName}`
    this.contracts.contractOutputs =
      this.contracts.contractOutputs.set(contractName, contractOutput)
    return awaitOutputter(
      this.outputter(compileKey, contractOutput, true),
      () => { return contractOutput }
    )
  }

  async cleanContract(contractName) {
    this.contracts.contractOutputs =
      this.contracts.contractOutputs.set(contractName, null)
    return this.outputter(`${COMPILES_DIR}/${contractName}`, null)
  }

  async cleanAllCompiles() {
    if (this.contracts) {
      this.contracts.contractOutputs = Map({})
    }
    return this.outputter(`${COMPILES_DIR}`, null)
  }

  async cleanCompile(compile) {
    return Promise.all(List(
      compile.map((compile, compileName) => {
        return this.cleanContract(compileName)
      }).values()).toJS()
    )
  }

}

/**
 * @return true if the given object is a compile output from a Compiler, otherwise false
 */
cm.isCompile = (_compile) => {
  return (_compile && Map.isMap(_compile) && _compile.count() > 0 &&
          _compile.reduce((prev, val) => {
    return prev && val.get('type') === 'compile'
  }, true))
}

/**
 * @return true if the given object is a compile output retrieved from db, otherwise false
 */
cm.isContract = (_contract) => {
  return (Map.isMap(_contract) && _contract.get('type') === 'compile')
}

module.exports = cm