Source

operate/tape.js

const Cell = require('./cell')

/**
 * Class for working with Operate tapes.
 *
 * An Operate program is a tape made up of one or more cells, where each cell
 * contains a single atomic procedure call (known as an "Op").
 *
 * When a tape is run, each cell is executed in turn, with the result from each
 * cell is passed to the next cell. This is known as the "state". Each cell
 * returns a new state, until the final cell in the tape returns the result of
 * the tape.
 *
 * @class
 */
class Tape {

  /**
   * Creates a Tape instance.
   *
   * @param {Object} attrs Attributes
   * @return {Tape}
   */
  constructor(attrs = {}) {
    this.tx = attrs.tx
    this.index = attrs.index
    this.cells = attrs.cells || []
    this.result = null
    this.error = null
  }

  /**
   * Converts the given BPU Transaction into an Operate Tape.
   *
   * @static
   * @param {Object} attrs BPU Transaction
   * @return {Tape}
   */
  static fromBPU(tx, index) {
    if (typeof index === 'undefined' || index === null) {
      const i = tx.out.findIndex(o => this._isOpReturnOutput(o))
      return this.fromBPU(tx, i)
    }

    let cells;
    if (index > -1 && tx.out[index] && this._isOpReturnOutput(tx.out[index])) {
      cells = tx.out[index].tape
        .filter(c => !this._isOpReturnCell(c))
        .map(c => Cell.fromBPU(c))
    } else {
      throw new Error('No tape found in transaction.')
    }

    return new this({
      tx,
      index,
      cells
    })
  }

  /**
   * Runs the tape in the given VM state.
   *
   * @param {VM} vm VM state
   * @param {Object} opts Options
   * @return {Promise(any)}
   */
  async run(vm, opts = {}) {
    const state = opts.state,
          strict = typeof opts.strict === 'undefined' ? true : opts.strict;
    vm.set('ctx.tx', this.tx || null)
    vm.set('ctx.tape_index', this.index || 0)

    const result = await this.cells.reduce(async (prevState, cell) => {
      const state = await prevState
      return cell.exec(vm, { state })
        .catch(e => {
          if (strict) {
            this.error = e
            throw e
          } else {
            return state
          }
        })
    }, Promise.resolve(state))

    this.result = result
    return result
  }

  /**
   * Sets the given Ops into the cells of the tape. If an aliases object is
   * specifed, this is used to reverse map any procedure scripts onto aliased
   * cells.
   *
   * @param {Array} ops Op functions
   * @param {Object} aliases Aliases
   * @return {Tape}
   */
  setCellOps(ops, aliases = {}) {
    ops.forEach(op => {
      const refs = Object.keys(aliases)
        .filter(k => aliases[k] === op.ref)
      if (!refs.length) refs.push(op.ref);

      this.cells.forEach(cell => {
        if (refs.includes(cell.ref)) cell.op = op.fn;
      })

      return this;
    })
  }

  /**
   * Returns a list of Op references from the tape's cells. If an aliases object
   * is specifed, this is used to alias references to alternative values.
   *
   * @param {Object} aliases Aliases
   * @return {Array}
   */
  getOpRefs(aliases = {}) {
    return this.cells
      .map(c => c.ref)
      .filter((v, i, a) => a.indexOf(v) === i)
      .map(ref => aliases[ref] || ref)
  }

  /**
   * Validates the given tape. Returns true if all the tape's cells are valid.
   * @return {Boolean}
   */
  get isValid() {
    return this.cells.every(c => c.isValid)
  }


  /**
   * Returns true if the BPU Script is an OP_RETURN script.
   * @private
   */
  static _isOpReturnOutput({ tape }) {
    return this._isOpReturnCell(tape[0])
  }

  /**
   * Returns true if the BPU Cell is an OP_RETURN cell.
   * @private
   */
  static _isOpReturnCell({ cell }) {
    return cell.some(c => c.op === 106)
  }

}

module.exports = Tape