From e1f82dfd575be4c84b291c33f8169bf367493603 Mon Sep 17 00:00:00 2001 From: calvwang9 Date: Tue, 19 Jul 2022 16:20:25 +1000 Subject: [PATCH] fix: struct array hashing --- __mocks__/typedDataStructArrayExample.json | 44 ++++++++++++++++++++++ __tests__/utils/typedData.test.ts | 24 ++++++++++++ src/utils/typedData/index.ts | 18 +++++++++ 3 files changed, 86 insertions(+) create mode 100644 __mocks__/typedDataStructArrayExample.json diff --git a/__mocks__/typedDataStructArrayExample.json b/__mocks__/typedDataStructArrayExample.json new file mode 100644 index 000000000..fe4e6506b --- /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/__tests__/utils/typedData.test.ts b/__tests__/utils/typedData.test.ts index b59aed0da..30d52439a 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 { diff --git a/src/utils/typedData/index.ts b/src/utils/typedData/index.ts index 386e3a8a7..fbde6ee44 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)]; } + 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); + }); + return [type, computeHashOnElements(structHashes)]; + } + if (type === 'felt*') { return ['felt*', computeHashOnElements(data as string[])]; }