eip-712
Version:
Tiny library with utility functions that can help with signing and verifying EIP-712 based messages
289 lines (247 loc) • 14.3 kB
text/typescript
import { bytesToHex } from '@noble/hashes/utils';
import invalidArrayLength from './__fixtures__/invalid-array-length.json';
import invalidArrayType from './__fixtures__/invalid-array-type.json';
import invalidMissingData from './__fixtures__/invalid-missing-data.json';
import invalidMissingType from './__fixtures__/invalid-missing-type.json';
import invalidSchema from './__fixtures__/invalid-schema.json';
import invalidType from './__fixtures__/invalid-type.json';
import mailTypedData from './__fixtures__/typed-data-1.json';
import approvalTypedData from './__fixtures__/typed-data-2.json';
import arrayTypedData from './__fixtures__/typed-data-3.json';
import customTypedData from './__fixtures__/typed-data-4.json';
import { asArray, encodeData, encodeType, getDependencies, getMessage, getStructHash, getTypeHash } from './eip-712';
describe('getDependencies', () => {
it('returns all dependencies for the primary type', () => {
expect(getDependencies(mailTypedData, 'EIP712Domain')).toStrictEqual(['EIP712Domain']);
expect(getDependencies(mailTypedData, 'Person')).toStrictEqual(['Person']);
expect(getDependencies(mailTypedData, 'Mail')).toStrictEqual(['Mail', 'Person']);
expect(getDependencies(approvalTypedData, 'EIP712Domain')).toStrictEqual(['EIP712Domain']);
expect(getDependencies(approvalTypedData, 'Transaction')).toStrictEqual(['Transaction']);
expect(getDependencies(approvalTypedData, 'TransactionApproval')).toStrictEqual([
'TransactionApproval',
'Transaction'
]);
expect(getDependencies(arrayTypedData, 'EIP712Domain')).toStrictEqual(['EIP712Domain']);
expect(getDependencies(arrayTypedData, 'Person')).toStrictEqual(['Person']);
expect(getDependencies(arrayTypedData, 'Mail')).toStrictEqual(['Mail', 'Person']);
expect(getDependencies(customTypedData, 'FooBarDomain', { domain: 'FooBarDomain' })).toStrictEqual([
'FooBarDomain'
]);
});
it('throws for invalid JSON data', () => {
expect(() => getDependencies(invalidSchema, 'EIP712Domain')).toThrow();
});
it('throws for invalid types', () => {
expect(() => getDependencies(invalidType, 'EIP712Domain')).toThrow();
expect(() => getDependencies(invalidType, 'Person')).toThrow();
expect(() => getDependencies(invalidType, 'Mail')).toThrow();
});
});
describe('encodeType', () => {
it('encodes a type to a hashable string', () => {
expect(encodeType(mailTypedData, 'EIP712Domain')).toBe(
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
);
expect(encodeType(mailTypedData, 'Person')).toBe('Person(string name,address wallet)');
expect(encodeType(mailTypedData, 'Mail')).toBe(
'Mail(Person from,Person to,string contents)Person(string name,address wallet)'
);
expect(encodeType(approvalTypedData, 'EIP712Domain')).toBe(
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)'
);
expect(encodeType(approvalTypedData, 'Transaction')).toBe(
'Transaction(address to,uint256 amount,bytes data,uint256 nonce)'
);
expect(encodeType(approvalTypedData, 'TransactionApproval')).toBe(
'TransactionApproval(address owner,Transaction transaction)Transaction(address to,uint256 amount,bytes data,uint256 nonce)'
);
expect(encodeType(customTypedData, 'FooBarDomain', { domain: 'FooBarDomain' })).toBe(
'FooBarDomain(string name,string version,uint256 chainId,address verifyingContract)'
);
});
it('throws for invalid JSON data', () => {
expect(() => encodeType(invalidSchema, 'EIP712Domain')).toThrow();
});
it('throws for invalid types', () => {
expect(() => encodeType(invalidType, 'EIP712Domain')).toThrow();
expect(() => encodeType(invalidType, 'Person')).toThrow();
expect(() => encodeType(invalidType, 'Mail')).toThrow();
});
});
describe('getTypeHash', () => {
it('returns a 32 byte hash for a type', () => {
expect(bytesToHex(getTypeHash(mailTypedData, 'EIP712Domain'))).toBe(
'8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f'
);
expect(bytesToHex(getTypeHash(mailTypedData, 'Person'))).toBe(
'b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500'
);
expect(bytesToHex(getTypeHash(mailTypedData, 'Mail'))).toBe(
'a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2'
);
expect(bytesToHex(getTypeHash(approvalTypedData, 'EIP712Domain'))).toBe(
'd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472'
);
expect(bytesToHex(getTypeHash(approvalTypedData, 'Transaction'))).toBe(
'a826c254899945d99ae513c9f1275b904f19492f4438f3d8364fa98e70fbf233'
);
expect(bytesToHex(getTypeHash(approvalTypedData, 'TransactionApproval'))).toBe(
'5b360b7b2cc780b6a0687ac409805af3219ef7d9dcc865669e39a1dc7394ffc5'
);
expect(bytesToHex(getTypeHash(arrayTypedData, 'EIP712Domain'))).toBe(
'8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f'
);
expect(bytesToHex(getTypeHash(arrayTypedData, 'Mail'))).toBe(
'c81112a69b6596b8bc0678e67d97fbf9bed619811fc781419323ec02d1c7290d'
);
expect(bytesToHex(getTypeHash(arrayTypedData, 'Person'))).toBe(
'b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500'
);
expect(bytesToHex(getTypeHash(customTypedData, 'FooBarDomain', { domain: 'FooBarDomain' }))).toBe(
'85b412c5db9e26aa4f6bf794e72b1557f463a0978ceef9acaff7f6ff1eb24e57'
);
});
it('throws for invalid JSON data', () => {
expect(() => getTypeHash(invalidSchema, 'EIP712Domain')).toThrow();
});
});
describe('encodeData', () => {
it('encodes data to an ABI encoded string', () => {
expect(bytesToHex(encodeData(mailTypedData, 'EIP712Domain', mailTypedData.domain))).toBe(
'8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400fc70ef06638535b4881fafcac8287e210e3769ff1a8e91f1b95d6246e61e4d3c6c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cccccccccccccccccccccccccccccccccccccccc'
);
expect(bytesToHex(encodeData(mailTypedData, 'Person', mailTypedData.message.from))).toBe(
'b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c795008c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826'
);
expect(bytesToHex(encodeData(mailTypedData, 'Mail', mailTypedData.message))).toBe(
'a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8'
);
expect(bytesToHex(encodeData(approvalTypedData, 'EIP712Domain', approvalTypedData.domain))).toBe(
'd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472d210ccb0bd8574cfdb6efd17ae4e6ab527687a29dcf03060d4a41b9b56d0b637c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1dbbd6c8d75f4b446bcb44cee3ba5da8120e056d4d2f817213df8703ef065ed3'
);
expect(bytesToHex(encodeData(approvalTypedData, 'Transaction', approvalTypedData.message.transaction))).toBe(
'a826c254899945d99ae513c9f1275b904f19492f4438f3d8364fa98e70fbf2330000000000000000000000004bbeeb066ed09b7aed07bf39eee0460dfa2615200000000000000000000000000000000000000000000000000de0b6b3a7640000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4700000000000000000000000000000000000000000000000000000000000000001'
);
expect(bytesToHex(encodeData(approvalTypedData, 'TransactionApproval', approvalTypedData.message))).toBe(
'5b360b7b2cc780b6a0687ac409805af3219ef7d9dcc865669e39a1dc7394ffc5000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb9e7ba42b4ace63ae7d8ee163d5e642a085b32c2553717dcb37974e83fad289d0'
);
expect(bytesToHex(encodeData(arrayTypedData, 'Mail', arrayTypedData.message))).toBe(
'c81112a69b6596b8bc0678e67d97fbf9bed619811fc781419323ec02d1c7290dafd2599280d009dcb3e261f4bccebec901d67c3f54b56d49bf8327359fc69cd7392bb8ab5338a9075ce8fec1b431e334007d4de1e5e83201ca35762e24428e24b7c4150525d88db452c5f08f93f4593daa458ab6280b012532183aed3a8e4a01'
);
expect(
bytesToHex(encodeData(customTypedData, 'FooBarDomain', customTypedData.domain, { domain: 'FooBarDomain' }))
).toBe(
'85b412c5db9e26aa4f6bf794e72b1557f463a0978ceef9acaff7f6ff1eb24e57c70ef06638535b4881fafcac8287e210e3769ff1a8e91f1b95d6246e61e4d3c6c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cccccccccccccccccccccccccccccccccccccccc'
);
});
it('throws for invalid JSON data', () => {
expect(() => encodeData(invalidSchema, 'EIP712Domain', invalidSchema.domain)).toThrow();
});
it('throws when a type is missing', () => {
expect(() => encodeData(invalidMissingData, 'Mail', invalidMissingData.message)).toThrow();
});
it('throws when data is missing', () => {
expect(() => encodeData(invalidMissingType, 'Mail', invalidMissingType.message)).toThrow();
});
it('throws if the type is not an array', () => {
expect(() => encodeData(invalidArrayType, 'Mail', invalidArrayType.message)).toThrow();
});
it('throws if the array length is invalid', () => {
expect(() => encodeData(invalidArrayLength, 'Mail', invalidArrayLength.message)).toThrow();
});
});
describe('getStructHash', () => {
it('returns a 32 byte hash for a struct', () => {
expect(bytesToHex(getStructHash(mailTypedData, 'EIP712Domain', mailTypedData.domain))).toBe(
'f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f'
);
expect(bytesToHex(getStructHash(mailTypedData, 'Person', mailTypedData.message.from))).toBe(
'fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8'
);
expect(bytesToHex(getStructHash(mailTypedData, 'Mail', mailTypedData.message))).toBe(
'c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e'
);
expect(bytesToHex(getStructHash(approvalTypedData, 'EIP712Domain', approvalTypedData.domain))).toBe(
'67083568259b4a947b02ce4dca4cc91f1e7f01d109c8805668755be5ab5adbb9'
);
expect(bytesToHex(getStructHash(approvalTypedData, 'Transaction', approvalTypedData.message.transaction))).toBe(
'9e7ba42b4ace63ae7d8ee163d5e642a085b32c2553717dcb37974e83fad289d0'
);
expect(bytesToHex(getStructHash(approvalTypedData, 'TransactionApproval', approvalTypedData.message))).toBe(
'309886ad75ec7c2c6a69bffa2669bad00e3b1e0a85221eff4e8926a2f8ff5077'
);
expect(bytesToHex(getStructHash(customTypedData, 'FooBarDomain', customTypedData.domain))).toBe(
'6ff4505ed33bedaadf3491aa039d9ccb91a3114eeab940e69fdecb809fb26882'
);
});
it('throws for invalid JSON data', () => {
expect(() => getStructHash(invalidSchema, 'EIP712Domain', invalidSchema.domain)).toThrow();
});
it('throws when a type is missing', () => {
expect(() => encodeData(invalidMissingType, 'Mail', invalidSchema.message)).toThrow();
});
it('throws when data is missing', () => {
expect(() => encodeData(invalidMissingType, 'Mail', invalidSchema.message)).toThrow();
});
});
describe('getMessage', () => {
it('returns the full encoded and hashed message to sign', () => {
expect(bytesToHex(getMessage(mailTypedData))).toBe(
'1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090fc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e'
);
expect(bytesToHex(getMessage(approvalTypedData))).toBe(
'190167083568259b4a947b02ce4dca4cc91f1e7f01d109c8805668755be5ab5adbb9309886ad75ec7c2c6a69bffa2669bad00e3b1e0a85221eff4e8926a2f8ff5077'
);
expect(bytesToHex(getMessage(arrayTypedData))).toBe(
'1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f6757567025d2ba15d5ebb228ea677055b8b601007e60e9463f6ed7c68f918189'
);
expect(bytesToHex(getMessage(customTypedData, false, { domain: 'FooBarDomain' }))).toBe(
'19016ff4505ed33bedaadf3491aa039d9ccb91a3114eeab940e69fdecb809fb268826757567025d2ba15d5ebb228ea677055b8b601007e60e9463f6ed7c68f918189'
);
});
it('hashes the message with Keccak-256', () => {
expect(bytesToHex(getMessage(mailTypedData, true))).toBe(
'be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2'
);
expect(bytesToHex(getMessage(approvalTypedData, true))).toBe(
'ee0cdea747f4a81355be92dbf30e209dbd2954a82d5a82482b7c7800089c7f57'
);
expect(bytesToHex(getMessage(arrayTypedData, true))).toBe(
'c6f6c8028eadb17bc5c9e2ea2f738e92e49cfa627d19896c250fd2eac653e4e0'
);
expect(bytesToHex(getMessage(customTypedData, true, { domain: 'FooBarDomain' }))).toBe(
'e028c0622beef9bde70e78a98c1d09a95ffe0cd9cfa5ff6a99f7db7c9245e103'
);
});
it('throws for invalid JSON data', () => {
expect(() => getMessage(invalidSchema)).toThrow();
});
it('throws when a type is missing', () => {
expect(() => getMessage(invalidMissingType)).toThrow();
});
it('throws when data is missing', () => {
expect(() => getMessage(invalidMissingData)).toThrow();
});
});
describe('asArray', () => {
it('returns the typed data as array', () => {
expect(asArray(mailTypedData)).toStrictEqual([
['Cow', '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'],
['Bob', '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'],
'Hello, Bob!'
]);
expect(asArray(approvalTypedData)).toStrictEqual([
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
['0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520', '1000000000000000000', '', '1']
]);
});
it('throws for invalid JSON data', () => {
expect(() => asArray(invalidSchema)).toThrow();
});
it('throws when a type is missing', () => {
expect(() => asArray(invalidMissingType)).toThrow();
});
it('throws when data is missing', () => {
expect(() => asArray(invalidMissingData)).toThrow();
});
});