Skip to content

Commit 121337f

Browse files
committed
feat: add interface ID auto-generation system
- Add InterfaceIdExtractor contract for extracting ERC165 interface IDs - Add Python script to generate TypeScript interface ID constants - Update tests to use auto-generated interface IDs instead of manual calculation - Add interface ID consistency verification test - Integrate interface ID generation into build process This follows the same pattern as the issuance package and ensures interface IDs are always consistent with Solidity's calculations.
1 parent d811ba5 commit 121337f

File tree

5 files changed

+219
-2
lines changed

5 files changed

+219
-2
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity 0.7.6;
3+
4+
import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol";
5+
import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol";
6+
import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol";
7+
8+
/**
9+
* @title InterfaceIdExtractor
10+
* @author Edge & Node
11+
* @notice Utility contract for extracting ERC-165 interface IDs from Solidity interfaces
12+
* @dev This contract is used during the build process to generate interface ID constants
13+
* that match Solidity's own calculations, ensuring consistency between tests and actual
14+
* interface implementations.
15+
*/
16+
contract InterfaceIdExtractor {
17+
/**
18+
* @notice Returns the ERC-165 interface ID for IRewardsManager
19+
* @return The interface ID as calculated by Solidity
20+
*/
21+
function getIRewardsManagerId() external pure returns (bytes4) {
22+
return type(IRewardsManager).interfaceId;
23+
}
24+
25+
/**
26+
* @notice Returns the ERC-165 interface ID for IIssuanceTarget
27+
* @return The interface ID as calculated by Solidity
28+
*/
29+
function getIIssuanceTargetId() external pure returns (bytes4) {
30+
return type(IIssuanceTarget).interfaceId;
31+
}
32+
33+
/**
34+
* @notice Returns the ERC-165 interface ID for IERC165
35+
* @return The interface ID as calculated by Solidity
36+
*/
37+
function getIERC165Id() external pure returns (bytes4) {
38+
return type(IERC165).interfaceId;
39+
}
40+
}

packages/contracts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
"prepack": "pnpm build",
3131
"clean": "rm -rf artifacts/ cache/ types/ abis/ build/ dist/ coverage/",
3232
"build": "pnpm build:self",
33-
"build:self": "pnpm compile",
33+
"build:self": "pnpm compile && pnpm generate:interface-ids",
3434
"compile": "hardhat compile",
35+
"generate:interface-ids": "cd test && python3 scripts/generateInterfaceIds.py",
3536
"test": "pnpm --filter @graphprotocol/contracts-tests test",
3637
"test:coverage": "pnpm --filter @graphprotocol/contracts-tests run test:coverage",
3738
"deploy": "pnpm predeploy && pnpm build",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Auto-generated interface IDs from Solidity compilation
2+
export const INTERFACE_IDS = {
3+
IRewardsManager: '0xa31d8306',
4+
IIssuanceTarget: '0xaee4dc43',
5+
IERC165: '0x01ffc9a7',
6+
} as const
7+
8+
// Individual exports for convenience
9+
export const IRewardsManager = '0xa31d8306'
10+
export const IIssuanceTarget = '0xaee4dc43'
11+
export const IERC165 = '0x01ffc9a7'
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Generate interface ID constants by deploying and calling InterfaceIdExtractor contract
5+
"""
6+
7+
import json
8+
import os
9+
import subprocess
10+
import sys
11+
import tempfile
12+
from pathlib import Path
13+
14+
15+
def log(*args):
16+
"""Print log message if not in silent mode"""
17+
if "--silent" not in sys.argv:
18+
print(*args)
19+
20+
21+
def run_hardhat_task():
22+
"""Run hardhat script to extract interface IDs"""
23+
hardhat_script = """
24+
const hre = require('hardhat')
25+
26+
async function main() {
27+
const InterfaceIdExtractor = await hre.ethers.getContractFactory('InterfaceIdExtractor')
28+
const extractor = await InterfaceIdExtractor.deploy()
29+
await extractor.deployed()
30+
31+
const results = {
32+
IRewardsManager: await extractor.getIRewardsManagerId(),
33+
IIssuanceTarget: await extractor.getIIssuanceTargetId(),
34+
IERC165: await extractor.getIERC165Id(),
35+
}
36+
37+
console.log(JSON.stringify(results))
38+
}
39+
40+
main().catch((error) => {
41+
console.error(error)
42+
process.exit(1)
43+
})
44+
"""
45+
46+
script_dir = Path(__file__).parent
47+
project_dir = script_dir.parent.parent
48+
49+
# Write temporary script
50+
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as temp_file:
51+
temp_file.write(hardhat_script)
52+
temp_script = temp_file.name
53+
54+
try:
55+
# Run the script with hardhat
56+
result = subprocess.run(
57+
['npx', 'hardhat', 'run', temp_script, '--network', 'hardhat'],
58+
cwd=project_dir,
59+
capture_output=True,
60+
text=True,
61+
check=False
62+
)
63+
64+
if result.returncode != 0:
65+
raise RuntimeError(f"Hardhat script failed with code {result.returncode}: {result.stderr}")
66+
67+
# Extract JSON from output
68+
for line in result.stdout.split('\n'):
69+
line = line.strip()
70+
if line:
71+
try:
72+
data = json.loads(line)
73+
if isinstance(data, dict):
74+
return data
75+
except json.JSONDecodeError:
76+
# Not JSON, continue - this is expected for non-JSON output lines
77+
continue
78+
79+
raise RuntimeError("Could not parse interface IDs from output")
80+
81+
finally:
82+
# Clean up temp script
83+
try:
84+
os.unlink(temp_script)
85+
except OSError:
86+
# Ignore cleanup errors - temp file may not exist
87+
pass
88+
89+
90+
def extract_interface_ids():
91+
"""Extract interface IDs using the InterfaceIdExtractor contract"""
92+
script_dir = Path(__file__).parent
93+
extractor_path = script_dir.parent.parent / "artifacts" / "contracts" / "tests" / "InterfaceIdExtractor.sol" / "InterfaceIdExtractor.json"
94+
95+
if not extractor_path.exists():
96+
print("❌ InterfaceIdExtractor artifact not found")
97+
print("Run: pnpm compile to build the extractor contract")
98+
raise RuntimeError("InterfaceIdExtractor not compiled")
99+
100+
log("Deploying InterfaceIdExtractor contract to extract interface IDs...")
101+
102+
try:
103+
results = run_hardhat_task()
104+
105+
# Convert from ethers BigNumber format to hex strings
106+
processed = {}
107+
for name, value in results.items():
108+
if isinstance(value, str):
109+
processed[name] = value
110+
else:
111+
# Convert number to hex string
112+
processed[name] = f"0x{int(value):08x}"
113+
log(f"✅ Extracted {name}: {processed[name]}")
114+
115+
return processed
116+
117+
except Exception as error:
118+
print(f"Error extracting interface IDs: {error}")
119+
raise
120+
121+
122+
def main():
123+
"""Main function to generate interface IDs TypeScript file"""
124+
log("Extracting interface IDs from Solidity compilation...")
125+
126+
results = extract_interface_ids()
127+
128+
# Generate TypeScript content
129+
content = f"""// Auto-generated interface IDs from Solidity compilation
130+
export const INTERFACE_IDS = {{
131+
{chr(10).join(f" {name}: '{id_value}'," for name, id_value in results.items())}
132+
}} as const
133+
134+
// Individual exports for convenience
135+
{chr(10).join(f"export const {name} = '{id_value}'" for name, id_value in results.items())}
136+
"""
137+
138+
# Write to output file
139+
script_dir = Path(__file__).parent
140+
output_file = script_dir.parent / "helpers" / "interfaceIds.ts"
141+
142+
# Create helpers directory if it doesn't exist
143+
output_file.parent.mkdir(exist_ok=True)
144+
145+
with open(output_file, 'w') as f:
146+
f.write(content)
147+
148+
log(f"✅ Generated {output_file}")
149+
150+
151+
if __name__ == "__main__":
152+
main()

packages/contracts/test/tests/unit/rewards/rewards.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,24 @@ describe('Rewards', () => {
187187

188188
it('should support IRewardsManager interface', async function () {
189189
// Use the auto-generated interface ID from Solidity compilation
190-
const { IRewardsManager } = await import('../../helpers/interfaceIds')
190+
const { IRewardsManager } = require('../../../helpers/interfaceIds')
191191
const supports = await rewardsManager.supportsInterface(IRewardsManager)
192192
expect(supports).to.be.true
193193
})
194194

195+
it('should have consistent interface IDs with Solidity calculations', async function () {
196+
const InterfaceIdExtractorFactory = await hre.ethers.getContractFactory('InterfaceIdExtractor')
197+
const extractor = await InterfaceIdExtractorFactory.deploy()
198+
await extractor.deployed()
199+
200+
const { IRewardsManager, IIssuanceTarget, IERC165 } = require('../../../helpers/interfaceIds')
201+
202+
// Verify each interface ID matches what Solidity calculates
203+
expect(await extractor.getIRewardsManagerId()).to.equal(IRewardsManager)
204+
expect(await extractor.getIIssuanceTargetId()).to.equal(IIssuanceTarget)
205+
expect(await extractor.getIERC165Id()).to.equal(IERC165)
206+
})
207+
195208
it('should support IERC165 interface', async function () {
196209
// Test the specific IERC165 interface - this should hit the third branch
197210
// interfaceId == type(IERC165).interfaceId

0 commit comments

Comments
 (0)