trufflesuite/truffle

View on GitHub
packages/events/defaultSubscribers/migrate/Reporter.js

Summary

Maintainability
C
7 hrs
Test Coverage
const debug = require("debug")("reporters:migrations:reporter"); // eslint-disable-line no-unused-vars
const web3Utils = require("web3-utils");
const Spinner = require("@truffle/spinners").Spinner;

const Messages = require("./Messages");

/**
 *  Reporter consumed by a migrations sequence which iteself consumes a series of Migration and
 *  Deployer instances that emit both async `Emittery` events and conventional EventEmitter
 *  events (from Web3PromiEvent). This reporter is designed to track the execution of
 *  several migrations files in sequence and is analagous to the Mocha reporter in that:
 *
 *  test:: deployment
 *  suite:: deployer.start to deployer.finish
 *  test file:: migrations file
 *
 */
class Reporter {
  constructor({ subscriber }) {
    this.currentGasTotal = new web3Utils.BN(0);
    this.currentCostTotal = new web3Utils.BN(0);
    this.finalCostTotal = new web3Utils.BN(0);
    this.deployments = 0;
    this.separator = "\n";
    this.summary = [];
    this.currentFileIndex = -1;
    this.currentBlockWait = "";
    this.subscriber = subscriber;

    this.messages = new Messages(this);
  }

  // ------------------------------------  Utilities -----------------------------------------------

  /**
   * Retrieves gas usage totals per migrations file / totals since the reporter
   * started running. Calling this method resets the gas counters for migrations totals
   */
  getTotals(interfaceAdapter) {
    const gas = this.currentGasTotal.toString(10);
    const cost = interfaceAdapter.displayCost(this.currentCostTotal);
    this.finalCostTotal = this.finalCostTotal.add(this.currentCostTotal);

    this.currentGasTotal = new web3Utils.BN(0);
    this.currentCostTotal = new web3Utils.BN(0);

    return {
      gas,
      cost,
      finalCost: interfaceAdapter.displayCost(this.finalCostTotal),
      deployments: this.deployments.toString()
    };
  }

  /**
   * Error dispatcher. Parses the error returned from web3 and outputs a more verbose error after
   * doing what it can to evaluate the failure context from data passed to it.
   * @param  {Object} data info collected during deployment attempt
   */
  async processDeploymentError(data) {
    const error = data.estimateError || data.error;

    data.reason = data.error ? data.error.reason : null;

    const errors = {
      ETH: error.message.includes("funds"),
      OOG: error.message.includes("out of gas"),
      INT:
        error.message.includes("base fee") ||
        error.message.includes("intrinsic"),
      RVT: error.message.includes("revert"),
      BLK: error.message.includes("block gas limit"),
      NCE: error.message.includes("nonce"),
      INV: error.message.includes("invalid opcode"),
      GTH: error.message.includes("always failing transaction")
    };

    let type = Object.keys(errors).find(key => errors[key]);

    switch (type) {
      // `Intrinsic gas too low`
      case "INT":
        return data.gas
          ? this.messages.errors("intWithGas", data)
          : this.messages.errors("intNoGas", data);

      // `Out of gas`
      case "OOG":
        return data.gas && !(data.gas === data.blockLimit)
          ? this.messages.errors("intWithGas", data)
          : this.messages.errors("oogNoGas", data);

      // `Revert`
      case "RVT":
        return data.reason
          ? this.messages.errors("rvtReason", data)
          : this.messages.errors("rvtNoReason", data);

      // `Invalid opcode`
      case "INV":
        return data.reason
          ? this.messages.errors("asrtReason", data)
          : this.messages.errors("asrtNoReason", data);

      // `Exceeds block limit`
      case "BLK":
        return data.gas
          ? this.messages.errors("blockWithGas", data)
          : this.messages.errors("blockNoGas", data);

      // `Insufficient funds`
      case "ETH":
        const balance = await data.contract.interfaceAdapter.getBalance(
          data.from
        );
        data.balance = balance.toString();
        return this.messages.errors("noMoney", data);

      // `Invalid nonce`
      case "NCE":
        return this.messages.errors("nonce", data);

      // Generic geth error
      case "GTH":
        return this.messages.errors("geth", data);

      default:
        return this.messages.errors("default", data);
    }
  }

  // -------------------------  Migration File Handlers --------------------------------------------

  /**
   * Run when a migrations file is loaded, before deployments begin
   * @param  {Object} data
   */
  async preMigrate(data) {
    let message;
    if (data.isFirst) {
      message = this.messages.steps("firstMigrate", data);
    }

    this.summary.push({
      file: data.file,
      number: data.number,
      deployments: []
    });

    this.currentFileIndex++;

    const messagePart2 = this.messages.steps("preMigrate", data);
    if (message && messagePart2) {
      return message + "\n" + messagePart2;
    } else if (message) {
      return message;
    }
    return messagePart2;
  }

  /**
   * Run after a migrations file has completed and the migration has been saved.
   * @param  {Object} data
   */
  async postMigrate(data) {
    const totals = this.getTotals(data.interfaceAdapter);
    this.summary[this.currentFileIndex].totalCost = totals.cost;

    let messageData = {
      number: this.summary[this.currentFileIndex].number,
      cost: totals.cost,
      valueUnit: this.valueUnit
    };
    let message = this.messages.steps("postMigrate", messageData);

    if (data.isLast) {
      messageData.totalDeployments = totals.deployments;
      messageData.finalCost = totals.finalCost;

      this.summary.totalDeployments = messageData.totalDeployments;
      this.summary.finalCost = messageData.finalCost;

      message += this.messages.steps("lastMigrate", messageData);
    }
    return message;
  }

  // ----------------------------  Deployment Handlers --------------------------------------------

  /**
   * Runs after pre-flight estimate has executed, before the sendTx is attempted
   * @param  {Object} data
   */
  async preDeploy(data) {
    let message;
    data.deployed
      ? (message = this.messages.steps("replacing", data))
      : (message = this.messages.steps("deploying", data));

    return message;
  }

  /**
   * Run at intervals after the sendTx has executed, before the deployment resolves
   * @param  {Object} data
   */
  async block(data) {
    this.currentBlockWait =
      `Blocks: ${data.blocksWaited}`.padEnd(21) +
      `Seconds: ${data.secondsWaited}`;

    if (this.blockSpinner) {
      this.blockSpinner.text = this.currentBlockWait;
    }
  }

  /**
   * Run after a deployment instance has resolved. This handler collects deployment cost
   * data and stores it a `summary` map so that it can later be replayed in an interactive
   * preview (e.g. dry-run --> real). Also passes this data to the messaging utility for
   * output formatting.
   * @param  {Object} data
   */
  async postDeploy(data) {
    let message;
    if (data.deployed) {
      const txCostReport =
        await data.contract.interfaceAdapter.getTransactionCostReport(
          data.receipt
        );

      // if it returns null, try again!
      if (!txCostReport) {
        return this.postDeploy(data);
      }

      data = {
        ...data,
        ...txCostReport,
        cost: data.contract.interfaceAdapter.displayCost(txCostReport.cost)
      };

      this.valueUnit = data.valueUnit;
      this.currentGasTotal = this.currentGasTotal.add(txCostReport.gas);
      this.currentCostTotal = this.currentCostTotal.add(txCostReport.cost);
      this.currentAddress = this.from;
      this.deployments++;

      if (this.summary[this.currentFileIndex]) {
        this.summary[this.currentFileIndex].deployments.push(data);
      }

      message = this.messages.steps("deployed", data);
    } else {
      message = this.messages.steps("reusing", data);
    }

    return message;
  }

  /**
   * Runs on deployment error. Forwards err to the error parser/dispatcher after shutting down
   * any `pending` UI.any `pending` UI.
   * @param  {Object}  data  event args
   * @return {Promise}       resolves string error message
   */
  async deployFailed(data) {
    if (this.blockSpinner) {
      this.blockSpinner.fail();
    }

    if (this.transactionSpinner) {
      this.transactionSpinner.fail();
    }
    return await this.processDeploymentError(data);
  }

  // --------------------------  Transaction Handlers  ------------------------------------------

  /**
   * Run on `startTransaction` event. This is fired by migrations on save
   * but could also be fired within a migrations script by a user.
   * @param  {Object} data
   */
  async startTransaction(data) {
    const message = data.message || "Starting unknown transaction...";
    this.transactionSpinner = new Spinner(
      "events:subscribers:migrate:reporter:transactions",
      {
        text: message,
        indent: 3,
        prefixColor: "red"
      }
    );
  }

  /**
   * Run after a start transaction
   * @param  {Object} data
   */
  async endTransaction(data) {
    data.message = data.message || "Ending unknown transaction....";
    const message = this.messages.steps("endTransaction", data);
    this.transactionSpinner.remove();
    return message;
  }

  // ----------------------------  Library Event Handlers ------------------------------------------
  linking(data) {
    return this.messages.steps("linking", data);
  }

  // ----------------------------  PromiEvent Handlers --------------------------------------------

  /**
   * For misc error reporting that requires no context specific UI mgmt
   * @param  {Object} data
   */
  async error(data) {
    return this.messages.errors(data.type, data);
  }

  /**
   * Fired on Web3Promievent 'transactionHash' event. Begins running a UI
   * a block / time counter.
   * @param  {Object} data
   */
  async txHash(data) {
    if (this.subscriber.config.dryRun) {
      return;
    }

    let message = this.messages.steps("hash", data);
    this.subscriber.logger.log(message);

    this.currentBlockWait = `Blocks: 0`.padEnd(21) + `Seconds: 0`;

    this.blockSpinner = new Spinner("events:subscribers:migrate:reporter", {
      text: this.currentBlockWait,
      indent: 3,
      prefixColor: "red"
    });
  }

  /**
   * Fired on Web3Promievent 'confirmation' event.
   * @param  {Object} data
   */
  async confirmation(data) {
    return this.messages.steps("confirmation", data);
  }
}

module.exports = Reporter;