Repository: graphprotocol/graph-tooling
Pull request #1974 (2025-02-27 18:43:14)

`graph init`: handle non-standard Etherscan API responses
Kaia returned non-standard Etherscan response, i.e. object instead of array of objects, number instead of string for start block. This resulted in unhandled `graph init` exception.
This PR fixes it.

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: bb6cf87645c242389bdf93ff0e60b2ef575ff145

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                     | Type  |
| ------------------------ | ----- |
| @graphprotocol/graph-cli | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/graphprotocol/graph-tooling/new/yaro/non-standard-api?filename=.changeset/twelve-sheep-switch.md&value=---%0A%22%40graphprotocol%2Fgraph-cli%22%3A%20patch%0A---%0A%0A%60graph%20init%60%3A%20handle%20non-standard%20Etherscan%20API%20responses%0A)


github-actions[bot]: <!-- changesetsSnapshotPrCommentKey -->
### 🚀 Snapshot Release (`alpha`)

The latest changes of this PR are available as `alpha` on npm (based on the declared `changesets`):
| Package | Version | Info |
|------|---------|----|
| `@graphprotocol/graph-cli` | `0.96.1-alpha-20250227184351-bb6cf87645c242389bdf93ff0e60b2ef575ff145` | [npm ↗︎](https://www.npmjs.com/package/@graphprotocol/graph-cli/v/0.96.1-alpha-20250227184351-bb6cf87645c242389bdf93ff0e60b2ef575ff145) [unpkg ↗︎](https://unpkg.com/browse/@graphprotocol/graph-cli@0.96.1-alpha-20250227184351-bb6cf87645c242389bdf93ff0e60b2ef575ff145/) |

Patch:
diff --git a/.changeset/flat-hairs-study.md b/.changeset/flat-hairs-study.md
new file mode 100644
index 00000000..d74b5cfa
--- /dev/null
+++ b/.changeset/flat-hairs-study.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': patch
+---
+
+handle sized arrays in abi when changetype
diff --git a/.changeset/nervous-garlics-cover.md b/.changeset/nervous-garlics-cover.md
new file mode 100644
index 00000000..ee46db8a
--- /dev/null
+++ b/.changeset/nervous-garlics-cover.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': patch
+---
+
+show sourcify fetch status
diff --git a/.changeset/new-rules-bake.md b/.changeset/new-rules-bake.md
new file mode 100644
index 00000000..93c2044c
--- /dev/null
+++ b/.changeset/new-rules-bake.md
@@ -0,0 +1,5 @@
+---
+"@graphprotocol/graph-cli": patch
+---
+
+fix: generate types for mappings when int8 is used as id
diff --git a/.changeset/purple-files-think.md b/.changeset/purple-files-think.md
new file mode 100644
index 00000000..9b2864a6
--- /dev/null
+++ b/.changeset/purple-files-think.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': patch
+---
+
+`graph codegen`: fix bug with id field clash in example entity
diff --git a/.changeset/red-roses-speak.md b/.changeset/red-roses-speak.md
new file mode 100644
index 00000000..0896dba8
--- /dev/null
+++ b/.changeset/red-roses-speak.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': patch
+---
+
+handle invalid characters in contract name #1883
diff --git a/.changeset/sweet-spoons-accept.md b/.changeset/sweet-spoons-accept.md
new file mode 100644
index 00000000..55cd2ac6
--- /dev/null
+++ b/.changeset/sweet-spoons-accept.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': patch
+---
+
+handle non-standard Etherscan API responses, i.e. kaia
diff --git a/packages/cli/src/codegen/schema.ts b/packages/cli/src/codegen/schema.ts
index ef98482a..3bbcd5c9 100644
--- a/packages/cli/src/codegen/schema.ts
+++ b/packages/cli/src/codegen/schema.ts
@@ -16,8 +16,9 @@ import * as util from './util.js';
 class IdField {
   static BYTES = Symbol('Bytes');
   static STRING = Symbol('String');
+  static INT8 = Symbol('Int8');
 
-  private kind: typeof IdField.BYTES | typeof IdField.STRING;
+  private kind: typeof IdField.BYTES | typeof IdField.INT8 | typeof IdField.STRING;
 
   constructor(idField: FieldDefinitionNode | undefined) {
     if (idField?.type.kind !== 'NonNullType') {
@@ -27,15 +28,46 @@ class IdField {
       throw Error('id field must be a named type');
     }
     const typeName = idField.type.type.name.value;
-    this.kind = typeName === 'Bytes' ? IdField.BYTES : IdField.STRING;
+    switch (typeName) {
+      case 'Bytes':
+        this.kind = IdField.BYTES;
+        break;
+      case 'Int8':
+        this.kind = IdField.INT8;
+        break;
+      case 'String':
+        this.kind = IdField.STRING;
+        break;
+      default:
+        this.kind = IdField.STRING;
+        break;
+    }
   }
 
   typeName() {
-    return this.kind === IdField.BYTES ? 'Bytes' : 'string';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'Bytes';
+      case IdField.INT8:
+        return 'Int8';
+      case IdField.STRING:
+        return 'string';
+      default:
+        return 'string';
+    }
   }
 
   gqlTypeName() {
-    return this.kind === IdField.BYTES ? 'Bytes' : 'String';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'Bytes';
+      case IdField.INT8:
+        return 'Int8';
+      case IdField.STRING:
+        return 'String';
+      default:
+        return 'String';
+    }
   }
 
   tsNamedType() {
@@ -43,19 +75,55 @@ class IdField {
   }
 
   tsValueFrom() {
-    return this.kind === IdField.BYTES ? 'Value.fromBytes(id)' : 'Value.fromString(id)';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'Value.fromBytes(id)';
+      case IdField.INT8:
+        return 'Value.fromI64(id)';
+      case IdField.STRING:
+        return 'Value.fromString(id)';
+      default:
+        return 'Value.fromString(id)';
+    }
   }
 
   tsValueKind() {
-    return this.kind === IdField.BYTES ? 'ValueKind.BYTES' : 'ValueKind.STRING';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'ValueKind.BYTES';
+      case IdField.INT8:
+        return 'ValueKind.INT8';
+      case IdField.STRING:
+        return 'ValueKind.STRING';
+      default:
+        return 'ValueKind.STRING';
+    }
   }
 
   tsValueToString() {
-    return this.kind == IdField.BYTES ? 'id.toBytes().toHexString()' : 'id.toString()';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'id.toBytes().toHexString()';
+      case IdField.INT8:
+        return 'id.toI64().toString()';
+      case IdField.STRING:
+        return 'id.toString()';
+      default:
+        return 'id.toString()';
+    }
   }
 
   tsToString() {
-    return this.kind == IdField.BYTES ? 'id.toHexString()' : 'id';
+    switch (this.kind) {
+      case IdField.BYTES:
+        return 'id.toHexString()';
+      case IdField.INT8:
+        return 'id.toString()';
+      case IdField.STRING:
+        return 'id';
+      default:
+        return 'id';
+    }
   }
 
   static fromFields(fields: readonly FieldDefinitionNode[] | undefined) {
@@ -92,6 +160,7 @@ export default class SchemaCodeGenerator {
           'Bytes',
           'BigInt',
           'BigDecimal',
+          'Int8',
         ],
         '@graphprotocol/graph-ts',
       ),
diff --git a/packages/cli/src/command-helpers/contracts.ts b/packages/cli/src/command-helpers/contracts.ts
index 85a36a9f..a47e79ba 100644
--- a/packages/cli/src/command-helpers/contracts.ts
+++ b/packages/cli/src/command-helpers/contracts.ts
@@ -164,10 +164,10 @@ export class ContractService {
           this.fetchFromEtherscan(
             `${url}?module=contract&action=getsourcecode&address=${address}`,
           ).then(json => {
-            if (!json?.result?.length) {
+            if (!json?.result) {
               throw new Error(`No result: ${JSON.stringify(json)}`);
             }
-            const { ContractName } = json.result[0];
+            const { ContractName } = json.result?.[0] ?? json.result ?? {};
             if (!ContractName) {
               throw new Error('Contract name is empty');
             }
diff --git a/packages/cli/src/command-helpers/spinner.ts b/packages/cli/src/command-helpers/spinner.ts
index 7e379ee2..de6eb8e6 100644
--- a/packages/cli/src/command-helpers/spinner.ts
+++ b/packages/cli/src/command-helpers/spinner.ts
@@ -36,7 +36,7 @@ export const withSpinner = async (
   const spinner = print.spin(text);
   try {
     const result = await f(spinner);
-    if (typeof result === 'object') {
+    if (result && typeof result === 'object') {
       const hasError = Object.keys(result).includes('error');
       const hasWarning = Object.keys(result).includes('warning');
       const hasResult = Object.keys(result).includes('result');
diff --git a/packages/cli/src/command-helpers/subgraph.ts b/packages/cli/src/command-helpers/subgraph.ts
index 5ca396db..2a72b58b 100644
--- a/packages/cli/src/command-helpers/subgraph.ts
+++ b/packages/cli/src/command-helpers/subgraph.ts
@@ -9,3 +9,7 @@ export const formatSubgraphName = (slug: string) => {
     .replace(/\s+/g, '-')
     .replace(/[^a-z0-9_-]/g, '');
 };
+
+export const formatContractName = (contractName: string) => {
+  return contractName.replace(/[\s.]+/g, '_').replace(/[^a-zA-Z0-9_-]/g, '');
+};
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 9bb408ff..78f9c305 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -16,7 +16,11 @@ import { retryWithPrompt } from '../command-helpers/retry.js';
 import { generateScaffold, writeScaffold } from '../command-helpers/scaffold.js';
 import { sortWithPriority } from '../command-helpers/sort.js';
 import { withSpinner } from '../command-helpers/spinner.js';
-import { formatSubgraphName, getSubgraphBasename } from '../command-helpers/subgraph.js';
+import {
+  formatContractName,
+  formatSubgraphName,
+  getSubgraphBasename,
+} from '../command-helpers/subgraph.js';
 import { GRAPH_CLI_SHARED_HEADERS } from '../constants.js';
 import debugFactory from '../debug.js';
 import EthereumABI from '../protocols/ethereum/abi.js';
@@ -289,7 +293,7 @@ export default class InitCommand extends Command {
           indexEvents,
           network,
           subgraphName,
-          contractName: contractName || DEFAULT_CONTRACT_NAME,
+          contractName,
           node,
           startBlock,
           spkgPath,
@@ -351,7 +355,7 @@ export default class InitCommand extends Command {
           network: answers.network,
           source: answers.source,
           indexEvents: answers.indexEvents,
-          contractName: answers.contractName || DEFAULT_CONTRACT_NAME,
+          contractName: answers.contractName,
           node,
           startBlock: answers.startBlock,
           spkgPath: answers.spkgPath,
@@ -710,33 +714,34 @@ async function processInitForm(
           return address;
         }
 
-        const sourcifyContractInfo = await contractService.getFromSourcify(
-          EthereumABI,
-          network.id,
-          address,
-        );
-        if (sourcifyContractInfo) {
-          initStartBlock ??= sourcifyContractInfo.startBlock;
-          initContractName ??= sourcifyContractInfo.name;
-          initAbi ??= sourcifyContractInfo.abi;
-          initDebugger.extend('processInitForm')(
-            "infoFromSourcify: '%s'/'%s'",
-            initStartBlock,
-            initContractName,
-          );
-        }
-
-        // If ABI is not provided, try to fetch it from Etherscan API
+        // If ABI is not provided, try to fetch it from Sourcify/Etherscan API
         if (protocolInstance.hasABIs() && !initAbi) {
-          abiFromApi = await retryWithPrompt(() =>
-            withSpinner(
-              'Fetching ABI from contract API...',
-              'Failed to fetch ABI',
-              'Warning fetching ABI',
-              () => contractService.getABI(protocolInstance.getABI(), network.id, address),
-            ),
+          const sourcifyContractInfo = await withSpinner(
+            'Fetching ABI from Sourcify API...',
+            'Failed to fetch ABI',
+            'Warning fetching ABI',
+            () => contractService.getFromSourcify(protocolInstance.getABI(), network.id, address),
           );
-          initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name);
+          if (sourcifyContractInfo) {
+            initStartBlock ??= sourcifyContractInfo.startBlock;
+            initContractName ??= sourcifyContractInfo.name;
+            abiFromApi ??= sourcifyContractInfo.abi;
+            initDebugger.extend('processInitForm')(
+              "infoFromSourcify: '%s'/'%s'",
+              initStartBlock,
+              initContractName,
+            );
+          } else {
+            abiFromApi = await retryWithPrompt(() =>
+              withSpinner(
+                'Fetching ABI from Contract API...',
+                'Failed to fetch ABI',
+                'Warning fetching ABI',
+                () => contractService.getABI(protocolInstance.getABI(), network.id, address),
+              ),
+            );
+            initDebugger.extend('processInitForm')("abiFromEtherscan ABI: '%s'", abiFromApi?.name);
+          }
         } else {
           abiFromApi = initAbi;
         }
@@ -745,7 +750,7 @@ async function processInitForm(
         if (!initStartBlock) {
           startBlock = await retryWithPrompt(() =>
             withSpinner(
-              'Fetching start block from contract API...',
+              'Fetching start block from Contract API...',
               'Failed to fetch start block',
               'Warning fetching start block',
               () => contractService.getStartBlock(network.id, address),
@@ -758,7 +763,7 @@ async function processInitForm(
         if (!initContractName) {
           contractName = await retryWithPrompt(() =>
             withSpinner(
-              'Fetching contract name from contract API...',
+              'Fetching contract name from Contract API...',
               'Failed to fetch contract name',
               'Warning fetching contract name',
               () => contractService.getContractName(network.id, address),
@@ -854,7 +859,7 @@ async function processInitForm(
       type: 'input',
       name: 'startBlock',
       message: 'Start block',
-      initial: () => initStartBlock || startBlock || '0',
+      initial: () => String(initStartBlock || startBlock || 0),
       skip: () => initFromExample !== undefined || isSubstreams,
       validate: value =>
         initFromExample !== undefined ||
@@ -872,7 +877,7 @@ async function processInitForm(
       type: 'input',
       name: 'contractName',
       message: 'Contract name',
-      initial: () => initContractName || contractName || 'Contract',
+      initial: () => initContractName || contractName || DEFAULT_CONTRACT_NAME,
       skip: () => initFromExample !== undefined || !protocolInstance.hasContract() || isSubstreams,
       validate: value =>
         initFromExample !== undefined ||
@@ -1307,7 +1312,7 @@ async function initSubgraphFromContract(
           network,
           source,
           indexEvents,
-          contractName,
+          contractName: formatContractName(contractName || DEFAULT_CONTRACT_NAME),
           startBlock,
           node,
           spkgPath,
diff --git a/packages/cli/src/scaffold/mapping.ts b/packages/cli/src/scaffold/mapping.ts
index a363f21c..39d7196f 100644
--- a/packages/cli/src/scaffold/mapping.ts
+++ b/packages/cli/src/scaffold/mapping.ts
@@ -15,12 +15,13 @@ export const generateFieldAssignment = (
 ): { assignment: string; imports: string[] } => {
   const safeKey = key.map(k => util.handleReservedWord(k));
   const safeValue = value.map(v => util.handleReservedWord(v));
+  const cleanType = type.replace(/\[\d+\]/g, '[]');
 
   let rightSide = `event.params.${safeValue.join('.')}`;
   const imports = [];
 
-  if (type in VALUE_TYPECAST_MAP) {
-    const castTo = VALUE_TYPECAST_MAP[type];
+  if (cleanType in VALUE_TYPECAST_MAP) {
+    const castTo = VALUE_TYPECAST_MAP[cleanType];
     rightSide = `changetype<${castTo}>(${rightSide})`;
     imports.push(castTo.replace('[]', ''));
   }
diff --git a/packages/cli/src/scaffold/schema.ts b/packages/cli/src/scaffold/schema.ts
index e3b00b9b..9f66b506 100644
--- a/packages/cli/src/scaffold/schema.ts
+++ b/packages/cli/src/scaffold/schema.ts
@@ -89,11 +89,10 @@ export const generateExampleEntityType = (protocol: Protocol, events: any[]) =>
   id: Bytes!
   count: BigInt!
   ${events[0].inputs
-    .reduce(
-      (acc: any[], input: any, index: number) =>
-        acc.concat(generateEventFields({ input, index, protocolName: protocol.name })),
-      [],
-    )
+    .reduce((acc: any[], input: any, index: number) => {
+      input.name = renameNameIfNeeded(input.name);
+      return acc.concat(generateEventFields({ input, index, protocolName: protocol.name }));
+    }, [])
     .slice(0, 2)
     .join('\n')}
 }`;
diff --git a/packages/ts/common/value.ts b/packages/ts/common/value.ts
index ce2a576e..8f7a2959 100644
--- a/packages/ts/common/value.ts
+++ b/packages/ts/common/value.ts
@@ -339,6 +339,14 @@ export class Value {
     return Value.fromArray(output);
   }
 
+  static fromI64Array(input: Array<i64>): Value {
+    const output = new Array<Value>(input.length);
+    for (let i: i64 = 0; i < input.length; i++) {
+      output[i] = Value.fromI64(input[i]);
+    }
+    return Value.fromArray(output);
+  }
+
   static fromBigIntArray(input: Array<BigInt>): Value {
     const output = new Array<Value>(input.length);
     for (let i: i32 = 0; i < input.length; i++) {


Test patch:
diff --git a/packages/cli/src/codegen/schema.test.ts b/packages/cli/src/codegen/schema.test.ts
index cbf3002c..fd1da33f 100644
--- a/packages/cli/src/codegen/schema.test.ts
+++ b/packages/cli/src/codegen/schema.test.ts
@@ -583,6 +583,134 @@ describe('Schema code generator', { concurrent: true }, () => {
     });
   });
 
+  test('Should handle references with Int8 id types', async () => {
+    const codegen = createSchemaCodeGen(`
+    interface Employee {
+      id: Int8!
+      name: String!
+     }
+
+    type Worker implements Employee @entity {
+      id: Int8!
+      name: String!
+      tasks: [Task!]
+   }
+
+    type Task @entity {
+      id: Int8!
+      employee: Employee!
+      workers: [Worker!] @derivedFrom(field: "tasks")
+      worker: Worker!
+   }
+`);
+
+    const generatedTypes = codegen.generateTypes();
+    await testEntity(generatedTypes, {
+      name: 'Task',
+      members: [],
+      methods: [
+        {
+          name: 'constructor',
+          params: [new Param('id', new NamedType('Int8'))],
+          returnType: undefined,
+          body: "\n      super()\n      this.set('id', Value.fromI64(id))\n      ",
+        },
+        {
+          name: 'save',
+          params: [],
+          returnType: new NamedType('void'),
+          body:
+            '\n' +
+            "        let id = this.get('id')\n" +
+            '        assert(id != null,\n' +
+            "               'Cannot save Task entity without an ID')\n" +
+            '        if (id) {\n' +
+            '          assert(id.kind == ValueKind.INT8,\n' +
+            "                 `Entities of type Task must have an ID of type Int8 but the id '${id.displayData()}' is of type ${id.displayKind()}`)\n" +
+            "          store.set('Task', id.toI64().toString(), this)\n" +
+            '        }',
+        },
+        {
+          name: 'loadInBlock',
+          static: true,
+          params: [new Param('id', new NamedType('Int8'))],
+          returnType: new NullableType(new NamedType('Task')),
+          body:
+            '\n' +
+            "        return changetype<Task | null>(store.get_in_block('Task', id.toString()))\n" +
+            '        ',
+        },
+        {
+          name: 'load',
+          static: true,
+          params: [new Param('id', new NamedType('Int8'))],
+          returnType: new NullableType(new NamedType('Task')),
+          body:
+            '\n' +
+            "        return changetype<Task | null>(store.get('Task', id.toString()))\n" +
+            '        ',
+        },
+        {
+          name: 'get id',
+          params: [],
+          returnType: new NamedType('i64'),
+          body: `let value = this.get("id")
+            if (!value || value.kind == ValueKind.NULL) {
+               return 0;
+             } else {
+               return value.toI64()
+             }`,
+        },
+        {
+          name: 'set id',
+          params: [new Param('value', new NamedType('i64'))],
+          returnType: undefined,
+          body: "\n      this.set('id', Value.fromI64(value))\n    ",
+        },
+        {
+          name: 'get employee',
+          params: [],
+          returnType: new NamedType('i64'),
+          body: `let value = this.get('employee')
+            if (!value || value.kind == ValueKind.NULL) {
+              return 0;
+            } else {
+              return value.toI64()
+            }`,
+        },
+        {
+          name: 'set employee',
+          params: [new Param('value', new NamedType('i64'))],
+          returnType: undefined,
+          body: "\n      this.set('employee', Value.fromI64(value))\n    ",
+        },
+        {
+          name: 'get worker',
+          params: [],
+          returnType: new NamedType('i64'),
+          body: `let value = this.get('worker')
+          if (!value || value.kind == ValueKind.NULL) {
+            return 0;
+          } else {
+            return value.toI64()
+          }`,
+        },
+        {
+          name: 'set worker',
+          params: [new Param('value', new NamedType('i64'))],
+          returnType: undefined,
+          body: "\n      this.set('worker', Value.fromI64(value))\n    ",
+        },
+        {
+          name: 'get workers',
+          params: [],
+          returnType: new NamedType('WorkerLoader'),
+          body: "\n      return new WorkerLoader('Task', this.get('id')!.toString(), 'workers')\n    ",
+        },
+      ],
+    });
+  });
+
   test('get related method for WithBytes entity', async () => {
     const codegen = createSchemaCodeGen(`
       type WithBytes @entity {
diff --git a/packages/cli/src/command-helpers/subgraph.test.ts b/packages/cli/src/command-helpers/subgraph.test.ts
new file mode 100644
index 00000000..b3ac994e
--- /dev/null
+++ b/packages/cli/src/command-helpers/subgraph.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, it } from 'vitest';
+import { formatContractName, formatSubgraphName, getSubgraphBasename } from './subgraph';
+
+describe('getSubgraphBasename', () => {
+  it('returns the last segment of a subgraph name', () => {
+    expect(getSubgraphBasename('user/my-subgraph')).toBe('my-subgraph');
+    expect(getSubgraphBasename('org/project')).toBe('project');
+  });
+
+  it('returns the full name if no slash is present', () => {
+    expect(getSubgraphBasename('single-name')).toBe('single-name');
+  });
+});
+
+describe('formatSubgraphName', () => {
+  it('converts to lowercase', () => {
+    expect(formatSubgraphName('MySubGraph')).toBe('mysubgraph');
+  });
+
+  it('replaces spaces with hyphens', () => {
+    expect(formatSubgraphName('my subgraph name')).toBe('my-subgraph-name');
+    expect(formatSubgraphName('multiple   spaces')).toBe('multiple-spaces');
+  });
+
+  it('removes special characters', () => {
+    expect(formatSubgraphName('my$special@subgraph!')).toBe('myspecialsubgraph');
+  });
+
+  it('keeps alphanumeric characters, hyphens and underscores', () => {
+    expect(formatSubgraphName('my-subgraph_123')).toBe('my-subgraph_123');
+  });
+});
+
+describe('formatContractName', () => {
+  it('replaces spaces and dots with underscores', () => {
+    expect(formatContractName('My Contract')).toBe('My_Contract');
+    expect(formatContractName('contract.name')).toBe('contract_name');
+    expect(formatContractName('multiple...dots')).toBe('multiple_dots');
+  });
+
+  it('removes special characters but keeps alphanumeric, hyphens and underscores', () => {
+    expect(formatContractName('Contract$$$Name')).toBe('ContractName');
+    expect(formatContractName('My-Contract_123')).toBe('My-Contract_123');
+    expect(formatContractName('My Contract $$$$')).toBe('My_Contract_');
+  });
+
+  it('preserves case', () => {
+    expect(formatContractName('MyContractName')).toBe('MyContractName');
+  });
+});
diff --git a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap
index 5bc28e0f..e0264847 100644
--- a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap
+++ b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap
@@ -30,7 +30,7 @@ dataSources:
           handler: handleExampleEvent
         - event: ExampleEvent(bytes32)
           handler: handleExampleEvent1
-        - event: TupleArrayEvent((uint256,address)[],address[])
+        - event: TupleArrayEvent((uint256,address)[],address[],address[10])
           handler: handleTupleArrayEvent
       file: ./src/contract.ts
 "
@@ -153,6 +153,7 @@ export function handleTupleArrayEvent(event: TupleArrayEventEvent): void {
   )
   entity.tupleArray = changetype<Bytes[]>(event.params.tupleArray)
   entity.addressArray = changetype<Bytes[]>(event.params.addressArray)
+  entity.addressSizedArray = changetype<Bytes[]>(event.params.addressSizedArray)
 
   entity.blockNumber = event.block.number
   entity.blockTimestamp = event.block.timestamp
@@ -266,6 +267,7 @@ type TupleArrayEvent @entity(immutable: true) {
   id: Bytes!
   tupleArray: [Bytes!]! # tuple[]
   addressArray: [Bytes!]! # address[]
+  addressSizedArray: [Bytes!]! # address[10]
   blockNumber: BigInt!
   blockTimestamp: BigInt!
   transactionHash: Bytes!
@@ -409,7 +411,8 @@ export function createExampleEvent1Event(a: Bytes): ExampleEvent1 {
 
 export function createTupleArrayEventEvent(
   tupleArray: Array<ethereum.Tuple>,
-  addressArray: Array<Address>
+  addressArray: Array<Address>,
+  addressSizedArray: Array<Address>
 ): TupleArrayEvent {
   let tupleArrayEventEvent = changetype<TupleArrayEvent>(newMockEvent())
 
@@ -427,6 +430,12 @@ export function createTupleArrayEventEvent(
       ethereum.Value.fromAddressArray(addressArray)
     )
   )
+  tupleArrayEventEvent.parameters.push(
+    new ethereum.EventParam(
+      "addressSizedArray",
+      ethereum.Value.fromAddressArray(addressSizedArray)
+    )
+  )
 
   return tupleArrayEventEvent
 }
@@ -569,7 +578,8 @@ export function createExampleEvent1Event(a: Bytes): ExampleEvent1 {
 
 export function createTupleArrayEventEvent(
   tupleArray: Array<ethereum.Tuple>,
-  addressArray: Array<Address>
+  addressArray: Array<Address>,
+  addressSizedArray: Array<Address>
 ): TupleArrayEvent {
   let tupleArrayEventEvent = changetype<TupleArrayEvent>(newMockEvent())
 
@@ -587,6 +597,12 @@ export function createTupleArrayEventEvent(
       ethereum.Value.fromAddressArray(addressArray)
     )
   )
+  tupleArrayEventEvent.parameters.push(
+    new ethereum.EventParam(
+      "addressSizedArray",
+      ethereum.Value.fromAddressArray(addressSizedArray)
+    )
+  )
 
   return tupleArrayEventEvent
 }
diff --git a/packages/cli/src/scaffold/ethereum.test.ts b/packages/cli/src/scaffold/ethereum.test.ts
index da4d7e99..9652039c 100644
--- a/packages/cli/src/scaffold/ethereum.test.ts
+++ b/packages/cli/src/scaffold/ethereum.test.ts
@@ -69,6 +69,7 @@ const TEST_TUPLE_ARRAY_EVENT = {
       ],
     },
     { name: 'addressArray', type: 'address[]' },
+    { name: 'addressSizedArray', type: 'address[10]' },
   ],
 };