From b4564a71134c310dee983f6eff613b953bf481be Mon Sep 17 00:00:00 2001 From: Yaro Shkvorets Date: Tue, 7 Jan 2025 19:43:57 -0500 Subject: [PATCH] Add typecasting for tuple[] and address[] (#1892) * wrap address[] and tuple[] into changetype * tests * pass imports array * cleanup mapping codegen * changeset * add issue to changeset --- .changeset/wild-pears-yell.md | 5 + packages/cli/src/codegen/util.ts | 2 +- .../protocols/ethereum/scaffold/mapping.ts | 2 +- .../__snapshots__/ethereum.test.ts.snap | 171 +++++++++++++++++- packages/cli/src/scaffold/ethereum.test.ts | 28 ++- packages/cli/src/scaffold/mapping.ts | 124 +++++++++---- 6 files changed, 289 insertions(+), 43 deletions(-) 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 00000000..9df6f2b7 --- /dev/null +++ b/.changeset/wild-pears-yell.md @@ -0,0 +1,5 @@ +--- +'@graphprotocol/graph-cli': minor +--- + +handle tuple[] and address[] for event parameters - #949 diff --git a/packages/cli/src/codegen/util.ts b/packages/cli/src/codegen/util.ts index 0fc8b5c4..c675ad24 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 113996a7..6bdc2c5a 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/__snapshots__/ethereum.test.ts.snap b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap index ad94388f..b76a3860 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 3acdc649..da4d7e99 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']; diff --git a/packages/cli/src/scaffold/mapping.ts b/packages/cli/src/scaffold/mapping.ts index 60244a92..818f967b 100644 --- a/packages/cli/src/scaffold/mapping.ts +++ b/packages/cli/src/scaffold/mapping.ts @@ -1,17 +1,58 @@ import * as util from '../codegen/util.js'; -export const generateFieldAssignment = (key: string[], value: string[]) => - `entity.${key.join('_')} = event.params.${value.join('.')}`; - -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)) - : generateFieldAssignment( - [(input.mappedName ?? input.name) || `param${index}`], - [input.name || `param${index}`], - ); +/** + * 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 = ( + key: string[], + value: string[], + type: string, +): { assignment: string; imports: string[] } => { + let rightSide = `event.params.${value.join('.')}`; + const imports = []; + + if (type in VALUE_TYPECAST_MAP) { + const castTo = VALUE_TYPECAST_MAP[type]; + rightSide = `changetype<${castTo}>(${rightSide})`; + imports.push(castTo.replace('[]', '')); + } + + return { + assignment: `entity.${key.join('_')} = ${rightSide}`, + imports, + }; +}; + +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 @@ -27,26 +68,34 @@ 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' +export const generateEventIndexingHandlers = (events: any[], contractName: string) => { + const eventFieldAssignments = events.map(event => ({ + event, + ...generateEventFieldAssignments(event, contractName), + })); + const allImports = [...new Set(eventFieldAssignments.map(({ imports }) => imports).flat())]; - ${events - .map( - event => - ` + 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())) - ${generateEventFieldAssignments(event, contractName).join('\n')} + ${assignments.join('\n')} entity.blockNumber = event.block.number entity.blockTimestamp = event.block.timestamp @@ -54,7 +103,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 { ${allImports.join(', ')} } from '@graphprotocol/graph-ts'` + : '' + } + + ${eventHandlers.join('\n')} `; +};