Last active
September 3, 2023 15:01
-
-
Save ajb413/6ca63eb868e179a9c0a3b8dc735733cf to your computer and use it in GitHub Desktop.
Module for creating EIP-712 signatures with Ethers.js as the only dependency. Works in the browser and Node.js (Ethers.js Web3 Provider / JSON RPC Provider).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Based on https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.js | |
const ethers = require('ethers'); | |
function abiRawEncode(encTypes, encValues) { | |
const hexStr = ethers.utils.defaultAbiCoder.encode(encTypes, encValues); | |
return Buffer.from(hexStr.slice(2, hexStr.length), 'hex'); | |
} | |
function keccak256(arg) { | |
const hexStr = ethers.utils.keccak256(arg); | |
return Buffer.from(hexStr.slice(2, hexStr.length), 'hex'); | |
} | |
// Recursively finds all the dependencies of a type | |
function dependencies(primaryType, found = [], types = {}) { | |
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, types = {}) { | |
// 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) { | |
if (!types[type]) | |
throw new Error(`Type '${type}' not defined in types (${JSON.stringify(types)})`); | |
result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`; | |
} | |
return result; | |
} | |
function typeHash(primaryType, types = {}) { | |
return keccak256(Buffer.from(encodeType(primaryType, types))); | |
} | |
function encodeData(primaryType, data, types = {}) { | |
let encTypes = []; | |
let encValues = []; | |
// Add typehash | |
encTypes.push('bytes32'); | |
encValues.push(typeHash(primaryType, types)); | |
// 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 = keccak256(Buffer.from(value)); | |
encValues.push(value); | |
} else if (types[field.type] !== undefined) { | |
encTypes.push('bytes32'); | |
value = keccak256(encodeData(field.type, value, types)); | |
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 abiRawEncode(encTypes, encValues); | |
} | |
function domainSeparator(domain) { | |
const types = { | |
EIP712Domain: [ | |
{name: 'name', type: 'string'}, | |
{name: 'version', type: 'string'}, | |
{name: 'chainId', type: 'uint256'}, | |
{name: 'verifyingContract', type: 'address'}, | |
{name: 'salt', type: 'bytes32'} | |
].filter(a => domain[a.name]) | |
}; | |
return keccak256(encodeData('EIP712Domain', domain, types)); | |
} | |
function structHash(primaryType, data, types = {}) { | |
return keccak256(encodeData(primaryType, data, types)); | |
} | |
function digestToSign(domain, primaryType, message, types = {}) { | |
return keccak256( | |
Buffer.concat([ | |
Buffer.from('1901', 'hex'), | |
domainSeparator(domain), | |
structHash(primaryType, message, types), | |
]) | |
); | |
} | |
async function sign(domain, primaryType, message, types = {}, signer) { | |
let signature; | |
try { | |
if (signer._signingKey) { | |
const digest = digestToSign(domain, primaryType, message, types); | |
signature = signer._signingKey().signDigest(digest); | |
signature.v = '0x' + (signature.v).toString(16); | |
} else { | |
const address = await signer.getAddress(); | |
const msgParams = JSON.stringify({ domain, primaryType, message, types }); | |
signature = await signer.provider.jsonRpcFetchFunc( | |
'eth_signTypedData_v4', | |
[ address, msgParams ] | |
); | |
const r = '0x' + signature.substring(2).substring(0, 64); | |
const s = '0x' + signature.substring(2).substring(64, 128); | |
const v = '0x' + signature.substring(2).substring(128, 130); | |
signature = { r, s, v }; | |
} | |
} catch(e) { | |
throw new Error(e); | |
} | |
return signature; | |
} | |
module.exports = { | |
encodeType, | |
typeHash, | |
encodeData, | |
domainSeparator, | |
structHash, | |
digestToSign, | |
sign | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey @ajb413 have you implemented somewhere the arrays in encodedData by any chance?