diff --git a/.changeset/strange-fans-move.md b/.changeset/strange-fans-move.md new file mode 100644 index 0000000000..a6b11da73a --- /dev/null +++ b/.changeset/strange-fans-move.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Resolved issue where hex-like strings were incorrectly being lowercased in `signTypedData`. diff --git a/src/actions/wallet/signTypedData.ts b/src/actions/wallet/signTypedData.ts index 51e26e5093..439a39104b 100644 --- a/src/actions/wallet/signTypedData.ts +++ b/src/actions/wallet/signTypedData.ts @@ -18,12 +18,13 @@ import type { Chain } from '../../types/chain.js' import type { Hex } from '../../types/misc.js' import type { TypedDataDefinition } from '../../types/typedData.js' import type { RequestErrorType } from '../../utils/buildRequest.js' -import { type IsHexErrorType, isHex } from '../../utils/data/isHex.js' -import { type StringifyErrorType, stringify } from '../../utils/stringify.js' +import type { IsHexErrorType } from '../../utils/data/isHex.js' +import type { StringifyErrorType } from '../../utils/stringify.js' import { type GetTypesForEIP712DomainErrorType, type ValidateTypedDataErrorType, getTypesForEIP712Domain, + serializeTypedData, validateTypedData, } from '../../utils/typedData.js' @@ -181,10 +182,7 @@ export async function signTypedData< if (account.type === 'local') return account.signTypedData({ domain, message, primaryType, types }) - const typedData = stringify( - { domain: domain ?? {}, message, primaryType, types }, - (_, value) => (isHex(value) ? value.toLowerCase() : value), - ) + const typedData = serializeTypedData({ domain, message, primaryType, types }) return client.request( { method: 'eth_signTypedData_v4', diff --git a/src/utils/typedData.test.ts b/src/utils/typedData.test.ts index 59ce66e4f0..f0db83dd28 100644 --- a/src/utils/typedData.test.ts +++ b/src/utils/typedData.test.ts @@ -1,7 +1,136 @@ import { describe, expect, test } from 'vitest' import { pad, toHex } from './index.js' -import { domainSeparator, validateTypedData } from './typedData.js' +import { + domainSeparator, + serializeTypedData, + validateTypedData, +} from './typedData.js' + +describe('serializeTypedData', () => { + test('default', () => { + expect( + serializeTypedData({ + domain: { + name: 'Ether!', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + primaryType: 'Foo', + types: { + Foo: [ + { name: 'address', type: 'address' }, + { name: 'name', type: 'string' }, + { name: 'foo', type: 'string' }, + ], + }, + message: { + address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + name: 'jxom', + foo: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + }, + }), + ).toMatchInlineSnapshot( + `"{"domain":{},"message":{"address":"0xb9cab4f0e46f7f6b1024b5a7463734fa68e633f9","name":"jxom","foo":"0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9"},"primaryType":"Foo","types":{"Foo":[{"name":"address","type":"address"},{"name":"name","type":"string"},{"name":"foo","type":"string"}]}}"`, + ) + }) + + test('with domain', () => { + expect( + serializeTypedData({ + domain: { + name: 'Ether!', + version: '1', + address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + primaryType: 'Foo', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'address', type: 'address' }, + { name: 'chainId', type: 'uint32' }, + { name: 'verifyingContract', type: 'address' }, + ], + Foo: [ + { name: 'address', type: 'address' }, + { name: 'name', type: 'string' }, + { name: 'foo', type: 'string' }, + ], + }, + message: { + address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + name: 'jxom', + foo: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + }, + }), + ).toMatchInlineSnapshot( + `"{"domain":{"name":"Ether!","version":"1","address":"0xb9cab4f0e46f7f6b1024b5a7463734fa68e633f9","chainId":1,"verifyingContract":"0xcccccccccccccccccccccccccccccccccccccccc"},"message":{"address":"0xb9cab4f0e46f7f6b1024b5a7463734fa68e633f9","name":"jxom","foo":"0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9"},"primaryType":"Foo","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"address","type":"address"},{"name":"chainId","type":"uint32"},{"name":"verifyingContract","type":"address"}],"Foo":[{"name":"address","type":"address"},{"name":"name","type":"string"},{"name":"foo","type":"string"}]}}"`, + ) + }) + + test('domain as primary type', () => { + expect( + serializeTypedData({ + domain: { + name: 'Ether!', + version: '1', + address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + primaryType: 'EIP712Domain', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'address', type: 'address' }, + { name: 'chainId', type: 'uint32' }, + { name: 'verifyingContract', type: 'address' }, + ], + Foo: [ + { name: 'address', type: 'address' }, + { name: 'name', type: 'string' }, + { name: 'foo', type: 'string' }, + ], + }, + }), + ).toMatchInlineSnapshot( + `"{"domain":{"name":"Ether!","version":"1","address":"0xb9cab4f0e46f7f6b1024b5a7463734fa68e633f9","chainId":1,"verifyingContract":"0xcccccccccccccccccccccccccccccccccccccccc"},"primaryType":"EIP712Domain","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"address","type":"address"},{"name":"chainId","type":"uint32"},{"name":"verifyingContract","type":"address"}],"Foo":[{"name":"address","type":"address"},{"name":"name","type":"string"},{"name":"foo","type":"string"}]}}"`, + ) + }) + + test('no domain', () => { + expect( + serializeTypedData({ + primaryType: 'Foo', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint32' }, + { name: 'verifyingContract', type: 'address' }, + ], + Foo: [ + { name: 'address', type: 'address' }, + { name: 'name', type: 'string' }, + { name: 'foo', type: 'string' }, + ], + }, + message: { + address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + name: 'jxom', + foo: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9', + }, + }), + ).toMatchInlineSnapshot( + `"{"domain":{},"message":{"address":"0xb9cab4f0e46f7f6b1024b5a7463734fa68e633f9","name":"jxom","foo":"0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9"},"primaryType":"Foo","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint32"},{"name":"verifyingContract","type":"address"}],"Foo":[{"name":"address","type":"address"},{"name":"name","type":"string"},{"name":"foo","type":"string"}]}}"`, + ) + }) +}) describe('validateTypedData', () => { test('default', () => { diff --git a/src/utils/typedData.ts b/src/utils/typedData.ts index a3f0dc26e9..b934e9ec0b 100644 --- a/src/utils/typedData.ts +++ b/src/utils/typedData.ts @@ -14,6 +14,53 @@ import { type HashDomainErrorType, hashDomain, } from './signature/hashTypedData.js' +import { stringify } from './stringify.js' + +export type SerializeTypedDataErrorType = + | HashDomainErrorType + | IsAddressErrorType + | NumberToHexErrorType + | SizeErrorType + | ErrorType + +export function serializeTypedData< + const typedData extends TypedData | Record, + primaryType extends keyof typedData | 'EIP712Domain', +>(parameters: TypedDataDefinition) { + const { + domain: domain_, + message: message_, + primaryType, + types, + } = parameters as unknown as TypedDataDefinition + + const normalizeData = ( + struct: readonly TypedDataParameter[], + data_: Record, + ) => { + const data = { ...data_ } + for (const param of struct) { + const { name, type } = param + if (type === 'address') data[name] = (data[name] as string).toLowerCase() + } + return data + } + + const domain = (() => { + if (!types.EIP712Domain) return {} + if (!domain_) return {} + return normalizeData(types.EIP712Domain, domain_) + })() + + const message = (() => { + if (primaryType === 'EIP712Domain') return undefined + return normalizeData(types[primaryType], message_) + })() + + console.log(message) + + return stringify({ domain, message, primaryType, types }) +} export type ValidateTypedDataErrorType = | HashDomainErrorType @@ -72,11 +119,8 @@ export function validateTypedData< // Validate domain types. if (types.EIP712Domain && domain) validateData(types.EIP712Domain, domain) - if (primaryType !== 'EIP712Domain') { - // Validate message types. - const type = types[primaryType] - validateData(type, message) - } + // Validate message types. + if (primaryType !== 'EIP712Domain') validateData(types[primaryType], message) } export type GetTypesForEIP712DomainErrorType = ErrorType