diff --git a/.changeset/plenty-lemons-leave.md b/.changeset/plenty-lemons-leave.md new file mode 100644 index 0000000000..58671e009c --- /dev/null +++ b/.changeset/plenty-lemons-leave.md @@ -0,0 +1,5 @@ +--- +"@wagmi/cli": major +--- + +Fixed Foundry plugin to properly handle multiple addresses for the same ABI (e.g., different ERC20 tokens). diff --git a/packages/cli/src/plugins/foundry.test.ts b/packages/cli/src/plugins/foundry.test.ts index 5ff95c0b39..47fec2b789 100644 --- a/packages/cli/src/plugins/foundry.test.ts +++ b/packages/cli/src/plugins/foundry.test.ts @@ -98,6 +98,88 @@ test('contracts', () => { `) }) +test('contracts with multiple addresses for same ABI', () => { + expect( + foundry({ + project: resolve(__dirname, '__fixtures__/foundry/'), + exclude: ['Counter.sol/**'], + deployments: { + Foo: { + Token1: '0x1234567890123456789012345678901234567890', + Token2: '0x2345678901234567890123456789012345678901', + }, + }, + }).contracts?.(), + ).resolves.toMatchInlineSnapshot(` + [ + { + "abi": [ + { + "inputs": [], + "name": "bar", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "string", + "name": "baz", + "type": "string", + }, + ], + "name": "setFoo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + ], + "address": "0x1234567890123456789012345678901234567890", + "name": "Foo_Token1", + }, + { + "abi": [ + { + "inputs": [], + "name": "bar", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "string", + "name": "baz", + "type": "string", + }, + ], + "name": "setFoo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + ], + "address": "0x2345678901234567890123456789012345678901", + "name": "Foo_Token2", + }, + ] + `) +}) + test('contracts without project', async () => { const dir = resolve(__dirname, '__fixtures__/foundry/') const spy = vi.spyOn(process, 'cwd') @@ -151,3 +233,71 @@ test('contracts without project', async () => { ] `) }) + +test('watch handlers with multiple address deployments', async () => { + const dir = resolve(__dirname, '__fixtures__/foundry/') + const plugin = foundry({ + project: dir, + deployments: { + Foo: { + Token1: '0x1234567890123456789012345678901234567890', + Token2: '0x2345678901234567890123456789012345678901', + }, + }, + }) + + const path = resolve(dir, 'out/Foo.sol/Foo.json') + + if ( + !plugin.watch?.onAdd || + !plugin.watch?.onChange || + !plugin.watch?.onRemove + ) { + throw new Error('Watch handlers not properly configured') + } + + // Test onAdd handler + const addResult = await plugin.watch.onAdd(path) + expect(addResult).toMatchInlineSnapshot(` + { + "abi": [ + { + "inputs": [], + "name": "bar", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "string", + "name": "baz", + "type": "string", + }, + ], + "name": "setFoo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + ], + "address": "0x1234567890123456789012345678901234567890", + "name": "Foo_Token1", + } + `) + + // Test onChange handler + const changeResult = await plugin.watch.onChange(path) + expect(changeResult).toEqual(addResult) + + // Test onRemove handler + const removeResult = await plugin.watch.onRemove(path) + expect(removeResult).toBe('Foo') +}) diff --git a/packages/cli/src/plugins/foundry.ts b/packages/cli/src/plugins/foundry.ts index f95c8deed7..5d13106529 100644 --- a/packages/cli/src/plugins/foundry.ts +++ b/packages/cli/src/plugins/foundry.ts @@ -52,8 +52,17 @@ export type FoundryConfig = { * @default foundry.config#out | 'out' */ artifacts?: string | undefined - /** Mapping of addresses to attach to artifacts. */ - deployments?: { [key: string]: ContractConfig['address'] } | undefined + /** + * Mapping of addresses to attach to artifacts. + * Can be either a single address or a chain-id mapped address + */ + deployments?: + | { + [contractName: string]: + | ContractConfig['address'] + | Record + } + | undefined /** Artifact files to exclude. */ exclude?: string[] | undefined /** [Forge](https://book.getfoundry.sh/forge) configuration */ @@ -124,13 +133,51 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { return `${usePrefix ? namePrefix : ''}${filename.replace(extension, '')}` } - async function getContract(artifactPath: string) { + async function getContract( + artifactPath: string, + ): Promise { const artifact = await fs.readJSON(artifactPath) + const baseName = getContractName(artifactPath, false) + const deployment = deployments[baseName] + + // Check if ABI exists and is an array + if (!artifact.abi || !Array.isArray(artifact.abi)) { + return { + abi: [], + address: deployment as ContractConfig['address'], + name: getContractName(artifactPath), + } + } + + // Sort ABI to ensure consistent order + const sortedAbi = [...artifact.abi].sort((a, b) => { + if (a.type !== b.type) return a.type.localeCompare(b.type) + if ('name' in a && 'name' in b) return a.name.localeCompare(b.name) + return 0 + }) + + // Handle case where deployment is a record of multiple addresses + if ( + deployment && + typeof deployment === 'object' && + !('address' in deployment) + ) { + // Create separate contracts for each deployment address + const contracts: ContractConfig[] = [] + for (const [key, address] of Object.entries(deployment)) { + contracts.push({ + abi: sortedAbi, + address: address as ContractConfig['address'], + name: `${baseName}_${key}`, + }) + } + return contracts + } + + // Handle single address case return { - abi: artifact.abi, - address: (deployments as Record)[ - getContractName(artifactPath, false) - ], + abi: sortedAbi, + address: deployment as ContractConfig['address'], name: getContractName(artifactPath), } } @@ -179,9 +226,18 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { const artifactPaths = await getArtifactPaths(artifactsDirectory) const contracts = [] for (const artifactPath of artifactPaths) { - const contract = await getContract(artifactPath) - if (!contract.abi?.length) continue - contracts.push(contract) + const result = await getContract(artifactPath) + if (Array.isArray(result)) { + // Handle multiple contracts case + for (const contract of result) { + if (!contract.abi?.length) continue + contracts.push(contract) + } + } else { + // Handle single contract case + if (!result.abi?.length) continue + contracts.push(result) + } } return contracts }, @@ -231,13 +287,15 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { ...include.map((x) => `${artifactsDirectory}/**/${x}`), ...exclude.map((x) => `!${artifactsDirectory}/**/${x}`), ], - async onAdd(path) { - return getContract(path) + async onAdd(path): Promise { + const result = await getContract(path) + return Array.isArray(result) ? result[0] : result }, - async onChange(path) { - return getContract(path) + async onChange(path): Promise { + const result = await getContract(path) + return Array.isArray(result) ? result[0] : result }, - async onRemove(path) { + async onRemove(path): Promise { return getContractName(path) }, },