UNPKG

@sentio/truffle-source-fetcher

Version:

Fetches verified source code from services such as Etherscan

341 lines (340 loc) 15.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)("source-fetcher:etherscan"); // untyped import since no @types/web3-utils exists const Web3Utils = require("web3-utils"); const common_1 = require("./common"); const networks_1 = require("./networks"); // must polyfill AbortController to use axios >=0.20.0, <=0.27.2 on node <= v14.x require("./polyfill"); const axios_1 = __importDefault(require("axios")); const async_retry_1 = __importDefault(require("async-retry")); const etherscanCommentHeader = `/** *Submitted for verification at Etherscan.io on 20XX-XX-XX */ `; //note we include that final newline //this looks awkward but the TS docs actually suggest this :P const EtherscanFetcher = (_a = class EtherscanFetcher { static get fetcherName() { return "etherscan"; } get fetcherName() { return EtherscanFetcher.fetcherName; } static forNetworkId(id, options) { return __awaiter(this, void 0, void 0, function* () { debug("options: %O", options); debug("id:", id); return new EtherscanFetcher(id, options ? options.apiKey : ""); }); } constructor(networkId, apiKey = "") { const networkName = networks_1.networkNamesById[networkId]; if (networkName === undefined || !(networkName in EtherscanFetcher.apiDomainsByNetworkName)) { throw new common_1.InvalidNetworkError(networkId, "etherscan"); } this.networkName = networkName; debug("apiKey: %s", apiKey); this.apiKey = apiKey; const baseDelay = this.apiKey ? 200 : 3000; //etherscan permits 5 requests/sec w/a key, 1/3sec w/o const safetyFactor = 1; //no safety factor atm this.delay = baseDelay * safetyFactor; this.ready = (0, common_1.makeTimer)(0); //at start, it's ready to go immediately } static getSupportedNetworks() { return Object.fromEntries(Object.entries(networks_1.networksByName).filter(([name, _]) => name in EtherscanFetcher.apiDomainsByNetworkName)); } fetchSourcesForAddress(address) { return __awaiter(this, void 0, void 0, function* () { const response = yield this.getSuccessfulResponse(address); return EtherscanFetcher.processResult(response.result[0]); }); } getSuccessfulResponse(address) { return __awaiter(this, void 0, void 0, function* () { const initialTimeoutFactor = 1.5; //I guess? return yield (0, async_retry_1.default)(() => __awaiter(this, void 0, void 0, function* () { return yield this.makeRequest(address); }), { retries: 3, minTimeout: this.delay * initialTimeoutFactor }); }); } determineUrl() { const domain = EtherscanFetcher.apiDomainsByNetworkName[this.networkName]; return `https://${domain}/api`; } makeRequest(address) { return __awaiter(this, void 0, void 0, function* () { //not putting a try/catch around this; if it throws, we throw yield this.ready; const responsePromise = axios_1.default.get(this.determineUrl(), { params: { module: "contract", action: "getsourcecode", address, apikey: this.apiKey }, responseType: "json", maxRedirects: 50 }); this.ready = (0, common_1.makeTimer)(this.delay); const response = (yield responsePromise).data; if (response.status === "0") { throw new Error(response.result); } return response; }); } static processResult(result) { // blockscout compat if (result.Address && !result.SourceCode) { return null; } if (result.OptimizationUsed === "false") { result.OptimizationUsed = "0"; } if (result.OptimizationUsed === "true") { result.OptimizationUsed = "1"; } if (result.OptimizationRuns) { result.Runs = result.OptimizationRuns.toString(); } if (!result.Library) { result.Library = ""; } if (result.AdditionalSources) { const sources = { [result.FileName]: { content: result.SourceCode } }; for (const { Filename, SourceCode } of result.AdditionalSources) { sources[Filename] = { content: SourceCode }; } result.SourceCode = JSON.stringify(sources); } //we have 5 cases here. //case 1: the address doesn't exist if (result.SourceCode === "" && result.ABI === "Contract source code not verified") { return null; } //case 2: it's a Vyper contract if (result.CompilerVersion.startsWith("vyper:")) { return this.processVyperResult(result); } let multifileJson; try { //try to parse the source JSON. if it succeeds, //we're in the multi-file case. multifileJson = JSON.parse(result.SourceCode); } catch (_) { //otherwise, we could be single-file or we could be full JSON. //for full JSON input, etherscan will stick an extra pair of braces around it if (result.SourceCode.startsWith("{") && result.SourceCode.endsWith("}")) { const trimmedSource = result.SourceCode.slice(1).slice(0, -1); //remove braces let fullJson; try { fullJson = JSON.parse(trimmedSource); } catch (_) { //if it still doesn't parse, it's single-source I guess? //(note: we shouldn't really end up here?) debug("single-file input??"); return this.processSingleResult(result); } //case 5: full JSON input debug("json input"); return this.processJsonResult(result, fullJson); } //case 3 (the way it should happen): single source debug("single-file input"); return this.processSingleResult(result); } //case 4: multiple sources debug("multi-file input"); return this.processMultiResult(result, multifileJson); } static processSingleResult(result) { const filename = (0, common_1.makeFilename)(result.ContractName); return { contractName: result.ContractName, sources: { //we prepend this header comment so that line numbers in the debugger //will match up with what's displayed on the website; note that other //cases don't display a similar header on the website [filename]: etherscanCommentHeader + result.SourceCode }, options: { language: "Solidity", version: result.CompilerVersion, settings: this.extractSettings(result), specializations: { libraries: this.processLibraries(result.Library), constructorArguments: result.ConstructorArguments } } }; } static processMultiResult(result, sources) { return { contractName: result.ContractName, sources: this.processSources(sources), options: { language: "Solidity", version: result.CompilerVersion, settings: this.extractSettings(result), specializations: { libraries: this.processLibraries(result.Library), constructorArguments: result.ConstructorArguments } } }; } static processJsonResult(result, jsonInput) { return { contractName: result.ContractName, sources: this.processSources(jsonInput.sources), options: { language: jsonInput.language, version: result.CompilerVersion, settings: (0, common_1.removeLibraries)(jsonInput.settings), specializations: { libraries: jsonInput.settings.libraries, constructorArguments: result.ConstructorArguments } } }; } static processVyperResult(result) { const filename = (0, common_1.makeFilename)(result.ContractName, ".vy"); //note: this means filename will always be Vyper_contract.vy return { sources: { [filename]: result.SourceCode }, options: { language: "Vyper", version: result.CompilerVersion.replace(/^vyper:/, ""), settings: this.extractVyperSettings(result), specializations: { constructorArguments: result.ConstructorArguments } } }; } static processSources(sources) { return Object.assign({}, ...Object.entries(sources).map(([path, { content: source }]) => ({ [(0, common_1.makeFilename)(path)]: source }))); } static extractSettings(result) { const evmVersion = result.EVMVersion.toLowerCase() === "default" ? undefined : result.EVMVersion; const optimizer = { enabled: result.OptimizationUsed === "1", runs: parseInt(result.Runs || "200") }; //old version got libraries here, but we don't actually want that! if (evmVersion !== undefined) { return { optimizer, evmVersion }; } else { return { optimizer }; } } static processLibraries(librariesString) { let libraries; if (librariesString === "") { libraries = {}; } else { libraries = Object.assign({}, ...librariesString.split(";").map(pair => { const [name, address] = pair.split(":"); return { [name]: Web3Utils.toChecksumAddress(address) }; })); } return { "": libraries }; //empty string as key means it applies to all contracts } static extractVyperSettings(result) { const evmVersion = result.EVMVersion === "Default" ? undefined : result.EVMVersion; //the optimize flag is not currently supported by etherscan; //any Vyper contract currently verified on etherscan necessarily has //optimize flag left unspecified (and therefore effectively true). //do NOT look at OptimizationUsed for Vyper contracts; it will always //be "0" even though in fact optimization *was* used. just leave //the optimize flag unspecified. if (evmVersion !== undefined) { return { evmVersion }; } else { return {}; } } }, //then, afterwards, start a new timer. _a.apiDomainsByNetworkName = { "mainnet": "api.etherscan.io", "goerli": "api-goerli.etherscan.io", "sepolia": "api-sepolia.etherscan.io", "optimistic": "api-optimistic.etherscan.io", "goerli-optimistic": "api-goerli-optimism.etherscan.io", "arbitrum": "api.arbiscan.io", "nova-arbitrum": "api-nova.arbiscan.io", "goerli-arbitrum": "api-goerli.arbiscan.io", "polygon": "api.polygonscan.com", "mumbai-polygon": "api-mumbai.polygonscan.com", "zkevm-polygon": "api-zkevm.polygonscan.com", "testnet-zkevm-polygon": "api-testnet-zkevm.polygonscan.com", "binance": "api.bscscan.com", "testnet-binance": "api-testnet.bscscan.com", "fantom": "api.ftmscan.com", "testnet-fantom": "api-testnet.ftmscan.com", "avalanche": "api.snowtrace.io", "fuji-avalanche": "api-testnet.snowtrace.io", "heco": "api.hecoinfo.com", "testnet-heco": "api-testnet.hecoinfo.com", "moonbeam": "api-moonbeam.moonscan.io", "moonriver": "api-moonriver.moonscan.io", "moonbase-alpha": "api-moonbase.moonscan.io", "cronos": "api.cronoscan.com", "testnet-cronos": "api-testnet.cronoscan.com", "bttc": "api.bttcscan.com", "donau-bttc": "api-testnet.bttcscan.com", "celo": "api.celoscan.io", "alfajores-celo": "api-alfajores.celoscan.io", "boba": "api.bobascan.com", "goerli-boba": "api-testnet.bobascan.com", "gnosis": "api.gnosisscan.io", //etherscan does *not* support base mainnet? "goerli-base": "api-goerli.basescan.org", "astar": "blockscout.com/astar" }, _a); exports.default = EtherscanFetcher; //# sourceMappingURL=etherscan.js.map