Skip to content

Commit

Permalink
fix: resolves #2306
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed May 26, 2024
1 parent e3f9c9f commit ddfce93
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-fans-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Resolved issue where hex-like strings were incorrectly being lowercased in `signTypedData`.
10 changes: 4 additions & 6 deletions src/actions/wallet/signTypedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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',
Expand Down
131 changes: 130 additions & 1 deletion src/utils/typedData.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
54 changes: 49 additions & 5 deletions src/utils/typedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>,
primaryType extends keyof typedData | 'EIP712Domain',
>(parameters: TypedDataDefinition<typedData, primaryType>) {
const {
domain: domain_,
message: message_,
primaryType,
types,
} = parameters as unknown as TypedDataDefinition

const normalizeData = (
struct: readonly TypedDataParameter[],
data_: Record<string, unknown>,
) => {
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)

This comment has been minimized.

Copy link
@joaquim-verges

joaquim-verges Jun 6, 2024

@jxom is this meant to be left here?

This comment has been minimized.

Copy link
@joaquim-verges

joaquim-verges Jun 6, 2024

nevermind i see its gone


return stringify({ domain, message, primaryType, types })
}

export type ValidateTypedDataErrorType =
| HashDomainErrorType
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ddfce93

Please sign in to comment.