Repository: graphprotocol/graph-tooling (ts)

Issue:
Error for array of address field. 
With Array of address field `graph-cli` generate field as `[Bytes]` which fails while `codegen`.

On fixing `[Bytes]` to `[Bytes!]` in conversion file we are getting error below error as array of bytes cannot be directly assigned to array of address.

```
ERROR TS2322: Type '~lib/array/Array<~lib/@graphprotocol/graph-ts/common/numbers/Address>' is not assignable to type '~lib/array/Array<~lib/@graphprotocol/graph-ts/common/collections/Bytes>'.
```

Suggestion:
As we don't have support directly from `thegraph` for address type attributes (We get validation error), we need to generate code which will loop-over the array fields.


Pull request description:
Add typecasting for tuple[] and address[]
Fixes #949 #1017 

Patch:
diff --git a/.changeset/blue-insects-visit.md b/.changeset/blue-insects-visit.md
new file mode 100644
index 00000000..70092a2e
--- /dev/null
+++ b/.changeset/blue-insects-visit.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': patch
+---
+
+fix bug with duplicate IDs - #1604
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/codegen/abi.ts b/packages/cli/src/protocols/ethereum/codegen/abi.ts
index 46fa09d7..0773f74a 100644
--- a/packages/cli/src/protocols/ethereum/codegen/abi.ts
+++ b/packages/cli/src/protocols/ethereum/codegen/abi.ts
@@ -358,7 +358,8 @@ export default class AbiCodeGenerator {
     }
 
     const tupleIdentifier = parentClass + tsCodegen.namedType(name).capitalize();
-    const tupleClassName = tupleIdentifier + 'Struct';
+    const tupleClassName =
+      tupleIdentifier + (parentField === 'outputValues' ? 'Output' : '') + 'Struct';
     let tupleClasses: any[] = [];
 
     const isTupleType = util.isTupleType(type);
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/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<string, string> = {
+  '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')}
 `;
+};


Test patch:
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<Bytes[]>(event.params.tupleArray)
+  entity.addressArray = changetype<Bytes[]>(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<ethereum.Tuple>,
+  addressArray: Array<Address>
+): TupleArrayEvent {
+  let tupleArrayEventEvent = changetype<TupleArrayEvent>(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<ethereum.Tuple>,
+  addressArray: Array<Address>
+): TupleArrayEvent {
+  let tupleArrayEventEvent = changetype<TupleArrayEvent>(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'];