├── .babelrc.json ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── examples ├── ethereumjs-util │ └── index.ts └── ethers.js │ └── index.ts ├── jest.config.js ├── package.json ├── src ├── __fixtures__ │ ├── invalid-array-length.json │ ├── invalid-array-type.json │ ├── invalid-missing-data.json │ ├── invalid-missing-type.json │ ├── invalid-schema.json │ ├── invalid-type.json │ ├── typed-data-1.json │ ├── typed-data-2.json │ ├── typed-data-3.json │ ├── typed-data-4.json │ ├── typed-data-5.json │ └── types.json ├── eip-712.test.ts ├── eip-712.ts ├── index.ts ├── options.ts ├── types.test.ts ├── types.ts └── utils │ ├── __fixtures__ │ └── types.json │ ├── abi.test.ts │ ├── abi.ts │ ├── buffer.test.ts │ ├── buffer.ts │ ├── index.ts │ ├── json.test.ts │ └── json.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript"], 3 | "env": { 4 | "cjs": { 5 | "comments": false, 6 | "ignore": ["**/*.d.ts", "**/__mocks__/**/*", "**/*.test.ts"], 7 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 8 | }, 9 | "es": { 10 | "comments": false, 11 | "ignore": ["**/*.d.ts", "**/__mocks__/**/*", "**/*.test.ts"] 12 | }, 13 | "test": { 14 | "presets": [ 15 | [ 16 | "@babel/preset-env", 17 | { 18 | "targets": { 19 | "node": "current" 20 | } 21 | } 22 | ] 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | indent_size = 2 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": ["eslint:recommended", "eslint-config-prettier"], 5 | "plugins": ["eslint-plugin-import"], 6 | "rules": { 7 | "comma-dangle": ["error", "never"], 8 | "curly": "error", 9 | "import/first": "error", 10 | "import/newline-after-import": "error", 11 | "import/no-deprecated": "warn", 12 | "import/no-duplicates": "error", 13 | "import/no-mutable-exports": "error", 14 | "import/no-namespace": "error", 15 | "import/no-self-import": "error", 16 | "import/no-useless-path-segments": "error", 17 | "import/order": [ 18 | "error", 19 | { 20 | "newlines-between": "never", 21 | "alphabetize": { 22 | "order": "asc", 23 | "caseInsensitive": false 24 | } 25 | } 26 | ], 27 | "no-console": "error", 28 | "quotes": ["error", "single", "avoid-escape"] 29 | }, 30 | "overrides": [ 31 | { 32 | "files": ["*.ts"], 33 | "extends": [ 34 | "plugin:@typescript-eslint/recommended", 35 | "plugin:eslint-plugin-import/typescript", 36 | "eslint-config-prettier/@typescript-eslint" 37 | ], 38 | "plugins": ["@typescript-eslint"], 39 | "rules": { 40 | "@typescript-eslint/array-type": [ 41 | "error", 42 | { 43 | "default": "array-simple" 44 | } 45 | ], 46 | "@typescript-eslint/consistent-type-assertions": [ 47 | "error", 48 | { 49 | "assertionStyle": "as" 50 | } 51 | ], 52 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"], 53 | "@typescript-eslint/explicit-member-accessibility": [ 54 | "error", 55 | { 56 | "accessibility": "no-public" 57 | } 58 | ], 59 | "@typescript-eslint/method-signature-style": ["error", "method"], 60 | "@typescript-eslint/no-non-null-assertion": "off", 61 | "react/no-unescaped-entities": "off", 62 | "react/prop-types": "off", 63 | "react-hooks/exhaustive-deps": "off" 64 | } 65 | }, 66 | { 67 | "files": ["*.test.ts", "jest.config.js"], 68 | "env": { 69 | "node": true, 70 | "jest": true 71 | }, 72 | "extends": ["plugin:eslint-plugin-jest/recommended", "plugin:eslint-plugin-jest/style"], 73 | "plugins": ["eslint-plugin-jest"], 74 | "rules": { 75 | "@typescript-eslint/ban-ts-comment": [ 76 | "error", 77 | { 78 | "ts-expect-error": false 79 | } 80 | ] 81 | } 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | 8 | push: 9 | branches: 10 | - 'master' 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: 19 | - 12 20 | - 14 21 | - 16 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Setup Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v2-beta 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Cache Dependencies 32 | uses: actions/cache@v2 33 | with: 34 | path: node_modules 35 | key: yarn-${{ hashFiles('yarn.lock') }} 36 | 37 | - name: Install Dependencies 38 | run: yarn install --frozen-lockfile 39 | 40 | - name: Test 41 | run: yarn test 42 | 43 | - name: Upload Coverage 44 | uses: codecov/codecov-action@v1 45 | 46 | lint: 47 | name: Lint 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | node-version: 52 | - 16 53 | 54 | steps: 55 | - uses: actions/checkout@v2 56 | 57 | - name: Setup Node.js ${{ matrix.node-version }} 58 | uses: actions/setup-node@v2-beta 59 | with: 60 | node-version: ${{ matrix.node-version }} 61 | 62 | - name: Cache Dependencies 63 | uses: actions/cache@v2 64 | with: 65 | path: node_modules 66 | key: yarn-${{ hashFiles('yarn.lock') }} 67 | 68 | - name: Install Dependencies 69 | run: yarn install --frozen-lockfile 70 | 71 | - name: Lint TypeScript 72 | run: yarn lint:tsc 73 | 74 | - name: ESLint 75 | run: yarn lint:eslint 76 | 77 | - name: Prettier 78 | run: yarn lint:prettier 79 | 80 | - name: Lint Lockfile 81 | run: yarn lint:lockfile 82 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | deploy: 10 | name: Deploy 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: 16 | - 16 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Setup Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2-beta 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | registry-url: 'https://registry.npmjs.org' 26 | 27 | - name: Cache Dependencies 28 | uses: actions/cache@v2 29 | with: 30 | path: node_modules 31 | key: yarn-${{ hashFiles('yarn.lock') }} 32 | 33 | - name: Install Dependencies 34 | run: yarn install --frozen-lockfile 35 | 36 | - name: Build 37 | run: yarn build 38 | 39 | - name: Deploy 40 | run: yarn publish 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | lib/ 4 | typings/ 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": true, 8 | "trailingComma": "none", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Maarten Zuidhoorn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `eip-712` 2 | 3 | ![Version](https://img.shields.io/npm/v/eip-712) ![License](https://img.shields.io/github/license/Mrtenz/eip-712) [![Actions Status](https://github.com/Mrtenz/eip-712/workflows/CI/badge.svg)](https://github.com/Mrtenz/eip-712/actions) [![codecov](https://codecov.io/gh/Mrtenz/eip-712/branch/master/graph/badge.svg)](https://codecov.io/gh/Mrtenz/eip-712) 4 | 5 | This is a library for Node.js and web browsers with some utility functions that can help with signing and verifying [EIP-712](https://eips.ethereum.org/EIPS/eip-712) based messages. It is fully written in TypeScript, and is currently only compatible with the latest specification of EIP-712 ([eth_signTypedData_v4](https://docs.metamask.io/guide/signing-data.html#sign-typed-data-v4)). 6 | 7 | https://eips.ethereum.org/EIPS/eip-712 8 | 9 | Note that this library currently does not handle the signing itself. For this, you can use something like Ethers.js or ethereumjs-util. For examples, please see the [`examples`](https://github.com/Mrtenz/eip-712/blob/master/examples) folder. 10 | 11 | ## Installation 12 | 13 | You can install this library with Yarn or NPM: 14 | 15 | ``` 16 | $ yarn add eip-712 17 | ``` 18 | 19 | ``` 20 | $ npm install eip-712 21 | ``` 22 | 23 | There is a CommonJS version as well as an ES6 version available. Most tools should automatically pick the right version (e.g. Node.js, Webpack). 24 | 25 | ### Getting Started 26 | 27 | First, define your typed data as a JSON object, according to the JSON schema specified by EIP-712. For example: 28 | 29 | ```json 30 | { 31 | "types": { 32 | "EIP712Domain": [ 33 | { "name": "name", "type": "string" }, 34 | { "name": "version", "type": "string" }, 35 | { "name": "chainId", "type": "uint256" }, 36 | { "name": "verifyingContract", "type": "address" } 37 | ], 38 | "Person": [ 39 | { "name": "name", "type": "string" }, 40 | { "name": "wallet", "type": "address" } 41 | ], 42 | "Mail": [ 43 | { "name": "from", "type": "Person" }, 44 | { "name": "to", "type": "Person" }, 45 | { "name": "contents", "type": "string" } 46 | ] 47 | }, 48 | "primaryType": "Mail", 49 | "domain": { 50 | "name": "Ether Mail", 51 | "version": "1", 52 | "chainId": 1, 53 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 54 | }, 55 | "message": { 56 | "from": { 57 | "name": "Cow", 58 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 59 | }, 60 | "to": { 61 | "name": "Bob", 62 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 63 | }, 64 | "contents": "Hello, Bob!" 65 | } 66 | } 67 | ``` 68 | 69 | ### Functions 70 | 71 | Here is a brief description of the functions available in this library. For more detailed examples, you can refer to [`src/eip-712.test.ts`](https://github.com/Mrtenz/eip-712/blob/master/src/eip-712.test.ts), or to the examples in the [`examples`](https://github.com/Mrtenz/eip-712/blob/master/examples) folder. 72 | 73 | #### `getMessage(typedData, hash?)` 74 | 75 | This function will return the full EIP-191 encoded message to be signed as Buffer, for the typed data specified. If `hash` is enabled, the message will be hashed using Keccak256. 76 | 77 | ```js 78 | import { getMessage } from 'eip-712'; 79 | 80 | const typedData = { /*...*/ }; 81 | console.log(getMessage(typedData).toString('hex')); // 1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090fc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e 82 | console.log(getMessage(typedData, true).toString('hex')); // be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2 83 | ``` 84 | 85 | #### `asArray(typedData)` 86 | 87 | This function returns the typed data as an array. This can be useful for encoding typed data as ABI. 88 | 89 | ```js 90 | import { asArray } from 'eip-712'; 91 | 92 | const typedData = { /*...*/ }; 93 | console.log(asArray(typedData)); // [ ['Cow', '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'], ['Bob', '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'], 'Hello, Bob!' ] 94 | ``` 95 | 96 | #### `getStructHash(typedData, type, data)` 97 | 98 | This function returns a Keccak-256 hash for a single struct type (e.g. EIP712Domain, Person or Mail). 99 | 100 | ```js 101 | import { getStructHash } from 'eip-712'; 102 | 103 | const typedData = { /*...*/ }; 104 | console.log(getStructHash(typedData, 'EIP712Domain', typedData.domain).toString('hex')); // f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f 105 | ``` 106 | 107 | #### `encodeData(typedData, type, data)` 108 | 109 | This function returns the raw ABI encoded data for the struct type. 110 | 111 | ```js 112 | import { encodeData } from 'eip-712'; 113 | 114 | const typedData = { /*...*/ }; 115 | console.log(encodeData(typedData, 'EIP712Domain', typedData.domain).toString('hex')); // 8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400fc70ef06638535b4881fafcac8287e210e3769ff1a8e91f1b95d6246e61e4d3c6c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cccccccccccccccccccccccccccccccccccccccc 116 | ``` 117 | 118 | #### `getTypeHash(typedData, type)` 119 | 120 | This function returns the type hash for a struct type. This is the same as `Keccak256(EIP712Domain(string name,string version,uint256 chainId,address verifyingContract))`, with optional sub-types automatically included too. 121 | 122 | ```js 123 | import { getTypeHash } from 'eip-712'; 124 | 125 | const typedData = { /*...*/ }; 126 | console.log(getTypeHash(typedData, 'EIP712Domain').toString('hex')); // 8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f 127 | ``` 128 | 129 | #### `encodeType(typedData, type)` 130 | 131 | This function returns the type before hashing it, e.g. `EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)`, with optional sub-types automatically included too. 132 | 133 | ```js 134 | import { encodeType } from 'eip-712'; 135 | 136 | const typedData = { /*...*/ }; 137 | console.log(encodeType(typedData, 'EIP712Domain')); // EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) 138 | ``` 139 | 140 | #### `getDependencies(typedData, type)` 141 | 142 | This function returns all sub-types used by a struct as a string array. If the struct has no sub-types (like `EIP712Domain`), an array with only the type itself is returned. 143 | 144 | ```js 145 | import { getDependencies } from 'eip-712'; 146 | 147 | const typedData = { /*...*/ }; 148 | console.log(getDependencies(typedData, 'EIP712Domain')); // ['EIP712Domain'] 149 | console.log(getDependencies(typedData, 'Mail')); // ['Mail', 'Person'] 150 | ``` 151 | 152 | ### Non-standard domains (e.g., CIP-23) 153 | 154 | It's possible to use a custom domain format, like from the CIP-23 specification, or to disable verifying the domain format completely, if you want to use a custom implementation of EIP-712. 155 | 156 | To do this, you can pass options to each function, as last parameter. For example, in order to get the message hash for a message, you can do the following: 157 | 158 | ```ts 159 | import { getMessage } from 'eip-712'; 160 | import type { Options } from 'eip-712'; 161 | 162 | const typedData = { /*...*/ }; 163 | const options: Options = { 164 | // A custom domain identifier. Defaults to 'EIP712Domain'. 165 | domain: 'CIP23Domain', 166 | 167 | // Whether to verify the structure of the domain. Defaults to 'true'. 168 | verifyDomain: false 169 | }; 170 | 171 | console.log(getMessage(typedData, true, options).toString('hex')); 172 | ``` 173 | -------------------------------------------------------------------------------- /examples/ethereumjs-util/index.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { bytesToHex } from '@noble/hashes/utils'; 3 | import { ecsign } from 'ethereumjs-util'; 4 | import { getMessage, TypedData } from '../../src'; 5 | 6 | // The typed data to sign 7 | // prettier-ignore 8 | const typedData: TypedData = { 9 | types: { 10 | EIP712Domain: [ 11 | { name: 'name', type: 'string' }, 12 | { name: 'version', type: 'string' }, 13 | { name: 'chainId', type: 'uint256' }, 14 | { name: 'verifyingContract', type: 'address' } 15 | ], 16 | Person: [ 17 | { name: 'name', type: 'string' }, 18 | { name: 'wallet', type: 'address' } 19 | ], 20 | Mail: [ 21 | { name: 'from', type: 'Person' }, 22 | { name: 'to', type: 'Person' }, 23 | { name: 'contents', type: 'string' } 24 | ] 25 | }, 26 | primaryType: 'Mail', 27 | domain: { 28 | name: 'Ether Mail', 29 | version: '1', 30 | chainId: 1, 31 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' 32 | }, 33 | message: { 34 | from: { 35 | name: 'Cow', 36 | wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' 37 | }, 38 | to: { 39 | name: 'Bob', 40 | wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' 41 | }, 42 | contents: 'Hello, Bob!' 43 | } 44 | }; 45 | 46 | // Generate a random private key 47 | const privateKey = randomBytes(32); 48 | 49 | // Get a signable message from the typed data 50 | const message = getMessage(typedData, true); 51 | 52 | // Sign the message with the private key 53 | const { r, s, v } = ecsign(Buffer.from(message), privateKey); 54 | 55 | /* eslint-disable no-console */ 56 | console.log(`Message: 0x${bytesToHex(message)}`); 57 | console.log(`Signature: (0x${r.toString('hex')}, 0x${s.toString('hex')}, ${v})`); 58 | -------------------------------------------------------------------------------- /examples/ethers.js/index.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { bytesToHex } from '@noble/hashes/utils'; 3 | import { utils } from 'ethers'; 4 | import { getMessage, TypedData } from '../../src'; 5 | 6 | // The typed data to sign 7 | // prettier-ignore 8 | const typedData: TypedData = { 9 | types: { 10 | EIP712Domain: [ 11 | { name: 'name', type: 'string' }, 12 | { name: 'version', type: 'string' }, 13 | { name: 'chainId', type: 'uint256' }, 14 | { name: 'verifyingContract', type: 'address' } 15 | ], 16 | Person: [ 17 | { name: 'name', type: 'string' }, 18 | { name: 'wallet', type: 'address' } 19 | ], 20 | Mail: [ 21 | { name: 'from', type: 'Person' }, 22 | { name: 'to', type: 'Person' }, 23 | { name: 'contents', type: 'string' } 24 | ] 25 | }, 26 | primaryType: 'Mail', 27 | domain: { 28 | name: 'Ether Mail', 29 | version: '1', 30 | chainId: 1, 31 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' 32 | }, 33 | message: { 34 | from: { 35 | name: 'Cow', 36 | wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' 37 | }, 38 | to: { 39 | name: 'Bob', 40 | wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' 41 | }, 42 | contents: 'Hello, Bob!' 43 | } 44 | }; 45 | 46 | // Generate a random private key 47 | const privateKey = randomBytes(32); 48 | const signingKey = new utils.SigningKey(privateKey); 49 | 50 | // Get a signable message from the typed data 51 | const message = getMessage(typedData, true); 52 | 53 | // Sign the message with the private key 54 | const { r, s, v } = signingKey.signDigest(message); 55 | 56 | /* eslint-disable no-console */ 57 | console.log(`Message: 0x${bytesToHex(message)}`); 58 | console.log(`Signature: (${r}, ${s}, ${v})`); 59 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['src/'], 3 | clearMocks: true, 4 | collectCoverage: true, 5 | collectCoverageFrom: ['**/*.ts?(x)', '!**/*.d.ts'], 6 | transform: { 7 | '^.+\\.[t|j]sx?$': 'babel-jest' 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eip-712", 3 | "version": "1.0.0", 4 | "description": "Tiny library with utility functions that can help with signing and verifying EIP-712 based messages", 5 | "author": "Maarten Zuidhoorn ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Mrtenz/eip-712.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/Mrtenz/eip-712/issues" 12 | }, 13 | "keywords": [ 14 | "ethereum", 15 | "eip712" 16 | ], 17 | "license": "MIT", 18 | "main": "lib/cjs/index.js", 19 | "module": "lib/es/index.js", 20 | "typings": "typings/index.d.ts", 21 | "sideEffects": false, 22 | "engines": { 23 | "node": ">=10" 24 | }, 25 | "files": [ 26 | "lib", 27 | "src", 28 | "typings" 29 | ], 30 | "scripts": { 31 | "clean": "rimraf lib", 32 | "build": "yarn run clean && yarn run build:source && yarn run build:declarations", 33 | "build:source": "yarn run build:source:cjs && yarn run build:source:es", 34 | "build:source:cjs": "cross-env NODE_ENV=production BABEL_ENV=cjs babel src --extensions '.ts' --source-maps --out-dir lib/cjs", 35 | "build:source:es": "cross-env NODE_ENV=production BABEL_ENV=es babel src --extensions '.ts' --source-maps --out-dir lib/es", 36 | "build:declarations": "tsc -p tsconfig.build.json", 37 | "test": "jest", 38 | "lint": "yarn run lint:tsc && yarn run lint:eslint && yarn run lint:prettier && yarn run lint:lockfile", 39 | "lint:tsc": "tsc --noEmit", 40 | "lint:eslint": "eslint . --ignore-path .gitignore --ext .ts,.tsx,.js,.jsx", 41 | "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{ts,tsx,js,json}'", 42 | "lint:lockfile": "lockfile-lint --type yarn --path yarn.lock --allowed-hosts yarn --validate-https --validate-checksum --validate-integrity", 43 | "format": "prettier --write --ignore-path .gitignore '**/*.{ts,tsx,js,json}'", 44 | "prepare": "yarn run build" 45 | }, 46 | "dependencies": { 47 | "@findeth/abi": "^0.3.0", 48 | "@noble/hashes": "^1.0.0", 49 | "superstruct": "^0.15.3" 50 | }, 51 | "devDependencies": { 52 | "@babel/cli": "^7.10.5", 53 | "@babel/core": "^7.11.4", 54 | "@babel/plugin-transform-modules-commonjs": "^7.10.4", 55 | "@babel/preset-env": "^7.11.0", 56 | "@babel/preset-typescript": "^7.10.4", 57 | "@types/jest": "^26.0.10", 58 | "@types/keccak": "^3.0.1", 59 | "@typescript-eslint/eslint-plugin": "^3.10.1", 60 | "@typescript-eslint/parser": "^3.10.1", 61 | "babel-jest": "^26.3.0", 62 | "codecov": "^3.7.2", 63 | "cross-env": "^7.0.2", 64 | "eslint": "^7.7.0", 65 | "eslint-config-prettier": "^6.11.0", 66 | "eslint-plugin-import": "^2.22.0", 67 | "eslint-plugin-jest": "^23.20.0", 68 | "ethereumjs-util": "^7.0.4", 69 | "ethers": "^5.0.9", 70 | "husky": "^4.2.5", 71 | "jest": "^27.5.1", 72 | "lint-staged": "^10.2.13", 73 | "lockfile-lint": "^4.3.7", 74 | "prettier": "^2.1.1", 75 | "rimraf": "^3.0.2", 76 | "ts-node": "^9.0.0", 77 | "typescript": "^4.0.2" 78 | }, 79 | "lint-staged": { 80 | "*.{ts,tsx}": [ 81 | "prettier --write", 82 | "eslint --fix" 83 | ], 84 | "*.{js,json}": [ 85 | "prettier --write" 86 | ] 87 | }, 88 | "husky": { 89 | "hooks": { 90 | "pre-commit": "lint-staged" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/__fixtures__/invalid-array-length.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "Person" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person[]" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string[3]" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "name": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": { 55 | "name": "Cow", 56 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 57 | }, 58 | "to": [ 59 | { 60 | "name": "Bob", 61 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 62 | }, 63 | { 64 | "name": "Alice", 65 | "wallet": "0xdddddddddddddddddddddddddddddddddddddddd" 66 | }, 67 | { 68 | "name": "Michael", 69 | "wallet": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" 70 | } 71 | ], 72 | "contents": ["Hello, Bob!", "Hello, Alice!"] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/__fixtures__/invalid-array-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "Person" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person[]" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string[3]" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "name": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": { 55 | "name": "Cow", 56 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 57 | }, 58 | "to": "Bob", 59 | "contents": ["Hello, Bob!", "Hello, Alice!", "Hello, Michael!"] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/__fixtures__/invalid-missing-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "Person" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "name": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": { 55 | "name": "Cow", 56 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 57 | }, 58 | "contents": "Hello, Bob!" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/__fixtures__/invalid-missing-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ] 31 | }, 32 | "primaryType": "Mail", 33 | "domain": { 34 | "name": "Ether Mail", 35 | "version": "1", 36 | "chainId": 1, 37 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 38 | }, 39 | "message": { 40 | "from": { 41 | "name": "Cow", 42 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 43 | }, 44 | "to": { 45 | "name": "Bob", 46 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 47 | }, 48 | "contents": "Hello, Bob!" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/__fixtures__/invalid-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "Person" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "foo": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": { 55 | "name": "Cow", 56 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 57 | }, 58 | "to": { 59 | "name": "Bob", 60 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 61 | }, 62 | "contents": "Hello, Bob!" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/__fixtures__/invalid-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "[]" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "-string" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "name": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": { 55 | "name": "Cow", 56 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 57 | }, 58 | "to": { 59 | "name": "Bob", 60 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 61 | }, 62 | "contents": "Hello, Bob!" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/__fixtures__/typed-data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "Person" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "name": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": { 55 | "name": "Cow", 56 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 57 | }, 58 | "to": { 59 | "name": "Bob", 60 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 61 | }, 62 | "contents": "Hello, Bob!" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/__fixtures__/typed-data-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | }, 20 | { 21 | "name": "salt", 22 | "type": "bytes32" 23 | } 24 | ], 25 | "Transaction": [ 26 | { 27 | "name": "to", 28 | "type": "address" 29 | }, 30 | { 31 | "name": "amount", 32 | "type": "uint256" 33 | }, 34 | { 35 | "name": "data", 36 | "type": "bytes" 37 | }, 38 | { 39 | "name": "nonce", 40 | "type": "uint256" 41 | } 42 | ], 43 | "TransactionApproval": [ 44 | { 45 | "name": "owner", 46 | "type": "address" 47 | }, 48 | { 49 | "name": "transaction", 50 | "type": "Transaction" 51 | } 52 | ] 53 | }, 54 | "primaryType": "TransactionApproval", 55 | "domain": { 56 | "name": "Multisig Wallet", 57 | "version": "1", 58 | "chainId": 1, 59 | "verifyingContract": "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa", 60 | "salt": "0x1dbbd6c8d75f4b446bcb44cee3ba5da8120e056d4d2f817213df8703ef065ed3" 61 | }, 62 | "message": { 63 | "owner": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", 64 | "transaction": { 65 | "to": "0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520", 66 | "amount": "1000000000000000000", 67 | "data": "", 68 | "nonce": "1" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/__fixtures__/typed-data-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "EIP712Domain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "Person[]" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person[]" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string[3]" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "name": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": [ 55 | { 56 | "name": "Cow", 57 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 58 | } 59 | ], 60 | "to": [ 61 | { 62 | "name": "Bob", 63 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 64 | }, 65 | { 66 | "name": "Alice", 67 | "wallet": "0xdddddddddddddddddddddddddddddddddddddddd" 68 | }, 69 | { 70 | "name": "Michael", 71 | "wallet": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" 72 | } 73 | ], 74 | "contents": ["Hello, Bob!", "Hello, Alice!", "Hello, Michael!"] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/__fixtures__/typed-data-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "FooBarDomain": [ 4 | { 5 | "name": "name", 6 | "type": "string" 7 | }, 8 | { 9 | "name": "version", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "chainId", 14 | "type": "uint256" 15 | }, 16 | { 17 | "name": "verifyingContract", 18 | "type": "address" 19 | } 20 | ], 21 | "Person": [ 22 | { 23 | "name": "name", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "wallet", 28 | "type": "address" 29 | } 30 | ], 31 | "Mail": [ 32 | { 33 | "name": "from", 34 | "type": "Person[]" 35 | }, 36 | { 37 | "name": "to", 38 | "type": "Person[]" 39 | }, 40 | { 41 | "name": "contents", 42 | "type": "string[3]" 43 | } 44 | ] 45 | }, 46 | "primaryType": "Mail", 47 | "domain": { 48 | "name": "Ether Mail", 49 | "version": "1", 50 | "chainId": 1, 51 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 52 | }, 53 | "message": { 54 | "from": [ 55 | { 56 | "name": "Cow", 57 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 58 | } 59 | ], 60 | "to": [ 61 | { 62 | "name": "Bob", 63 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 64 | }, 65 | { 66 | "name": "Alice", 67 | "wallet": "0xdddddddddddddddddddddddddddddddddddddddd" 68 | }, 69 | { 70 | "name": "Michael", 71 | "wallet": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" 72 | } 73 | ], 74 | "contents": ["Hello, Bob!", "Hello, Alice!", "Hello, Michael!"] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/__fixtures__/typed-data-5.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "FooBarDomain": [ 4 | { 5 | "name": "foo", 6 | "type": "string" 7 | } 8 | ], 9 | "Person": [ 10 | { 11 | "name": "name", 12 | "type": "string" 13 | }, 14 | { 15 | "name": "wallet", 16 | "type": "address" 17 | } 18 | ], 19 | "Mail": [ 20 | { 21 | "name": "from", 22 | "type": "Person[]" 23 | }, 24 | { 25 | "name": "to", 26 | "type": "Person[]" 27 | }, 28 | { 29 | "name": "contents", 30 | "type": "string[3]" 31 | } 32 | ] 33 | }, 34 | "primaryType": "Mail", 35 | "domain": { 36 | "foo": "Ether Mail" 37 | }, 38 | "message": { 39 | "from": [ 40 | { 41 | "name": "Cow", 42 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 43 | } 44 | ], 45 | "to": [ 46 | { 47 | "name": "Bob", 48 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 49 | }, 50 | { 51 | "name": "Alice", 52 | "wallet": "0xdddddddddddddddddddddddddddddddddddddddd" 53 | }, 54 | { 55 | "name": "Michael", 56 | "wallet": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" 57 | } 58 | ], 59 | "contents": ["Hello, Bob!", "Hello, Alice!", "Hello, Michael!"] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/__fixtures__/types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": { 4 | "EIP712Domain": [{ "name": "name", "type": "string" }] 5 | } 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /src/eip-712.test.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHex } from '@noble/hashes/utils'; 2 | import invalidArrayLength from './__fixtures__/invalid-array-length.json'; 3 | import invalidArrayType from './__fixtures__/invalid-array-type.json'; 4 | import invalidMissingData from './__fixtures__/invalid-missing-data.json'; 5 | import invalidMissingType from './__fixtures__/invalid-missing-type.json'; 6 | import invalidSchema from './__fixtures__/invalid-schema.json'; 7 | import invalidType from './__fixtures__/invalid-type.json'; 8 | import mailTypedData from './__fixtures__/typed-data-1.json'; 9 | import approvalTypedData from './__fixtures__/typed-data-2.json'; 10 | import arrayTypedData from './__fixtures__/typed-data-3.json'; 11 | import customTypedData from './__fixtures__/typed-data-4.json'; 12 | import { asArray, encodeData, encodeType, getDependencies, getMessage, getStructHash, getTypeHash } from './eip-712'; 13 | 14 | describe('getDependencies', () => { 15 | it('returns all dependencies for the primary type', () => { 16 | expect(getDependencies(mailTypedData, 'EIP712Domain')).toStrictEqual(['EIP712Domain']); 17 | expect(getDependencies(mailTypedData, 'Person')).toStrictEqual(['Person']); 18 | expect(getDependencies(mailTypedData, 'Mail')).toStrictEqual(['Mail', 'Person']); 19 | 20 | expect(getDependencies(approvalTypedData, 'EIP712Domain')).toStrictEqual(['EIP712Domain']); 21 | expect(getDependencies(approvalTypedData, 'Transaction')).toStrictEqual(['Transaction']); 22 | expect(getDependencies(approvalTypedData, 'TransactionApproval')).toStrictEqual([ 23 | 'TransactionApproval', 24 | 'Transaction' 25 | ]); 26 | 27 | expect(getDependencies(arrayTypedData, 'EIP712Domain')).toStrictEqual(['EIP712Domain']); 28 | expect(getDependencies(arrayTypedData, 'Person')).toStrictEqual(['Person']); 29 | expect(getDependencies(arrayTypedData, 'Mail')).toStrictEqual(['Mail', 'Person']); 30 | 31 | expect(getDependencies(customTypedData, 'FooBarDomain', { domain: 'FooBarDomain' })).toStrictEqual([ 32 | 'FooBarDomain' 33 | ]); 34 | }); 35 | 36 | it('throws for invalid JSON data', () => { 37 | expect(() => getDependencies(invalidSchema, 'EIP712Domain')).toThrow(); 38 | }); 39 | 40 | it('throws for invalid types', () => { 41 | expect(() => getDependencies(invalidType, 'EIP712Domain')).toThrow(); 42 | expect(() => getDependencies(invalidType, 'Person')).toThrow(); 43 | expect(() => getDependencies(invalidType, 'Mail')).toThrow(); 44 | }); 45 | }); 46 | 47 | describe('encodeType', () => { 48 | it('encodes a type to a hashable string', () => { 49 | expect(encodeType(mailTypedData, 'EIP712Domain')).toBe( 50 | 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' 51 | ); 52 | expect(encodeType(mailTypedData, 'Person')).toBe('Person(string name,address wallet)'); 53 | expect(encodeType(mailTypedData, 'Mail')).toBe( 54 | 'Mail(Person from,Person to,string contents)Person(string name,address wallet)' 55 | ); 56 | 57 | expect(encodeType(approvalTypedData, 'EIP712Domain')).toBe( 58 | 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)' 59 | ); 60 | expect(encodeType(approvalTypedData, 'Transaction')).toBe( 61 | 'Transaction(address to,uint256 amount,bytes data,uint256 nonce)' 62 | ); 63 | expect(encodeType(approvalTypedData, 'TransactionApproval')).toBe( 64 | 'TransactionApproval(address owner,Transaction transaction)Transaction(address to,uint256 amount,bytes data,uint256 nonce)' 65 | ); 66 | 67 | expect(encodeType(customTypedData, 'FooBarDomain', { domain: 'FooBarDomain' })).toBe( 68 | 'FooBarDomain(string name,string version,uint256 chainId,address verifyingContract)' 69 | ); 70 | }); 71 | 72 | it('throws for invalid JSON data', () => { 73 | expect(() => encodeType(invalidSchema, 'EIP712Domain')).toThrow(); 74 | }); 75 | 76 | it('throws for invalid types', () => { 77 | expect(() => encodeType(invalidType, 'EIP712Domain')).toThrow(); 78 | expect(() => encodeType(invalidType, 'Person')).toThrow(); 79 | expect(() => encodeType(invalidType, 'Mail')).toThrow(); 80 | }); 81 | }); 82 | 83 | describe('getTypeHash', () => { 84 | it('returns a 32 byte hash for a type', () => { 85 | expect(bytesToHex(getTypeHash(mailTypedData, 'EIP712Domain'))).toBe( 86 | '8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f' 87 | ); 88 | expect(bytesToHex(getTypeHash(mailTypedData, 'Person'))).toBe( 89 | 'b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500' 90 | ); 91 | expect(bytesToHex(getTypeHash(mailTypedData, 'Mail'))).toBe( 92 | 'a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2' 93 | ); 94 | 95 | expect(bytesToHex(getTypeHash(approvalTypedData, 'EIP712Domain'))).toBe( 96 | 'd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472' 97 | ); 98 | expect(bytesToHex(getTypeHash(approvalTypedData, 'Transaction'))).toBe( 99 | 'a826c254899945d99ae513c9f1275b904f19492f4438f3d8364fa98e70fbf233' 100 | ); 101 | expect(bytesToHex(getTypeHash(approvalTypedData, 'TransactionApproval'))).toBe( 102 | '5b360b7b2cc780b6a0687ac409805af3219ef7d9dcc865669e39a1dc7394ffc5' 103 | ); 104 | 105 | expect(bytesToHex(getTypeHash(arrayTypedData, 'EIP712Domain'))).toBe( 106 | '8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f' 107 | ); 108 | expect(bytesToHex(getTypeHash(arrayTypedData, 'Mail'))).toBe( 109 | 'c81112a69b6596b8bc0678e67d97fbf9bed619811fc781419323ec02d1c7290d' 110 | ); 111 | expect(bytesToHex(getTypeHash(arrayTypedData, 'Person'))).toBe( 112 | 'b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500' 113 | ); 114 | 115 | expect(bytesToHex(getTypeHash(customTypedData, 'FooBarDomain', { domain: 'FooBarDomain' }))).toBe( 116 | '85b412c5db9e26aa4f6bf794e72b1557f463a0978ceef9acaff7f6ff1eb24e57' 117 | ); 118 | }); 119 | 120 | it('throws for invalid JSON data', () => { 121 | expect(() => getTypeHash(invalidSchema, 'EIP712Domain')).toThrow(); 122 | }); 123 | }); 124 | 125 | describe('encodeData', () => { 126 | it('encodes data to an ABI encoded string', () => { 127 | expect(bytesToHex(encodeData(mailTypedData, 'EIP712Domain', mailTypedData.domain))).toBe( 128 | '8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400fc70ef06638535b4881fafcac8287e210e3769ff1a8e91f1b95d6246e61e4d3c6c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cccccccccccccccccccccccccccccccccccccccc' 129 | ); 130 | expect(bytesToHex(encodeData(mailTypedData, 'Person', mailTypedData.message.from))).toBe( 131 | 'b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c795008c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826' 132 | ); 133 | expect(bytesToHex(encodeData(mailTypedData, 'Mail', mailTypedData.message))).toBe( 134 | 'a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8' 135 | ); 136 | 137 | expect(bytesToHex(encodeData(approvalTypedData, 'EIP712Domain', approvalTypedData.domain))).toBe( 138 | 'd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472d210ccb0bd8574cfdb6efd17ae4e6ab527687a29dcf03060d4a41b9b56d0b637c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1dbbd6c8d75f4b446bcb44cee3ba5da8120e056d4d2f817213df8703ef065ed3' 139 | ); 140 | expect(bytesToHex(encodeData(approvalTypedData, 'Transaction', approvalTypedData.message.transaction))).toBe( 141 | 'a826c254899945d99ae513c9f1275b904f19492f4438f3d8364fa98e70fbf2330000000000000000000000004bbeeb066ed09b7aed07bf39eee0460dfa2615200000000000000000000000000000000000000000000000000de0b6b3a7640000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4700000000000000000000000000000000000000000000000000000000000000001' 142 | ); 143 | expect(bytesToHex(encodeData(approvalTypedData, 'TransactionApproval', approvalTypedData.message))).toBe( 144 | '5b360b7b2cc780b6a0687ac409805af3219ef7d9dcc865669e39a1dc7394ffc5000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb9e7ba42b4ace63ae7d8ee163d5e642a085b32c2553717dcb37974e83fad289d0' 145 | ); 146 | 147 | expect(bytesToHex(encodeData(arrayTypedData, 'Mail', arrayTypedData.message))).toBe( 148 | 'c81112a69b6596b8bc0678e67d97fbf9bed619811fc781419323ec02d1c7290dafd2599280d009dcb3e261f4bccebec901d67c3f54b56d49bf8327359fc69cd7392bb8ab5338a9075ce8fec1b431e334007d4de1e5e83201ca35762e24428e24b7c4150525d88db452c5f08f93f4593daa458ab6280b012532183aed3a8e4a01' 149 | ); 150 | 151 | expect( 152 | bytesToHex(encodeData(customTypedData, 'FooBarDomain', customTypedData.domain, { domain: 'FooBarDomain' })) 153 | ).toBe( 154 | '85b412c5db9e26aa4f6bf794e72b1557f463a0978ceef9acaff7f6ff1eb24e57c70ef06638535b4881fafcac8287e210e3769ff1a8e91f1b95d6246e61e4d3c6c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cccccccccccccccccccccccccccccccccccccccc' 155 | ); 156 | }); 157 | 158 | it('throws for invalid JSON data', () => { 159 | expect(() => encodeData(invalidSchema, 'EIP712Domain', invalidSchema.domain)).toThrow(); 160 | }); 161 | 162 | it('throws when a type is missing', () => { 163 | expect(() => encodeData(invalidMissingData, 'Mail', invalidMissingData.message)).toThrow(); 164 | }); 165 | 166 | it('throws when data is missing', () => { 167 | expect(() => encodeData(invalidMissingType, 'Mail', invalidMissingType.message)).toThrow(); 168 | }); 169 | 170 | it('throws if the type is not an array', () => { 171 | expect(() => encodeData(invalidArrayType, 'Mail', invalidArrayType.message)).toThrow(); 172 | }); 173 | 174 | it('throws if the array length is invalid', () => { 175 | expect(() => encodeData(invalidArrayLength, 'Mail', invalidArrayLength.message)).toThrow(); 176 | }); 177 | }); 178 | 179 | describe('getStructHash', () => { 180 | it('returns a 32 byte hash for a struct', () => { 181 | expect(bytesToHex(getStructHash(mailTypedData, 'EIP712Domain', mailTypedData.domain))).toBe( 182 | 'f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f' 183 | ); 184 | expect(bytesToHex(getStructHash(mailTypedData, 'Person', mailTypedData.message.from))).toBe( 185 | 'fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8' 186 | ); 187 | expect(bytesToHex(getStructHash(mailTypedData, 'Mail', mailTypedData.message))).toBe( 188 | 'c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e' 189 | ); 190 | 191 | expect(bytesToHex(getStructHash(approvalTypedData, 'EIP712Domain', approvalTypedData.domain))).toBe( 192 | '67083568259b4a947b02ce4dca4cc91f1e7f01d109c8805668755be5ab5adbb9' 193 | ); 194 | expect(bytesToHex(getStructHash(approvalTypedData, 'Transaction', approvalTypedData.message.transaction))).toBe( 195 | '9e7ba42b4ace63ae7d8ee163d5e642a085b32c2553717dcb37974e83fad289d0' 196 | ); 197 | expect(bytesToHex(getStructHash(approvalTypedData, 'TransactionApproval', approvalTypedData.message))).toBe( 198 | '309886ad75ec7c2c6a69bffa2669bad00e3b1e0a85221eff4e8926a2f8ff5077' 199 | ); 200 | 201 | expect(bytesToHex(getStructHash(customTypedData, 'FooBarDomain', customTypedData.domain))).toBe( 202 | '6ff4505ed33bedaadf3491aa039d9ccb91a3114eeab940e69fdecb809fb26882' 203 | ); 204 | }); 205 | 206 | it('throws for invalid JSON data', () => { 207 | expect(() => getStructHash(invalidSchema, 'EIP712Domain', invalidSchema.domain)).toThrow(); 208 | }); 209 | 210 | it('throws when a type is missing', () => { 211 | expect(() => encodeData(invalidMissingType, 'Mail', invalidSchema.message)).toThrow(); 212 | }); 213 | 214 | it('throws when data is missing', () => { 215 | expect(() => encodeData(invalidMissingType, 'Mail', invalidSchema.message)).toThrow(); 216 | }); 217 | }); 218 | 219 | describe('getMessage', () => { 220 | it('returns the full encoded and hashed message to sign', () => { 221 | expect(bytesToHex(getMessage(mailTypedData))).toBe( 222 | '1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090fc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e' 223 | ); 224 | expect(bytesToHex(getMessage(approvalTypedData))).toBe( 225 | '190167083568259b4a947b02ce4dca4cc91f1e7f01d109c8805668755be5ab5adbb9309886ad75ec7c2c6a69bffa2669bad00e3b1e0a85221eff4e8926a2f8ff5077' 226 | ); 227 | expect(bytesToHex(getMessage(arrayTypedData))).toBe( 228 | '1901f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f6757567025d2ba15d5ebb228ea677055b8b601007e60e9463f6ed7c68f918189' 229 | ); 230 | expect(bytesToHex(getMessage(customTypedData, false, { domain: 'FooBarDomain' }))).toBe( 231 | '19016ff4505ed33bedaadf3491aa039d9ccb91a3114eeab940e69fdecb809fb268826757567025d2ba15d5ebb228ea677055b8b601007e60e9463f6ed7c68f918189' 232 | ); 233 | }); 234 | 235 | it('hashes the message with Keccak-256', () => { 236 | expect(bytesToHex(getMessage(mailTypedData, true))).toBe( 237 | 'be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2' 238 | ); 239 | expect(bytesToHex(getMessage(approvalTypedData, true))).toBe( 240 | 'ee0cdea747f4a81355be92dbf30e209dbd2954a82d5a82482b7c7800089c7f57' 241 | ); 242 | expect(bytesToHex(getMessage(arrayTypedData, true))).toBe( 243 | 'c6f6c8028eadb17bc5c9e2ea2f738e92e49cfa627d19896c250fd2eac653e4e0' 244 | ); 245 | expect(bytesToHex(getMessage(customTypedData, true, { domain: 'FooBarDomain' }))).toBe( 246 | 'e028c0622beef9bde70e78a98c1d09a95ffe0cd9cfa5ff6a99f7db7c9245e103' 247 | ); 248 | }); 249 | 250 | it('throws for invalid JSON data', () => { 251 | expect(() => getMessage(invalidSchema)).toThrow(); 252 | }); 253 | 254 | it('throws when a type is missing', () => { 255 | expect(() => getMessage(invalidMissingType)).toThrow(); 256 | }); 257 | 258 | it('throws when data is missing', () => { 259 | expect(() => getMessage(invalidMissingData)).toThrow(); 260 | }); 261 | }); 262 | 263 | describe('asArray', () => { 264 | it('returns the typed data as array', () => { 265 | expect(asArray(mailTypedData)).toStrictEqual([ 266 | ['Cow', '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'], 267 | ['Bob', '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'], 268 | 'Hello, Bob!' 269 | ]); 270 | 271 | expect(asArray(approvalTypedData)).toStrictEqual([ 272 | '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 273 | ['0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520', '1000000000000000000', '', '1'] 274 | ]); 275 | }); 276 | 277 | it('throws for invalid JSON data', () => { 278 | expect(() => asArray(invalidSchema)).toThrow(); 279 | }); 280 | 281 | it('throws when a type is missing', () => { 282 | expect(() => asArray(invalidMissingType)).toThrow(); 283 | }); 284 | 285 | it('throws when data is missing', () => { 286 | expect(() => asArray(invalidMissingData)).toThrow(); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /src/eip-712.ts: -------------------------------------------------------------------------------- 1 | import { getOptions, Options } from './options'; 2 | import { ARRAY_REGEX, TYPE_REGEX, TypedData } from './types'; 3 | import { keccak256, toBuffer, validateTypedData, encode } from './utils'; 4 | 5 | const EIP_191_PREFIX = Buffer.from('1901', 'hex'); 6 | 7 | /** 8 | * Get the dependencies of a struct type. If a struct has the same dependency multiple times, it's only included once 9 | * in the resulting array. 10 | */ 11 | export const getDependencies = ( 12 | typedData: TypedData, 13 | type: string, 14 | options?: Options, 15 | dependencies: string[] = [] 16 | ): string[] => { 17 | // `getDependencies` is called by most other functions, so we validate the JSON schema here 18 | if (!validateTypedData(typedData, options)) { 19 | throw new Error('Typed data does not match JSON schema'); 20 | } 21 | 22 | const match = type.match(TYPE_REGEX)!; 23 | const actualType = match[0]; 24 | if (dependencies.includes(actualType)) { 25 | return dependencies; 26 | } 27 | 28 | if (!typedData.types[actualType]) { 29 | return dependencies; 30 | } 31 | 32 | return [ 33 | actualType, 34 | ...typedData.types[actualType].reduce( 35 | (previous, type) => [ 36 | ...previous, 37 | ...getDependencies(typedData, type.type, options, previous).filter( 38 | (dependency) => !previous.includes(dependency) 39 | ) 40 | ], 41 | [] 42 | ) 43 | ]; 44 | }; 45 | 46 | /** 47 | * Encode a type to a string. All dependant types are alphabetically sorted. 48 | * 49 | * @param {TypedData} typedData 50 | * @param {string} type 51 | * @param {Options} [options] 52 | * @return {string} 53 | */ 54 | export const encodeType = (typedData: TypedData, type: string, options?: Options): string => { 55 | const [primary, ...dependencies] = getDependencies(typedData, type, options); 56 | const types = [primary, ...dependencies.sort()]; 57 | 58 | return types 59 | .map((dependency) => { 60 | return `${dependency}(${typedData.types[dependency].map((type) => `${type.type} ${type.name}`)})`; 61 | }) 62 | .join(''); 63 | }; 64 | 65 | /** 66 | * Get a type string as hash. 67 | */ 68 | export const getTypeHash = (typedData: TypedData, type: string, options?: Options): Uint8Array => { 69 | return keccak256(encodeType(typedData, type, options), 'utf8'); 70 | }; 71 | 72 | /** 73 | * Encodes a single value to an ABI serialisable string, number or Buffer. Returns the data as tuple, which consists of 74 | * an array of ABI compatible types, and an array of corresponding values. 75 | */ 76 | const encodeValue = ( 77 | typedData: TypedData, 78 | type: string, 79 | data: unknown, 80 | options?: Options 81 | ): [string, string | Uint8Array | number] => { 82 | const match = type.match(ARRAY_REGEX); 83 | 84 | // Checks for array types 85 | if (match) { 86 | const arrayType = match[1]; 87 | const length = Number(match[2]) || undefined; 88 | 89 | if (!Array.isArray(data)) { 90 | throw new Error('Cannot encode data: value is not of array type'); 91 | } 92 | 93 | if (length && data.length !== length) { 94 | throw new Error(`Cannot encode data: expected length of ${length}, but got ${data.length}`); 95 | } 96 | 97 | const encodedData = data.map((item) => encodeValue(typedData, arrayType, item, options)); 98 | const types = encodedData.map((item) => item[0]); 99 | const values = encodedData.map((item) => item[1]); 100 | 101 | return ['bytes32', keccak256(encode(types, values))]; 102 | } 103 | 104 | if (typedData.types[type]) { 105 | return ['bytes32', getStructHash(typedData, type, data as Record, options)]; 106 | } 107 | 108 | // Strings and arbitrary byte arrays are hashed to bytes32 109 | if (type === 'string') { 110 | return ['bytes32', keccak256(data as string, 'utf8')]; 111 | } 112 | 113 | if (type === 'bytes') { 114 | return ['bytes32', keccak256(Buffer.isBuffer(data) ? data : toBuffer(data as string), 'hex')]; 115 | } 116 | 117 | return [type, data as string]; 118 | }; 119 | 120 | /** 121 | * Encode the data to an ABI encoded Buffer. The data should be a key -> value object with all the required values. All 122 | * dependant types are automatically encoded. 123 | */ 124 | export const encodeData = ( 125 | typedData: TypedData, 126 | type: string, 127 | data: Record, 128 | options?: Options 129 | ): Uint8Array => { 130 | const [types, values] = typedData.types[type].reduce<[string[], unknown[]]>( 131 | ([types, values], field) => { 132 | if (data[field.name] === undefined || data[field.name] === null) { 133 | throw new Error(`Cannot encode data: missing data for '${field.name}'`); 134 | } 135 | 136 | const value = data[field.name]; 137 | const [type, encodedValue] = encodeValue(typedData, field.type, value, options); 138 | 139 | return [ 140 | [...types, type], 141 | [...values, encodedValue] 142 | ]; 143 | }, 144 | [['bytes32'], [getTypeHash(typedData, type, options)]] 145 | ); 146 | 147 | return encode(types, values); 148 | }; 149 | 150 | /** 151 | * Get encoded data as a hash. The data should be a key -> value object with all the required values. All dependant 152 | * types are automatically encoded. 153 | */ 154 | export const getStructHash = ( 155 | typedData: TypedData, 156 | type: string, 157 | data: Record, 158 | options?: Options 159 | ): Uint8Array => { 160 | return keccak256(encodeData(typedData, type, data, options)); 161 | }; 162 | 163 | /** 164 | * Get the EIP-191 encoded message to sign, from the typedData object. If `hash` is enabled, the message will be hashed 165 | * with Keccak256. 166 | */ 167 | export const getMessage = (typedData: TypedData, hash?: boolean, options?: Options): Uint8Array => { 168 | const { domain } = getOptions(options); 169 | const message = Buffer.concat([ 170 | EIP_191_PREFIX, 171 | getStructHash(typedData, domain, typedData.domain as Record, options), 172 | getStructHash(typedData, typedData.primaryType, typedData.message, options) 173 | ]); 174 | 175 | if (hash) { 176 | return keccak256(message); 177 | } 178 | 179 | return message; 180 | }; 181 | 182 | /** 183 | * Get the typed data as array. This can be useful for encoding the typed data with the contract ABI. 184 | */ 185 | export const asArray = ( 186 | typedData: TypedData, 187 | type: string = typedData.primaryType, 188 | data: Record = typedData.message, 189 | options?: Options 190 | ): unknown[] => { 191 | if (!validateTypedData(typedData, options)) { 192 | throw new Error('Typed data does not match JSON schema'); 193 | } 194 | 195 | if (!typedData.types[type]) { 196 | throw new Error('Cannot get data as array: type does not exist'); 197 | } 198 | 199 | return typedData.types[type].reduce((array, { name, type }) => { 200 | if (typedData.types[type]) { 201 | if (!data[name]) { 202 | throw new Error(`Cannot get data as array: missing data for '${name}'`); 203 | } 204 | 205 | return [...array, asArray(typedData, type, data[name] as Record, options)]; 206 | } 207 | 208 | const value = data[name]; 209 | return [...array, value]; 210 | }, []); 211 | }; 212 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './eip-712'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { boolean, create, defaulted, Infer, object, string } from 'superstruct'; 2 | 3 | export const OPTIONS_TYPE = object({ 4 | /** 5 | * The name of the domain struct. 6 | * 7 | * Default: "EIP712Domain" 8 | */ 9 | domain: defaulted(string(), 'EIP712Domain'), 10 | 11 | /** 12 | * Whether to verify if the domain matches the EIP-712 specification. When this is disabled, you can use any arbitrary 13 | * fields in the domain. 14 | * 15 | * Default: true 16 | */ 17 | verifyDomain: defaulted(boolean(), true) 18 | }); 19 | 20 | export type Options = Partial>; 21 | 22 | export const getOptions = (options: unknown): Required => { 23 | return create(options ?? {}, OPTIONS_TYPE); 24 | }; 25 | -------------------------------------------------------------------------------- /src/types.test.ts: -------------------------------------------------------------------------------- 1 | import { EIP712Type, isValidType } from './types'; 2 | 3 | describe('isValidType', () => { 4 | it('checks if a type is valid for the given typed data', () => { 5 | // prettier-ignore 6 | const types: Record = { 7 | EIP712Domain: [ 8 | { name: 'name', type: 'string' }, 9 | { name: 'version', type: 'string' }, 10 | { name: 'chainId', type: 'uint256' }, 11 | { name: 'verifyingContract', type: 'address' } 12 | ], 13 | Person: [ 14 | { name: 'name', type: 'string' }, 15 | { name: 'wallet', type: 'address' } 16 | ], 17 | Mail: [ 18 | { name: 'from', type: 'Person' }, 19 | { name: 'to', type: 'Person' }, 20 | { name: 'contents', type: 'string' } 21 | ] 22 | }; 23 | 24 | expect(isValidType(types, 'EIP712Domain')).toBe(true); 25 | expect(isValidType(types, 'EIP712Domain[]')).toBe(true); 26 | expect(isValidType(types, 'Person')).toBe(true); 27 | expect(isValidType(types, 'Mail')).toBe(true); 28 | 29 | expect(isValidType(types, 'address')).toBe(true); 30 | expect(isValidType(types, 'address[]')).toBe(true); 31 | expect(isValidType(types, 'bool')).toBe(true); 32 | expect(isValidType(types, 'bytes')).toBe(true); 33 | expect(isValidType(types, 'string')).toBe(true); 34 | 35 | expect(isValidType(types, 'bytes1')).toBe(true); 36 | expect(isValidType(types, 'bytes16')).toBe(true); 37 | expect(isValidType(types, 'bytes32')).toBe(true); 38 | 39 | expect(isValidType(types, 'uint256')).toBe(true); 40 | expect(isValidType(types, 'int256')).toBe(true); 41 | expect(isValidType(types, 'uint8')).toBe(true); 42 | expect(isValidType(types, 'int8')).toBe(true); 43 | 44 | expect(isValidType(types, 'Foo')).toBe(false); 45 | expect(isValidType(types, 'Foo[]')).toBe(false); 46 | expect(isValidType(types, 'Foo Bar[]')).toBe(false); 47 | 48 | expect(isValidType(types, 'bytes0')).toBe(false); 49 | expect(isValidType(types, 'bytes33')).toBe(false); 50 | 51 | expect(isValidType(types, 'uint')).toBe(false); 52 | expect(isValidType(types, 'int')).toBe(false); 53 | expect(isValidType(types, 'uint123')).toBe(false); 54 | expect(isValidType(types, 'int123')).toBe(false); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { array, assign, Infer, number, object, optional, pattern, record, refine, string, union } from 'superstruct'; 2 | 3 | export const TYPE_REGEX = /^\w+/; 4 | export const ARRAY_REGEX = /^(.*)\[([0-9]*?)]$/; 5 | export const BYTES_REGEX = /^bytes([0-9]{1,2})$/; 6 | export const NUMBER_REGEX = /^u?int([0-9]{0,3})$/; 7 | 8 | export const STATIC_TYPES = ['address', 'bool', 'bytes', 'string']; 9 | 10 | const TYPE = refine(string(), 'Type', (type, context) => { 11 | return isValidType(context.branch[0].types, type); 12 | }); 13 | 14 | export const EIP_712_TYPE = object({ 15 | name: string(), 16 | type: TYPE 17 | }); 18 | 19 | /** 20 | * A single type, as part of a struct. The `type` field can be any of the EIP-712 supported types. Currently those are: 21 | * - Atomic types: bytes1..32, uint8..256, int8..256, bool, address 22 | * - Dynamic types: bytes, string 23 | * - Reference types: array type (e.g. uint8[], SomeStruct[]), struct type (e.g. SomeStruct) 24 | * 25 | * Note that the `uint` and `int` aliases like in Solidity, and fixed point numbers are not supported by the EIP-712 26 | * standard. 27 | */ 28 | export type EIP712Type = Infer; 29 | 30 | export const EIP_712_DOMAIN_TYPE = object({ 31 | name: optional(string()), 32 | version: optional(string()), 33 | chainId: optional(union([string(), number()])), 34 | verifyingContract: optional(pattern(string(), /^0x[0-9a-z]{40}$/i)), 35 | salt: optional(union([array(number()), pattern(string(), /^0x[0-9a-z]{64}$/i)])) 36 | }); 37 | 38 | /** 39 | * The EIP712 domain struct. Any of these fields are optional, but it must contain at least one field. 40 | */ 41 | export type EIP712Domain = Infer; 42 | 43 | export const EIP_712_TYPED_DATA_TYPE = object({ 44 | types: record(string(), array(EIP_712_TYPE)), 45 | primaryType: string(), 46 | domain: object(), 47 | message: object() 48 | }); 49 | 50 | export const EIP_712_STRICT_TYPED_DATA_TYPE = assign( 51 | EIP_712_TYPED_DATA_TYPE, 52 | object({ 53 | domain: EIP_712_DOMAIN_TYPE 54 | }) 55 | ); 56 | 57 | /** 58 | * The complete typed data, with all the structs, domain data, primary type of the message, and the message itself. 59 | */ 60 | export type TypedData = Infer; 61 | export type StrictTypedData = Infer; 62 | 63 | /** 64 | * Checks if a type is valid with the given `typedData`. The following types are valid: 65 | * - Atomic types: bytes1..32, uint8..256, int8..256, bool, address 66 | * - Dynamic types: bytes, string 67 | * - Reference types: array type (e.g. uint8[], SomeStruct[]), struct type (e.g. SomeStruct) 68 | * 69 | * The `uint` and `int` aliases like in Solidity are not supported. Fixed point numbers are not supported. 70 | */ 71 | export const isValidType = (types: Record, type: string): boolean => { 72 | if (STATIC_TYPES.includes(type as string)) { 73 | return true; 74 | } 75 | 76 | if (types[type]) { 77 | return true; 78 | } 79 | 80 | if (type.match(ARRAY_REGEX)) { 81 | const match = type.match(TYPE_REGEX); 82 | if (match) { 83 | const innerType = match[0]; 84 | return isValidType(types, innerType); 85 | } 86 | } 87 | 88 | const bytesMatch = type.match(BYTES_REGEX); 89 | if (bytesMatch) { 90 | const length = Number(bytesMatch[1]); 91 | if (length >= 1 && length <= 32) { 92 | return true; 93 | } 94 | } 95 | 96 | const numberMatch = type.match(NUMBER_REGEX); 97 | if (numberMatch) { 98 | const length = Number(numberMatch[1]); 99 | if (length >= 8 && length <= 256 && length % 8 === 0) { 100 | return true; 101 | } 102 | } 103 | 104 | return false; 105 | }; 106 | -------------------------------------------------------------------------------- /src/utils/__fixtures__/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": [ 3 | "address", 4 | "bool", 5 | "bytes", 6 | "bytes1", 7 | "bytes16", 8 | "bytes32", 9 | "int8", 10 | "int128", 11 | "int256", 12 | "string", 13 | "uint8", 14 | "uint128", 15 | "uint256", 16 | "Mail", 17 | "Person", 18 | "EIP712Domain", 19 | "address[]", 20 | "address[1]", 21 | "address[100]" 22 | ], 23 | "invalid": [ 24 | "bytes0", 25 | "bytes33", 26 | "foo bar", 27 | "int", 28 | "int0", 29 | "int123", 30 | "int264", 31 | "uint", 32 | "uint0", 33 | "uint123", 34 | "uint264", 35 | "SomeStruct" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/abi.test.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHex } from '@noble/hashes/utils'; 2 | import { encode } from './abi'; 3 | 4 | describe('encode', () => { 5 | it('encodes types and values to ABI', () => { 6 | expect(bytesToHex(encode(['uint256', 'uint256'], [12345, 56789]))).toBe( 7 | '0000000000000000000000000000000000000000000000000000000000003039000000000000000000000000000000000000000000000000000000000000ddd5' 8 | ); 9 | expect(bytesToHex(encode(['bytes32', 'string'], ['0x123456789', 'foo bar']))).toBe( 10 | '123456780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000007666f6f2062617200000000000000000000000000000000000000000000000000' 11 | ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/utils/abi.ts: -------------------------------------------------------------------------------- 1 | import { encode as encodeAbi } from '@findeth/abi'; 2 | 3 | /** 4 | * Encode the values with the provided types. 5 | */ 6 | export const encode = (types: string[], values: unknown[]): Uint8Array => { 7 | return encodeAbi(types, values); 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/buffer.test.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHex } from '@noble/hashes/utils'; 2 | import { keccak256, toBuffer } from './buffer'; 3 | 4 | describe('keccak256', () => { 5 | it('returns a keccak256 hash of a string', () => { 6 | expect(bytesToHex(keccak256('foo bar'))).toBe('737fe0cb366697912e27136f93dfb531c58bab1b09c40842d999110387c86b54'); 7 | expect(bytesToHex(keccak256('foo bar', 'utf8'))).toBe( 8 | '737fe0cb366697912e27136f93dfb531c58bab1b09c40842d999110387c86b54' 9 | ); 10 | }); 11 | 12 | it('returns a keccak256 hash of a buffer', () => { 13 | const buffer = Buffer.from('foo bar', 'utf8'); 14 | expect(bytesToHex(keccak256(buffer))).toBe('737fe0cb366697912e27136f93dfb531c58bab1b09c40842d999110387c86b54'); 15 | }); 16 | }); 17 | 18 | describe('toBuffer', () => { 19 | it('returns a buffer for a string', () => { 20 | expect(new TextDecoder().decode(toBuffer('666f6f20626172'))).toBe('foo bar'); 21 | expect(new TextDecoder().decode(toBuffer('foo bar', 'utf8'))).toBe('foo bar'); 22 | }); 23 | 24 | it('works with a 0x prefix', () => { 25 | expect(new TextDecoder().decode(toBuffer('0x666f6f20626172'))).toBe('foo bar'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/utils/buffer.ts: -------------------------------------------------------------------------------- 1 | import { keccak_256 } from '@noble/hashes/sha3'; 2 | import { hexToBytes, utf8ToBytes } from '@noble/hashes/utils'; 3 | 4 | /** 5 | * Hashes the data with the optional encoding specified. If no encoding is specified, it is assumed that the data is 6 | * already a Buffer. 7 | */ 8 | export const keccak256 = (data: string | Uint8Array, encoding?: 'utf8' | 'hex'): Uint8Array => { 9 | if (typeof data === 'string' && encoding === 'utf8') { 10 | return keccak_256(toBuffer(data, encoding)); 11 | } 12 | 13 | return keccak_256(data); 14 | }; 15 | 16 | /** 17 | * Get a string as Buffer, with the optional encoding specified. If no encoding is specified, it is assumed that the 18 | * data is a hexadecimal string. The string can optionally contain the 0x prefix. 19 | */ 20 | export const toBuffer = (data: string, encoding: 'utf8' | 'hex' = 'hex'): Uint8Array => { 21 | if (encoding === 'hex') { 22 | if (data.startsWith('0x')) { 23 | return hexToBytes(data.substring(2)); 24 | } 25 | 26 | return hexToBytes(data); 27 | } 28 | 29 | return utf8ToBytes(data); 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abi'; 2 | export * from './buffer'; 3 | export * from './json'; 4 | -------------------------------------------------------------------------------- /src/utils/json.test.ts: -------------------------------------------------------------------------------- 1 | import invalidSchema from '../__fixtures__/invalid-schema.json'; 2 | import validSchema from '../__fixtures__/typed-data-1.json'; 3 | import validCustomSchema from '../__fixtures__/typed-data-5.json'; 4 | import { validateTypedData } from './json'; 5 | 6 | describe('validateTypedData', () => { 7 | it('validates an EIP-712 JSON schema', () => { 8 | expect(validateTypedData(validSchema)).toBe(true); 9 | expect(validateTypedData(validCustomSchema, { verifyDomain: false })).toBe(true); 10 | }); 11 | 12 | it('returns false for invalid JSON schemas', () => { 13 | expect(validateTypedData({})).toBe(false); 14 | expect(validateTypedData(invalidSchema)).toBe(false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/json.ts: -------------------------------------------------------------------------------- 1 | import { is } from 'superstruct'; 2 | import { getOptions, Options } from '../options'; 3 | import { EIP_712_STRICT_TYPED_DATA_TYPE, EIP_712_TYPED_DATA_TYPE, TypedData } from '../types'; 4 | 5 | /** 6 | * Validates that `data` matches the EIP-712 JSON schema. 7 | */ 8 | export const validateTypedData = (data: unknown, options?: Options): data is TypedData => { 9 | const { verifyDomain } = getOptions(options); 10 | 11 | if (verifyDomain) { 12 | return is(data, EIP_712_STRICT_TYPED_DATA_TYPE); 13 | } 14 | 15 | return is(data, EIP_712_TYPED_DATA_TYPE); 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "declarationDir": "typings", 6 | "emitDeclarationOnly": true 7 | }, 8 | "include": ["src"], 9 | "exclude": ["jest", "src/**/*.test.ts", "src/**/__mocks__/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "outDir": "lib", 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "noEmitOnError": true, 10 | "esModuleInterop": true, 11 | "declaration": true, 12 | "sourceMap": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true 18 | }, 19 | "include": ["src", "jest", "examples"] 20 | } 21 | --------------------------------------------------------------------------------