From a5c31d07dcf0de34d8d14da031d73468666068ee Mon Sep 17 00:00:00 2001 From: Silas Davis Date: Mon, 2 Jun 2025 13:41:16 +0200 Subject: [PATCH] feat: Soften duplicate contract name in the case contracts have identical ABIs Foundry seems to duplicate some imported sub-files within a contract, for example: `/src/parts/primitives/Foo.sol` Is defined only once, and imported from `/src/Bar.sol` But ends up appearing twice in artifacts directory: ``` /out/Foo.sol/Foo.json /out/primitives/Foo.sol/Foo.json ``` In this case `getArtifactPaths()` in the Foundry plugin pick up the same contracts twice since they end up existing in multiple subdirectories of `out/`. I don't think there is a reliable to heuristic to de-duplicate them at this level. It seems like duck-typing contracts by the ABI should be fine and may help other plugin sources. it could always be disabled by default and enabled via an option at the cost of yet more config Signed-off-by: Silas Davis --- packages/cli/src/commands/generate.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index 992a2e2923..41fdfefc4f 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -6,7 +6,7 @@ import { watch } from 'chokidar' import { default as dedent } from 'dedent' import { basename, dirname, resolve } from 'pathe' import pc from 'picocolors' -import { type Abi, type Address, getAddress } from 'viem' +import { type Abi, type Address, getAddress, keccak256 } from 'viem' import { z } from 'zod' import type { Contract, ContractConfig, Plugin, Watch } from '../config.js' @@ -98,10 +98,19 @@ export async function generate(options: Generate = {}) { const contractNames = new Set() const contractMap = new Map() for (const contractConfig of contractConfigs) { - if (contractNames.has(contractConfig.name)) + const previouslySeenContract = contractMap.get(contractConfig.name) + if (previouslySeenContract) { + if ( + hashAbi(previouslySeenContract.abi) === hashAbi(contractConfig.abi) + ) { + // If the contract name and ABI match, skip adding it again, but allow it since this can occur when generating + // from sources that mutually import each other, such as peer Foundry projects. + continue + } throw new Error( - `Contract name "${contractConfig.name}" must be unique.`, + `Duplicate contract name "${contractConfig.name}" found with different ABI. Contract names must be unique up to ABI.`, ) + } const contract = await getContract({ ...contractConfig, isTypeScript }) contractMap.set(contract.name, contract) @@ -407,3 +416,9 @@ function getBannerContent({ name }: { name: string }) { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ` } + +function hashAbi(abi: Abi): string { + // Could use something faster, e.g. non-cryptographic hash or CRC32, but only called in the case + // we hit duplicate contract names, so probably not worth it. + return keccak256(Buffer.from(JSON.stringify(abi))) +}