UNPKG

hardhat-diamond-abi

Version:

Hardhat plugin to combine multiple ABIs into a Diamond ABI artifact.

293 lines (243 loc) 9.23 kB
import * as path from "path"; import { createHash } from "crypto"; import debug from "debug"; import { utils } from "ethers"; import { TASK_COMPILE_SOLIDITY_COMPILE_JOBS } from "hardhat/builtin-tasks/task-names"; import { extendConfig, subtask } from "hardhat/config"; import { HardhatPluginError } from "hardhat/plugins"; import type { HardhatConfig, HardhatRuntimeEnvironment, HardhatUserConfig, RunSuperFunction, TaskArguments, } from "hardhat/types"; // TODO: Avoid Hardhat internals import { CompilationJob } from "hardhat/internal/solidity/compilation-job"; // TODO: Avoid Hardhat internals import { ResolvedFile } from "hardhat/internal/solidity/resolver"; import * as pkg from "../package.json"; export const PLUGIN_NAME = pkg.name; export const PLUGIN_VERSION = pkg.version; // An empty contract file is provided in the plugin otherwise Hardhat will eject us from the cache export const CONTRACT_PATH = path.join(__dirname, "contract.sol"); const CONTRACT_NAME = "HardhatDiamondABI.sol"; const { Fragment, FormatTypes } = utils; const log = debug(PLUGIN_NAME); // TODO: Export from Hardhat internals because this type isn't exposed by them currently type ArtifactsEmittedPerFile = Array<{ file: ResolvedFile; artifactsEmitted: string[]; }>; // TODO: Export from Hardhat internals because this type isn't exposed by them currently type ArtifactsEmittedPerJob = Array<{ compilationJob: CompilationJob; artifactsEmittedPerFile: ArtifactsEmittedPerFile; }>; function createArtifact(artifactName: string, abi: unknown[]) { return { _format: "hh-sol-artifact-1", contractName: artifactName, sourceName: `${PLUGIN_NAME}/${CONTRACT_NAME}`, abi: abi, deployedBytecode: "", bytecode: "", linkReferences: {}, deployedLinkReferences: {}, } as const; } // This is our custom CompilationJob with information about the Diamond ABI class DiamondAbiCompilationJob extends CompilationJob { private pluginName = PLUGIN_NAME; private pluginVersion = PLUGIN_VERSION; private _file: ResolvedFile; private artifacts: ReturnType<typeof createArtifact>[] = []; constructor() { // Dummy solidity version that can never be valid super({ version: "X.X.X", settings: {} }); const sourceName = `${this.pluginName}/${CONTRACT_NAME}`; const absolutePath = CONTRACT_PATH; const content = { rawContent: "", imports: [], versionPragmas: [] }; // Dummy a content hash with the plugin name & version const contentHash = createHash("md5").update(`${this.pluginName}_${this.pluginVersion}`).digest("hex"); const lastModificationDate = new Date(); this._file = new ResolvedFile( sourceName, absolutePath, content, contentHash, lastModificationDate, this.pluginName, this.pluginVersion ); } emitsArtifacts() { return true; } hasSolc9573Bug() { return false; } getResolvedFiles() { return [this._file]; } getFile() { return this._file; } addArtifact(artifact: ReturnType<typeof createArtifact>) { this.artifacts.push(artifact); } getArtifactsEmitted() { return this.artifacts.map((artifact) => artifact.contractName); } } interface DiamondAbiUserConfig { name: string; // We can't accept RegExp until https://github.com/nomiclabs/hardhat/issues/2181 include?: string[]; exclude?: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any filter?: (abiElement: any, index: number, abi: any[], fullyQualifiedName: string) => boolean; strict?: boolean; } // Add our types to the Hardhat config declare module "hardhat/types/config" { interface HardhatUserConfig { diamondAbi?: DiamondAbiUserConfig | DiamondAbiUserConfig[]; } interface HardhatConfig { diamondAbi: { name: string; // We can't accept RegExp until https://github.com/nomiclabs/hardhat/issues/2181 include: string[]; exclude: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any filter?: (abiElement: any, index: number, abi: any[], fullyQualifiedName: string) => boolean; strict: boolean; }[]; } } extendConfig((config: HardhatConfig, userConfig: Readonly<HardhatUserConfig>) => { config.diamondAbi = [userConfig.diamondAbi].flat().map(function (userConfig) { const { name, include = [], exclude = [], filter, strict = true } = userConfig ?? {}; if (!name) { throw new HardhatPluginError(PLUGIN_NAME, "`name` config is required."); } if (typeof name !== "string") { throw new HardhatPluginError(PLUGIN_NAME, "`name` config must be a string."); } if (include && !Array.isArray(include)) { throw new HardhatPluginError(PLUGIN_NAME, "`include` config must be an array if provided."); } if (exclude && !Array.isArray(exclude)) { throw new HardhatPluginError(PLUGIN_NAME, "`exclude` config must be an array if provided."); } if (filter && typeof filter !== "function") { throw new HardhatPluginError(PLUGIN_NAME, "`filter` config must be a function if provided."); } if (typeof strict !== "boolean") { throw new HardhatPluginError(PLUGIN_NAME, "`strict` config must be a boolean if provided."); } return { name, include, exclude, filter, strict, }; }); }); // We ONLY hook this task, instead of providing a separate task to run, because // Hardhat will clear out old artifacts on next run if we don't work around their // caching mechanisms. subtask(TASK_COMPILE_SOLIDITY_COMPILE_JOBS).setAction(generateDiamondAbi); export async function generateDiamondAbi( args: TaskArguments, hre: HardhatRuntimeEnvironment, runSuper: RunSuperFunction<TaskArguments> ): Promise<{ artifactsEmittedPerJob: ArtifactsEmittedPerJob }> { const out: { artifactsEmittedPerJob: ArtifactsEmittedPerJob } = await runSuper(args); if (out.artifactsEmittedPerJob.length === 0) { return out; } const compilationJob = new DiamondAbiCompilationJob(); const contracts = await hre.artifacts.getAllFullyQualifiedNames(); for (const config of hre.config.diamondAbi) { const mergedAbis = []; for (const contractName of contracts) { // We can't accept a RegExp until https://github.com/nomiclabs/hardhat/issues/2181 if (config.include && config.include.length && !config.include.some((m) => contractName.match(m))) { log(`Skipping ${contractName} because it didn't match any \`include\` patterns.`); continue; } // We can't accept a RegExp until https://github.com/nomiclabs/hardhat/issues/2181 if (config.exclude && config.exclude.length && config.exclude.some((m) => contractName.match(m))) { log(`Skipping ${contractName} because it did matched an \`exclude\` pattern.`); continue; } // this should be the output filename, but this will work too if (contractName.startsWith(PLUGIN_NAME)) { log(`Skipping ${contractName} because it is the stub ABI produced by ${PLUGIN_NAME}.`); continue; } // debug(including contractName in Name ABI) log(`Including ${contractName} in your ${config.name} ABI.`); const { abi } = await hre.artifacts.readArtifact(contractName); mergedAbis.push( ...abi.filter((abiElement, index, abi) => { if (abiElement.type === "constructor") { return false; } if (abiElement.type === "fallback") { return false; } if (abiElement.type === "receive") { return false; } if (typeof config.filter === "function") { return config.filter(abiElement, index, abi, contractName); } return true; }) ); } if (config.strict) { // Validate the ABI if `strict` option is `true` // Consumers may opt to validate their Diamond doesn't contain duplicate // functions before a deployment. There isn't a great way to determine // this before a deployment, but `diamondCut` will fail if you try to cut // multiple functions (thus failing a deploy). const diamondAbiSet = new Set(); mergedAbis.forEach((abi) => { const sighash = Fragment.fromObject(abi).format(FormatTypes.sighash); if (diamondAbiSet.has(sighash)) { throw new HardhatPluginError( PLUGIN_NAME, `Failed to create ${config.name} ABI - \`${sighash}\` appears twice.` ); } diamondAbiSet.add(sighash); }); } const artifact = createArtifact(config.name, mergedAbis); // Save into the Hardhat cache so artifact utilities can load it await hre.artifacts.saveArtifactAndDebugFile(artifact); compilationJob.addArtifact(artifact); } const file = compilationJob.getFile(); const artifactsEmitted = compilationJob.getArtifactsEmitted(); return { artifactsEmittedPerJob: [ ...out.artifactsEmittedPerJob, // Add as another job to the list { compilationJob, artifactsEmittedPerFile: [ { file, artifactsEmitted, }, ], }, ], }; }