diff --git a/package.json b/package.json index 4b87f32e..52adae22 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@types/node": "^14.14.25", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", + "ajv": "^8.11.0", "eslint": "^7.30.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.23.4", diff --git a/src/sign-typed-data.test.ts b/src/sign-typed-data.test.ts index c8e8486a..af858e97 100644 --- a/src/sign-typed-data.test.ts +++ b/src/sign-typed-data.test.ts @@ -1,10 +1,12 @@ import * as ethUtil from 'ethereumjs-util'; +import Ajv from 'ajv'; import { recoverTypedSignature, signTypedData, TypedDataUtils, typedSignatureHash, SignTypedDataVersion, + TYPED_MESSAGE_SCHEMA, } from './sign-typed-data'; const privateKey = Buffer.from( @@ -12,6 +14,215 @@ const privateKey = Buffer.from( 'hex', ); +/** + * Get a list of all Solidity types supported by EIP-712. + * + * @returns A list of all supported Solidity types. + */ +function getEip712SolidityTypes() { + const types = ['bool', 'address', 'string', 'bytes']; + const ints = Array.from(new Array(32)).map( + (_, index) => `int${(index + 1) * 8}`, + ); + const uints = Array.from(new Array(32)).map( + (_, index) => `uint${(index + 1) * 8}`, + ); + const bytes = Array.from(new Array(32)).map( + (_, index) => `bytes${index + 1}`, + ); + + return [...types, ...ints, ...uints, ...bytes]; +} + +const eip712SolidityTypes = getEip712SolidityTypes(); + +/** + * Validate the given message with the typed message schema. + * + * @param typedMessage - The typed message to validate. + * @returns Whether the message is valid. + */ +function validateTypedMessageSchema( + typedMessage: Record, +): boolean { + const ajv = new Ajv(); + const validate = ajv.compile(TYPED_MESSAGE_SCHEMA); + return validate(typedMessage); +} + +describe('TYPED_MESSAGE_SCHEMA', () => { + it('should match valid typed message', () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'object', + types: { + EIP712Domain: [], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(true); + }); + + it('should allow custom types in addition to domain', () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'Message', + types: { + EIP712Domain: [], + Message: [], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(true); + }); + + eip712SolidityTypes.forEach((solidityType) => { + it(`should allow custom type to have type of '${solidityType}'`, () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'Message', + types: { + EIP712Domain: [], + Message: [{ name: 'data', type: solidityType }], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(true); + }); + }); + + it('should allow custom type to have a custom type', () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'Message', + types: { + CustomValue: [{ name: 'value', type: 'string' }], + EIP712Domain: [], + Message: [{ name: 'data', type: 'CustomValue' }], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(true); + }); + + const invalidStrings = [undefined, null, 0, 1, [], {}]; + + for (const invalidString of invalidStrings) { + // eslint-disable-next-line no-loop-func + it(`should disallow a primary type with value '${invalidString}'`, () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: invalidString, + types: { + EIP712Domain: [], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(false); + }); + } + + const invalidObjects = [undefined, null, 0, 1, [], '', 'test']; + for (const invalidObject of invalidObjects) { + // eslint-disable-next-line no-loop-func + it(`should disallow a typed message with value'${invalidObject}'`, () => { + expect(validateTypedMessageSchema(invalidObject as any)).toBe(false); + }); + + // eslint-disable-next-line no-loop-func + it(`should disallow a domain with value '${invalidObject}'`, () => { + const typedMessage = { + domain: invalidObject, + message: {}, + primaryType: 'object', + types: { + EIP712Domain: [], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(false); + }); + + // eslint-disable-next-line no-loop-func + it(`should disallow a message with value '${invalidObject}'`, () => { + const typedMessage = { + domain: {}, + message: invalidObject, + primaryType: 'object', + types: { + EIP712Domain: [], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(false); + }); + + // eslint-disable-next-line no-loop-func + it(`should disallow types with value '${invalidObject}'`, () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'object', + types: invalidObject, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(false); + }); + } + + it('should require custom type properties to have a name', () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'Message', + types: { + EIP712Domain: [], + Message: [{ type: 'string' }], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(false); + }); + + it('should require custom type properties to have a type', () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'Message', + types: { + EIP712Domain: [], + Message: [{ name: 'name' }], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(false); + }); + + const invalidTypes = [undefined, null, 0, 1, [], {}]; + + for (const invalidType of invalidTypes) { + // eslint-disable-next-line no-loop-func + it(`should disallow a type of '${invalidType}'`, () => { + const typedMessage = { + domain: {}, + message: {}, + primaryType: 'Message', + types: { + EIP712Domain: [], + Message: [{ name: 'name', type: invalidType }], + }, + }; + + expect(validateTypedMessageSchema(typedMessage)).toBe(false); + }); + } +}); + const encodeDataExamples = { // dynamic types supported by EIP-712: bytes: [10, '10', '0x10', Buffer.from('10', 'utf8')], diff --git a/src/sign-typed-data.ts b/src/sign-typed-data.ts index ca85d2cb..61ca1862 100644 --- a/src/sign-typed-data.ts +++ b/src/sign-typed-data.ts @@ -100,7 +100,7 @@ export const TYPED_MESSAGE_SCHEMA = { type: 'object', properties: { name: { type: 'string' }, - type: { type: 'string', enum: getSolidityTypes() }, + type: { type: 'string' }, }, required: ['name', 'type'], }, @@ -113,26 +113,6 @@ export const TYPED_MESSAGE_SCHEMA = { required: ['types', 'primaryType', 'domain', 'message'], }; -/** - * Get a list of all Solidity types. - * - * @returns A list of all Solidity types. - */ -function getSolidityTypes() { - const types = ['bool', 'address', 'string', 'bytes']; - const ints = Array.from(new Array(32)).map( - (_, index) => `int${(index + 1) * 8}`, - ); - const uints = Array.from(new Array(32)).map( - (_, index) => `uint${(index + 1) * 8}`, - ); - const bytes = Array.from(new Array(32)).map( - (_, index) => `bytes${index + 1}`, - ); - - return [...types, ...ints, ...uints, ...bytes]; -} - /** * Validate that the given value is a valid version string. * diff --git a/yarn.lock b/yarn.lock index b8ee7e8a..58f80c27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,6 +1005,16 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"