├── src ├── index.ts └── typed-data.ts ├── dist ├── index.d.ts ├── typed-data.d.ts ├── index.es.js └── index.js ├── .gitignore ├── tsconfig.json ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md └── tests └── typed-data.test.ts /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typed-data' 2 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './typed-data'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | *.swp 9 | 10 | # Dependency directory 11 | node_modules 12 | 13 | # Editors 14 | .idea 15 | *.iml 16 | 17 | # OS metadata 18 | .DS_Store 19 | Thumbs.db 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "allowSyntheticDefaultImports": true, 8 | "outDir": "./dist" 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import pkg from './package.json' 3 | 4 | export default { 5 | input: 'src/index.ts', 6 | output: [ 7 | { 8 | file: pkg.main, 9 | format: 'cjs' 10 | }, 11 | { 12 | file: pkg.module, 13 | format: 'es' 14 | } 15 | ], 16 | external: [ 17 | ...Object.keys(pkg.dependencies || {}), 18 | ...Object.keys(pkg.peerDependencies || {}) 19 | ], 20 | plugins: [ 21 | typescript({ 22 | typescript: require('typescript') 23 | }) 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (C) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | 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 | -------------------------------------------------------------------------------- /dist/typed-data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TypedData { 2 | types: TypedDataTypes; 3 | primaryType: string; 4 | domain: TypedDataDomain; 5 | message: object; 6 | } 7 | export declare type TypedDataTypes = { 8 | [key: string]: TypedDataArgument[]; 9 | }; 10 | export interface TypedDataArgument { 11 | name: string; 12 | type: string; 13 | } 14 | export interface TypedDataDomain { 15 | name?: string; 16 | version?: string; 17 | chainId?: number; 18 | verifyingContract?: string; 19 | salt?: string; 20 | } 21 | export declare const TypedDataUtils: { 22 | encodeDigest(typedData: TypedData): Uint8Array; 23 | encodeData(typedData: TypedData, primaryType: string, data: object): Uint8Array; 24 | hashStruct(typedData: TypedData, primaryType: string, data: object): Uint8Array; 25 | typeHash(typedDataTypes: TypedDataTypes, primaryType: string): Uint8Array; 26 | encodeType(typedDataTypes: TypedDataTypes, primaryType: string): string; 27 | domainType(domain: TypedDataDomain): TypedDataArgument[]; 28 | buildTypedData(domain: TypedDataDomain, messageTypes: TypedDataTypes, primaryType: string, message: object): TypedData; 29 | }; 30 | export declare const encodeTypedDataDigest: (typedData: TypedData) => Uint8Array; 31 | export declare const buildTypedData: (domain: TypedDataDomain, messageTypes: TypedDataTypes, primaryType: string, message: object) => TypedData; 32 | export declare const domainType: (domain: TypedDataDomain) => TypedDataArgument[]; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethers-eip712", 3 | "version": "0.2.0", 4 | "description": "Ethereum Typed Data Hashing and Signing (EIP712) implementation for ethers.js", 5 | "repository": "https://github.com/arcadeum/ethers-eip712", 6 | "main": "dist/index.js", 7 | "module": "dist/index.es.js", 8 | "types": "dist/index.d.ts", 9 | "author": "github.com/arcadeum", 10 | "license": "MIT", 11 | "scripts": { 12 | "build": "rimraf ./dist && rollup -c", 13 | "prepublishOnly": "yarn test", 14 | "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", 15 | "test:watch": "jest --watchAll", 16 | "test": "jest --ci --runInBand" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "dependencies": { 22 | }, 23 | "peerDependencies": { 24 | "ethers": "^4.0.47 || ^5.0.8" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^26.0.9", 28 | "ethers": "^5.0.8", 29 | "jest": "^26.3.0", 30 | "rimraf": "^3.0.2", 31 | "rollup": "^2.23.1", 32 | "rollup-plugin-typescript2": "^0.27.2", 33 | "ts-jest": "^26.1.4", 34 | "ts-node": "^8.8.2", 35 | "typescript": "^3.9.7" 36 | }, 37 | "jest": { 38 | "setupFiles": [], 39 | "moduleFileExtensions": [ 40 | "ts", 41 | "tsx", 42 | "js", 43 | "json" 44 | ], 45 | "roots": [ 46 | "src", 47 | "tests" 48 | ], 49 | "transform": { 50 | "^.+\\.tsx?$": "ts-jest" 51 | }, 52 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 53 | "moduleNameMapper": {} 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ethers-eip712 2 | ============= 3 | 4 | `ethers-eip712` is an npm package that implements the Ethereum Typed Data (EIP712) 5 | for structured data hashing and signing proposal, written in TypeScript for ethers.js. 6 | 7 | EIP712: https://eips.ethereum.org/EIPS/eip-712 8 | 9 | ## Usage 10 | 11 | `yarn install ethers-eip712` or `npm install ethers-eip712` 12 | 13 | NOTE: both ethers v4 and ethers v5 are compatible by this single library. 14 | 15 | 16 | ## Example 17 | 18 | ```typescript 19 | import { ethers } from 'ethers' 20 | import { TypedDataUtils } from 'ethers-eip712' 21 | 22 | const typedData = { 23 | types: { 24 | EIP712Domain: [ 25 | {name: "name", type: "string"}, 26 | {name: "version", type: "string"}, 27 | {name: "chainId", type: "uint256"}, 28 | {name: "verifyingContract", type: "address"}, 29 | ], 30 | Person: [ 31 | {name: "name", type: "string"}, 32 | {name: "wallet", type: "address"}, 33 | ] 34 | }, 35 | primaryType: 'Person' as const, 36 | domain: { 37 | name: 'Ether Mail', 38 | version: '1', 39 | chainId: 1, 40 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' 41 | }, 42 | message: { 43 | 'name': 'Bob', 44 | 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' 45 | } 46 | } 47 | 48 | const digest = TypedDataUtils.encodeDigest(typedData) 49 | const digestHex = ethers.utils.hexlify(digest) 50 | 51 | const wallet = ethers.Wallet.createRandom() 52 | const signature = wallet.signMessage(digest) 53 | ``` 54 | 55 | [See tests for more examples](./tests/typed-data.test.ts) 56 | 57 | 58 | ## License 59 | 60 | MIT 61 | -------------------------------------------------------------------------------- /src/typed-data.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | // EIP-712 -- https://eips.ethereum.org/EIPS/eip-712 4 | 5 | export interface TypedData { 6 | types: TypedDataTypes 7 | primaryType: string 8 | domain: TypedDataDomain 9 | message: object 10 | } 11 | 12 | export type TypedDataTypes = { [key: string]: TypedDataArgument[] } 13 | 14 | export interface TypedDataArgument { 15 | name: string 16 | type: string 17 | } 18 | 19 | export interface TypedDataDomain { 20 | name?: string 21 | version?: string 22 | chainId?: number 23 | verifyingContract?: string 24 | salt?: string 25 | } 26 | 27 | export const TypedDataUtils = { 28 | encodeDigest(typedData: TypedData): Uint8Array { 29 | const eip191Header = ethers.utils.arrayify('0x1901') 30 | const domainHash = TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain) 31 | const messageHash = TypedDataUtils.hashStruct(typedData, typedData.primaryType, typedData.message) 32 | 33 | const pack = ethers.utils.solidityPack( 34 | ['bytes', 'bytes32', 'bytes32'], 35 | [eip191Header, zeroPad(domainHash, 32), zeroPad(messageHash, 32)] 36 | ) 37 | 38 | const hashPack = ethers.utils.keccak256(pack) 39 | return ethers.utils.arrayify(hashPack) 40 | }, 41 | 42 | encodeData(typedData: TypedData, primaryType: string, data: object): Uint8Array { 43 | const types = typedData.types 44 | const args = types[primaryType] 45 | if (!args || args.length === 0) { 46 | throw new Error(`TypedDataUtils: ${typedData.primaryType} type is not unknown`) 47 | } 48 | 49 | const abiCoder = new ethers.utils.AbiCoder() 50 | const abiTypes: string[] = [] 51 | const abiValues: any[] = [] 52 | 53 | const typeHash = TypedDataUtils.typeHash(typedData.types, primaryType) 54 | abiTypes.push('bytes32') 55 | abiValues.push(zeroPad(typeHash, 32)) 56 | 57 | const encodeField = (name: string, type: string, value: any): (string | Uint8Array)[] => { 58 | if (types[type] !== undefined) { 59 | return ['bytes32', ethers.utils.arrayify( 60 | ethers.utils.keccak256(TypedDataUtils.encodeData(typedData, type, value)) 61 | )] 62 | } 63 | 64 | if (type === 'bytes' || type === 'string') { 65 | let v: any 66 | if (type === 'string') { 67 | v = ethers.utils.toUtf8Bytes(value) 68 | } else { 69 | v = ethers.utils.arrayify(value) 70 | } 71 | return ['bytes32', ethers.utils.arrayify( 72 | ethers.utils.hexZeroPad(ethers.utils.keccak256(v), 32) 73 | )] 74 | 75 | } else if (type.lastIndexOf('[') > 0) { 76 | const t = type.slice(0, type.lastIndexOf('[')) 77 | const v = value.map((item) => encodeField(name, t, item)) 78 | return ['bytes32', ethers.utils.arrayify( 79 | ethers.utils.keccak256( 80 | ethers.utils.arrayify( 81 | abiCoder.encode( 82 | v.map(([tt]) => tt), 83 | v.map(([, vv]) => vv) 84 | ) 85 | ) 86 | ) 87 | ) 88 | ] 89 | 90 | } else { 91 | return [type, value] 92 | } 93 | } 94 | 95 | for (const field of args) { 96 | const [type, value] = encodeField(field.name, field.type, data[field.name]); 97 | abiTypes.push(type as string) 98 | abiValues.push(value) 99 | } 100 | 101 | return ethers.utils.arrayify(abiCoder.encode(abiTypes, abiValues)) 102 | }, 103 | 104 | hashStruct(typedData: TypedData, primaryType: string, data: object): Uint8Array { 105 | return ethers.utils.arrayify( 106 | ethers.utils.keccak256( 107 | TypedDataUtils.encodeData(typedData, primaryType, data) 108 | ) 109 | ) 110 | }, 111 | 112 | typeHash(typedDataTypes: TypedDataTypes, primaryType: string): Uint8Array { 113 | return ethers.utils.arrayify( 114 | ethers.utils.keccak256( 115 | ethers.utils.toUtf8Bytes( 116 | TypedDataUtils.encodeType(typedDataTypes, primaryType) 117 | ) 118 | ) 119 | ) 120 | }, 121 | 122 | encodeType(typedDataTypes: TypedDataTypes, primaryType: string): string { 123 | const args = typedDataTypes[primaryType] 124 | if (!args || args.length === 0) { 125 | throw new Error(`TypedDataUtils: ${primaryType} type is not defined`) 126 | } 127 | 128 | const subTypes: string[] = [] 129 | let s = primaryType + '(' 130 | 131 | for (let i=0; i < args.length; i++) { 132 | const arg = args[i] 133 | const arrayArg = arg.type.indexOf('[') 134 | const argType = arrayArg < 0 ? arg.type : arg.type.slice(0, arrayArg) 135 | 136 | if (typedDataTypes[argType] && typedDataTypes[argType].length > 0) { 137 | let set = false 138 | for (let x=0; x < subTypes.length; x++) { 139 | if (subTypes[x] === argType) { 140 | set = true 141 | } 142 | } 143 | if (!set) { 144 | subTypes.push(argType) 145 | } 146 | } 147 | 148 | s += arg.type + ' ' + arg.name 149 | if (i < args.length-1) { 150 | s += ',' 151 | } 152 | } 153 | s += ')' 154 | 155 | subTypes.sort() 156 | for (let i=0; i < subTypes.length; i++) { 157 | const subEncodeType = TypedDataUtils.encodeType(typedDataTypes, subTypes[i]) 158 | s += subEncodeType 159 | } 160 | 161 | return s 162 | }, 163 | 164 | domainType(domain: TypedDataDomain): TypedDataArgument[] { 165 | const type: TypedDataArgument[] = [] 166 | if (domain.name) { 167 | type.push({ name: 'name', type: 'string' }) 168 | } 169 | if (domain.version) { 170 | type.push({ name: 'version', type: 'string' }) 171 | } 172 | if (domain.chainId) { 173 | type.push({ name: 'chainId', type: 'uint256' }) 174 | } 175 | if (domain.verifyingContract) { 176 | type.push({ name: 'verifyingContract', type: 'address' }) 177 | } 178 | if (domain.salt) { 179 | type.push({ name: 'salt', type: 'bytes32' }) 180 | } 181 | return type 182 | }, 183 | 184 | buildTypedData(domain: TypedDataDomain, messageTypes: TypedDataTypes, primaryType: string, message: object): TypedData { 185 | const domainType = TypedDataUtils.domainType(domain) 186 | 187 | const typedData: TypedData = { 188 | domain: domain, 189 | types: { 190 | 'EIP712Domain': domainType, 191 | ...messageTypes 192 | }, 193 | primaryType: primaryType, 194 | message: message 195 | } 196 | 197 | return typedData 198 | } 199 | } 200 | 201 | export const encodeTypedDataDigest = (typedData: TypedData): Uint8Array => { 202 | return TypedDataUtils.encodeDigest(typedData) 203 | } 204 | 205 | export const buildTypedData = (domain: TypedDataDomain, messageTypes: TypedDataTypes, primaryType: string, message: object): TypedData => { 206 | return TypedDataUtils.buildTypedData(domain, messageTypes, primaryType, message) 207 | } 208 | 209 | export const domainType = (domain: TypedDataDomain): TypedDataArgument[] => { 210 | return TypedDataUtils.domainType(domain) 211 | } 212 | 213 | // zeroPad is implemented as a compat layer between ethers v4 and ethers v5 214 | const zeroPad = (value: any, length: number): Uint8Array => { 215 | return ethers.utils.arrayify(ethers.utils.hexZeroPad(ethers.utils.hexlify(value), length)) 216 | } 217 | -------------------------------------------------------------------------------- /dist/index.es.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | /*! ***************************************************************************** 4 | Copyright (c) Microsoft Corporation. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | ***************************************************************************** */ 17 | 18 | var __assign = function() { 19 | __assign = Object.assign || function __assign(t) { 20 | for (var s, i = 1, n = arguments.length; i < n; i++) { 21 | s = arguments[i]; 22 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 23 | } 24 | return t; 25 | }; 26 | return __assign.apply(this, arguments); 27 | }; 28 | 29 | var TypedDataUtils = { 30 | encodeDigest: function (typedData) { 31 | var eip191Header = ethers.utils.arrayify('0x1901'); 32 | var domainHash = TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain); 33 | var messageHash = TypedDataUtils.hashStruct(typedData, typedData.primaryType, typedData.message); 34 | var pack = ethers.utils.solidityPack(['bytes', 'bytes32', 'bytes32'], [eip191Header, zeroPad(domainHash, 32), zeroPad(messageHash, 32)]); 35 | var hashPack = ethers.utils.keccak256(pack); 36 | return ethers.utils.arrayify(hashPack); 37 | }, 38 | encodeData: function (typedData, primaryType, data) { 39 | var types = typedData.types; 40 | var args = types[primaryType]; 41 | if (!args || args.length === 0) { 42 | throw new Error("TypedDataUtils: " + typedData.primaryType + " type is not unknown"); 43 | } 44 | var abiCoder = new ethers.utils.AbiCoder(); 45 | var abiTypes = []; 46 | var abiValues = []; 47 | var typeHash = TypedDataUtils.typeHash(typedData.types, primaryType); 48 | abiTypes.push('bytes32'); 49 | abiValues.push(zeroPad(typeHash, 32)); 50 | var encodeField = function (name, type, value) { 51 | if (types[type] !== undefined) { 52 | return ['bytes32', ethers.utils.arrayify(ethers.utils.keccak256(TypedDataUtils.encodeData(typedData, type, value)))]; 53 | } 54 | if (type === 'bytes' || type === 'string') { 55 | var v = void 0; 56 | if (type === 'string') { 57 | v = ethers.utils.toUtf8Bytes(value); 58 | } 59 | else { 60 | v = ethers.utils.arrayify(value); 61 | } 62 | return ['bytes32', ethers.utils.arrayify(ethers.utils.hexZeroPad(ethers.utils.keccak256(v), 32))]; 63 | } 64 | else if (type.lastIndexOf('[') > 0) { 65 | var t_1 = type.slice(0, type.lastIndexOf('[')); 66 | var v = value.map(function (item) { return encodeField(name, t_1, item); }); 67 | return ['bytes32', ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.arrayify(abiCoder.encode(v.map(function (_a) { 68 | var tt = _a[0]; 69 | return tt; 70 | }), v.map(function (_a) { 71 | var vv = _a[1]; 72 | return vv; 73 | }))))) 74 | ]; 75 | } 76 | else { 77 | return [type, value]; 78 | } 79 | }; 80 | for (var _i = 0, args_1 = args; _i < args_1.length; _i++) { 81 | var field = args_1[_i]; 82 | var _a = encodeField(field.name, field.type, data[field.name]), type = _a[0], value = _a[1]; 83 | abiTypes.push(type); 84 | abiValues.push(value); 85 | } 86 | return ethers.utils.arrayify(abiCoder.encode(abiTypes, abiValues)); 87 | }, 88 | hashStruct: function (typedData, primaryType, data) { 89 | return ethers.utils.arrayify(ethers.utils.keccak256(TypedDataUtils.encodeData(typedData, primaryType, data))); 90 | }, 91 | typeHash: function (typedDataTypes, primaryType) { 92 | return ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(TypedDataUtils.encodeType(typedDataTypes, primaryType)))); 93 | }, 94 | encodeType: function (typedDataTypes, primaryType) { 95 | var args = typedDataTypes[primaryType]; 96 | if (!args || args.length === 0) { 97 | throw new Error("TypedDataUtils: " + primaryType + " type is not defined"); 98 | } 99 | var subTypes = []; 100 | var s = primaryType + '('; 101 | for (var i = 0; i < args.length; i++) { 102 | var arg = args[i]; 103 | var arrayArg = arg.type.indexOf('['); 104 | var argType = arrayArg < 0 ? arg.type : arg.type.slice(0, arrayArg); 105 | if (typedDataTypes[argType] && typedDataTypes[argType].length > 0) { 106 | var set = false; 107 | for (var x = 0; x < subTypes.length; x++) { 108 | if (subTypes[x] === argType) { 109 | set = true; 110 | } 111 | } 112 | if (!set) { 113 | subTypes.push(argType); 114 | } 115 | } 116 | s += arg.type + ' ' + arg.name; 117 | if (i < args.length - 1) { 118 | s += ','; 119 | } 120 | } 121 | s += ')'; 122 | subTypes.sort(); 123 | for (var i = 0; i < subTypes.length; i++) { 124 | var subEncodeType = TypedDataUtils.encodeType(typedDataTypes, subTypes[i]); 125 | s += subEncodeType; 126 | } 127 | return s; 128 | }, 129 | domainType: function (domain) { 130 | var type = []; 131 | if (domain.name) { 132 | type.push({ name: 'name', type: 'string' }); 133 | } 134 | if (domain.version) { 135 | type.push({ name: 'version', type: 'string' }); 136 | } 137 | if (domain.chainId) { 138 | type.push({ name: 'chainId', type: 'uint256' }); 139 | } 140 | if (domain.verifyingContract) { 141 | type.push({ name: 'verifyingContract', type: 'address' }); 142 | } 143 | if (domain.salt) { 144 | type.push({ name: 'salt', type: 'bytes32' }); 145 | } 146 | return type; 147 | }, 148 | buildTypedData: function (domain, messageTypes, primaryType, message) { 149 | var domainType = TypedDataUtils.domainType(domain); 150 | var typedData = { 151 | domain: domain, 152 | types: __assign({ 'EIP712Domain': domainType }, messageTypes), 153 | primaryType: primaryType, 154 | message: message 155 | }; 156 | return typedData; 157 | } 158 | }; 159 | var encodeTypedDataDigest = function (typedData) { 160 | return TypedDataUtils.encodeDigest(typedData); 161 | }; 162 | var buildTypedData = function (domain, messageTypes, primaryType, message) { 163 | return TypedDataUtils.buildTypedData(domain, messageTypes, primaryType, message); 164 | }; 165 | var domainType = function (domain) { 166 | return TypedDataUtils.domainType(domain); 167 | }; 168 | // zeroPad is implemented as a compat layer between ethers v4 and ethers v5 169 | var zeroPad = function (value, length) { 170 | return ethers.utils.arrayify(ethers.utils.hexZeroPad(ethers.utils.hexlify(value), length)); 171 | }; 172 | 173 | export { TypedDataUtils, buildTypedData, domainType, encodeTypedDataDigest }; 174 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var ethers = require('ethers'); 6 | 7 | /*! ***************************************************************************** 8 | Copyright (c) Microsoft Corporation. 9 | 10 | Permission to use, copy, modify, and/or distribute this software for any 11 | purpose with or without fee is hereby granted. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 14 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 15 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 17 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 18 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | PERFORMANCE OF THIS SOFTWARE. 20 | ***************************************************************************** */ 21 | 22 | var __assign = function() { 23 | __assign = Object.assign || function __assign(t) { 24 | for (var s, i = 1, n = arguments.length; i < n; i++) { 25 | s = arguments[i]; 26 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 27 | } 28 | return t; 29 | }; 30 | return __assign.apply(this, arguments); 31 | }; 32 | 33 | var TypedDataUtils = { 34 | encodeDigest: function (typedData) { 35 | var eip191Header = ethers.ethers.utils.arrayify('0x1901'); 36 | var domainHash = TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain); 37 | var messageHash = TypedDataUtils.hashStruct(typedData, typedData.primaryType, typedData.message); 38 | var pack = ethers.ethers.utils.solidityPack(['bytes', 'bytes32', 'bytes32'], [eip191Header, zeroPad(domainHash, 32), zeroPad(messageHash, 32)]); 39 | var hashPack = ethers.ethers.utils.keccak256(pack); 40 | return ethers.ethers.utils.arrayify(hashPack); 41 | }, 42 | encodeData: function (typedData, primaryType, data) { 43 | var types = typedData.types; 44 | var args = types[primaryType]; 45 | if (!args || args.length === 0) { 46 | throw new Error("TypedDataUtils: " + typedData.primaryType + " type is not unknown"); 47 | } 48 | var abiCoder = new ethers.ethers.utils.AbiCoder(); 49 | var abiTypes = []; 50 | var abiValues = []; 51 | var typeHash = TypedDataUtils.typeHash(typedData.types, primaryType); 52 | abiTypes.push('bytes32'); 53 | abiValues.push(zeroPad(typeHash, 32)); 54 | var encodeField = function (name, type, value) { 55 | if (types[type] !== undefined) { 56 | return ['bytes32', ethers.ethers.utils.arrayify(ethers.ethers.utils.keccak256(TypedDataUtils.encodeData(typedData, type, value)))]; 57 | } 58 | if (type === 'bytes' || type === 'string') { 59 | var v = void 0; 60 | if (type === 'string') { 61 | v = ethers.ethers.utils.toUtf8Bytes(value); 62 | } 63 | else { 64 | v = ethers.ethers.utils.arrayify(value); 65 | } 66 | return ['bytes32', ethers.ethers.utils.arrayify(ethers.ethers.utils.hexZeroPad(ethers.ethers.utils.keccak256(v), 32))]; 67 | } 68 | else if (type.lastIndexOf('[') > 0) { 69 | var t_1 = type.slice(0, type.lastIndexOf('[')); 70 | var v = value.map(function (item) { return encodeField(name, t_1, item); }); 71 | return ['bytes32', ethers.ethers.utils.arrayify(ethers.ethers.utils.keccak256(ethers.ethers.utils.arrayify(abiCoder.encode(v.map(function (_a) { 72 | var tt = _a[0]; 73 | return tt; 74 | }), v.map(function (_a) { 75 | var vv = _a[1]; 76 | return vv; 77 | }))))) 78 | ]; 79 | } 80 | else { 81 | return [type, value]; 82 | } 83 | }; 84 | for (var _i = 0, args_1 = args; _i < args_1.length; _i++) { 85 | var field = args_1[_i]; 86 | var _a = encodeField(field.name, field.type, data[field.name]), type = _a[0], value = _a[1]; 87 | abiTypes.push(type); 88 | abiValues.push(value); 89 | } 90 | return ethers.ethers.utils.arrayify(abiCoder.encode(abiTypes, abiValues)); 91 | }, 92 | hashStruct: function (typedData, primaryType, data) { 93 | return ethers.ethers.utils.arrayify(ethers.ethers.utils.keccak256(TypedDataUtils.encodeData(typedData, primaryType, data))); 94 | }, 95 | typeHash: function (typedDataTypes, primaryType) { 96 | return ethers.ethers.utils.arrayify(ethers.ethers.utils.keccak256(ethers.ethers.utils.toUtf8Bytes(TypedDataUtils.encodeType(typedDataTypes, primaryType)))); 97 | }, 98 | encodeType: function (typedDataTypes, primaryType) { 99 | var args = typedDataTypes[primaryType]; 100 | if (!args || args.length === 0) { 101 | throw new Error("TypedDataUtils: " + primaryType + " type is not defined"); 102 | } 103 | var subTypes = []; 104 | var s = primaryType + '('; 105 | for (var i = 0; i < args.length; i++) { 106 | var arg = args[i]; 107 | var arrayArg = arg.type.indexOf('['); 108 | var argType = arrayArg < 0 ? arg.type : arg.type.slice(0, arrayArg); 109 | if (typedDataTypes[argType] && typedDataTypes[argType].length > 0) { 110 | var set = false; 111 | for (var x = 0; x < subTypes.length; x++) { 112 | if (subTypes[x] === argType) { 113 | set = true; 114 | } 115 | } 116 | if (!set) { 117 | subTypes.push(argType); 118 | } 119 | } 120 | s += arg.type + ' ' + arg.name; 121 | if (i < args.length - 1) { 122 | s += ','; 123 | } 124 | } 125 | s += ')'; 126 | subTypes.sort(); 127 | for (var i = 0; i < subTypes.length; i++) { 128 | var subEncodeType = TypedDataUtils.encodeType(typedDataTypes, subTypes[i]); 129 | s += subEncodeType; 130 | } 131 | return s; 132 | }, 133 | domainType: function (domain) { 134 | var type = []; 135 | if (domain.name) { 136 | type.push({ name: 'name', type: 'string' }); 137 | } 138 | if (domain.version) { 139 | type.push({ name: 'version', type: 'string' }); 140 | } 141 | if (domain.chainId) { 142 | type.push({ name: 'chainId', type: 'uint256' }); 143 | } 144 | if (domain.verifyingContract) { 145 | type.push({ name: 'verifyingContract', type: 'address' }); 146 | } 147 | if (domain.salt) { 148 | type.push({ name: 'salt', type: 'bytes32' }); 149 | } 150 | return type; 151 | }, 152 | buildTypedData: function (domain, messageTypes, primaryType, message) { 153 | var domainType = TypedDataUtils.domainType(domain); 154 | var typedData = { 155 | domain: domain, 156 | types: __assign({ 'EIP712Domain': domainType }, messageTypes), 157 | primaryType: primaryType, 158 | message: message 159 | }; 160 | return typedData; 161 | } 162 | }; 163 | var encodeTypedDataDigest = function (typedData) { 164 | return TypedDataUtils.encodeDigest(typedData); 165 | }; 166 | var buildTypedData = function (domain, messageTypes, primaryType, message) { 167 | return TypedDataUtils.buildTypedData(domain, messageTypes, primaryType, message); 168 | }; 169 | var domainType = function (domain) { 170 | return TypedDataUtils.domainType(domain); 171 | }; 172 | // zeroPad is implemented as a compat layer between ethers v4 and ethers v5 173 | var zeroPad = function (value, length) { 174 | return ethers.ethers.utils.arrayify(ethers.ethers.utils.hexZeroPad(ethers.ethers.utils.hexlify(value), length)); 175 | }; 176 | 177 | exports.TypedDataUtils = TypedDataUtils; 178 | exports.buildTypedData = buildTypedData; 179 | exports.domainType = domainType; 180 | exports.encodeTypedDataDigest = encodeTypedDataDigest; 181 | -------------------------------------------------------------------------------- /tests/typed-data.test.ts: -------------------------------------------------------------------------------- 1 | import { TypedData, TypedDataTypes, TypedDataUtils } from '../src/typed-data' 2 | import { ethers } from 'ethers' 3 | 4 | describe('TypedData', () => { 5 | 6 | test('types', () => { 7 | const types = { 8 | 'Person': [ 9 | {name: 'name', type: 'string'}, 10 | {name: 'wallet', type: 'address'}, 11 | ], 12 | 'Mail': [ 13 | {name: 'from', type: 'Person'}, 14 | {name: 'to', type: 'Person'}, 15 | {name: 'contents', type: 'string'}, 16 | {name: 'asset', type: 'Asset'}, 17 | ], 18 | 'Asset': [ 19 | {name: 'name', type: 'string'} 20 | ] 21 | } 22 | 23 | const encodeType = TypedDataUtils.encodeType(types, 'Person') 24 | expect(encodeType).toEqual('Person(string name,address wallet)') 25 | 26 | const typeHash = TypedDataUtils.typeHash(types, 'Person') 27 | const typeHashHex = ethers.utils.hexlify(typeHash) 28 | expect(typeHashHex).toEqual('0xb9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500') 29 | 30 | const encodeType2 = TypedDataUtils.encodeType(types, 'Mail') 31 | expect(encodeType2).toEqual('Mail(Person from,Person to,string contents,Asset asset)Asset(string name)Person(string name,address wallet)') 32 | }) 33 | 34 | test('encoding-1', () => { 35 | const typedData = { 36 | types: { 37 | EIP712Domain: [ 38 | {name: "name", type: "string"}, 39 | {name: "version", type: "string"}, 40 | {name: "chainId", type: "uint256"}, 41 | {name: "verifyingContract", type: "address"}, 42 | ], 43 | Person: [ 44 | {name: "name", type: "string"}, 45 | {name: "wallet", type: "address"}, 46 | ] 47 | }, 48 | primaryType: 'Person' as const, 49 | domain: { 50 | name: 'Ether Mail', 51 | version: '1', 52 | chainId: 1, 53 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' 54 | }, 55 | message: { 56 | 'name': 'Bob', 57 | 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' 58 | } 59 | } 60 | 61 | const domainHash = TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain) 62 | const domainHashHex = ethers.utils.hexlify(domainHash) 63 | expect(domainHashHex).toEqual('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f') 64 | 65 | const digest = TypedDataUtils.encodeDigest(typedData) 66 | const digestHex = ethers.utils.hexlify(digest) 67 | expect(digestHex).toEqual('0x0a94cf6625e5860fc4f330d75bcd0c3a4737957d2321d1a024540ab5320fe903') 68 | }) 69 | 70 | test('encoding-2', () => { 71 | const typedData = { 72 | types: { 73 | EIP712Domain: [ 74 | {name: "name", type: "string"}, 75 | {name: "version", type: "string"}, 76 | {name: "chainId", type: "uint256"}, 77 | {name: "verifyingContract", type: "address"}, 78 | ], 79 | Person: [ 80 | {name: "name", type: "string"}, 81 | {name: "wallet", type: "address"}, 82 | {name: 'count', type: 'uint8'} 83 | ] 84 | }, 85 | primaryType: 'Person' as const, 86 | domain: { 87 | name: 'Ether Mail', 88 | version: '1', 89 | chainId: 1, 90 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' 91 | }, 92 | message: { 93 | 'name': 'Bob', 94 | 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 95 | 'count': 4 96 | } 97 | } 98 | 99 | const domainHash = TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain) 100 | const domainHashHex = ethers.utils.hexlify(domainHash) 101 | expect(domainHashHex).toEqual('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f') 102 | 103 | const digest = TypedDataUtils.encodeDigest(typedData) 104 | const digestHex = ethers.utils.hexlify(digest) 105 | expect(digestHex).toEqual('0x2218fda59750be7bb9e5dfb2b49e4ec000dc2542862c5826f1fe980d6d727e95') 106 | }) 107 | 108 | test('encoding-3', () => { 109 | const typedData = { 110 | types: { 111 | EIP712Domain: [ 112 | { name: 'name', type: 'string' }, 113 | { name: 'version', type: 'string' }, 114 | { name: 'chainId', type: 'uint256' }, 115 | { name: 'verifyingContract', type: 'address' }, 116 | ], 117 | Person: [ 118 | { name: 'name', type: 'string' }, 119 | { name: 'wallets', type: 'address[]' }, 120 | ], 121 | Mail: [ 122 | { name: 'from', type: 'Person' }, 123 | { name: 'to', type: 'Person[]' }, 124 | { name: 'contents', type: 'string' }, 125 | ], 126 | Group: [ 127 | { name: 'name', type: 'string' }, 128 | { name: 'members', type: 'Person[]' }, 129 | ], 130 | }, 131 | domain: { 132 | name: 'Ether Mail', 133 | version: '1', 134 | chainId: 1, 135 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', 136 | }, 137 | primaryType: 'Mail' as const, 138 | message: { 139 | from: { 140 | name: 'Cow', 141 | wallets: [ 142 | '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', 143 | '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', 144 | ], 145 | }, 146 | to: [{ 147 | name: 'Bob', 148 | wallets: [ 149 | '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 150 | '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', 151 | '0xB0B0b0b0b0b0B000000000000000000000000000', 152 | ], 153 | }], 154 | contents: 'Hello, Bob!', 155 | }, 156 | } 157 | 158 | expect( 159 | TypedDataUtils.encodeType(typedData.types, 'Group') 160 | ).toEqual('Group(string name,Person[] members)Person(string name,address[] wallets)') 161 | 162 | expect( 163 | TypedDataUtils.encodeType(typedData.types, 'Person') 164 | ).toEqual('Person(string name,address[] wallets)') 165 | 166 | expect( 167 | ethers.utils.hexlify( 168 | TypedDataUtils.typeHash(typedData.types, 'Person') 169 | ) 170 | ).toEqual('0xfabfe1ed996349fc6027709802be19d047da1aa5d6894ff5f6486d92db2e6860') 171 | 172 | expect( 173 | ethers.utils.hexlify( 174 | TypedDataUtils.encodeData(typedData, 'Person', typedData.message.from) 175 | ) 176 | ).toEqual( 177 | `0x${[ 178 | 'fabfe1ed996349fc6027709802be19d047da1aa5d6894ff5f6486d92db2e6860', 179 | '8c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648', 180 | '8a8bfe642b9fc19c25ada5dadfd37487461dc81dd4b0778f262c163ed81b5e2a', 181 | ].join('')}` 182 | ) 183 | 184 | expect( 185 | ethers.utils.hexlify( 186 | TypedDataUtils.hashStruct(typedData, 'Person', typedData.message.from) 187 | ) 188 | ).toEqual('0x9b4846dd48b866f0ac54d61b9b21a9e746f921cefa4ee94c4c0a1c49c774f67f') 189 | 190 | expect( 191 | ethers.utils.hexlify( 192 | TypedDataUtils.encodeData(typedData, 'Person', typedData.message.to[0]) 193 | ) 194 | ).toEqual( 195 | `0x${[ 196 | 'fabfe1ed996349fc6027709802be19d047da1aa5d6894ff5f6486d92db2e6860', 197 | '28cac318a86c8a0a6a9156c2dba2c8c2363677ba0514ef616592d81557e679b6', 198 | 'd2734f4c86cc3bd9cabf04c3097589d3165d95e4648fc72d943ed161f651ec6d', 199 | ].join('')}` 200 | ) 201 | 202 | expect( 203 | ethers.utils.hexlify( 204 | TypedDataUtils.hashStruct(typedData, 'Person', typedData.message.to[0]) 205 | ) 206 | ).toEqual('0xefa62530c7ae3a290f8a13a5fc20450bdb3a6af19d9d9d2542b5a94e631a9168') 207 | 208 | expect( 209 | TypedDataUtils.encodeType(typedData.types, 'Mail') 210 | ).toEqual('Mail(Person from,Person[] to,string contents)Person(string name,address[] wallets)') 211 | 212 | expect( 213 | ethers.utils.hexlify( 214 | TypedDataUtils.typeHash(typedData.types, 'Mail') 215 | ) 216 | ).toEqual('0x4bd8a9a2b93427bb184aca81e24beb30ffa3c747e2a33d4225ec08bf12e2e753') 217 | 218 | expect( 219 | ethers.utils.hexlify( 220 | TypedDataUtils.encodeData(typedData, typedData.primaryType, typedData.message) 221 | ) 222 | ).toEqual( 223 | `0x${[ 224 | '4bd8a9a2b93427bb184aca81e24beb30ffa3c747e2a33d4225ec08bf12e2e753', 225 | '9b4846dd48b866f0ac54d61b9b21a9e746f921cefa4ee94c4c0a1c49c774f67f', 226 | 'ca322beec85be24e374d18d582a6f2997f75c54e7993ab5bc07404ce176ca7cd', 227 | 'b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8', 228 | ].join('')}` 229 | ) 230 | 231 | expect( 232 | ethers.utils.hexlify( 233 | TypedDataUtils.hashStruct(typedData, typedData.primaryType, typedData.message) 234 | ) 235 | ).toEqual('0xeb4221181ff3f1a83ea7313993ca9218496e424604ba9492bb4052c03d5c3df8') 236 | 237 | expect( 238 | ethers.utils.hexlify( 239 | TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain) 240 | ) 241 | ).toEqual('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f') 242 | 243 | expect( 244 | ethers.utils.hexlify( 245 | TypedDataUtils.encodeDigest(typedData) 246 | ) 247 | ).toEqual('0xa85c2e2b118698e88db68a8105b794a8cc7cec074e89ef991cb4f5f533819cc2') 248 | }) 249 | 250 | test('encoding-4', () => { 251 | const typedData = { 252 | types: { 253 | EIP712Domain: [ 254 | {name: "name", type: "string"}, 255 | {name: "version", type: "string"}, 256 | {name: "chainId", type: "uint256"}, 257 | {name: "verifyingContract", type: "address"}, 258 | ], 259 | Person: [ 260 | {name: "name", type: "string"}, 261 | {name: "wallet", type: "address"}, 262 | {name: 'count', type: 'bytes8'} 263 | ] 264 | }, 265 | primaryType: 'Person' as const, 266 | domain: { 267 | name: 'Ether Mail', 268 | version: '1', 269 | chainId: 1, 270 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' 271 | }, 272 | message: { 273 | 'name': 'Bob', 274 | 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 275 | 'count': '0x1122334455667788' 276 | } 277 | } 278 | 279 | const domainHash = TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain) 280 | const domainHashHex = ethers.utils.hexlify(domainHash) 281 | expect(domainHashHex).toEqual('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f') 282 | 283 | const digest = TypedDataUtils.encodeDigest(typedData) 284 | const digestHex = ethers.utils.hexlify(digest) 285 | expect(digestHex).toEqual('0x2a3e64893ed4ba30ea34dbff3b0aa08c7677876cfdf7112362eccf3111f58d1d') 286 | }) 287 | 288 | test('encoding-5', () => { 289 | const typedData = TypedDataUtils.buildTypedData( 290 | { 291 | name: 'Ether Mail', 292 | version: '1', 293 | chainId: 1, 294 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' 295 | }, 296 | { 297 | 'Person': [ 298 | {name: "name", type: "string"}, 299 | {name: "wallet", type: "address"}, 300 | ] 301 | }, 302 | 'Person', 303 | { 304 | 'name': 'Bob', 305 | 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' 306 | } 307 | ) 308 | 309 | const domainHash = TypedDataUtils.hashStruct(typedData, 'EIP712Domain', typedData.domain) 310 | const domainHashHex = ethers.utils.hexlify(domainHash) 311 | expect(domainHashHex).toEqual('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f') 312 | 313 | const digest = TypedDataUtils.encodeDigest(typedData) 314 | const digestHex = ethers.utils.hexlify(digest) 315 | expect(digestHex).toEqual('0x0a94cf6625e5860fc4f330d75bcd0c3a4737957d2321d1a024540ab5320fe903') 316 | }) 317 | 318 | }) 319 | --------------------------------------------------------------------------------