diff --git a/packages/cli/src/commands/codegen/import-abi.ts b/packages/cli/src/commands/codegen/import-abi.ts index e1b7c00118..7f97df3f93 100644 --- a/packages/cli/src/commands/codegen/import-abi.ts +++ b/packages/cli/src/commands/codegen/import-abi.ts @@ -195,7 +195,7 @@ async function generateAdapter( tsExtractor ); - await generateManifestTs(manifest, userInput, existingManifest); + await generateManifestTs(manifest, userInput, existingManifest, abiInterface); } else { // yaml const existingManifest = await getManifestData(manifest); @@ -211,10 +211,10 @@ async function generateAdapter( yamlExtractor ); - await generateManifestYaml(manifest, userInput, existingManifest); + await generateManifestYaml(manifest, userInput, existingManifest, abiInterface); } - await generateHandlers([userInput.events, userInput.functions], root, abiName); + await generateHandlers([userInput.events, userInput.functions], root, abiName, abiInterface); return { address: args.address, diff --git a/packages/cli/src/controller/generate-controller.ts b/packages/cli/src/controller/generate-controller.ts index 2b5abb9654..71000315aa 100644 --- a/packages/cli/src/controller/generate-controller.ts +++ b/packages/cli/src/controller/generate-controller.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; -import type {ConstructorFragment, EventFragment, Fragment, FunctionFragment} from '@ethersproject/abi'; +import type {ConstructorFragment, EventFragment, Fragment, FunctionFragment, Interface} from '@ethersproject/abi'; import {NETWORK_FAMILY} from '@subql/common'; import type { EthereumDatasourceKind, @@ -25,6 +25,7 @@ import { resolveToAbsolutePath, splitArrayString, } from '../utils'; +import {EthereumTypeResolver} from '../utils/ethereum-type-resolver'; export interface SelectedMethod { name: string; @@ -161,27 +162,29 @@ export function generateHandlerName(name: string, abiName: string, type: 'tx' | function generateFormattedHandlers( userInput: UserInput, abiName: string, - kindModifier: (kind: string) => EthereumHandlerKind | string + kindModifier: (kind: string) => EthereumHandlerKind | string, + abiInterface?: Interface ): SubqlRuntimeHandler[] { const formattedHandlers: SubqlRuntimeHandler[] = []; - - userInput.functions.forEach((fn) => { - const handler: SubqlRuntimeHandler = { - handler: generateHandlerName(fn.name, abiName, 'tx'), - kind: kindModifier('EthereumHandlerKind.Call') as any, // union type - filter: { - function: fn.method, - }, - }; - formattedHandlers.push(handler); - }); + const typeResolver = abiInterface ? new EthereumTypeResolver(abiInterface) : null; userInput.events.forEach((event) => { + let topicFilter = event.method; + + if (typeResolver) { + try { + const resolved = typeResolver.resolveEventSignature(event.method); + topicFilter = resolved.keccak256Hash; + } catch (e: any) { + throw new Error(`Failed to resolve custom types in event signature: ${event.method}\n${e.message}`); + } + } + const handler: SubqlRuntimeHandler = { handler: generateHandlerName(event.name, abiName, 'log'), kind: kindModifier('EthereumHandlerKind.Event') as any, // Should be union type filter: { - topics: [event.method], + topics: [topicFilter], }, }; formattedHandlers.push(handler); @@ -190,10 +193,10 @@ function generateFormattedHandlers( return formattedHandlers; } -export function constructDatasourcesTs(userInput: UserInput, projectPath: string): string { +export function constructDatasourcesTs(userInput: UserInput, projectPath: string, abiInterface?: Interface): string { const ethModule = loadDependency(NETWORK_FAMILY.ethereum, projectPath); const abiName = ethModule.parseContractPath(userInput.abiPath).name; - const formattedHandlers = generateFormattedHandlers(userInput, abiName, (kind) => kind); + const formattedHandlers = generateFormattedHandlers(userInput, abiName, (kind) => kind, abiInterface); const handlersString = tsStringify(formattedHandlers); return `{ @@ -211,13 +214,22 @@ export function constructDatasourcesTs(userInput: UserInput, projectPath: string }`; } -export function constructDatasourcesYaml(userInput: UserInput, projectPath: string): EthereumDs { +export function constructDatasourcesYaml( + userInput: UserInput, + projectPath: string, + abiInterface?: Interface +): EthereumDs { const ethModule = loadDependency(NETWORK_FAMILY.ethereum, projectPath); const abiName = ethModule.parseContractPath(userInput.abiPath).name; - const formattedHandlers = generateFormattedHandlers(userInput, abiName, (kind) => { - if (kind === 'EthereumHandlerKind.Call') return 'ethereum/TransactionHandler' as EthereumHandlerKind.Call; - return 'ethereum/LogHandler' as EthereumHandlerKind.Event; - }); + const formattedHandlers = generateFormattedHandlers( + userInput, + abiName, + (kind) => { + if (kind === 'EthereumHandlerKind.Call') return 'ethereum/TransactionHandler' as EthereumHandlerKind.Call; + return 'ethereum/LogHandler' as EthereumHandlerKind.Event; + }, + abiInterface + ); const assets = new Map([[abiName, {file: userInput.abiPath}]]); return { @@ -402,9 +414,10 @@ export function prependDatasources(dsStr: string, toPendStr: string): string { export async function generateManifestTs( manifestPath: string, userInput: UserInput, - existingManifestData: string + existingManifestData: string, + abiInterface?: Interface ): Promise { - const inputDs = constructDatasourcesTs(userInput, manifestPath); + const inputDs = constructDatasourcesTs(userInput, manifestPath, abiInterface); const extractedDs = extractFromTs(existingManifestData, {dataSources: undefined}) as {dataSources: string}; const v = prependDatasources(extractedDs.dataSources, inputDs); @@ -415,9 +428,10 @@ export async function generateManifestTs( export async function generateManifestYaml( manifestPath: string, userInput: UserInput, - existingManifestData: Document + existingManifestData: Document, + abiInterface?: Interface ): Promise { - const inputDs = constructDatasourcesYaml(userInput, manifestPath); + const inputDs = constructDatasourcesYaml(userInput, manifestPath, abiInterface); const dsNode = existingManifestData.get('dataSources') as YAMLSeq; if (!dsNode || !dsNode.items.length) { // To ensure output is in yaml format @@ -459,7 +473,8 @@ export function constructHandlerProps(methods: [SelectedMethod[], SelectedMethod export async function generateHandlers( selectedMethods: [SelectedMethod[], SelectedMethod[]], projectPath: string, - abiName: string + abiName: string, + abiInterface?: Interface ): Promise { const abiProps = constructHandlerProps(selectedMethods, abiName); diff --git a/packages/cli/src/utils/ethereum-type-resolver.ts b/packages/cli/src/utils/ethereum-type-resolver.ts new file mode 100644 index 0000000000..e6bfb10b80 --- /dev/null +++ b/packages/cli/src/utils/ethereum-type-resolver.ts @@ -0,0 +1,308 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import {Fragment, EventFragment, Interface} from '@ethersproject/abi'; +import {keccak256} from '@ethersproject/keccak256'; +import {toUtf8Bytes} from '@ethersproject/strings'; + +export interface TypeResolutionResult { + canonicalSignature: string; + keccak256Hash: string; + resolvedTypes: Map; +} + +export interface StructDefinition { + name: string; + components: Array<{ + name: string; + type: string; + components?: any[]; + }>; +} + +export class EthereumTypeResolver { + private structDefinitions: Map = new Map(); + private enumDefinitions: Map = new Map(); + private typeCache: Map = new Map(); + + constructor(private abiInterface: Interface) { + this.analyzeAbiForCustomTypes(); + } + + private analyzeAbiForCustomTypes(): void { + this.abiInterface.fragments.forEach((fragment) => { + if (fragment.type === 'event' || fragment.type === 'function') { + const inputs = (fragment as any).inputs || []; + this.extractCustomTypesFromInputs(inputs); + } + }); + } + + private extractCustomTypesFromInputs(inputs: any[]): void { + inputs.forEach((input) => { + if (input.type.startsWith('tuple')) { + this.analyzeStructType(input); + } + + if (input.type === 'uint8' && this.looksLikeEnum(input)) { + this.analyzeEnumType(input); + } + + if (input.components) { + this.extractCustomTypesFromInputs(input.components); + } + }); + } + + private analyzeStructType(input: any): void { + if (input.components) { + const structName = this.inferStructName(input); + if (structName && !this.structDefinitions.has(structName)) { + this.registerStructFromComponents(structName, input.components); + } + } + } + + private registerStructFromComponents(structName: string, components: any[]): void { + this.structDefinitions.set(structName, { + name: structName, + components: components, + }); + } + + private looksLikeEnum(input: any): boolean { + const enumPatterns = [/Type$/, /Status$/, /Kind$/, /State$/, /Mode$/, /Flag$/]; + return enumPatterns.some((pattern) => pattern.test(input.name)); + } + + private analyzeEnumType(input: any): void { + const enumName = input.name; + if (!this.enumDefinitions.has(enumName)) { + this.enumDefinitions.set(enumName, []); + } + } + + /** + * Resolve custom types in event signature to basic Solidity types + */ + resolveEventSignature(eventSignature: string): TypeResolutionResult { + const fragment = this.parseEventSignature(eventSignature); + const resolvedInputs = this.resolveInputTypes(fragment.inputs); + const canonicalSignature = this.buildCanonicalSignature(fragment.name, resolvedInputs); + const keccak256Hash = keccak256(toUtf8Bytes(canonicalSignature)); + + return { + canonicalSignature, + keccak256Hash, + resolvedTypes: this.getResolvedTypeMap(fragment.inputs, resolvedInputs), + }; + } + + private parseEventSignature(eventSignature: string): EventFragment { + try { + const cleanSignature = eventSignature.replace(/^event\s+/, ''); + return EventFragment.from(`event ${cleanSignature}`); + } catch (error) { + throw new Error(`Invalid event signature format: ${eventSignature}\n${(error as Error).message}`); + } + } + + private resolveInputTypes(inputs: any[]): string[] { + return inputs.map((input) => this.resolveType(input.type, input.name)); + } + + private buildCanonicalSignature(eventName: string, resolvedInputs: string[]): string { + return `${eventName}(${resolvedInputs.join(',')})`; + } + + private getResolvedTypeMap(originalInputs: any[], resolvedTypes: string[]): Map { + const typeMap = new Map(); + + originalInputs.forEach((input, index) => { + if (input.type !== resolvedTypes[index]) { + typeMap.set(input.type, resolvedTypes[index]); + } + }); + + return typeMap; + } + + private resolveType(type: string, paramName?: string): string { + const cacheKey = `${type}:${paramName || ''}`; + const cacheValue = this.typeCache.get(cacheKey); + if (cacheValue) { + return cacheValue; + } + + const resolvedType = this.doResolveType(type, paramName); + this.typeCache.set(cacheKey, resolvedType); + return resolvedType; + } + + private doResolveType(type: string, paramName?: string): string { + // Handle arrays first + if (type.endsWith('[]')) { + const baseType = type.slice(0, -2); + return `${this.resolveType(baseType, paramName)}[]`; + } + + const arrayMatch = type.match(/^(.+)\[(\d+)\]$/); + if (arrayMatch) { + const baseType = arrayMatch[1]; + const size = arrayMatch[2]; + return `${this.resolveType(baseType, paramName)}[${size}]`; + } + + if (this.isBasicSolidityType(type)) { + return type; + } + + if (type.startsWith('tuple(')) { + return this.resolveTupleType(type); + } + + return this.resolveCustomType(type, paramName); + } + + private resolveTupleType(tupleType: string): string { + const componentsMatch = tupleType.match(/^tuple\((.+)\)$/); + if (!componentsMatch) { + return tupleType; + } + + const components = this.parseCommaSeperatedTypes(componentsMatch[1]); + const resolvedComponents = components.map((comp) => this.resolveType(comp)); + return `tuple(${resolvedComponents.join(',')})`; + } + + private parseCommaSeperatedTypes(typesString: string): string[] { + const types: string[] = []; + let current = ''; + let depth = 0; + + for (let i = 0; i < typesString.length; i++) { + const char = typesString[i]; + + if (char === '(') { + depth++; + } else if (char === ')') { + depth--; + } else if (char === ',' && depth === 0) { + types.push(current.trim()); + current = ''; + continue; + } + + current += char; + } + + if (current.trim()) { + types.push(current.trim()); + } + + return types; + } + + private resolveCustomType(typeName: string, paramName?: string): string { + if (this.isLikelyEnum(typeName, paramName)) { + return 'uint8'; + } + + const structInfo = this.structDefinitions.get(typeName); + if (structInfo) { + const tupleTypes = structInfo.components.map((comp) => this.resolveType(comp.type, comp.name)); + return `tuple(${tupleTypes.join(',')})`; + } + + const inferredType = this.inferTypeFromAbi(typeName, paramName); + if (inferredType) { + return inferredType; + } + + throw new Error( + `Unknown custom type: ${typeName}${paramName ? ` (parameter: ${paramName})` : ''}. ` + + `Please ensure the type is defined in the ABI or use a basic Solidity type. ` + + `Available struct types: [${Array.from(this.structDefinitions.keys()).join(', ')}]` + ); + } + + private isBasicSolidityType(type: string): boolean { + const basicTypePatterns = [ + /^uint(\d+)?$/, + /^int(\d+)?$/, + /^bytes(\d+)?$/, + /^bool$/, + /^address$/, + /^string$/, + /^bytes$/, + ]; + return basicTypePatterns.some((pattern) => pattern.test(type)); + } + + private isLikelyEnum(typeName: string, paramName?: string): boolean { + const enumSuffixes = [/Type$/, /Status$/, /Kind$/, /State$/, /Mode$/, /Flag$/]; + + const typeMatches = enumSuffixes.some((pattern) => pattern.test(typeName)); + const paramMatches = paramName ? enumSuffixes.some((pattern) => pattern.test(paramName)) : false; + + return typeMatches || paramMatches; + } + + private inferTypeFromAbi(typeName: string, paramName?: string): string | null { + for (const fragment of this.abiInterface.fragments) { + if (fragment.type === 'event' || fragment.type === 'function') { + const inputs = (fragment as any).inputs || []; + + for (const input of inputs) { + if (input.type.includes(typeName) || input.name === typeName) { + if (input.type.startsWith('tuple') && input.components) { + this.registerStructFromComponents(typeName, input.components); + const tupleTypes = input.components.map((comp: any) => this.resolveType(comp.type, comp.name)); + return `tuple(${tupleTypes.join(',')})`; + } + } + } + } + } + + return null; + } + + private inferStructName(input: any): string { + if (input.name && this.looksLikeTypeName(input.name)) { + return this.capitalizeFirstLetter(input.name); + } + + if (input.internalType && input.internalType.includes('struct')) { + const match = input.internalType.match(/struct\s+(\w+)/); + if (match) { + return match[1]; + } + } + + return `CustomStruct_${input.components.length}Fields`; + } + + private looksLikeTypeName(name: string): boolean { + return /^[A-Z][a-zA-Z0-9]*$/.test(name); + } + + private capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + /** + * Get debug information about discovered types + */ + getDiscoveredTypes(): { + structs: string[]; + enums: string[]; + typeCache: Record; + } { + return { + structs: Array.from(this.structDefinitions.keys()), + enums: Array.from(this.enumDefinitions.keys()), + typeCache: Object.fromEntries(this.typeCache.entries()), + }; + } +}