From 7219ea22b8d94832e9bfc9f1860c641906121496 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:05:07 +0000 Subject: [PATCH 1/6] feat(interfaces): add interface IDs directly to generated factory classes - Replace generateInterfaceIds.js with addInterfaceIds.ts utility - Add interface IDs as static properties on Typechain factory classes - Export utility functions via src/utils.ts for programmatic use - Add ts-node dependency to run TypeScript build scripts - Remove separate interfaceIds.ts constants file --- packages/interfaces/package.json | 13 +- packages/interfaces/scripts/build.sh | 9 +- .../scripts/generateInterfaceIds.js | 179 ----------------- .../scripts/utils/addInterfaceIds.ts | 181 ++++++++++++++++++ packages/interfaces/src/index.ts | 1 - packages/interfaces/src/interfaceIds.ts | 22 --- packages/interfaces/src/utils.ts | 1 + pnpm-lock.yaml | 53 +---- 8 files changed, 210 insertions(+), 249 deletions(-) delete mode 100644 packages/interfaces/scripts/generateInterfaceIds.js create mode 100644 packages/interfaces/scripts/utils/addInterfaceIds.ts delete mode 100644 packages/interfaces/src/interfaceIds.ts create mode 100644 packages/interfaces/src/utils.ts diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index 5d6333567..9a7d2bca0 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -12,6 +12,10 @@ "types": "./dist/src/index.d.ts", "default": "./dist/src/index.js" }, + "./types": { + "types": "./dist/types/index.d.ts", + "default": "./dist/types/index.js" + }, "./types-v5": { "types": "./dist/types-v5/index.d.ts", "default": "./dist/types-v5/index.js" @@ -19,6 +23,10 @@ "./wagmi": { "types": "./dist/wagmi/generated.d.ts", "default": "./dist/wagmi/generated.js" + }, + "./utils": { + "types": "./dist/src/utils.d.ts", + "default": "./dist/src/utils.js" } }, "files": [ @@ -47,6 +55,8 @@ "prepublishOnly": "pnpm run build" }, "devDependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/providers": "5.7.2", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@openzeppelin/contracts": "3.4.2", "@openzeppelin/contracts-upgradeable": "3.4.2", @@ -54,13 +64,12 @@ "@wagmi/cli": "^2.3.1", "ethers": "catalog:", "ethers-v5": "npm:ethers@5.7.2", - "@ethersproject/abi": "5.7.0", - "@ethersproject/providers": "5.7.2", "hardhat": "catalog:", "markdownlint-cli": "catalog:", "prettier": "catalog:", "prettier-plugin-solidity": "catalog:", "solhint": "catalog:", + "ts-node": "catalog:", "typechain": "^8.3.2", "viem": "^2.31.7" } diff --git a/packages/interfaces/scripts/build.sh b/packages/interfaces/scripts/build.sh index 009328dd9..32359f8ca 100755 --- a/packages/interfaces/scripts/build.sh +++ b/packages/interfaces/scripts/build.sh @@ -41,8 +41,13 @@ find_files() { echo "📦 Compiling contracts with Hardhat..." pnpm hardhat compile -# Step 1.5: Generate interface IDs -node scripts/generateInterfaceIds.js +# Step 1.5: Add interface IDs to generated factory files (only if needed) +missing_ids=$(grep -rL "static readonly interfaceId" types/factories --include="*__factory.ts" 2>/dev/null | wc -l) + +if [[ $missing_ids -gt 0 ]]; then + # Slow operation, only run if needed + npx ts-node scripts/utils/addInterfaceIds.ts types/factories +fi # Step 2: Generate types (only if needed) echo "🏗️ Checking type definitions..." diff --git a/packages/interfaces/scripts/generateInterfaceIds.js b/packages/interfaces/scripts/generateInterfaceIds.js deleted file mode 100644 index 806b63a7b..000000000 --- a/packages/interfaces/scripts/generateInterfaceIds.js +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env node - -/** - * Generate interface ID constants from compiled contract artifacts - * - * This script calculates ERC-165 interface IDs by: - * 1. Reading ABIs from compiled Hardhat artifacts - * 2. Extracting function signatures from the ABI - * 3. Computing function selectors (first 4 bytes of keccak256) - * 4. XORing all selectors together to get the interface ID - * - * No contract deployment needed - works directly with JSON artifacts. - */ - -const fs = require('fs') -const path = require('path') -const { utils } = require('ethers-v5') - -/** - * Calculate function selector (first 4 bytes of keccak256 hash) - * @param {string} signature - Function signature like "transfer(address,uint256)" - * @returns {string} Function selector as hex string (e.g., "0xa9059cbb") - */ -function functionSelector(signature) { - const hash = utils.keccak256(utils.toUtf8Bytes(signature)) - return '0x' + hash.slice(2, 10) // Take first 4 bytes (8 hex chars after 0x) -} - -/** - * Calculate ERC-165 interface ID from function signatures - * @param {string[]} signatures - Array of function signatures - * @returns {string} Interface ID as hex string (e.g., "0x01ffc9a7") - */ -function calculateInterfaceId(signatures) { - if (signatures.length === 0) { - return '0x00000000' - } - - // XOR all function selectors together - let interfaceId = 0n - for (const sig of signatures) { - interfaceId ^= BigInt(functionSelector(sig)) - } - - return '0x' + interfaceId.toString(16).padStart(8, '0') -} - -/** - * Extract function signatures from a compiled Hardhat artifact - * @param {string} artifactPath - Path to the artifact JSON file - * @param {string} interfaceName - Name of the interface (for logging) - * @returns {string} Interface ID - */ -function extractInterfaceIdFromArtifact(artifactPath, interfaceName) { - const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf-8')) - const abi = artifact.abi - - // Extract function signatures (only functions, not events or errors) - const signatures = abi - .filter((item) => item.type === 'function') - .map((func) => { - const inputs = func.inputs.map((input) => input.type).join(',') - return `${func.name}(${inputs})` - }) - .sort() // Sort for consistency - - if (signatures.length === 0) { - console.warn(`Warning: ${interfaceName} has no functions`) - return '0x00000000' - } - - return calculateInterfaceId(signatures) -} - -/** - * Configuration: Interface names mapped to their artifact paths - * Path is relative to the package root (packages/interfaces/) - * - * To add a new interface: - * 1. Ensure the contract is compiled (run `pnpm hardhat compile`) - * 2. Add an entry: InterfaceName: 'artifacts/path/to/Interface.sol/InterfaceName.json' - * 3. Run this script - */ -const INTERFACES = { - IERC165: 'artifacts/@openzeppelin/contracts/introspection/IERC165.sol/IERC165.json', - IRewardsManager: 'artifacts/contracts/contracts/rewards/IRewardsManager.sol/IRewardsManager.json', -} - -async function main() { - const outputFile = path.join(__dirname, '..', 'src', 'interfaceIds.ts') - const packageRoot = path.join(__dirname, '..') - - // Check if regeneration is needed by comparing artifact modification times - let needsRegeneration = !fs.existsSync(outputFile) - - if (!needsRegeneration) { - const outputStat = fs.statSync(outputFile) - for (const artifactPath of Object.values(INTERFACES)) { - const fullPath = path.join(packageRoot, artifactPath) - if (fs.existsSync(fullPath)) { - const artifactStat = fs.statSync(fullPath) - if (artifactStat.mtime > outputStat.mtime) { - needsRegeneration = true - break - } - } - } - } - - if (!needsRegeneration) { - // Output is up to date - return - } - - // Extract interface IDs - const results = {} - let errorCount = 0 - - for (const [interfaceName, artifactPath] of Object.entries(INTERFACES)) { - const fullPath = path.join(packageRoot, artifactPath) - - if (!fs.existsSync(fullPath)) { - console.error(`Error: Artifact not found for ${interfaceName} - run 'pnpm hardhat compile' first`) - errorCount++ - continue - } - - try { - results[interfaceName] = extractInterfaceIdFromArtifact(fullPath, interfaceName) - } catch { - console.error(`Error: Failed to extract interface ID for ${interfaceName}`) - errorCount++ - } - } - - if (Object.keys(results).length === 0) { - console.error('Error: No interface IDs were successfully extracted') - process.exit(1) - } - - // Generate TypeScript content - const content = `/** - * Auto-generated interface IDs from Solidity compilation - * - * DO NOT EDIT THIS FILE MANUALLY! - * - * This file is automatically generated by running: - * node scripts/generateInterfaceIds.js - * - * To add a new interface ID: - * 1. Add the interface to the INTERFACES object in scripts/generateInterfaceIds.js - * 2. Ensure contracts are compiled: pnpm hardhat compile - * 3. Run the generation script above - */ - -export const INTERFACE_IDS = { -${Object.entries(results) - .map(([name, id]) => ` ${name}: '${id}',`) - .join('\n')} -} as const - -// Individual exports for convenience -${Object.entries(results) - .map(([name]) => `export const ${name} = INTERFACE_IDS.${name}`) - .join('\n')} -` - - // Write to output file - fs.mkdirSync(path.dirname(outputFile), { recursive: true }) - fs.writeFileSync(outputFile, content) - - const status = errorCount > 0 ? ` (${errorCount} error${errorCount > 1 ? 's' : ''})` : '' - console.log(`Generated ${Object.keys(results).length} interface ID(s)${status}`) -} - -main().catch((error) => { - console.error(error) - process.exit(1) -}) diff --git a/packages/interfaces/scripts/utils/addInterfaceIds.ts b/packages/interfaces/scripts/utils/addInterfaceIds.ts new file mode 100644 index 000000000..f66a245c0 --- /dev/null +++ b/packages/interfaces/scripts/utils/addInterfaceIds.ts @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Post-process Typechain-generated factory files to add interface metadata + * + * This utility adds ERC-165 interface IDs and interface names to Typechain-generated + * factory classes as static readonly properties. + * + * @example + * ```typescript + * import { addInterfaceIds } from './utils/addInterfaceIds' + * addInterfaceIds('./types/factories') + * ``` + */ + +import { ethers } from 'ethers' +import * as fs from 'fs' +import * as path from 'path' + +interface ProcessStats { + processed: number + skipped: number + total: number +} + +interface AbiItem { + type: string + name?: string + inputs?: Array<{ type: string; name?: string }> + [key: string]: unknown +} + +/** + * Calculate ERC-165 interface ID from contract ABI + * @param abi - Contract ABI array + * @returns Interface ID as hex string (e.g., "0x12345678") + */ +export function calculateInterfaceId(abi: AbiItem[]): string | null { + try { + // Filter to only functions (not events, errors, etc.) + const functions = abi.filter((item) => item.type === 'function') + + if (functions.length === 0) return '0x00000000' + + // XOR all function selectors together (ERC-165 standard) + let interfaceId = BigInt(0) + for (const func of functions) { + // Build full function signature: name(type1,type2,...) + const inputs = func.inputs?.map((input) => input.type).join(',') ?? '' + const signature = `${func.name}(${inputs})` + + // Calculate selector (first 4 bytes of keccak256) + const hash = ethers.id(signature) + const selector = hash.slice(0, 10) // '0x' + 8 hex chars + + interfaceId ^= BigInt(selector) + } + + return '0x' + interfaceId.toString(16).padStart(8, '0') + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(`Error calculating interface ID: ${message}`) + return null + } +} + +/** + * Add interface metadata to a single factory file + * @param factoryPath - Absolute path to the factory file + * @returns True if metadata was added, false if skipped + */ +export function addInterfaceIdToFactory(factoryPath: string): boolean { + try { + let content = fs.readFileSync(factoryPath, 'utf-8') + + // Check if already has interface metadata + if (content.includes('static readonly interfaceId') && content.includes('static readonly interfaceName')) { + return false + } + + // Extract ABI from the file + const abiMatch = content.match(/const _abi = (\[[\s\S]*?\]) as const;/) + if (!abiMatch) { + return false + } + + // Parse ABI - handle TypeScript syntax (trailing commas, unquoted keys, etc.) + const abiString = abiMatch[1] + .replace(/,(\s*[\]}])/g, '$1') // Remove trailing commas + .replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g, '$1"$2"$3') // Quote keys + + const abi = JSON.parse(abiString) + + // Calculate interface ID + const interfaceId = calculateInterfaceId(abi) + if (!interfaceId) { + return false + } + + // Extract interface name from filename (e.g., "IPausableControl__factory.ts" -> "IPausableControl") + const fileName = path.basename(factoryPath) + const interfaceName = fileName.replace(/__factory\.ts$/, '') + + // Add interface metadata as static properties after the ABI + const interfaceMetadata = ` // The following properties are automatically generated during the build process\n static readonly interfaceId = "${interfaceId}" as const;\n static readonly interfaceName = "${interfaceName}" as const;\n` + + // Insert after "static readonly abi" + content = content.replace(/(static readonly abi = _abi;)\n/, `$1\n${interfaceMetadata}`) + + // Write back to file + fs.writeFileSync(factoryPath, content) + + return true + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(`Error processing ${path.basename(factoryPath)}: ${message}`) + return false + } +} + +/** + * Recursively process all factory files in a directory + * @param dir - Directory path to process + * @returns Statistics about processing + */ +function processDirectory(dir: string): ProcessStats { + const stats: ProcessStats = { processed: 0, skipped: 0, total: 0 } + + if (!fs.existsSync(dir)) { + console.warn(`Directory does not exist: ${dir}`) + return stats + } + + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + const subStats = processDirectory(fullPath) + stats.processed += subStats.processed + stats.skipped += subStats.skipped + stats.total += subStats.total + } else if (entry.name.endsWith('__factory.ts')) { + stats.total++ + const added = addInterfaceIdToFactory(fullPath) + if (added) { + stats.processed++ + } else { + stats.skipped++ + } + } + } + + return stats +} + +/** + * Add interface IDs to all Typechain-generated factory files in a directory + * @param factoriesDir - Path to the factories directory + */ +export function addInterfaceIds(factoriesDir: string): void { + const stats = processDirectory(factoriesDir) + + if (stats.total === 0) { + console.log('🔢 Factory files interface IDs: none found') + } else if (stats.processed === 0) { + console.log('🔢 Factory files interface IDs: up to date') + } else { + console.log(`🔢 Factory files interface IDs: generated for ${stats.processed} files`) + } +} + +// CLI entry point +if (require.main === module) { + const factoriesDir = process.argv[2] + if (!factoriesDir) { + console.error('Usage: addInterfaceIds.ts ') + process.exit(1) + } + addInterfaceIds(factoriesDir) +} diff --git a/packages/interfaces/src/index.ts b/packages/interfaces/src/index.ts index cf196aabc..77065a38c 100644 --- a/packages/interfaces/src/index.ts +++ b/packages/interfaces/src/index.ts @@ -2,7 +2,6 @@ import { ContractRunner, Interface } from 'ethers' import { factories } from '../types' -export * from './interfaceIds' export * from './types/horizon' export * from './types/subgraph-service' diff --git a/packages/interfaces/src/interfaceIds.ts b/packages/interfaces/src/interfaceIds.ts deleted file mode 100644 index 1fdadbb89..000000000 --- a/packages/interfaces/src/interfaceIds.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Auto-generated interface IDs from Solidity compilation - * - * DO NOT EDIT THIS FILE MANUALLY! - * - * This file is automatically generated by running: - * node scripts/generateInterfaceIds.js - * - * To add a new interface ID: - * 1. Add the interface to the INTERFACES object in scripts/generateInterfaceIds.js - * 2. Ensure contracts are compiled: pnpm hardhat compile - * 3. Run the generation script above - */ - -export const INTERFACE_IDS = { - IERC165: '0x01ffc9a7', - IRewardsManager: '0x0d63a8cd', -} as const - -// Individual exports for convenience -export const IERC165 = INTERFACE_IDS.IERC165 -export const IRewardsManager = INTERFACE_IDS.IRewardsManager diff --git a/packages/interfaces/src/utils.ts b/packages/interfaces/src/utils.ts new file mode 100644 index 000000000..445dbb99a --- /dev/null +++ b/packages/interfaces/src/utils.ts @@ -0,0 +1 @@ +export * from '../scripts/utils/addInterfaceIds' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a65386ab3..32418f07f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ catalogs: solhint: specifier: ^6.0.1 version: 6.0.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -150,7 +153,7 @@ importers: version: 9.1.7 lint-staged: specifier: 'catalog:' - version: 16.2.4 + version: 16.2.3 markdownlint-cli: specifier: 'catalog:' version: 0.45.0 @@ -168,7 +171,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: 'catalog:' - version: 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) yaml-lint: specifier: 'catalog:' version: 1.7.0 @@ -931,6 +934,9 @@ importers: solhint: specifier: 'catalog:' version: 6.0.1(typescript@5.9.3) + ts-node: + specifier: 'catalog:' + version: 10.9.2(@types/node@20.19.21)(typescript@5.9.3) typechain: specifier: ^8.3.2 version: 8.3.2(patch_hash=b34ed6afcf99760666fdc85ecb2094fdd20ce509f947eb09cef21665a2a6a1d6)(typescript@5.9.3) @@ -8262,11 +8268,6 @@ packages: engines: {node: '>=20.17'} hasBin: true - lint-staged@16.2.4: - resolution: {integrity: sha512-Pkyr/wd90oAyXk98i/2KwfkIhoYQUMtss769FIT9hFM5ogYZwrk+GRE46yKXSg2ZGhcJ1p38Gf5gmI5Ohjg2yg==} - engines: {node: '>=20.17'} - hasBin: true - listr2@4.0.5: resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} engines: {node: '>=12'} @@ -8952,10 +8953,6 @@ packages: resolution: {integrity: sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==} engines: {node: '>=20.17'} - nano-spawn@2.0.0: - resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} - engines: {node: '>=20.17'} - nanomatch@1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -11229,13 +11226,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript-eslint@8.46.1: - resolution: {integrity: sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -16963,8 +16953,8 @@ snapshots: '@typescript-eslint/project-service@8.45.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) - '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/tsconfig-utils': 8.46.1(typescript@5.9.3) + '@typescript-eslint/types': 8.46.1 debug: 4.4.3(supports-color@9.4.0) typescript: 5.9.3 transitivePeerDependencies: @@ -22744,16 +22734,6 @@ snapshots: string-argv: 0.3.2 yaml: 2.8.1 - lint-staged@16.2.4: - dependencies: - commander: 14.0.1 - listr2: 9.0.4 - micromatch: 4.0.8 - nano-spawn: 2.0.0 - pidtree: 0.6.0 - string-argv: 0.3.2 - yaml: 2.8.1 - listr2@4.0.5(enquirer@2.4.1): dependencies: cli-truncate: 2.1.0 @@ -23752,8 +23732,6 @@ snapshots: nano-spawn@1.0.3: {} - nano-spawn@2.0.0: {} - nanomatch@1.2.13: dependencies: arr-diff: 4.0.0 @@ -26493,17 +26471,6 @@ snapshots: transitivePeerDependencies: - supports-color - typescript-eslint@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.46.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.37.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - typescript@5.9.3: {} typewise-core@1.2.0: {} From 6115ac96a326026bd1dbf5284b6beb06d619465a Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:36:00 +0000 Subject: [PATCH 2/6] refactor(interfaces): improve addInterfaceIds code quality Address Copilot review feedback for PR 1240: - Replace magic numbers with named constants for better readability - Replace fragile regex-based JSON parsing with Function constructor for more robust ABI parsing that handles complex TypeScript syntax --- .../scripts/utils/addInterfaceIds.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/interfaces/scripts/utils/addInterfaceIds.ts b/packages/interfaces/scripts/utils/addInterfaceIds.ts index f66a245c0..bbb7956fa 100644 --- a/packages/interfaces/scripts/utils/addInterfaceIds.ts +++ b/packages/interfaces/scripts/utils/addInterfaceIds.ts @@ -16,6 +16,11 @@ import { ethers } from 'ethers' import * as fs from 'fs' import * as path from 'path' +// Constants for ERC-165 interface ID calculation +const EMPTY_INTERFACE_ID = '0x00000000' +const SELECTOR_LENGTH_WITH_PREFIX = 10 // '0x' + 8 hex characters +const INTERFACE_ID_LENGTH = 8 // 8 hex characters (4 bytes) + interface ProcessStats { processed: number skipped: number @@ -39,7 +44,7 @@ export function calculateInterfaceId(abi: AbiItem[]): string | null { // Filter to only functions (not events, errors, etc.) const functions = abi.filter((item) => item.type === 'function') - if (functions.length === 0) return '0x00000000' + if (functions.length === 0) return EMPTY_INTERFACE_ID // XOR all function selectors together (ERC-165 standard) let interfaceId = BigInt(0) @@ -50,12 +55,12 @@ export function calculateInterfaceId(abi: AbiItem[]): string | null { // Calculate selector (first 4 bytes of keccak256) const hash = ethers.id(signature) - const selector = hash.slice(0, 10) // '0x' + 8 hex chars + const selector = hash.slice(0, SELECTOR_LENGTH_WITH_PREFIX) interfaceId ^= BigInt(selector) } - return '0x' + interfaceId.toString(16).padStart(8, '0') + return '0x' + interfaceId.toString(16).padStart(INTERFACE_ID_LENGTH, '0') } catch (error) { const message = error instanceof Error ? error.message : String(error) console.error(`Error calculating interface ID: ${message}`) @@ -83,12 +88,10 @@ export function addInterfaceIdToFactory(factoryPath: string): boolean { return false } - // Parse ABI - handle TypeScript syntax (trailing commas, unquoted keys, etc.) - const abiString = abiMatch[1] - .replace(/,(\s*[\]}])/g, '$1') // Remove trailing commas - .replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g, '$1"$2"$3') // Quote keys - - const abi = JSON.parse(abiString) + // Parse ABI using eval in a safe context + // The ABI is extracted from generated TypeScript code we control (Typechain output), + // so this is safe. We wrap it in a function to isolate the scope. + const abi = new Function(`return ${abiMatch[1]}`)() as AbiItem[] // Calculate interface ID const interfaceId = calculateInterfaceId(abi) From 3a1c02fb331cf968bc6d6ef421b37e178df29949 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:47:06 +0000 Subject: [PATCH 3/6] refactor(interfaces): address additional code review feedback Add validation and improve code safety: - Add explicit justification comments for Function constructor usage - Add validation to ensure regex replacement succeeds before writing file - Fix ESLint warning by using const instead of let for content variable - Return false and log warning if metadata injection pattern not found --- .../scripts/utils/addInterfaceIds.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/interfaces/scripts/utils/addInterfaceIds.ts b/packages/interfaces/scripts/utils/addInterfaceIds.ts index bbb7956fa..0975f6333 100644 --- a/packages/interfaces/scripts/utils/addInterfaceIds.ts +++ b/packages/interfaces/scripts/utils/addInterfaceIds.ts @@ -75,7 +75,7 @@ export function calculateInterfaceId(abi: AbiItem[]): string | null { */ export function addInterfaceIdToFactory(factoryPath: string): boolean { try { - let content = fs.readFileSync(factoryPath, 'utf-8') + const content = fs.readFileSync(factoryPath, 'utf-8') // Check if already has interface metadata if (content.includes('static readonly interfaceId') && content.includes('static readonly interfaceName')) { @@ -88,9 +88,12 @@ export function addInterfaceIdToFactory(factoryPath: string): boolean { return false } - // Parse ABI using eval in a safe context - // The ABI is extracted from generated TypeScript code we control (Typechain output), - // so this is safe. We wrap it in a function to isolate the scope. + // Parse ABI from Typechain-generated code + // We use Function constructor here because: + // 1. The source is Typechain-generated code (not user input) + // 2. TypeScript syntax (trailing commas, unquoted keys) makes JSON.parse unsuitable + // 3. A full AST parser would be overkill for this build-time utility + // This is safe as it only runs during build on controlled, generated code. const abi = new Function(`return ${abiMatch[1]}`)() as AbiItem[] // Calculate interface ID @@ -107,10 +110,19 @@ export function addInterfaceIdToFactory(factoryPath: string): boolean { const interfaceMetadata = ` // The following properties are automatically generated during the build process\n static readonly interfaceId = "${interfaceId}" as const;\n static readonly interfaceName = "${interfaceName}" as const;\n` // Insert after "static readonly abi" - content = content.replace(/(static readonly abi = _abi;)\n/, `$1\n${interfaceMetadata}`) + const replacementPattern = /(static readonly abi = _abi;)\n/ + const newContent = content.replace(replacementPattern, `$1\n${interfaceMetadata}`) + + // Validate that replacement succeeded + if (newContent === content) { + console.warn( + `Warning: Failed to inject interface metadata into ${path.basename(factoryPath)} - pattern not found`, + ) + return false + } // Write back to file - fs.writeFileSync(factoryPath, content) + fs.writeFileSync(factoryPath, newContent) return true } catch (error) { From 03a63fd7e7b692057db015da90a16518cc77a5ed Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:51:15 +0000 Subject: [PATCH 4/6] fix(horizon): fix failing testSlash_RoundDown_TokensThawing_Delegation fuzz test The test was failing when fuzzer generated inputs that left less than MIN_DELEGATION shares after undelegating but more than 0. Added constraint to ensure after undelegation either: - All tokens are undelegated (remaining = 0), which is valid, OR - At least MIN_DELEGATION remains, which is the minimum required The root cause: The contract correctly enforces that delegators must either fully exit (0 shares) or maintain at least MIN_DELEGATION shares. The test was missing this constraint, allowing the fuzzer to generate invalid intermediate amounts like 0.95e18 shares remaining. Fixes the intermittent CI failure on fuzzing seed 0xd5262092. --- packages/horizon/test/unit/staking/slash/slash.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/horizon/test/unit/staking/slash/slash.t.sol b/packages/horizon/test/unit/staking/slash/slash.t.sol index e5c365d67..64cd53af5 100644 --- a/packages/horizon/test/unit/staking/slash/slash.t.sol +++ b/packages/horizon/test/unit/staking/slash/slash.t.sol @@ -172,6 +172,8 @@ contract HorizonStakingSlashTest is HorizonStakingTest { vm.assume(delegationTokensToSlash <= delegationTokens); vm.assume(delegationTokensToUndelegate <= delegationTokens); vm.assume(delegationTokensToUndelegate > 0); + uint256 remaining = delegationTokens - delegationTokensToUndelegate; + vm.assume(remaining == 0 || MIN_DELEGATION <= remaining); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); From 2185f885c0ad3424a16f2d1ec589511f080db8a9 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:24:16 +0000 Subject: [PATCH 5/6] refactor(interfaces): improve error handling and indentation matching - Extract indentation from existing code instead of hardcoding 2 spaces - Track and report processing failures separately from skipped files - Return null from addInterfaceIdToFactory on errors instead of false - Throw error if any files failed processing to fail the build - Change warning to error for pattern match failures --- .../scripts/utils/addInterfaceIds.ts | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/interfaces/scripts/utils/addInterfaceIds.ts b/packages/interfaces/scripts/utils/addInterfaceIds.ts index 0975f6333..e7251bf91 100644 --- a/packages/interfaces/scripts/utils/addInterfaceIds.ts +++ b/packages/interfaces/scripts/utils/addInterfaceIds.ts @@ -24,6 +24,7 @@ const INTERFACE_ID_LENGTH = 8 // 8 hex characters (4 bytes) interface ProcessStats { processed: number skipped: number + failed: number total: number } @@ -71,9 +72,9 @@ export function calculateInterfaceId(abi: AbiItem[]): string | null { /** * Add interface metadata to a single factory file * @param factoryPath - Absolute path to the factory file - * @returns True if metadata was added, false if skipped + * @returns True if metadata was added, false if skipped, null if failed */ -export function addInterfaceIdToFactory(factoryPath: string): boolean { +export function addInterfaceIdToFactory(factoryPath: string): boolean | null { try { const content = fs.readFileSync(factoryPath, 'utf-8') @@ -106,8 +107,12 @@ export function addInterfaceIdToFactory(factoryPath: string): boolean { const fileName = path.basename(factoryPath) const interfaceName = fileName.replace(/__factory\.ts$/, '') + // Extract indentation from existing code to match file style + const indentMatch = content.match(/^(\s*)static readonly abi/m) + const indent = indentMatch ? indentMatch[1] : ' ' + // Add interface metadata as static properties after the ABI - const interfaceMetadata = ` // The following properties are automatically generated during the build process\n static readonly interfaceId = "${interfaceId}" as const;\n static readonly interfaceName = "${interfaceName}" as const;\n` + const interfaceMetadata = `${indent}static readonly interfaceId = "${interfaceId}" as const;\n${indent}static readonly interfaceName = "${interfaceName}" as const;\n` // Insert after "static readonly abi" const replacementPattern = /(static readonly abi = _abi;)\n/ @@ -115,10 +120,8 @@ export function addInterfaceIdToFactory(factoryPath: string): boolean { // Validate that replacement succeeded if (newContent === content) { - console.warn( - `Warning: Failed to inject interface metadata into ${path.basename(factoryPath)} - pattern not found`, - ) - return false + console.error(`Failed to inject interface metadata into ${path.basename(factoryPath)} - pattern not found`) + return null } // Write back to file @@ -128,7 +131,7 @@ export function addInterfaceIdToFactory(factoryPath: string): boolean { } catch (error) { const message = error instanceof Error ? error.message : String(error) console.error(`Error processing ${path.basename(factoryPath)}: ${message}`) - return false + return null } } @@ -138,7 +141,7 @@ export function addInterfaceIdToFactory(factoryPath: string): boolean { * @returns Statistics about processing */ function processDirectory(dir: string): ProcessStats { - const stats: ProcessStats = { processed: 0, skipped: 0, total: 0 } + const stats: ProcessStats = { processed: 0, skipped: 0, failed: 0, total: 0 } if (!fs.existsSync(dir)) { console.warn(`Directory does not exist: ${dir}`) @@ -154,14 +157,17 @@ function processDirectory(dir: string): ProcessStats { const subStats = processDirectory(fullPath) stats.processed += subStats.processed stats.skipped += subStats.skipped + stats.failed += subStats.failed stats.total += subStats.total } else if (entry.name.endsWith('__factory.ts')) { stats.total++ - const added = addInterfaceIdToFactory(fullPath) - if (added) { + const result = addInterfaceIdToFactory(fullPath) + if (result === true) { stats.processed++ - } else { + } else if (result === false) { stats.skipped++ + } else { + stats.failed++ } } } @@ -183,6 +189,10 @@ export function addInterfaceIds(factoriesDir: string): void { } else { console.log(`🔢 Factory files interface IDs: generated for ${stats.processed} files`) } + + if (stats.failed > 0) { + throw new Error(`Failed to process ${stats.failed} factory file(s)`) + } } // CLI entry point From b3913dad56d620d9ff0a7f2c67f41ba9d6a51925 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:50:12 +0000 Subject: [PATCH 6/6] chore: switching to version of fix already on other branch --- packages/horizon/test/unit/staking/slash/slash.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/horizon/test/unit/staking/slash/slash.t.sol b/packages/horizon/test/unit/staking/slash/slash.t.sol index 64cd53af5..003625d3b 100644 --- a/packages/horizon/test/unit/staking/slash/slash.t.sol +++ b/packages/horizon/test/unit/staking/slash/slash.t.sol @@ -172,8 +172,10 @@ contract HorizonStakingSlashTest is HorizonStakingTest { vm.assume(delegationTokensToSlash <= delegationTokens); vm.assume(delegationTokensToUndelegate <= delegationTokens); vm.assume(delegationTokensToUndelegate > 0); - uint256 remaining = delegationTokens - delegationTokensToUndelegate; - vm.assume(remaining == 0 || MIN_DELEGATION <= remaining); + vm.assume( + delegationTokensToUndelegate == delegationTokens || + MIN_DELEGATION <= delegationTokens - delegationTokensToUndelegate + ); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0);