trufflesuite/truffle

View on GitHub
packages/fetch-and-compile/lib/fetch.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import debugModule from "debug";
const debug = debugModule("fetch-and-compile:fetch");
import semver from "semver";
import Fetchers from "@truffle/source-fetcher";
import {
  InvalidNetworkError,
  FetcherConstructor,
  Fetcher,
  SourceInfo
} from "@truffle/source-fetcher";
import Config from "@truffle/config";
const { Compile } = require("@truffle/compile-solidity"); //sorry for untyped import!
import type { Recognizer, FailureType, FetchAndCompileOptions } from "./types";
import type { WorkflowCompileResult } from "@truffle/compile-common";
import {
  normalizeFetchAndCompileOptions,
  normalizeFetcherNames
} from "./utils";

export async function fetchAndCompileForRecognizer(
  recognizer: Recognizer,
  options: FetchAndCompileOptions | Config
): Promise<void> {
  const normalizedOptions = normalizeFetchAndCompileOptions(options);
  const fetcherConstructors: FetcherConstructor[] =
    getSortedFetcherConstructors(normalizeFetcherNames(normalizedOptions));
  const fetchers = await getFetchers(
    fetcherConstructors,
    normalizedOptions,
    recognizer
  );
  //now: the main loop!
  let address: string | undefined;
  while ((address = recognizer.getAnUnrecognizedAddress()) !== undefined) {
    await tryFetchAndCompileAddress(
      address,
      fetchers,
      recognizer,
      normalizedOptions
    );
  }
}

//sort/filter fetchers by user's order, if given; otherwise use default order
export function getSortedFetcherConstructors(
  userFetcherNames?: string[]
): FetcherConstructor[] {
  let sortedFetchers: FetcherConstructor[] = [];
  if (userFetcherNames) {
    for (let name of userFetcherNames) {
      let Fetcher = Fetchers.find(Fetcher => Fetcher.fetcherName === name);
      if (Fetcher) {
        sortedFetchers.push(Fetcher);
      } else {
        throw new Error(`Unknown external source service ${name}.`);
      }
    }
  } else {
    sortedFetchers = Fetchers;
  }
  return sortedFetchers;
}

async function getFetchers(
  fetcherConstructors: FetcherConstructor[],
  options: FetchAndCompileOptions,
  recognizer: Recognizer
): Promise<Fetcher[]> {
  const networkId: number = options.network.networkId;
  //make fetcher instances. we'll filter out ones that don't support this
  //network (and note ones that yielded errors)
  return (
    await Promise.all(
      fetcherConstructors.map(async Fetcher => {
        try {
          return await Fetcher.forNetworkId(
            networkId,
            ((options.fetch || {}).fetcherOptions || {})[Fetcher.fetcherName]
          );
        } catch (error) {
          if (!(error instanceof InvalidNetworkError)) {
            //if it's *not* just an invalid network, log the error.
            recognizer.markBadFetcher(Fetcher.fetcherName);
          }
          //either way, filter this fetcher out
          return null;
        }
      })
    )
  ).filter((fetcher): fetcher is Fetcher => fetcher !== null);
}

async function tryFetchAndCompileAddress(
  address: string,
  fetchers: Fetcher[],
  recognizer: Recognizer,
  fetchAndCompileOptions: FetchAndCompileOptions
): Promise<void> {
  let found: boolean = false;
  let failureReason: FailureType | undefined; //undefined if no failure
  let failureError: Error | undefined;
  //(this includes if no source is found)
  for (const fetcher of fetchers) {
    //now comes all the hard parts!
    //get our sources
    let result: SourceInfo | null;
    try {
      debug("getting sources for %s via %s", address, fetcher.fetcherName);
      result = await fetcher.fetchSourcesForAddress(address);
    } catch (error) {
      debug("error in getting sources! %o", error);
      failureReason = "fetch";
      failureError = error;
      continue;
    }
    if (result === null) {
      debug("no sources found");
      //null means they don't have that address
      continue;
    }
    //if we do have it, extract sources & options
    debug("got sources!");
    const { sources, options } = result; //not same options as above, sorry for name confusion
    if (options.language === "Vyper") {
      //if it's not Solidity, bail out now
      debug("found Vyper, bailing out!");
      recognizer.markUnrecognizable(address, "language");
      //break out of the fetcher loop, since *no* fetcher will work here
      break;
    }
    //set up the config
    let externalConfig: Config = Config.default().with({
      compilers: {
        solc: options
      }
    });
    //if using docker, transform it (this does nothing if not using docker)
    externalConfig = transformIfUsingDocker(
      externalConfig,
      fetchAndCompileOptions
    );
    //compile the sources
    let compileResult: WorkflowCompileResult;
    try {
      compileResult = await Compile.sources({
        options: externalConfig.with({ quiet: true }),
        sources
      });
    } catch (error) {
      debug("compile error: %O", error);
      failureReason = "compile";
      failureError = error;
      continue; //try again with a different fetcher, I guess?
    }
    //add it!
    await recognizer.addCompiledInfo(
      {
        compileResult,
        sourceInfo: result,
        fetchedVia: fetcher.fetcherName
      },
      address
    );
    failureReason = undefined; //mark as *not* failed in case a previous fetcher failed
    failureError = undefined;
    //check: did this actually help?
    debug("checking result");
    if (!recognizer.isAddressUnrecognized(address)) {
      debug(
        "address %s successfully recognized via %s",
        address,
        fetcher.fetcherName
      );
      found = true;
      //break out of the fetcher loop -- we got what we want
      break;
    }
    debug("address %s still unrecognized", address);
  }
  if (found === false) {
    //if we couldn't find it, add it to the list of addresses to skip
    recognizer.markUnrecognizable(address, failureReason, failureError);
  }
}

function transformIfUsingDocker(
  externalConfig: Config,
  fetchAndCompileOptions: FetchAndCompileOptions
): Config {
  const useDocker = Boolean((fetchAndCompileOptions.compile || {}).docker);
  if (!useDocker) {
    //if they're not using docker, no need to transform anything :)
    return externalConfig;
  }
  const givenVersion: string = externalConfig.compilers.solc.version;
  //if they are, we have to ask: are they using a nightly?
  if (semver.prerelease(givenVersion)) {
    //we're not going to attempt to make Docker work with nightlies.
    //just keep Docker turned off.
    return externalConfig;
  }
  //otherwise, turn on Docker, and reduce the version to its simple form.
  const simpleVersion: string | null = semver.valid(givenVersion);
  if (simpleVersion === null) {
    //this should never happen
    throw new Error("Fetched source has unparseable compiler version");
  }
  return externalConfig.merge({
    compilers: {
      solc: {
        version: simpleVersion,
        docker: true
      }
    }
  });
}