@tenderly/hardhat-integration
Version:
Hardhat plugin for integration with Tenderly
480 lines (477 loc) • 21.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.classFunctions = exports.isTenderlyNetworkConfig = exports.extractCompilerVersion = exports.newCompilerConfig = exports.compareConfigs = exports.resolveDependencies = exports.getContracts = exports.getCompilerDataFromContracts = exports.getCompilationJob = exports.makeVerifyContractsRequest = void 0;
exports.getChainId = getChainId;
exports.isTenderlyGatewayNetworkConfig = isTenderlyGatewayNetworkConfig;
exports.isHttpNetworkConfig = isHttpNetworkConfig;
const plugins_1 = require("hardhat/plugins");
const constants_1 = require("@tenderly/api-client/common/constants");
const task_names_1 = require("hardhat/builtin-tasks/task-names");
const constants_2 = require("../constants");
const errors_1 = require("../tenderly/errors");
const logger_1 = require("./logger");
const errors_2 = require("../errors");
const makeVerifyContractsRequest = async (hre, flatContracts, platformID) => {
logger_1.logger.info("Processing data needed for verification.");
const contracts = [];
for (const flatContract of flatContracts) {
logger_1.logger.info("Processing contract:", flatContract.name);
let job;
try {
job = await (0, exports.getCompilationJob)(hre, flatContract.name);
}
catch (err) {
// TODO(dusan): See how to wrap errors, don't return errors like this
logger_1.logger.error(`Error while trying to get compilation job for contract '${flatContract.name}'. The provided contract name probably doesn't exist or is mistyped.`);
throw err;
}
const networkName = hre.hardhatArguments.network;
if (networkName === undefined) {
logger_1.logger.error(`Error in ${constants_2.PLUGIN_NAME}: Please provide a network via the hardhat --network argument or directly in the contract`);
return null;
}
logger_1.logger.trace("Found network is:", networkName);
let chainId;
if ((0, exports.isTenderlyNetworkConfig)(hre.config.networks[networkName]) &&
platformID !== undefined) {
chainId = platformID;
}
else {
chainId = (await getChainId(hre)).toString();
}
logger_1.logger.trace(`ChainId for network '${networkName}' is ${chainId}`);
const compiler = await insertLibraries(hre, job.getSolcConfig(), flatContract.libraries);
contracts.push({
contractToVerify: flatContract.name,
// TODO(dusan) this can be done with TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT hardhat task
sources: await extractSources(hre, flatContract.name, job),
compiler: await repackLibraries(compiler),
networks: {
[chainId]: {
address: flatContract.address,
links: flatContract.libraries,
},
},
});
}
// TODO(dusan) see about merging compilation jobs?
return {
contracts,
};
};
exports.makeVerifyContractsRequest = makeVerifyContractsRequest;
async function getChainId(hre) {
let chainId;
const networkName = hre.network.name;
if (hre.network?.config?.chainId !== undefined) {
chainId = hre.network.config.chainId.toString();
}
else if (constants_1.NETWORK_NAME_CHAIN_ID_MAP[networkName.toLowerCase()] !== undefined) {
chainId = constants_1.NETWORK_NAME_CHAIN_ID_MAP[networkName.toLowerCase()].toString();
}
if (!chainId) {
throw new errors_2.UndefinedChainIdError(networkName);
}
return parseInt(chainId);
}
async function extractSources(hre, contractToVerify, job) {
const sources = {};
logger_1.logger.info("Extracting sources from compilation job.");
const mainArtifact = hre.artifacts.readArtifactSync(contractToVerify);
for (const file of job.getResolvedFiles()) {
let contractName = constants_2.CONTRACT_NAME_PLACEHOLDER;
// Only the contract to verify should have its name extracted since we need to attach a network to it.
// Other names aren't important since they are only needed for compilation purposes.
if (mainArtifact.sourceName === file.sourceName) {
contractName = mainArtifact.contractName;
}
sources[file.sourceName] = {
name: contractName,
code: file.content.rawContent,
};
}
return sources;
}
async function insertLibraries(hre, originalCompiler, libraries) {
// we need to copy the compiler in order to not modify the hardhat's compiler from the settings
const copiedCompiler = {
version: originalCompiler.version,
settings: {
...originalCompiler.settings,
},
};
if (libraries === undefined || libraries === null) {
return copiedCompiler;
}
if (copiedCompiler.settings.libraries !== undefined &&
copiedCompiler.settings.libraries !== null) {
throw new Error(`There are multiple definitions of libraries the contract should use. One is defined in the verify request and the other as an compiler config override. Please remove one of them.`);
}
copiedCompiler.settings.libraries = {};
for (const [libName, libAddress] of Object.entries(libraries)) {
const libArtifact = hre.artifacts.readArtifactSync(libName);
if (copiedCompiler.settings.libraries[libArtifact.sourceName] === undefined) {
copiedCompiler.settings.libraries[libArtifact.sourceName] = {};
}
copiedCompiler.settings.libraries[libArtifact.sourceName][libName] =
libAddress;
}
return copiedCompiler;
}
/*
The only difference between SolcConfig and TenderlySolcConfig is in the settings.libraries object.
SolcConfig.settings.libraries is in the format of:
compiler.settings.libraries = {
"contracts/path/Token.sol": {
"contracts/library-path/Library.sol": "0x...."
}
}
TenderlySolcConfig.settings.libraries is in the format of:
compiler.settings.libraries = {
"contracts/path/Token.sol": {
addresses: {
"contracts/library-path/Library.sol": "0x...."
}
}
}
The reason for this are the definition limitations of proto messages.
Proto doesn't allow for a map to have a map as a value like map<string, map<string, string>>.
So we have to wrap the inner map in an object like map<string, Libraries> where Libraries is a message with a map<string, string> field.
*/
async function repackLibraries(compiler) {
if (!compiler?.settings?.libraries) {
return compiler;
}
const libraries = {};
for (const [fileName, libVal] of Object.entries(compiler.settings.libraries)) {
if (libraries[fileName] === undefined) {
libraries[fileName] = { addresses: {} };
}
for (const [libName, libAddress] of Object.entries(libVal)) {
libraries[fileName].addresses[libName] = libAddress;
}
}
compiler.settings.libraries = libraries;
return compiler;
}
const getCompilationJob = async (hre, contractName) => {
logger_1.logger.trace("Getting compilation job for contract:", contractName);
const dependencyGraph = await getDependencyGraph(hre);
const artifact = hre.artifacts.readArtifactSync(contractName);
const file = dependencyGraph.getResolvedFiles().find((resolvedFile) => {
return resolvedFile.sourceName === artifact.sourceName;
});
return hre.run(task_names_1.TASK_COMPILE_SOLIDITY_GET_COMPILATION_JOB_FOR_FILE, {
dependencyGraph,
file,
});
};
exports.getCompilationJob = getCompilationJob;
async function getDependencyGraph(hre) {
const sourcePaths = await hre.run(task_names_1.TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS);
const sourceNames = await hre.run(task_names_1.TASK_COMPILE_SOLIDITY_GET_SOURCE_NAMES, {
sourcePaths,
});
return hre.run(task_names_1.TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH, {
sourceNames,
});
}
const getCompilerDataFromContracts = (contracts, flatContracts, hhConfig) => {
logger_1.logger.debug("Obtaining compiler data from contracts.");
let contract;
let mainContract;
let config;
for (contract of contracts) {
for (mainContract of flatContracts) {
if (mainContract.name !== contract.contractName) {
continue;
}
logger_1.logger.trace("Obtaining compiler data from contract:", mainContract.name);
const contractConfig = (0, exports.newCompilerConfig)(hhConfig, contract.sourcePath, contract.compiler?.version);
if (config !== null &&
config !== undefined &&
!(0, exports.compareConfigs)(contractConfig, config)) {
logger_1.logger.error(`Error in ${constants_2.PLUGIN_NAME}: Different compiler versions provided in same request`);
throw new Error("Compiler version mismatch");
}
else {
config = contractConfig;
}
}
}
logger_1.logger.debug("Compiler data has been obtained.");
logger_1.logger.silly("Obtained compiler configuration is:", config);
return config;
};
exports.getCompilerDataFromContracts = getCompilerDataFromContracts;
const getContracts = async (hre, flatContracts) => {
logger_1.logger.debug("Processing contracts from the artifacts/ directory.");
const sourcePaths = await hre.run("compile:solidity:get-source-paths");
const sourceNames = await hre.run("compile:solidity:get-source-names", {
sourcePaths,
});
const data = await hre.run("compile:solidity:get-dependency-graph", {
sourceNames,
});
if (data.length === 0) {
throw new plugins_1.HardhatPluginError(constants_2.PLUGIN_NAME, errors_1.CONTRACTS_NOT_DETECTED);
}
let contract;
const requestContracts = [];
const metadata = {
defaultCompiler: {
version: (0, exports.extractCompilerVersion)(hre.config),
},
sources: {},
};
logger_1.logger.trace("Extracted compiler version is:", metadata.defaultCompiler.version);
data._resolvedFiles.forEach((resolvedFile, _) => {
const sourcePath = resolvedFile.sourceName;
logger_1.logger.trace("Processing file:", sourcePath);
const name = sourcePath.split("/").slice(-1)[0].split(".")[0];
logger_1.logger.trace("Obtained name from source file:", name);
for (contract of flatContracts) {
if (contract.name !== name) {
continue;
}
logger_1.logger.trace("Found contract:", contract.name);
metadata.sources[sourcePath] = {
content: resolvedFile.content.rawContent,
versionPragma: resolvedFile.content.versionPragmas[0],
};
if (metadata.sources[sourcePath].content === undefined ||
metadata.sources[sourcePath].content === null ||
metadata.sources[sourcePath].content === "") {
logger_1.logger.error("Metadata source content is empty!");
}
if (metadata.sources[sourcePath].versionPragma === undefined ||
metadata.sources[sourcePath].versionPragma === null ||
metadata.sources[sourcePath].versionPragma === "") {
logger_1.logger.error("Metadata source version pragma is empty!");
}
const visited = {};
(0, exports.resolveDependencies)(data, sourcePath, metadata, visited);
}
});
for (const [key, value] of Object.entries(metadata.sources)) {
const name = key.split("/").slice(-1)[0].split(".")[0];
const contractToPush = {
contractName: name,
source: value.content,
sourcePath: key,
networks: {},
compiler: {
name: "solc",
version: (0, exports.extractCompilerVersion)(hre.config, key, value.versionPragma),
},
};
requestContracts.push(contractToPush);
}
logger_1.logger.silly("Finished processing contracts from the artifacts/ folder:", requestContracts);
logger_1.logger.silly("Finished processing contracts from the artifacts/ folder:", requestContracts);
return requestContracts;
};
exports.getContracts = getContracts;
const resolveDependencies = (dependencyData, sourcePath, metadata, visited) => {
if (visited[sourcePath]) {
return;
}
visited[sourcePath] = true;
dependencyData._dependenciesPerFile
.get(sourcePath)
.forEach((resolvedDependency, _) => {
(0, exports.resolveDependencies)(dependencyData, resolvedDependency.sourceName, metadata, visited);
metadata.sources[resolvedDependency.sourceName] = {
content: resolvedDependency.content.rawContent,
versionPragma: resolvedDependency.content.versionPragmas[0],
};
});
};
exports.resolveDependencies = resolveDependencies;
const compareConfigs = (originalConfig, newConfig) => {
if (originalConfig.compiler_version !== newConfig.compiler_version) {
return false;
}
if (originalConfig.optimizations_used !== newConfig.optimizations_used) {
return false;
}
if (originalConfig.optimizations_count !== newConfig.optimizations_count) {
return false;
}
if (originalConfig.evm_version !== newConfig.evm_version) {
return false;
}
return true;
};
exports.compareConfigs = compareConfigs;
const newCompilerConfig = (config, sourcePath, contractCompiler) => {
if (sourcePath !== undefined &&
config.solidity.overrides[sourcePath] !== undefined) {
logger_1.logger.trace("There is a compiler config override for:", sourcePath);
return {
compiler_version: config.solidity.overrides[sourcePath].version,
optimizations_used: config.solidity.overrides[sourcePath].settings.optimizer.enabled,
optimizations_count: config.solidity.overrides[sourcePath].settings.optimizer.runs,
evm_version: config.solidity.overrides[sourcePath].settings.evmVersion,
debug: config.solidity.overrides[sourcePath].settings.debug,
};
}
if (contractCompiler !== undefined) {
logger_1.logger.trace("There is a provided compiler configuration, determining it.");
return determineCompilerConfig(config.solidity.compilers, contractCompiler);
}
logger_1.logger.trace("Returning the first compiler in the configuration");
return {
compiler_version: config.solidity.compilers[0].version,
optimizations_used: config.solidity.compilers[0].settings.optimizer.enabled,
optimizations_count: config.solidity.compilers[0].settings.optimizer.runs,
evm_version: config.solidity.compilers[0].settings.evmVersion,
debug: config.solidity.compilers[0].settings.debug,
};
};
exports.newCompilerConfig = newCompilerConfig;
const extractCompilerVersion = (config, sourcePath, versionPragma) => {
if (sourcePath !== undefined &&
config.solidity.overrides[sourcePath] !== undefined) {
return config.solidity.overrides[sourcePath].version;
}
if (versionPragma !== undefined) {
for (const compiler of config.solidity.compilers) {
if (compareVersions(compiler.version, versionPragma)) {
return compiler.version;
}
}
}
return config.solidity.compilers[0].version;
};
exports.extractCompilerVersion = extractCompilerVersion;
// isTenderlyNetworkConfig checks if a network belongs to tenderly by checking the rpc_url.
// This is done so the user can put custom network names in the hardhat config and still use them with tenderly.
// This is also done so the user can use multiple tenderly networks in their hardhat config file.
const isTenderlyNetworkConfig = (nw) => {
if (nw === undefined || nw === null) {
return false;
}
if (!isHttpNetworkConfig(nw)) {
return false;
}
// The network belongs to tenderly if the rpc_url is one of the following:
// - https://rpc.vnet.tenderly.co/devnet/...
// - https://<network_name>.rpc.tenderly.co/...
// - https://virtual.<network_name>.rpc.tenderly.co/...
// - https://rpc.tenderly.co/...
const regex = /^https?:\/\/(?:rpc\.vnet\.tenderly\.co\/devnet\/|(?:[\w-]+\.rpc|rpc)\.tenderly\.co\/|virtual\.[\w-]+\.rpc\.tenderly\.co\/).*$/;
return regex.test(nw.url);
};
exports.isTenderlyNetworkConfig = isTenderlyNetworkConfig;
function isTenderlyGatewayNetworkConfig(nw) {
if (nw === undefined || nw === null) {
return false;
}
if (!isHttpNetworkConfig(nw)) {
return false;
}
const regex = /^https?:\/\/[\w-]+\.gateway\.tenderly\.co\/.*$/;
return regex.test(nw.url);
}
function isHttpNetworkConfig(config) {
return config.url !== undefined;
}
const determineCompilerConfig = (compilers, contractCompiler) => {
for (const compiler of compilers) {
if (compareVersions(compiler.version, contractCompiler)) {
logger_1.logger.trace("Provided compiler matched the version: ", compiler.version);
return {
compiler_version: compiler.version,
optimizations_used: compiler.settings.optimizer.enabled,
optimizations_count: compiler.settings.optimizer.runs,
evm_version: compiler.settings.evmVersion,
debug: compiler.settings.debug,
};
}
}
logger_1.logger.trace("Couldn't find the provided compiler among compilers in the configuration, returning the configuration of the first one");
return {
compiler_version: compilers[0].version,
optimizations_used: compilers[0].settings.optimizer.enabled,
optimizations_count: compilers[0].settings.optimizer.runs,
evm_version: compilers[0].settings.evmVersion,
debug: compilers[0].settings.debug,
};
};
const compareVersions = (compilerVersion, contractVersionPragma) => {
switch (contractVersionPragma[0]) {
case "^":
return checkGTEVersion(compilerVersion, contractVersionPragma.slice(1));
case ">":
if (contractVersionPragma.length === 6) {
return checkGTVersion(compilerVersion, contractVersionPragma.slice(1));
}
if (contractVersionPragma.length === 7) {
return checkGTEVersion(compilerVersion, contractVersionPragma.slice(2));
}
if (contractVersionPragma.length > 8) {
const [gt, lt] = contractVersionPragma.split(" ");
let isGT = false;
let isLT = false;
if (gt.length === 6) {
isGT = checkGTVersion(compilerVersion, gt.slice(1));
}
if (gt.length === 7) {
isGT = checkGTEVersion(compilerVersion, gt.slice(2));
}
if (lt.length === 6) {
isLT = !checkGTEVersion(compilerVersion, lt.slice(1));
}
if (lt.length === 7) {
isLT = !checkGTVersion(compilerVersion, lt.slice(2));
}
return isGT && isLT;
}
break;
default:
return checkGTEVersion(compilerVersion, contractVersionPragma);
}
return false;
};
const checkGTEVersion = (compilerVersion, contractVersionPragma) => {
const compilerVersionSplit = compilerVersion.split(".");
const contractVersionSplit = contractVersionPragma.split(".");
for (let i = 0; i < compilerVersionSplit.length; i++) {
if (compilerVersionSplit[i] > contractVersionSplit[i]) {
break;
}
if (compilerVersionSplit[i] < contractVersionSplit[i]) {
return false;
}
}
return true;
};
const checkGTVersion = (compilerVersion, contractVersionPragma) => {
const compilerVersionSplit = compilerVersion.split(".");
const contractVersionSplit = contractVersionPragma.split(".");
for (let i = 0; i < compilerVersionSplit.length; i++) {
if (compilerVersionSplit[i] > contractVersionSplit[i]) {
break;
}
if (compilerVersionSplit[i] <= contractVersionSplit[i]) {
return false;
}
}
return true;
};
// ------------------------------------------------------------------------------------
const isGetter = (x, name) => (Object.getOwnPropertyDescriptor(x, name) !== null || {}).get;
const isFunction = (x, name) => typeof x[name] === "function";
const deepFunctions = (x) => {
if (x && x !== Object.prototype) {
return Object.getOwnPropertyNames(x)
.filter((name) => isGetter(x, name) !== null || isFunction(x, name))
.concat(deepFunctions(Object.getPrototypeOf(x)) ?? []);
}
return [];
};
const distinctDeepFunctions = (x) => Array.from(new Set(deepFunctions(x)));
const classFunctions = (x) => distinctDeepFunctions(x).filter((name) => name !== "constructor" && name.indexOf("__") === -1);
exports.classFunctions = classFunctions;
//# sourceMappingURL=util.js.map