From 4e87cf4b4df49432832fa8750a12c3147c755b3d Mon Sep 17 00:00:00 2001 From: YaroShkvorets Date: Wed, 1 Jan 2025 23:14:45 -0500 Subject: [PATCH 1/6] wrap address[] and tuple[] into changetype --- packages/cli/src/scaffold/mapping.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/scaffold/mapping.ts b/packages/cli/src/scaffold/mapping.ts index 60244a92e..a3852ad21 100644 --- a/packages/cli/src/scaffold/mapping.ts +++ b/packages/cli/src/scaffold/mapping.ts @@ -1,16 +1,30 @@ import * as util from '../codegen/util.js'; -export const generateFieldAssignment = (key: string[], value: string[]) => - `entity.${key.join('_')} = event.params.${value.join('.')}`; +/** + * Map of value types that need to be changeType'd to their corresponding AssemblyScript type + */ +export const VALUE_TYPECAST_MAP: Record = { + 'address[]': 'Bytes[]', + 'tuple[]': 'Bytes[]', +}; + +export const generateFieldAssignment = (keyPath: string[], value: string[], type?: string) => { + let rightSide = `event.params.${value.join('.')}`; + if (type && VALUE_TYPECAST_MAP[type]) { + rightSide = `changeType<${VALUE_TYPECAST_MAP[type]}>(${rightSide})`; + } + return `entity.${keyPath.join('_')} = ${rightSide}`; +}; export const generateFieldAssignments = ({ index, input }: { index: number; input: any }) => input.type === 'tuple' ? util .unrollTuple({ value: input, index, path: [input.name || `param${index}`] }) - .map(({ path }: any) => generateFieldAssignment(path, path)) + .map(({ path, type }: any) => generateFieldAssignment(path, path, type)) : generateFieldAssignment( [(input.mappedName ?? input.name) || `param${index}`], [input.name || `param${index}`], + input.type, ); /** @@ -39,6 +53,7 @@ export const generateEventIndexingHandlers = (events: any[], contractName: strin event => `${event._alias} as ${event._alias}Event`, )}} from '../generated/${contractName}/${contractName}' import { ${events.map(event => event._alias)} } from '../generated/schema' + import { Bytes } from '@graphprotocol/graph-ts' ${events .map( From 7f698812990b48709f02a312d1bb2c77ae30008f Mon Sep 17 00:00:00 2001 From: YaroShkvorets Date: Wed, 1 Jan 2025 23:29:21 -0500 Subject: [PATCH 2/6] tests --- .../__snapshots__/ethereum.test.ts.snap | 171 +++++++++++++++++- packages/cli/src/scaffold/ethereum.test.ts | 28 ++- 2 files changed, 189 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap index ad94388fe..e0aa397df 100644 --- a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap +++ b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap @@ -21,6 +21,7 @@ dataSources: entities: - ExampleEvent - ExampleEvent1 + - TupleArrayEvent abis: - name: Contract file: ./abis/Contract.json @@ -29,6 +30,8 @@ dataSources: handler: handleExampleEvent - event: ExampleEvent(bytes32) handler: handleExampleEvent1 + - event: TupleArrayEvent((uint256,address)[],address[]) + handler: handleTupleArrayEvent file: ./src/contract.ts " `; @@ -38,7 +41,8 @@ exports[`Ethereum subgraph scaffolding > Mapping (default) 1`] = ` import { Contract, ExampleEvent, - ExampleEvent1 + ExampleEvent1, + TupleArrayEvent } from "../generated/Contract/Contract" import { ExampleEntity } from "../generated/schema" @@ -89,15 +93,23 @@ export function handleExampleEvent(event: ExampleEvent): void { } export function handleExampleEvent1(event: ExampleEvent1): void {} + +export function handleTupleArrayEvent(event: TupleArrayEvent): void {} " `; exports[`Ethereum subgraph scaffolding > Mapping (for indexing events) 1`] = ` "import { ExampleEvent as ExampleEventEvent, - ExampleEvent1 as ExampleEvent1Event + ExampleEvent1 as ExampleEvent1Event, + TupleArrayEvent as TupleArrayEventEvent } from "../generated/Contract/Contract" -import { ExampleEvent, ExampleEvent1 } from "../generated/schema" +import { + ExampleEvent, + ExampleEvent1, + TupleArrayEvent +} from "../generated/schema" +import { Bytes } from "@graphprotocol/graph-ts" export function handleExampleEvent(event: ExampleEventEvent): void { let entity = new ExampleEvent( @@ -134,6 +146,82 @@ export function handleExampleEvent1(event: ExampleEvent1Event): void { entity.save() } + +export function handleTupleArrayEvent(event: TupleArrayEventEvent): void { + let entity = new TupleArrayEvent( + event.transaction.hash.concatI32(event.logIndex.toI32()) + ) + entity.tupleArray = changeType(event.params.tupleArray) + entity.addressArray = changeType(event.params.addressArray) + + entity.blockNumber = event.block.number + entity.blockTimestamp = event.block.timestamp + entity.transactionHash = event.transaction.hash + + entity.save() +} +" +`; + +exports[`Ethereum subgraph scaffolding > Mapping handles tuple array type conversion 1`] = ` +"import { BigInt, Bytes } from "@graphprotocol/graph-ts" +import { + Contract, + ExampleEvent, + ExampleEvent1, + TupleArrayEvent +} from "../generated/Contract/Contract" +import { ExampleEntity } from "../generated/schema" + +export function handleExampleEvent(event: ExampleEvent): void { + // Entities can be loaded from the store using an ID; this ID + // needs to be unique across all entities of the same type + const id = event.transaction.hash.concat( + Bytes.fromByteArray(Bytes.fromBigInt(event.logIndex)) + ) + let entity = ExampleEntity.load(id) + + // Entities only exist after they have been saved to the store; + // \`null\` checks allow to create entities on demand + if (!entity) { + entity = new ExampleEntity(id) + + // Entity fields can be set using simple assignments + entity.count = BigInt.fromI32(0) + } + + // BigInt and BigDecimal math are supported + entity.count = entity.count + BigInt.fromI32(1) + + // Entity fields can be set based on event parameters + entity.a = event.params.a + entity.b = event.params.b + + // Entities can be written to the store with \`.save()\` + entity.save() + + // Note: If a handler doesn't require existing field values, it is faster + // _not_ to load the entity from the store. Instead, create it fresh with + // \`new Entity(...)\`, set the fields that should be updated and save the + // entity back to the store. Fields that were not set or unset remain + // unchanged, allowing for partial updates to be applied. + + // It is also possible to access smart contracts from mappings. For + // example, the contract that has emitted the event can be connected to + // with: + // + // let contract = Contract.bind(event.address) + // + // The following functions can then be called on this contract to access + // state variables and other data: + // + // - contract.someVariable(...) + // - contract.getSomeValue(...) +} + +export function handleExampleEvent1(event: ExampleEvent1): void {} + +export function handleTupleArrayEvent(event: TupleArrayEvent): void {} " `; @@ -173,6 +261,15 @@ type ExampleEvent1 @entity(immutable: true) { blockTimestamp: BigInt! transactionHash: Bytes! } + +type TupleArrayEvent @entity(immutable: true) { + id: Bytes! + tupleArray: [Bytes!]! # tuple[] + addressArray: [Bytes!]! # address[] + blockNumber: BigInt! + blockTimestamp: BigInt! + transactionHash: Bytes! +} " `; @@ -185,7 +282,7 @@ exports[`Ethereum subgraph scaffolding > Test Files (default) 1`] = ` beforeAll, afterAll } from "matchstick-as/assembly/index" -import { BigInt, Bytes } from "@graphprotocol/graph-ts" +import { BigInt, Bytes, Address } from "@graphprotocol/graph-ts" import { ExampleEvent } from "../generated/schema" import { ExampleEvent as ExampleEventEvent } from "../generated/Contract/Contract" import { handleExampleEvent } from "../src/contract" @@ -257,8 +354,12 @@ describe("Describe entity assertions", () => { exports[`Ethereum subgraph scaffolding > Test Files (default) 2`] = ` "import { newMockEvent } from "matchstick-as" -import { ethereum, BigInt, Bytes } from "@graphprotocol/graph-ts" -import { ExampleEvent, ExampleEvent1 } from "../generated/Contract/Contract" +import { ethereum, BigInt, Bytes, Address } from "@graphprotocol/graph-ts" +import { + ExampleEvent, + ExampleEvent1, + TupleArrayEvent +} from "../generated/Contract/Contract" export function createExampleEventEvent( a: BigInt, @@ -305,6 +406,30 @@ export function createExampleEvent1Event(a: Bytes): ExampleEvent1 { return exampleEvent1Event } + +export function createTupleArrayEventEvent( + tupleArray: Array, + addressArray: Array
+): TupleArrayEvent { + let tupleArrayEventEvent = changetype(newMockEvent()) + + tupleArrayEventEvent.parameters = new Array() + + tupleArrayEventEvent.parameters.push( + new ethereum.EventParam( + "tupleArray", + ethereum.Value.fromTupleArray(tupleArray) + ) + ) + tupleArrayEventEvent.parameters.push( + new ethereum.EventParam( + "addressArray", + ethereum.Value.fromAddressArray(addressArray) + ) + ) + + return tupleArrayEventEvent +} " `; @@ -317,7 +442,7 @@ exports[`Ethereum subgraph scaffolding > Test Files (for indexing events) 1`] = beforeAll, afterAll } from "matchstick-as/assembly/index" -import { BigInt, Bytes } from "@graphprotocol/graph-ts" +import { BigInt, Bytes, Address } from "@graphprotocol/graph-ts" import { ExampleEvent } from "../generated/schema" import { ExampleEvent as ExampleEventEvent } from "../generated/Contract/Contract" import { handleExampleEvent } from "../src/contract" @@ -389,8 +514,12 @@ describe("Describe entity assertions", () => { exports[`Ethereum subgraph scaffolding > Test Files (for indexing events) 2`] = ` "import { newMockEvent } from "matchstick-as" -import { ethereum, BigInt, Bytes } from "@graphprotocol/graph-ts" -import { ExampleEvent, ExampleEvent1 } from "../generated/Contract/Contract" +import { ethereum, BigInt, Bytes, Address } from "@graphprotocol/graph-ts" +import { + ExampleEvent, + ExampleEvent1, + TupleArrayEvent +} from "../generated/Contract/Contract" export function createExampleEventEvent( a: BigInt, @@ -437,5 +566,29 @@ export function createExampleEvent1Event(a: Bytes): ExampleEvent1 { return exampleEvent1Event } + +export function createTupleArrayEventEvent( + tupleArray: Array, + addressArray: Array
+): TupleArrayEvent { + let tupleArrayEventEvent = changetype(newMockEvent()) + + tupleArrayEventEvent.parameters = new Array() + + tupleArrayEventEvent.parameters.push( + new ethereum.EventParam( + "tupleArray", + ethereum.Value.fromTupleArray(tupleArray) + ) + ) + tupleArrayEventEvent.parameters.push( + new ethereum.EventParam( + "addressArray", + ethereum.Value.fromAddressArray(addressArray) + ) + ) + + return tupleArrayEventEvent +} " `; diff --git a/packages/cli/src/scaffold/ethereum.test.ts b/packages/cli/src/scaffold/ethereum.test.ts index 3acdc6494..da4d7e999 100644 --- a/packages/cli/src/scaffold/ethereum.test.ts +++ b/packages/cli/src/scaffold/ethereum.test.ts @@ -56,10 +56,32 @@ const TEST_CALLABLE_FUNCTIONS = [ }, ]; +const TEST_TUPLE_ARRAY_EVENT = { + name: 'TupleArrayEvent', + type: 'event', + inputs: [ + { + name: 'tupleArray', + type: 'tuple[]', + components: [ + { name: 'field1', type: 'uint256' }, + { name: 'field2', type: 'address' }, + ], + }, + { name: 'addressArray', type: 'address[]' }, + ], +}; + const TEST_ABI = new ABI( 'Contract', undefined, - immutable.fromJS([TEST_EVENT, OVERLOADED_EVENT, TEST_CONTRACT, ...TEST_CALLABLE_FUNCTIONS]), + immutable.fromJS([ + TEST_EVENT, + OVERLOADED_EVENT, + TEST_TUPLE_ARRAY_EVENT, + TEST_CONTRACT, + ...TEST_CALLABLE_FUNCTIONS, + ]), ); const protocol = new Protocol('ethereum'); @@ -101,6 +123,10 @@ describe('Ethereum subgraph scaffolding', () => { expect(await scaffoldWithIndexEvents.generateMapping()).toMatchSnapshot(); }); + test('Mapping handles tuple array type conversion', async () => { + expect(await scaffold.generateMapping()).toMatchSnapshot(); + }); + test('Test Files (default)', async () => { const files = await scaffoldWithIndexEvents.generateTests(); const testFile = files?.['contract.test.ts']; From 73fc5ba6bed4d82ec1258d432b211a0f0bdb9593 Mon Sep 17 00:00:00 2001 From: YaroShkvorets Date: Thu, 2 Jan 2025 19:07:06 -0500 Subject: [PATCH 3/6] pass imports array --- packages/cli/src/codegen/util.ts | 2 +- .../protocols/ethereum/scaffold/mapping.ts | 2 +- packages/cli/src/scaffold/mapping.ts | 113 ++++++++++++------ 3 files changed, 81 insertions(+), 36 deletions(-) diff --git a/packages/cli/src/codegen/util.ts b/packages/cli/src/codegen/util.ts index 0fc8b5c47..c675ad241 100644 --- a/packages/cli/src/codegen/util.ts +++ b/packages/cli/src/codegen/util.ts @@ -43,7 +43,7 @@ export const unrollTuple = ({ path: string[]; index: number; // TODO: index is unused, do we really need it? value: any; -}) => +}): { path: string[]; type: string }[] => value.components.reduce((acc: any[], component: any, index: number) => { const name = component.name || `value${index}`; return acc.concat( diff --git a/packages/cli/src/protocols/ethereum/scaffold/mapping.ts b/packages/cli/src/protocols/ethereum/scaffold/mapping.ts index 113996a72..6bdc2c5ac 100644 --- a/packages/cli/src/protocols/ethereum/scaffold/mapping.ts +++ b/packages/cli/src/protocols/ethereum/scaffold/mapping.ts @@ -39,7 +39,7 @@ export const generatePlaceholderHandlers = ({ entity.count = entity.count + BigInt.fromI32(1) // Entity fields can be set based on event parameters - ${generateEventFieldAssignments(event, contractName).slice(0, 2).join('\n')} + ${generateEventFieldAssignments(event, contractName).assignments.slice(0, 2).join('\n')} // Entities can be written to the store with \`.save()\` entity.save() diff --git a/packages/cli/src/scaffold/mapping.ts b/packages/cli/src/scaffold/mapping.ts index a3852ad21..296770066 100644 --- a/packages/cli/src/scaffold/mapping.ts +++ b/packages/cli/src/scaffold/mapping.ts @@ -8,24 +8,51 @@ export const VALUE_TYPECAST_MAP: Record = { 'tuple[]': 'Bytes[]', }; -export const generateFieldAssignment = (keyPath: string[], value: string[], type?: string) => { +export const generateFieldAssignment = ( + key: string[], + value: string[], + type: string, +): { assignment: string; imports: string[] } => { let rightSide = `event.params.${value.join('.')}`; - if (type && VALUE_TYPECAST_MAP[type]) { - rightSide = `changeType<${VALUE_TYPECAST_MAP[type]}>(${rightSide})`; + const imports = []; + + if (type in VALUE_TYPECAST_MAP) { + const castTo = VALUE_TYPECAST_MAP[type]; + rightSide = `changeType<${castTo}>(${rightSide})`; + imports.push(castTo.replace('[]', '')); } - return `entity.${keyPath.join('_')} = ${rightSide}`; + + return { + assignment: `entity.${key.join('_')} = ${rightSide}`, + imports, + }; }; -export const generateFieldAssignments = ({ index, input }: { index: number; input: any }) => - input.type === 'tuple' - ? util - .unrollTuple({ value: input, index, path: [input.name || `param${index}`] }) - .map(({ path, type }: any) => generateFieldAssignment(path, path, type)) - : generateFieldAssignment( - [(input.mappedName ?? input.name) || `param${index}`], - [input.name || `param${index}`], - input.type, - ); +export const generateFieldAssignments = ({ + index, + input, +}: { + index: number; + input: any; +}): { assignments: string[]; imports: string[] } => { + const fields = + input.type === 'tuple' + ? util + .unrollTuple({ value: input, index, path: [input.name || `param${index}`] }) + .map(({ path, type }) => generateFieldAssignment(path, path, type)) + : [ + generateFieldAssignment( + [(input.mappedName ?? input.name) || `param${index}`], + [input.name || `param${index}`], + input.type, + ), + ]; + + return { + assignments: fields.map(a => a.assignment), + imports: fields.map(a => a.imports).flat(), + }; +}; /** * Map of input names that are reserved so we do not use them as field names to avoid conflicts @@ -41,27 +68,32 @@ export const renameNameIfNeeded = (name: string) => { return NAMES_REMAP_DICTIONARY[name] ?? name; }; -export const generateEventFieldAssignments = (event: any, _contractName: string) => - event.inputs.reduce((acc: any[], input: any, index: number) => { - input.mappedName = renameNameIfNeeded(input.name); - return acc.concat(generateFieldAssignments({ input, index })); - }, []); +export const generateEventFieldAssignments = ( + event: any, + _contractName: string, +): { assignments: string[]; imports: string[] } => + event.inputs.reduce( + (acc: any, input: any, index: number) => { + input.mappedName = renameNameIfNeeded(input.name); + const { assignments, imports } = generateFieldAssignments({ input, index }); + return { + assignments: acc.assignments.concat(assignments), + imports: acc.imports.concat(imports), + }; + }, + { assignments: [], imports: [] }, + ); -export const generateEventIndexingHandlers = (events: any[], contractName: string) => - ` - import { ${events.map( - event => `${event._alias} as ${event._alias}Event`, - )}} from '../generated/${contractName}/${contractName}' - import { ${events.map(event => event._alias)} } from '../generated/schema' - import { Bytes } from '@graphprotocol/graph-ts' +export const generateEventIndexingHandlers = (events: any[], contractName: string) => { + const allImports: string[] = []; + const eventHandlers = events.map(event => { + const { assignments, imports } = generateEventFieldAssignments(event, contractName); + allImports.push(...imports); - ${events - .map( - event => - ` + return ` export function handle${event._alias}(event: ${event._alias}Event): void { let entity = new ${event._alias}(event.transaction.hash.concatI32(event.logIndex.toI32())) - ${generateEventFieldAssignments(event, contractName).join('\n')} + ${assignments.join('\n')} entity.blockNumber = event.block.number entity.blockTimestamp = event.block.timestamp @@ -69,7 +101,20 @@ export const generateEventIndexingHandlers = (events: any[], contractName: strin entity.save() } - `, - ) - .join('\n')} + `; + }); + + return ` + import { ${events.map( + event => `${event._alias} as ${event._alias}Event`, + )}} from '../generated/${contractName}/${contractName}' + import { ${events.map(event => event._alias)} } from '../generated/schema' + ${ + allImports.length > 0 + ? `import { ${[...new Set(allImports)].join(', ')} } from '@graphprotocol/graph-ts'` + : '' + } + + ${eventHandlers.join('\n')} `; +}; From f62d657c6261c30ebba5c7a675bfd34098634c84 Mon Sep 17 00:00:00 2001 From: YaroShkvorets Date: Thu, 2 Jan 2025 19:29:26 -0500 Subject: [PATCH 4/6] cleanup mapping codegen --- .../scaffold/__snapshots__/ethereum.test.ts.snap | 4 ++-- packages/cli/src/scaffold/mapping.ts | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap index e0aa397df..b76a3860d 100644 --- a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap +++ b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap @@ -151,8 +151,8 @@ export function handleTupleArrayEvent(event: TupleArrayEventEvent): void { let entity = new TupleArrayEvent( event.transaction.hash.concatI32(event.logIndex.toI32()) ) - entity.tupleArray = changeType(event.params.tupleArray) - entity.addressArray = changeType(event.params.addressArray) + entity.tupleArray = changetype(event.params.tupleArray) + entity.addressArray = changetype(event.params.addressArray) entity.blockNumber = event.block.number entity.blockTimestamp = event.block.timestamp diff --git a/packages/cli/src/scaffold/mapping.ts b/packages/cli/src/scaffold/mapping.ts index 296770066..818f967b2 100644 --- a/packages/cli/src/scaffold/mapping.ts +++ b/packages/cli/src/scaffold/mapping.ts @@ -1,7 +1,7 @@ import * as util from '../codegen/util.js'; /** - * Map of value types that need to be changeType'd to their corresponding AssemblyScript type + * Map of value types that need to be changetype'd to their corresponding AssemblyScript type */ export const VALUE_TYPECAST_MAP: Record = { 'address[]': 'Bytes[]', @@ -18,7 +18,7 @@ export const generateFieldAssignment = ( if (type in VALUE_TYPECAST_MAP) { const castTo = VALUE_TYPECAST_MAP[type]; - rightSide = `changeType<${castTo}>(${rightSide})`; + rightSide = `changetype<${castTo}>(${rightSide})`; imports.push(castTo.replace('[]', '')); } @@ -85,11 +85,13 @@ export const generateEventFieldAssignments = ( ); export const generateEventIndexingHandlers = (events: any[], contractName: string) => { - const allImports: string[] = []; - const eventHandlers = events.map(event => { - const { assignments, imports } = generateEventFieldAssignments(event, contractName); - allImports.push(...imports); + const eventFieldAssignments = events.map(event => ({ + event, + ...generateEventFieldAssignments(event, contractName), + })); + const allImports = [...new Set(eventFieldAssignments.map(({ imports }) => imports).flat())]; + const eventHandlers = eventFieldAssignments.map(({ event, assignments }) => { return ` export function handle${event._alias}(event: ${event._alias}Event): void { let entity = new ${event._alias}(event.transaction.hash.concatI32(event.logIndex.toI32())) @@ -111,7 +113,7 @@ export const generateEventIndexingHandlers = (events: any[], contractName: strin import { ${events.map(event => event._alias)} } from '../generated/schema' ${ allImports.length > 0 - ? `import { ${[...new Set(allImports)].join(', ')} } from '@graphprotocol/graph-ts'` + ? `import { ${allImports.join(', ')} } from '@graphprotocol/graph-ts'` : '' } From 84a494157d0f41c2677cc11b68c3a659d4c6bc49 Mon Sep 17 00:00:00 2001 From: YaroShkvorets Date: Thu, 2 Jan 2025 19:42:28 -0500 Subject: [PATCH 5/6] changeset --- .changeset/wild-pears-yell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wild-pears-yell.md diff --git a/.changeset/wild-pears-yell.md b/.changeset/wild-pears-yell.md new file mode 100644 index 000000000..40fd1ff66 --- /dev/null +++ b/.changeset/wild-pears-yell.md @@ -0,0 +1,5 @@ +--- +'@graphprotocol/graph-cli': minor +--- + +handle tuple[] and address[] for event parameters From e77c119a5e5d9332b95f46eca61bab74932bf72d Mon Sep 17 00:00:00 2001 From: YaroShkvorets Date: Fri, 3 Jan 2025 16:45:53 -0500 Subject: [PATCH 6/6] add issue to changeset --- .changeset/wild-pears-yell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wild-pears-yell.md b/.changeset/wild-pears-yell.md index 40fd1ff66..9df6f2b74 100644 --- a/.changeset/wild-pears-yell.md +++ b/.changeset/wild-pears-yell.md @@ -2,4 +2,4 @@ '@graphprotocol/graph-cli': minor --- -handle tuple[] and address[] for event parameters +handle tuple[] and address[] for event parameters - #949