Repository: 0xs34n/starknet.js
Pull request #263 (2022-07-19 06:42:10)

Struct array hashing
Fixes https://github.com/0xs34n/starknet.js/issues/260 as well as generation of the type hash with struct array dependencies.

Hashes an array of structs like so:
```
// struct s
a = [s1, s2, ..., sn]

// hash each struct individually to get a list of hashes(felts)
[H(s1), H(s2), ..., H(sn)]

// hash list of felts as per usual (computeHashOnElements in starknet.js)
H(a) = H(...H(H(H(s1), H(s2)), H(s3)), ..., H(sn))
```



Patch:
diff --git a/__mocks__/typedDataStructArrayExample.json b/__mocks__/typedDataStructArrayExample.json
new file mode 100644
index 00000000..fe4e6506
--- /dev/null
+++ b/__mocks__/typedDataStructArrayExample.json
@@ -0,0 +1,44 @@
+{
+  "types": {
+    "StarkNetDomain": [
+      { "name": "name", "type": "felt" },
+      { "name": "version", "type": "felt" },
+      { "name": "chainId", "type": "felt" }
+    ],
+    "Person": [
+      { "name": "name", "type": "felt" },
+      { "name": "wallet", "type": "felt" }
+    ],
+    "Post": [
+      { "name": "title", "type": "felt" },
+      { "name": "content", "type": "felt" }
+    ],
+    "Mail": [
+      { "name": "from", "type": "Person" },
+      { "name": "to", "type": "Person" },
+      { "name": "posts_len", "type": "felt" },
+      { "name": "posts", "type": "Post*" }
+    ]
+  },
+  "primaryType": "Mail",
+  "domain": {
+    "name": "StarkNet Mail",
+    "version": "1",
+    "chainId": 1
+  },
+  "message": {
+    "from": {
+      "name": "Cow",
+      "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
+    },
+    "to": {
+      "name": "Bob",
+      "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
+    },
+    "posts_len": 2,
+    "posts": [
+      { "title": "Greeting", "content": "Hello, Bob!" },
+      { "title": "Farewell", "content": "Goodbye, Bob!" }
+    ]
+  }
+}
diff --git a/src/utils/typedData/index.ts b/src/utils/typedData/index.ts
index 386e3a8a..fbde6ee4 100644
--- a/src/utils/typedData/index.ts
+++ b/src/utils/typedData/index.ts
@@ -36,6 +36,12 @@ export const getDependencies = (
     throw new Error('Typed data does not match JSON schema');
   }
 
+  // Include pointers (struct arrays)
+  if (type[type.length - 1] === '*') {
+    // eslint-disable-next-line no-param-reassign
+    type = type.slice(0, -1);
+  }
+
   if (dependencies.includes(type)) {
     return dependencies;
   }
@@ -102,6 +108,18 @@ const encodeValue = (typedData: TypedData, type: string, data: unknown): [string
     return [type, getStructHash(typedData, type, data as Record<string, unknown>)];
   }
 
+  if (
+    Object.keys(typedData.types)
+      .map((x) => `${x}*`)
+      .includes(type)
+  ) {
+    const structHashes: string[] = (data as unknown[]).map((struct) => {
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      return getStructHash(typedData, type.slice(0, -1), struct as Record<string, unknown>);
+    });
+    return [type, computeHashOnElements(structHashes)];
+  }
+
   if (type === 'felt*') {
     return ['felt*', computeHashOnElements(data as string[])];
   }


Test patch:
diff --git a/__tests__/utils/typedData.test.ts b/__tests__/utils/typedData.test.ts
index b59aed0d..30d52439 100644
--- a/__tests__/utils/typedData.test.ts
+++ b/__tests__/utils/typedData.test.ts
@@ -1,4 +1,5 @@
 import typedDataExample from '../../__mocks__/typedDataExample.json';
+import typedDataStructArrayExample from '../../__mocks__/typedDataStructArrayExample.json';
 import { number } from '../../src';
 import { BigNumberish } from '../../src/utils/number';
 import { encodeType, getMessageHash, getStructHash, getTypeHash } from '../../src/utils/typedData';
@@ -9,7 +10,12 @@ describe('typedData', () => {
     expect(typeEncoding).toMatchInlineSnapshot(
       `"Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)"`
     );
+    const typeEncodingStructArr = encodeType(typedDataStructArrayExample, 'Mail');
+    expect(typeEncodingStructArr).toMatchInlineSnapshot(
+      `"Mail(from:Person,to:Person,posts_len:felt,posts:Post*)Person(name:felt,wallet:felt)Post(title:felt,content:felt)"`
+    );
   });
+
   test('should get right type hash', () => {
     const typeHashDomain = getTypeHash(typedDataExample, 'StarkNetDomain');
     expect(typeHashDomain).toMatchInlineSnapshot(
@@ -23,18 +29,36 @@ describe('typedData', () => {
     expect(typeHashMail).toMatchInlineSnapshot(
       `"0x13d89452df9512bf750f539ba3001b945576243288137ddb6c788457d4b2f79"`
     );
+    const typeHashPost = getTypeHash(typedDataStructArrayExample, 'Post');
+    expect(typeHashPost).toMatchInlineSnapshot(
+      `"0x1d71e69bf476486b43cdcfaf5a85c00bb2d954c042b281040e513080388356d"`
+    );
+    const typeHashMailWithStructArray = getTypeHash(typedDataStructArrayExample, 'Mail');
+    expect(typeHashMailWithStructArray).toMatchInlineSnapshot(
+      `"0x873b878e35e258fc99e3085d5aaad3a81a0c821f189c08b30def2cde55ff27"`
+    );
   });
+
   test('should get right hash for StarkNetDomain', () => {
     const hash = getStructHash(typedDataExample, 'StarkNetDomain', typedDataExample.domain as any);
     expect(hash).toMatchInlineSnapshot(
       `"0x54833b121883a3e3aebff48ec08a962f5742e5f7b973469c1f8f4f55d470b07"`
     );
   });
+
   test('should get right hash for entire message', () => {
     const hash = getMessageHash(typedDataExample, '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826');
     expect(hash).toMatchInlineSnapshot(
       `"0x6fcff244f63e38b9d88b9e3378d44757710d1b244282b435cb472053c8d78d0"`
     );
+
+    const hashStructArr = getMessageHash(
+      typedDataStructArrayExample,
+      '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
+    );
+    expect(hashStructArr).toMatchInlineSnapshot(
+      `"0x5914ed2764eca2e6a41eb037feefd3d2e33d9af6225a9e7fe31ac943ff712c"`
+    );
   });
 
   interface StringStruct {