Skip to content

Instantly share code, notes, and snippets.

@gregorynicholas
Forked from ajb413/EIP712.js
Created November 8, 2020 18:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gregorynicholas/bbe975d83f030d738076f0a714ac6535 to your computer and use it in GitHub Desktop.
Save gregorynicholas/bbe975d83f030d738076f0a714ac6535 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).
// 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