@sentio/truffle-source-fetcher
Version:
Fetches verified source code from services such as Etherscan
341 lines (340 loc) • 15.1 kB
JavaScript
"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