Move SignTypedData to utils package

This commit is contained in:
Jacob Evans 2018-10-04 17:12:35 +10:00
parent 07926ded6e
commit 2a82ff48c0
No known key found for this signature in database
GPG Key ID: 2036DA2ADDFB0842
9 changed files with 234 additions and 152 deletions

View File

@ -14,3 +14,15 @@ export const constants = {
INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite
ZERO_AMOUNT: new BigNumber(0),
};
export const EIP712_DOMAIN_NAME = '0x Protocol';
export const EIP712_DOMAIN_VERSION = '2';
export const EIP712_DOMAIN_SCHEMA = {
name: 'EIP712Domain',
parameters: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string ' },
{ name: 'verifyingContract', type: 'address' },
],
};

View File

@ -1,109 +0,0 @@
import ethUtil = require('ethereumjs-util');
import * as _ from 'lodash';
import { crypto } from './crypto';
import { EIP712Schema, EIP712Types } from './types';
const EIP191_PREFIX = '\x19\x01';
const EIP712_VALUE_LENGTH = 32;
export const EIP712_DOMAIN_NAME = '0x Protocol';
export const EIP712_DOMAIN_VERSION = '2';
export const EIP712_DOMAIN_SCHEMA: EIP712Schema = {
name: 'EIP712Domain',
parameters: [
{ name: 'name', type: EIP712Types.String },
{ name: 'version', type: EIP712Types.String },
{ name: 'verifyingContract', type: EIP712Types.Address },
],
};
export const eip712Utils = {
/**
* Compiles the EIP712Schema and returns the hash of the schema.
* @param schema The EIP712 schema.
* @return The hash of the compiled schema
*/
compileSchema(schema: EIP712Schema): Buffer {
const eip712Schema = eip712Utils._encodeType(schema);
const eip712SchemaHashBuffer = crypto.solSHA3([eip712Schema]);
return eip712SchemaHashBuffer;
},
/**
* Merges the EIP712 hash of a struct with the DomainSeparator for 0x v2.
* @param hashStruct the EIP712 hash of a struct
* @param contractAddress the exchange contract address
* @return The hash of an EIP712 message with domain separator prefixed
*/
createEIP712Message(hashStruct: Buffer, contractAddress: string): Buffer {
const domainSeparatorHashBuffer = eip712Utils._getDomainSeparatorHashBuffer(contractAddress);
const messageBuff = crypto.solSHA3([EIP191_PREFIX, domainSeparatorHashBuffer, hashStruct]);
return messageBuff;
},
/**
* Pad an address to 32 bytes
* @param address Address to pad
* @return padded address
*/
pad32Address(address: string): Buffer {
const addressBuffer = ethUtil.toBuffer(address);
const addressPadded = eip712Utils.pad32Buffer(addressBuffer);
return addressPadded;
},
/**
* Pad an buffer to 32 bytes
* @param buffer Address to pad
* @return padded buffer
*/
pad32Buffer(buffer: Buffer): Buffer {
const bufferPadded = ethUtil.setLengthLeft(buffer, EIP712_VALUE_LENGTH);
return bufferPadded;
},
/**
* Hash together a EIP712 schema with the corresponding data
* @param schema EIP712-compliant schema
* @param data Data the complies to the schema
* @return A buffer containing the SHA256 hash of the schema and encoded data
*/
structHash(schema: EIP712Schema, data: { [key: string]: any }): Buffer {
const encodedData = eip712Utils._encodeData(schema, data);
const schemaHash = eip712Utils.compileSchema(schema);
const hashBuffer = crypto.solSHA3([schemaHash, ...encodedData]);
return hashBuffer;
},
_getDomainSeparatorSchemaBuffer(): Buffer {
return eip712Utils.compileSchema(EIP712_DOMAIN_SCHEMA);
},
_getDomainSeparatorHashBuffer(exchangeAddress: string): Buffer {
const domainSeparatorSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer();
const encodedData = eip712Utils._encodeData(EIP712_DOMAIN_SCHEMA, {
name: EIP712_DOMAIN_NAME,
version: EIP712_DOMAIN_VERSION,
verifyingContract: exchangeAddress,
});
const domainSeparatorHashBuff2 = crypto.solSHA3([domainSeparatorSchemaBuffer, ...encodedData]);
return domainSeparatorHashBuff2;
},
_encodeType(schema: EIP712Schema): string {
const namedTypes = _.map(schema.parameters, ({ name, type }) => `${type} ${name}`);
const namedTypesJoined = namedTypes.join(',');
const encodedType = `${schema.name}(${namedTypesJoined})`;
return encodedType;
},
_encodeData(schema: EIP712Schema, data: { [key: string]: any }): any {
const encodedValues = [];
for (const parameter of schema.parameters) {
const value = data[parameter.name];
if (parameter.type === EIP712Types.String || parameter.type === EIP712Types.Bytes) {
encodedValues.push(crypto.solSHA3([ethUtil.toBuffer(value)]));
} else if (parameter.type === EIP712Types.Uint256) {
encodedValues.push(value);
} else if (parameter.type === EIP712Types.Address) {
encodedValues.push(eip712Utils.pad32Address(value));
} else {
throw new Error(`Unable to encode ${parameter.type}`);
}
}
return encodedValues;
},
};

View File

@ -2,7 +2,6 @@ export { orderHashUtils } from './order_hash';
export { signatureUtils } from './signature_utils';
export { generatePseudoRandomSalt } from './salt';
export { assetDataUtils } from './asset_data_utils';
export { eip712Utils } from './eip712_utils';
export { marketUtils } from './market_utils';
export { rateUtils } from './rate_utils';
export { sortingUtils } from './sorting_utils';
@ -36,9 +35,6 @@ export {
} from '@0xproject/types';
export {
OrderError,
EIP712Parameter,
EIP712Schema,
EIP712Types,
TradeSide,
TransferType,
FindFeeOrdersThatCoverFeesForTargetOrdersOpts,

View File

@ -1,28 +1,28 @@
import { schemas, SchemaValidator } from '@0xproject/json-schemas';
import { Order, SignedOrder } from '@0xproject/types';
import { signTypedDataUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import { assert } from './assert';
import { eip712Utils } from './eip712_utils';
import { EIP712Schema, EIP712Types } from './types';
import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants';
const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string';
export const EIP712_ORDER_SCHEMA: EIP712Schema = {
export const EIP712_ORDER_SCHEMA = {
name: 'Order',
parameters: [
{ name: 'makerAddress', type: EIP712Types.Address },
{ name: 'takerAddress', type: EIP712Types.Address },
{ name: 'feeRecipientAddress', type: EIP712Types.Address },
{ name: 'senderAddress', type: EIP712Types.Address },
{ name: 'makerAssetAmount', type: EIP712Types.Uint256 },
{ name: 'takerAssetAmount', type: EIP712Types.Uint256 },
{ name: 'makerFee', type: EIP712Types.Uint256 },
{ name: 'takerFee', type: EIP712Types.Uint256 },
{ name: 'expirationTimeSeconds', type: EIP712Types.Uint256 },
{ name: 'salt', type: EIP712Types.Uint256 },
{ name: 'makerAssetData', type: EIP712Types.Bytes },
{ name: 'takerAssetData', type: EIP712Types.Bytes },
{ name: 'makerAddress', type: 'address' },
{ name: 'takerAddress', type: 'address' },
{ name: 'feeRecipientAddress', type: 'address' },
{ name: 'senderAddress', type: 'address' },
{ name: 'makerAssetAmount', type: 'uint256' },
{ name: 'takerAssetAmount', type: 'uint256' },
{ name: 'makerFee', type: 'uint256' },
{ name: 'takerFee', type: 'uint256' },
{ name: 'expirationTimeSeconds', type: 'uint256' },
{ name: 'salt', type: 'uint256' },
{ name: 'makerAssetData', type: 'bytes' },
{ name: 'takerAssetData', type: 'bytes' },
],
};
@ -69,11 +69,23 @@ export const orderHashUtils = {
* @return The resulting orderHash from hashing the supplied order as a Buffer
*/
getOrderHashBuffer(order: SignedOrder | Order): Buffer {
const orderParamsHashBuff = eip712Utils.structHash(EIP712_ORDER_SCHEMA, order);
const orderHashBuff = eip712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress);
const normalizedOrder = _.mapValues(order, value => {
return _.isObject(value) ? value.toString() : value;
});
const typedData = {
types: {
EIP712Domain: EIP712_DOMAIN_SCHEMA.parameters,
Order: EIP712_ORDER_SCHEMA.parameters,
},
domain: {
name: EIP712_DOMAIN_NAME,
version: EIP712_DOMAIN_VERSION,
verifyingContract: order.exchangeAddress,
},
message: normalizedOrder,
primaryType: EIP712_ORDER_SCHEMA.name,
};
const orderHashBuff = signTypedDataUtils.signTypedDataHash(typedData);
return orderHashBuff;
},
_getOrderSchemaBuffer(): Buffer {
return eip712Utils.compileSchema(EIP712_ORDER_SCHEMA);
},
};

View File

@ -7,7 +7,7 @@ import * as _ from 'lodash';
import { artifacts } from './artifacts';
import { assert } from './assert';
import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './eip712_utils';
import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants';
import { ExchangeContract } from './generated_contract_wrappers/exchange';
import { IValidatorContract } from './generated_contract_wrappers/i_validator';
import { IWalletContract } from './generated_contract_wrappers/i_wallet';

View File

@ -14,24 +14,6 @@ export enum TransferType {
Fee = 'fee',
}
export interface EIP712Parameter {
name: string;
type: EIP712Types;
}
export interface EIP712Schema {
name: string;
parameters: EIP712Parameter[];
}
export enum EIP712Types {
Address = 'address',
Bytes = 'bytes',
Bytes32 = 'bytes32',
String = 'string',
Uint256 = 'uint256',
}
export interface CreateOrderOpts {
takerAddress?: string;
senderAddress?: string;

View File

@ -9,3 +9,4 @@ export { abiUtils } from './abi_utils';
export { NULL_BYTES } from './constants';
export { errorUtils } from './error_utils';
export { fetchAsync } from './fetch_async';
export { signTypedDataUtils } from './sign_typed_data_utils';

View File

@ -0,0 +1,81 @@
import * as ethUtil from 'ethereumjs-util';
import * as ethers from 'ethers';
export interface EIP712Parameter {
name: string;
type: string;
}
export interface EIP712Types {
[key: string]: EIP712Parameter[];
}
export interface EIP712TypedData {
types: EIP712Types;
domain: any;
message: any;
primaryType: string;
}
export const signTypedDataUtils = {
findDependencies(primaryType: string, types: EIP712Types, found: string[] = []): string[] {
if (found.includes(primaryType) || types[primaryType] === undefined) {
return found;
}
found.push(primaryType);
for (const field of types[primaryType]) {
for (const dep of signTypedDataUtils.findDependencies(field.type, types, found)) {
if (!found.includes(dep)) {
found.push(dep);
}
}
}
return found;
},
encodeType(primaryType: string, types: EIP712Types): string {
let deps = signTypedDataUtils.findDependencies(primaryType, types);
deps = deps.filter(d => d !== primaryType);
deps = [primaryType].concat(deps.sort());
let result = '';
for (const dep of deps) {
result += `${dep}(${types[dep].map(({ name, type }) => `${type} ${name}`).join(',')})`;
}
return result;
},
encodeData(primaryType: string, data: any, types: EIP712Types): string {
const encodedTypes = ['bytes32'];
const encodedValues = [signTypedDataUtils.typeHash(primaryType, types)];
for (const field of types[primaryType]) {
let value = data[field.name];
if (field.type === 'string' || field.type === 'bytes') {
value = ethUtil.sha3(value);
encodedTypes.push('bytes32');
encodedValues.push(value);
} else if (types[field.type] !== undefined) {
encodedTypes.push('bytes32');
value = ethUtil.sha3(signTypedDataUtils.encodeData(field.type, value, types));
encodedValues.push(value);
} else if (field.type.lastIndexOf(']') === field.type.length - 1) {
throw new Error('Arrays currently unimplemented in encodeData');
} else {
encodedTypes.push(field.type);
encodedValues.push(value);
}
}
return ethers.utils.defaultAbiCoder.encode(encodedTypes, encodedValues);
},
typeHash(primaryType: string, types: EIP712Types): Buffer {
return ethUtil.sha3(signTypedDataUtils.encodeType(primaryType, types));
},
structHash(primaryType: string, data: any, types: EIP712Types): Buffer {
return ethUtil.sha3(signTypedDataUtils.encodeData(primaryType, data, types));
},
signTypedDataHash(typedData: EIP712TypedData): Buffer {
return ethUtil.sha3(
Buffer.concat([
Buffer.from('1901', 'hex'),
signTypedDataUtils.structHash('EIP712Domain', typedData.domain, typedData.types),
signTypedDataUtils.structHash(typedData.primaryType, typedData.message, typedData.types),
]),
);
},
};

View File

@ -0,0 +1,107 @@
import * as chai from 'chai';
import 'mocha';
import { signTypedDataUtils } from '../src/sign_typed_data_utils';
const expect = chai.expect;
describe('signTypedDataUtils', () => {
describe('signTypedDataHash', () => {
const signTypedDataHashHex = '0x55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692';
const signTypedData = {
types: {
EIP712Domain: [
{
name: 'name',
type: 'string',
},
{
name: 'version',
type: 'string',
},
{
name: 'verifyingContract',
type: 'address',
},
],
Order: [
{
name: 'makerAddress',
type: 'address',
},
{
name: 'takerAddress',
type: 'address',
},
{
name: 'feeRecipientAddress',
type: 'address',
},
{
name: 'senderAddress',
type: 'address',
},
{
name: 'makerAssetAmount',
type: 'uint256',
},
{
name: 'takerAssetAmount',
type: 'uint256',
},
{
name: 'makerFee',
type: 'uint256',
},
{
name: 'takerFee',
type: 'uint256',
},
{
name: 'expirationTimeSeconds',
type: 'uint256',
},
{
name: 'salt',
type: 'uint256',
},
{
name: 'makerAssetData',
type: 'bytes',
},
{
name: 'takerAssetData',
type: 'bytes',
},
],
},
domain: {
name: '0x Protocol',
version: '2',
verifyingContract: '0x0000000000000000000000000000000000000000',
},
message: {
makerAddress: '0x0000000000000000000000000000000000000000',
takerAddress: '0x0000000000000000000000000000000000000000',
makerAssetAmount: '1000000000000000000',
takerAssetAmount: '1000000000000000000',
expirationTimeSeconds: '12345',
makerFee: '0',
takerFee: '0',
feeRecipientAddress: '0x0000000000000000000000000000000000000000',
senderAddress: '0x0000000000000000000000000000000000000000',
salt: '12345',
makerAssetData: '0x0000000000000000000000000000000000000000',
takerAssetData: '0x0000000000000000000000000000000000000000',
exchangeAddress: '0x0000000000000000000000000000000000000000',
},
primaryType: 'Order',
};
it.only('creates a known hash of the sign typed data', () => {
const hash = signTypedDataUtils.signTypedDataHash(signTypedData).toString('hex');
const hashHex = `0x${hash}`;
expect(hashHex).to.be.eq(signTypedDataHashHex);
console.log(hash);
});
});
});