const ethUtil = require('ethereumjs-util'); const abi = require('ethereumjs-abi'); const chai = require('chai'); const typedData = { types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, ], Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' } ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' } ], }, primaryType: 'Mail', domain: { name: 'Ether Mail', version: '1', chainId: 1, verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', }, message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }; const types = typedData.types; // Recursively finds all the dependencies of a type function dependencies(primaryType, found = []) { if (found.includes(primaryType)) { return found; } if (types[primaryType] === undefined) { return found; } found.push(primaryType); for (let field of types[primaryType]) { for (let dep of dependencies(field.type, found)) { if (!found.includes(dep)) { found.push(dep); } } } return found; } function encodeType(primaryType) { // Get dependencies primary first, then alphabetical let deps = dependencies(primaryType); deps = deps.filter(t => t != primaryType); deps = [primaryType].concat(deps.sort()); // Format as a string with fields let result = ''; for (let type of deps) { result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`; } return result; } function typeHash(primaryType) { return ethUtil.keccak256(encodeType(primaryType)); } function encodeData(primaryType, data) { let encTypes = []; let encValues = []; // Add typehash encTypes.push('bytes32'); encValues.push(typeHash(primaryType)); // Add field contents for (let field of types[primaryType]) { let value = data[field.name]; if (field.type == 'string' || field.type == 'bytes') { encTypes.push('bytes32'); value = ethUtil.keccak256(value); encValues.push(value); } else if (types[field.type] !== undefined) { encTypes.push('bytes32'); value = ethUtil.keccak256(encodeData(field.type, value)); encValues.push(value); } else if (field.type.lastIndexOf(']') === field.type.length - 1) { throw 'TODO: Arrays currently unimplemented in encodeData'; } else { encTypes.push(field.type); encValues.push(value); } } return abi.rawEncode(encTypes, encValues); } function structHash(primaryType, data) { return ethUtil.keccak256(encodeData(primaryType, data)); } function signHash() { return ethUtil.keccak256( Buffer.concat([ Buffer.from('1901', 'hex'), structHash('EIP712Domain', typedData.domain), structHash(typedData.primaryType, typedData.message), ]), ); } const privateKey = ethUtil.keccak256('cow'); const address = ethUtil.privateToAddress(privateKey); const sig = ethUtil.ecsign(signHash(), privateKey); const expect = chai.expect; expect(encodeType('Mail')).to.equal('Mail(Person from,Person to,string contents)Person(string name,address wallet)'); expect(ethUtil.bufferToHex(typeHash('Mail'))).to.equal( '0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2', ); expect(ethUtil.bufferToHex(encodeData(typedData.primaryType, typedData.message))).to.equal( '0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8', ); expect(ethUtil.bufferToHex(structHash(typedData.primaryType, typedData.message))).to.equal( '0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e', ); expect(ethUtil.bufferToHex(structHash('EIP712Domain', typedData.domain))).to.equal( '0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f', ); expect(ethUtil.bufferToHex(signHash())).to.equal('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2'); expect(ethUtil.bufferToHex(address)).to.equal('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826'); expect(sig.v).to.equal(28); expect(ethUtil.bufferToHex(sig.r)).to.equal('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d'); expect(ethUtil.bufferToHex(sig.s)).to.equal('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562');