Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions src/__snapshots__/sign-typed-data.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with extra dom

exports[`TypedDataUtils.eip712Hash V4 should hash a typed message with only custom domain seperator fields 1`] = `"3efa3ef0305f56ba5bba62000500e29fe82c5314bca2f958c64e31b2498560f8"`;

exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0X" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`;

exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"`;

exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0x" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`;

exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0x0" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000000"`;

exports[`TypedDataUtils.encodeData V3 example data type "address" should encode "0x1fffffffffffff" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000000000000000000000000000001fffffffffffff"`;
Expand Down Expand Up @@ -202,6 +208,12 @@ exports[`TypedDataUtils.encodeData V3 should encode data with an atomic property

exports[`TypedDataUtils.encodeData V3 should encode data with custom type 1`] = `"a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8"`;

exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0X" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`;

exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"`;

exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0x" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000021"`;

exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0x0" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada2990000000000000000000000000000000000000000000000000000000000000000"`;

exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "0x1fffffffffffff" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada299000000000000000000000000000000000000000000000000001fffffffffffff"`;
Expand All @@ -218,7 +230,7 @@ exports[`TypedDataUtils.encodeData V4 example data type "address" should encode

exports[`TypedDataUtils.encodeData V4 example data type "address" should encode "bBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"c92db6bca97089c40b3f6512400e065cf2c27db90a50ce13440d951b13ada29900000000000000000000000000000023eafa60608dacea43aa64cbe38e38e38d"`;

exports[`TypedDataUtils.encodeData V4 example data type "address" should encode array of all address example data 1`] = `"1cf487eff7ac1d95e8069104ae1f91e4ecc076c3a5c23645a8b93a413d1bc7118eb4f5f4cd3a16acaa954558d08ceb9f1a1dfd26dbf9eb2b780b871fa554bbcd"`;
exports[`TypedDataUtils.encodeData V4 example data type "address" should encode array of all address example data 1`] = `"1cf487eff7ac1d95e8069104ae1f91e4ecc076c3a5c23645a8b93a413d1bc711ff407f9cddb5e3ea5a2e1b929a5e42c275df9f78cbc1b361aab9a05c40a7769e"`;

exports[`TypedDataUtils.encodeData V4 example data type "bool" should encode "-1" (type "number") 1`] = `"83478c46da931c2f6871fdb6febd69a27b0ebc8c91f3e2cf580c3bd8777690060000000000000000000000000000000000000000000000000000000000000001"`;

Expand Down Expand Up @@ -404,6 +416,12 @@ exports[`TypedDataUtils.encodeData V4 should encode data with a recursive data t

exports[`TypedDataUtils.encodeData V4 should encode data with custom type 1`] = `"a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8"`;

exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0X" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`;

exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"ab14541443c6f456616e472ecca6d4fc887f862760d3a056b348441a9e0fd1c6"`;

exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0x" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`;

exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0x0" (type "string") 1`] = `"c93aa5d5f1b1efece209c34fceee0aeb33da38ea34dbdd4df854e9708a636fea"`;

exports[`TypedDataUtils.hashStruct V3 example data type "address" should hash "0x1fffffffffffff" (type "string") 1`] = `"edb736a49622b7860bcec9c56c2b0346870f5bf02b7e516722c51be85c740bd3"`;
Expand Down Expand Up @@ -582,6 +600,12 @@ exports[`TypedDataUtils.hashStruct V3 should hash data with an atomic property s

exports[`TypedDataUtils.hashStruct V3 should hash data with custom type 1`] = `"c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e"`;

exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0X" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`;

exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"ab14541443c6f456616e472ecca6d4fc887f862760d3a056b348441a9e0fd1c6"`;

exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0x" (type "string") 1`] = `"42e838ca0d946c3518ba662f0be93b31523385507994c8939ed72a63b2c40b96"`;

exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0x0" (type "string") 1`] = `"c93aa5d5f1b1efece209c34fceee0aeb33da38ea34dbdd4df854e9708a636fea"`;

exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "0x1fffffffffffff" (type "string") 1`] = `"edb736a49622b7860bcec9c56c2b0346870f5bf02b7e516722c51be85c740bd3"`;
Expand All @@ -598,7 +622,7 @@ exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "9

exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash "bBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"4d9b02d96f7647b8d3acfafc97b2faa65ef7b442ea647f8c8f6f606d4d794d34"`;

exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash array of all address example data 1`] = `"d714a2e5e9f4db8583cd86e447c95fdbb86e6fe7a8fdc576197c3f0805d1a693"`;
exports[`TypedDataUtils.hashStruct V4 example data type "address" should hash array of all address example data 1`] = `"380bb6cbe741d8b2158a8aed568d46298719e56806d9be5c13bdd130b3ade39a"`;

exports[`TypedDataUtils.hashStruct V4 example data type "bool" should hash "-1" (type "number") 1`] = `"df410941012cfe7070ec9abfdb367531856dda97faaa5134e46c313058e8415e"`;

Expand Down Expand Up @@ -936,6 +960,12 @@ exports[`signTypedData V1 should sign data with a dynamic property set to undefi

exports[`signTypedData V1 should sign data with an atomic property set to undefined 1`] = `[Function]`;

exports[`signTypedData V3 example data type "address" should sign "0X" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`;

exports[`signTypedData V3 example data type "address" should sign "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"0x9dfeb4cb581e1cc8ef7f1192dbbaa05a67545116c1ecb951b8aa5e7e2167e1ca463e8260231fe82395a38a597c90537f3282f6197c17b7e04baf52c5250fa3831b"`;

exports[`signTypedData V3 example data type "address" should sign "0x" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`;

exports[`signTypedData V3 example data type "address" should sign "0x0" (type "string") 1`] = `"0xe73f6e0860ebb2660c79396142325bd00033405615b608c757901a928f81533d78441e8c323c832e72c96bf6f95d874848336537f30081b5de518a26e9fae1891c"`;

exports[`signTypedData V3 example data type "address" should sign "0x1fffffffffffff" (type "string") 1`] = `"0xd27fb914606ad217b3a238d24e86dd73499806fd45f6b229f50e242ec8eefe4b6325b3bcc8dc310c851379bc55d41b928d8b2828565f892819daccba3aeba0381b"`;
Expand Down Expand Up @@ -1122,6 +1152,12 @@ exports[`signTypedData V3 should sign data with an atomic property set to undefi

exports[`signTypedData V3 should sign data with custom type 1`] = `"0x22ee0cb3a379f3a122f7b59456ebcf404ca139320a0723560abde49cc95f4a2f69774bf94c4e776f1a9c8c8a67e9e2bdda131e04bde935f73fae376ee788920d1c"`;

exports[`signTypedData V4 example data type "address" should sign "0X" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`;

exports[`signTypedData V4 example data type "address" should sign "0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"0x9dfeb4cb581e1cc8ef7f1192dbbaa05a67545116c1ecb951b8aa5e7e2167e1ca463e8260231fe82395a38a597c90537f3282f6197c17b7e04baf52c5250fa3831b"`;

exports[`signTypedData V4 example data type "address" should sign "0x" (type "string") 1`] = `"0x15e3079498e4024a4f18f46e6dc9d960523e84616b5c0452d804fb70231cab5b6a8a9277b94d3a3121b088bdc7de735fe2966f600f808d2c9e57a37ff2dc34da1b"`;

exports[`signTypedData V4 example data type "address" should sign "0x0" (type "string") 1`] = `"0xe73f6e0860ebb2660c79396142325bd00033405615b608c757901a928f81533d78441e8c323c832e72c96bf6f95d874848336537f30081b5de518a26e9fae1891c"`;

exports[`signTypedData V4 example data type "address" should sign "0x1fffffffffffff" (type "string") 1`] = `"0xd27fb914606ad217b3a238d24e86dd73499806fd45f6b229f50e242ec8eefe4b6325b3bcc8dc310c851379bc55d41b928d8b2828565f892819daccba3aeba0381b"`;
Expand All @@ -1138,7 +1174,7 @@ exports[`signTypedData V4 example data type "address" should sign "9007199254740

exports[`signTypedData V4 example data type "address" should sign "bBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" (type "string") 1`] = `"0x47ad9b795affe960475274f8b067225cff2451eada53b0ccf9bebd4336674b4b35b9ac6bad764f197435c7d53b6992a6d5f835796346a6ee4fd2313f8dd5b8991b"`;

exports[`signTypedData V4 example data type "address" should sign array of all address example data 1`] = `"0x4a47328b668266b3bdaa67d9c1ac201a4a2245ed185f9b81adb4acb6eb1231fe58e08ef8dd8ed85b4a264219d297e8275b761216688c8ed9bb7b367e9b99435b1c"`;
exports[`signTypedData V4 example data type "address" should sign array of all address example data 1`] = `"0x99a49e40de4b2776ac55c077b5040232a3f3b4fd6ad604c4800b6b121889748a2eef76371778e18d2ad6522997803aea41d898ff61b9ab3b99862085ca3e77bc1c"`;

exports[`signTypedData V4 example data type "bool" should sign "-1" (type "number") 1`] = `"0x5266f7fdc7b8d6552656609f7160760f323a4b37ba80e41b33fdb5637349538c123e338354fd3c0fa0741725c3a273a3857d9a2f490b7ebd612b83d10b2246a11b"`;

Expand Down
34 changes: 33 additions & 1 deletion src/sign-typed-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ const encodeDataExamples = {
// atomic types supported by EIP-712:
address: [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0XbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',

// backward compatibility for non-standard Ethereum address values
'0x',
'0X',
'0x0', // odd
'0x10', // even
10,
Expand Down Expand Up @@ -318,7 +323,34 @@ const encodeDataErrorExamples = {
{
input: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB0',
errorMessage:
'Unable to encode value: Invalid address value. Expected address to be 20 bytes long, but received 21 bytes.',
'Unable to encode field: Invalid address value. Expected address to be 20 bytes long, but received 21 bytes.',
},
{
input: true,
errorMessage:
'Invalid address value. Address must be a number or a string.',
},
{
input: '0xa+ddress+$ymbols#',
errorMessage: 'Invalid address value. Contains invalid character "+".',
},
{
input: '0x12345你好67890',
errorMessage: 'Invalid address value. Contains invalid character "你".',
},
{
input: '0xbBzZBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
errorMessage:
'Invalid address value. Contains invalid letter "z". Only a-f and A-F are valid hex letters.',
},
{
input: '0xbBzZBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
errorMessage:
'Invalid address value. Contains invalid letter "z". Only a-f and A-F are valid hex letters.',
},
{
input: '0xbBbbBBB bbBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
errorMessage: 'Invalid address value. Contains whitespace character " ".',
},
],
int8: [
Expand Down
88 changes: 81 additions & 7 deletions src/sign-typed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from '@metamask/abi-utils/dist/parsers';
import { padStart } from '@metamask/abi-utils/dist/utils';
import {
add0x,
assert,
bigIntToBytes,
bytesToHex,
Expand Down Expand Up @@ -180,6 +179,33 @@ function parseNumber(type: string, value: string | number | bigint) {
return bigIntValue;
}

/**
* Asserts that the given value does not contain any invalid hex characters.
*
* @param value - The value to validate.
* @throws Error if the value contains any invalid hex characters.
*/
function assertHexAddressChars(value: string) {
const has0xPrefix = value.toLowerCase().startsWith('0x');
const valueWithoutPrefix = has0xPrefix ? value.slice(2) : value;

// find any invalid characters including unicode characters
// eslint-disable-next-line require-unicode-regexp
const invalidCharMatch = valueWithoutPrefix.match(/[^0-9a-fA-F]/gu);
if (invalidCharMatch) {
const invalidChar = invalidCharMatch[0];
assert(
!invalidChar.match(/[g-zG-Z]/u),
`Contains invalid letter "${invalidChar}". Only a-f and A-F are valid hex letters.`,
);
assert(
!invalidChar.match(/[\s\n\r\t\f\v]/u),
`Contains whitespace character "${invalidChar}".`,
);
assert(false, `Contains invalid character "${invalidChar}".`);
}
}

/**
* Parse an address string to a `Uint8Array`. The behaviour of this is quite
* strange, in that it does not parse the address as hexadecimal string, nor as
Expand Down Expand Up @@ -220,6 +246,48 @@ function reallyStrangeAddressToBytes(address: string): Uint8Array {
return padStart(bigIntToBytes(addressValue), 20);
}

/**
* Validates and normalizes an address value to ensure it's a valid EVM address.
*
* @param value - The address value to validate and normalize.
* @returns The normalized address as a Uint8Array.
* @throws Error if the address is invalid.
*/
function validateAndNormalizeAddress(value: unknown): Uint8Array {
let addressBytes: Uint8Array | undefined;
let addressHex: string | undefined;

if (typeof value === 'number') {
addressBytes = padStart(numberToBytes(value), 20);
addressHex = bytesToHex(addressBytes);
}

if (typeof value === 'string') {
assertHexAddressChars(value);

if (isStrictHexString(value)) {
addressHex = value;
addressBytes = hexToBytes(value);
} else {
// For non-hex strings, use legacy conversion
addressBytes = reallyStrangeAddressToBytes(value).subarray(0, 20);
addressHex = bytesToHex(addressBytes);
}
}

assert(addressBytes, 'Address must be a number or a string.');
assert(
addressBytes.length <= 20,
`Expected address to be 20 bytes long, but received ${addressBytes.length} bytes.`,
);
assert(
addressHex?.match(/^0[xX]([a-fA-F0-9]{0,40})$/u) !== null,
`Address must be a 0x-prefixed hex string with no more than 40 characters.`,
);

return addressBytes;
}

/**
* Encode a single field.
*
Expand Down Expand Up @@ -261,12 +329,18 @@ function encodeField(
}

if (type === 'address') {
if (typeof value === 'number') {
return ['address', padStart(numberToBytes(value), 20)];
} else if (isStrictHexString(value)) {
return ['address', add0x(value)];
} else if (typeof value === 'string') {
return ['address', reallyStrangeAddressToBytes(value).subarray(0, 20)];
try {
const addressBytes = validateAndNormalizeAddress(value);
return ['address', addressBytes];
} catch (err) {
if (typeof err?.message === 'string' && err.message.length) {
throw new Error(
`Unable to encode field: Invalid address value. ${
err.message as string
}`,
);
}
throw new Error(`Unable to encode field: Invalid address value.`);
}
}

Expand Down
Loading