trufflesuite/truffle

View on GitHub
packages/core/lib/commands/call/run.js

Summary

Maintainability
D
2 days
Test Coverage
module.exports = async function (options) {
  const debug = require("debug")("core:commands:call");
  const fs = require("fs");
  const util = require("util");
  const { Environment } = require("@truffle/environment");
  const OS = require("os");
  const Codec = require("@truffle/codec");
  const Encoder = require("@truffle/encoder");
  const Decoder = require("@truffle/decoder");
  const TruffleError = require("@truffle/error");
  const { fetchAndCompile } = require("@truffle/fetch-and-compile");
  const loadConfig = require("../../loadConfig");
  const DebugUtils = require("@truffle/debug-utils");
  const web3Utils = require("web3-utils");

  if (options.url && options.network) {
    const message =
      "" +
      "Mutually exclusive options, --url and --network detected!" +
      OS.EOL +
      "Please use either --url or --network and try again." +
      OS.EOL +
      "See: https://trufflesuite.com/docs/truffle/reference/truffle-commands/#call" +
      OS.EOL;
    throw new TruffleError(message);
  }

  let config = loadConfig(options);
  await Environment.detect(config);

  const [contractNameOrAddress, functionNameOrSignature, ...args] = config._;
  let functionEntry, transaction;

  const fromAddress =
    options.from ??
    config.networks[config.network]?.from ??
    Codec.Evm.Utils.ZERO_ADDRESS;
  if (!web3Utils.isAddress(fromAddress)) {
    throw new TruffleError(
      `Error: Address ${fromAddress} is not a valid Ethereum address.` +
        OS.EOL +
        "Please check the address and try again."
    );
  }

  let blockNumber = options.block ?? "latest";
  if (!Number.isNaN(Number(blockNumber))) {
    blockNumber = Number(blockNumber);
  }
  if (
    !(Number.isSafeInteger(blockNumber) && blockNumber >= 0) &&
    !["latest", "pending", "genesis", "earliest"].includes(blockNumber)
  ) {
    throw new TruffleError(
      "Error: Invalid block number.  Block number must be nonnegative integer or one of 'latest', 'pending', 'genesis', or 'earliest'."
    );
  }

  const { encoder, decoder } = config.fetchExternal
    ? await sourceFromExternal(contractNameOrAddress, config)
    : await sourceFromLocal(contractNameOrAddress, config);

  try {
    ({ abi: functionEntry, tx: transaction } = await encoder.encodeTransaction(
      functionNameOrSignature,
      args,
      {
        allowJson: true,
        strictBooleans: true
      }
    ));
  } catch (error) {
    const expectedErrors = [
      Encoder.NoFunctionByThatNameError,
      Codec.Wrap.NoOverloadsMatchedError,
      Codec.Wrap.NoUniqueBestOverloadError,
      Codec.Wrap.TypeMismatchError
    ];
    debug("expectedErrors: %O", expectedErrors);
    if (expectedErrors.some(errorClass => error instanceof errorClass)) {
      //if it was an expected error, turn it into a TruffleError so that it
      //displays nicely
      throw new TruffleError("Error: " + error.message);
    } else {
      //unexpected error, rethrow
      throw error;
    }
  }

  if (!["pure", "view"].includes(functionEntry.stateMutability)) {
    console.log(
      "WARNING: Making read-only call to non-view function." +
        OS.EOL +
        "Any changes this function attempts to make will not be saved to the blockchain."
    );
  }

  const adapter = new Encoder.ProviderAdapter(config.provider);
  let result;
  let status = undefined;

  try {
    result = await adapter.call(
      fromAddress,
      transaction.to,
      transaction.data,
      blockNumber
    );
    //note we don't set status to true... a revert need not cause an
    //error, depending on client
    if (typeof result !== "string") {
      //if we couldn't extract a return value, something's gone badly wrong;
      //let's just throw
      throw new Error("Malformed response from call");
    }
  } catch (error) {
    status = false;
    result = extractResult(error);
    if (result === undefined) {
      //if we couldn't extract a return value, something's gone badly wrong;
      //let's just rethrow the error in that case
      throw error;
    }
  }

  debug("result: %O", result);

  const decodings = await decoder.decodeReturnValue(functionEntry, result, {
    status
  });
  if (decodings.length === 0) {
    throw new TruffleError("Error: Could not decode result.");
  }
  const decoding = decodings[0];

  if (decoding.status) {
    //successful return
    config.logger.log(
      util.inspect(new Codec.Export.ReturndataDecodingInspector(decoding), {
        colors: true,
        depth: null,
        maxArrayLength: null,
        breakLength: 79
      })
    );
  } else {
    //revert case
    if (
      decoding.kind === "revert" &&
      Codec.AbiData.Utils.abiSignature(decoding.abi) === "Panic(uint256)"
    ) {
      // for panics specifically, we'll want a bit more interpretation
      // (shouldn't this be a proper interpretation? yes, but there's no
      // time to refactor that right now)
      const panicCode = decoding.arguments[0].value.value.asBN;
      throw new TruffleError(
        `The call resulted in a panic: ${DebugUtils.panicString(panicCode)}`
      );
    }
    //usual revert case
    throw new TruffleError(
      util.inspect(new Codec.Export.ReturndataDecodingInspector(decoding), {
        colors: false, //don't want colors in an error message
        depth: null,
        maxArrayLength: null,
        breakLength: 79
      })
    );
  }

  return;
  //Note: This is the end of the function.  After this point is just inner
  //function declarations.  These declarations are made as inner functions
  //so they can use the imports above.

  async function sourceFromLocal(contractNameOrAddress, config) {
    if (
      contractNameOrAddress.startsWith("0x") &&
      !web3Utils.isAddress(contractNameOrAddress)
    ) {
      throw new TruffleError(
        `Error: Address ${contractNameOrAddress} is not a valid Ethereum address.` +
          OS.EOL +
          "Please check the address and try again."
      );
    }

    const contractNames = fs
      .readdirSync(config.contracts_build_directory)
      .filter(filename => filename.endsWith(".json"))
      .map(filename => filename.slice(0, -".json".length));

    const contracts = contractNames
      .map(contractName => ({
        [contractName]: config.resolver.require(contractName)
      }))
      .reduce((a, b) => ({ ...a, ...b }), {});

    if (Object.keys(contracts).length === 0) {
      throw new TruffleError(
        "Error: No artifacts found; please run `truffle compile` first to compile your contracts."
      );
    }

    if (contractNameOrAddress.startsWith("0x")) {
      //note in this case we already performed validation above
      const contractAddress = contractNameOrAddress;
      const projectInfo = {
        artifacts: Object.values(contracts)
      };

      return await getEncoderDecoderForContractAddress(
        contractAddress,
        projectInfo
      );
    } else {
      // contract name case
      const contractName = contractNameOrAddress;
      const settings = {
        provider: config.provider,
        projectInfo: {
          artifacts: Object.values(contracts)
        }
      };

      const contract = contracts[contractName];
      if (!contract) {
        throw new TruffleError(
          `Error: No artifacts found for contract named ${contractName} found.  Check the name and make sure you have compiled your contracts.`
        );
      }
      let instance;
      try {
        instance = await contract.deployed();
      } catch (error) {
        throw new TruffleError(
          "Error: This contract has not been deployed to the detected network." +
            OS.EOL +
            "Please run `truffle migrate` to deploy the contract."
        );
      }
      const encoder = await Encoder.forContractInstance(instance, settings);
      const decoder = await Decoder.forContractInstance(instance, settings);
      return { encoder, decoder };
    }
  }

  async function sourceFromExternal(contractAddress, config) {
    if (!web3Utils.isAddress(contractAddress)) {
      throw new TruffleError(
        `Error: Address ${contractAddress} is not a valid Ethereum address.` +
          OS.EOL +
          "Please check the address and try again, or remove `-x` if you are supplying a contract name."
      );
    }

    const { compileResult } = await fetchAndCompile(contractAddress, config);

    const projectInfo = {
      commonCompilations: compileResult.compilations
    };

    return await getEncoderDecoderForContractAddress(
      contractAddress,
      projectInfo
    );
  }

  async function getEncoderDecoderForContractAddress(
    contractAddress,
    projectInfo
  ) {
    const projectEncoder = await Encoder.forProject({
      provider: config.provider,
      projectInfo
    });
    const encoder = await projectEncoder.forAddress(
      contractAddress,
      blockNumber
    );

    const projectDecoder = await Decoder.forProject({
      provider: config.provider,
      projectInfo
    });
    const decoder = await projectDecoder.forAddress(
      contractAddress,
      blockNumber
    );

    return { encoder, decoder };
  }
};

function extractResult(error) {
  //CODE DUPLICATION WARNING!!
  //the following code is copied (w/slight adaptations) from contract/lib/reason.js
  //it should really be factored!!  but there may not be time to do that right now
  if (!error || !error.data) {
    return undefined;
  }

  // NOTE that Ganache >=2 returns the reason string when
  // vmErrorsOnRPCResponse === true, which this code could
  // be updated to respect (instead of computing here)
  const { data } = error;
  if (typeof data === "string") {
    return data; // geth, Ganache >7.0.0
  } else if ("result" in data) {
    // there is a single result (Ganache 7.0.0)
    return data.result;
  } else {
    // handle `evm_mine`, `miner_start`, batch payloads, and ganache 2.0
    // NOTE this only works for a single failed transaction at a time.
    const hash = Object.keys(data)[0];
    const errorDetails = data[hash];
    return errorDetails.return /* ganache 2.0 */;
  }
}