diff options
20 files changed, 326 insertions, 86 deletions
diff --git a/packages/contract-wrappers/src/utils/transaction_encoder.ts b/packages/contract-wrappers/src/utils/transaction_encoder.ts index 87cbb43fd..1800f49ad 100644 --- a/packages/contract-wrappers/src/utils/transaction_encoder.ts +++ b/packages/contract-wrappers/src/utils/transaction_encoder.ts @@ -1,19 +1,19 @@ import { schemas } from '@0xproject/json-schemas'; -import { EIP712Schema, EIP712Types, eip712Utils } from '@0xproject/order-utils'; +import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from '@0xproject/order-utils'; import { Order, SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; +import { BigNumber, signTypedDataUtils } from '@0xproject/utils'; import _ = require('lodash'); import { ExchangeContract } from '../contract_wrappers/generated/exchange'; import { assert } from './assert'; -const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = { +const EIP712_ZEROEX_TRANSACTION_SCHEMA = { name: 'ZeroExTransaction', parameters: [ - { name: 'salt', type: EIP712Types.Uint256 }, - { name: 'signerAddress', type: EIP712Types.Address }, - { name: 'data', type: EIP712Types.Bytes }, + { name: 'salt', type: 'uint256' }, + { name: 'signerAddress', type: 'address' }, + { name: 'data', type: 'bytes' }, ], }; @@ -37,16 +37,25 @@ export class TransactionEncoder { public getTransactionHex(data: string, salt: BigNumber, signerAddress: string): string { const exchangeAddress = this._getExchangeContract().address; const executeTransactionData = { - salt, + salt: salt.toString(), signerAddress, data, }; - const executeTransactionHashBuff = eip712Utils.structHash( - EIP712_ZEROEX_TRANSACTION_SCHEMA, - executeTransactionData, - ); - const eip721MessageBuffer = eip712Utils.createEIP712Message(executeTransactionHashBuff, exchangeAddress); - const messageHex = `0x${eip721MessageBuffer.toString('hex')}`; + const typedData = { + types: { + EIP712Domain: EIP712_DOMAIN_SCHEMA.parameters, + ZeroExTransaction: EIP712_ZEROEX_TRANSACTION_SCHEMA.parameters, + }, + domain: { + name: EIP712_DOMAIN_NAME, + version: EIP712_DOMAIN_VERSION, + verifyingContract: exchangeAddress, + }, + message: executeTransactionData, + primaryType: EIP712_ZEROEX_TRANSACTION_SCHEMA.name, + }; + const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData); + const messageHex = `0x${eip712MessageBuffer.toString('hex')}`; return messageHex; } /** diff --git a/packages/contracts/test/exchange/libs.ts b/packages/contracts/test/exchange/libs.ts index 37234489e..049b7f37a 100644 --- a/packages/contracts/test/exchange/libs.ts +++ b/packages/contracts/test/exchange/libs.ts @@ -1,5 +1,5 @@ import { BlockchainLifecycle } from '@0xproject/dev-utils'; -import { assetDataUtils, eip712Utils, orderHashUtils } from '@0xproject/order-utils'; +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; import { SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; @@ -126,22 +126,6 @@ describe('Exchange libs', () => { }); describe('LibOrder', () => { - describe('getOrderSchema', () => { - it('should output the correct order schema hash', async () => { - const orderSchema = await libs.getOrderSchemaHash.callAsync(); - const schemaHashBuffer = orderHashUtils._getOrderSchemaBuffer(); - const schemaHashHex = `0x${schemaHashBuffer.toString('hex')}`; - expect(schemaHashHex).to.be.equal(orderSchema); - }); - }); - describe('getDomainSeparatorSchema', () => { - it('should output the correct domain separator schema hash', async () => { - const domainSeparatorSchema = await libs.getDomainSeparatorSchemaHash.callAsync(); - const domainSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer(); - const schemaHashHex = `0x${domainSchemaBuffer.toString('hex')}`; - expect(schemaHashHex).to.be.equal(domainSeparatorSchema); - }); - }); describe('getOrderHash', () => { it('should output the correct orderHash', async () => { signedOrder = await orderFactory.newSignedOrderAsync(); diff --git a/packages/contracts/test/utils/transaction_factory.ts b/packages/contracts/test/utils/transaction_factory.ts index 8465a6a30..47880cca5 100644 --- a/packages/contracts/test/utils/transaction_factory.ts +++ b/packages/contracts/test/utils/transaction_factory.ts @@ -1,16 +1,22 @@ -import { EIP712Schema, EIP712Types, eip712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { + EIP712_DOMAIN_NAME, + EIP712_DOMAIN_SCHEMA, + EIP712_DOMAIN_VERSION, + generatePseudoRandomSalt, +} from '@0xproject/order-utils'; import { SignatureType } from '@0xproject/types'; +import { signTypedDataUtils } from '@0xproject/utils'; import * as ethUtil from 'ethereumjs-util'; import { signingUtils } from './signing_utils'; import { SignedTransaction } from './types'; -const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = { +const EIP712_ZEROEX_TRANSACTION_SCHEMA = { name: 'ZeroExTransaction', parameters: [ - { name: 'salt', type: EIP712Types.Uint256 }, - { name: 'signerAddress', type: EIP712Types.Address }, - { name: 'data', type: EIP712Types.Bytes }, + { name: 'salt', type: 'uint256' }, + { name: 'signerAddress', type: 'address' }, + { name: 'data', type: 'bytes' }, ], }; @@ -27,20 +33,30 @@ export class TransactionFactory { const salt = generatePseudoRandomSalt(); const signerAddress = `0x${this._signerBuff.toString('hex')}`; const executeTransactionData = { - salt, + salt: salt.toString(), signerAddress, data, }; - const executeTransactionHashBuff = eip712Utils.structHash( - EIP712_ZEROEX_TRANSACTION_SCHEMA, - executeTransactionData, - ); - const txHash = eip712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress); - const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType); + const typedData = { + types: { + EIP712Domain: EIP712_DOMAIN_SCHEMA.parameters, + ZeroExTransaction: EIP712_ZEROEX_TRANSACTION_SCHEMA.parameters, + }, + domain: { + name: EIP712_DOMAIN_NAME, + version: EIP712_DOMAIN_VERSION, + verifyingContract: this._exchangeAddress, + }, + message: executeTransactionData, + primaryType: EIP712_ZEROEX_TRANSACTION_SCHEMA.name, + }; + const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData); + const signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType); const signedTx = { exchangeAddress: this._exchangeAddress, signature: `0x${signature.toString('hex')}`, ...executeTransactionData, + salt, }; return signedTx; } diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 3e841c43c..a9d2fde8b 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,5 +1,22 @@ [ { + "version": "2.0.0", + "changes": [ + { + "note": "Added ecSignOrderAsync to first sign an order as EIP712 and fallback to EthSign", + "pr": 1102 + }, + { + "note": "Added ecSignTypedDataOrderAsync to sign an order exclusively as EIP712", + "pr": 1102 + }, + { + "note": "Rename ecSignOrderHashAsync to ecSignHashAsync removing SignerType parameter", + "pr": 1102 + } + ] + }, + { "version": "1.0.7", "changes": [ { diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index cc03755c3..5403606c3 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -22,7 +22,7 @@ export const EIP712_DOMAIN_SCHEMA = { name: 'EIP712Domain', parameters: [ { name: 'name', type: 'string' }, - { name: 'version', type: 'string ' }, + { name: 'version', type: 'string' }, { name: 'verifyingContract', type: 'address' }, ], }; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 7194b9780..89a843d8f 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -18,6 +18,8 @@ export { ExchangeTransferSimulator } from './exchange_transfer_simulator'; export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store'; export { OrderFilledCancelledLazyStore } from './store/order_filled_cancelled_lazy_store'; +export { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants'; + export { Provider, JSONRPCRequestPayload, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types'; export { SignedOrder, diff --git a/packages/subproviders/CHANGELOG.json b/packages/subproviders/CHANGELOG.json index 30887c6fe..6a6f7848b 100644 --- a/packages/subproviders/CHANGELOG.json +++ b/packages/subproviders/CHANGELOG.json @@ -3,7 +3,12 @@ "version": "2.1.0", "changes": [ { - "note": "Add Metamask Subprovider to handle inconsistent JSON RPC behaviour" + "note": "Add Metamask Subprovider to handle inconsistent JSON RPC behaviour", + "pr": 1102 + }, + { + "note": "Add support for eth_signTypedData in Mnemonic, Private and EthLightWallet", + "pr": 1102 } ] }, diff --git a/packages/subproviders/src/subproviders/base_wallet_subprovider.ts b/packages/subproviders/src/subproviders/base_wallet_subprovider.ts index 4342e47e9..409a0d330 100644 --- a/packages/subproviders/src/subproviders/base_wallet_subprovider.ts +++ b/packages/subproviders/src/subproviders/base_wallet_subprovider.ts @@ -23,6 +23,7 @@ export abstract class BaseWalletSubprovider extends Subprovider { public abstract async getAccountsAsync(): Promise<string[]>; public abstract async signTransactionAsync(txParams: PartialTxParams): Promise<string>; public abstract async signPersonalMessageAsync(data: string, address: string): Promise<string>; + public abstract async signTypedDataAsync(address: string, typedData: any): Promise<string>; /** * This method conforms to the web3-provider-engine interface. @@ -36,6 +37,8 @@ export abstract class BaseWalletSubprovider extends Subprovider { public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> { let accounts; let txParams; + let address; + let typedData; switch (payload.method) { case 'eth_coinbase': try { @@ -86,7 +89,7 @@ export abstract class BaseWalletSubprovider extends Subprovider { case 'eth_sign': case 'personal_sign': const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0]; - const address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1]; + address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1]; try { const ecSignatureHex = await this.signPersonalMessageAsync(data, address); end(null, ecSignatureHex); @@ -94,6 +97,15 @@ export abstract class BaseWalletSubprovider extends Subprovider { end(err); } return; + case 'eth_signTypedData': + [address, typedData] = payload.params; + try { + const signature = await this.signTypedDataAsync(address, typedData); + end(null, signature); + } catch (err) { + end(err); + } + return; default: next(); diff --git a/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts b/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts index 6afd71422..e3afeff1b 100644 --- a/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts +++ b/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts @@ -57,7 +57,7 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider { /** * Sign a personal Ethereum signed message. The signing account will be the account * associated with the provided address. - * If you've added the MnemonicWalletSubprovider to your app's provider, you can simply send an `eth_sign` + * If you've added the this Subprovider to your app's provider, you can simply send an `eth_sign` * or `personal_sign` JSON RPC request, and this method will be called auto-magically. * If you are not using this via a ProviderEngine instance, you can call it directly. * @param data Hex string message to sign @@ -71,4 +71,20 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider { const result = privKeyWallet.signPersonalMessageAsync(data, address); return result; } + /** + * Sign an EIP712 Typed Data message. The signing address will associated with the provided address. + * If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData` + * JSON RPC request, and this method will be called auto-magically. + * If you are not using this via a ProviderEngine instance, you can call it directly. + * @param address Address of the account to sign with + * @param data the typed data object + * @return Signature hex string (order: rsv) + */ + public async signTypedDataAsync(address: string, typedData: any): Promise<string> { + let privKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey); + const privKeyWallet = new PrivateKeyWalletSubprovider(privKey); + privKey = ''; + const result = privKeyWallet.signTypedDataAsync(address, typedData); + return result; + } } diff --git a/packages/subproviders/src/subproviders/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts index 6ad5de2e2..ee8edde92 100644 --- a/packages/subproviders/src/subproviders/ledger.ts +++ b/packages/subproviders/src/subproviders/ledger.ts @@ -187,6 +187,16 @@ export class LedgerSubprovider extends BaseWalletSubprovider { throw err; } } + /** + * eth_signTypedData is currently not supported on Ledger devices. + * @param address Address of the account to sign with + * @param data the typed data object + * @return Signature hex string (order: rsv) + */ + // tslint:disable-next-line:prefer-function-over-method + public async signTypedDataAsync(address: string, typedData: any): Promise<string> { + throw new Error(WalletSubproviderErrors.MethodNotSupported); + } private async _createLedgerClientAsync(): Promise<LedgerEthereumClient> { await this._connectionLock.acquire(); if (!_.isUndefined(this._ledgerClientIfExists)) { diff --git a/packages/subproviders/src/subproviders/mnemonic_wallet.ts b/packages/subproviders/src/subproviders/mnemonic_wallet.ts index 1495112b6..de99b632a 100644 --- a/packages/subproviders/src/subproviders/mnemonic_wallet.ts +++ b/packages/subproviders/src/subproviders/mnemonic_wallet.ts @@ -108,6 +108,25 @@ export class MnemonicWalletSubprovider extends BaseWalletSubprovider { const sig = await privateKeyWallet.signPersonalMessageAsync(data, address); return sig; } + /** + * Sign an EIP712 Typed Data message. The signing account will be the account + * associated with the provided address. + * If you've added this MnemonicWalletSubprovider to your app's provider, you can simply send an `eth_signTypedData` + * JSON RPC request, and this method will be called auto-magically. + * If you are not using this via a ProviderEngine instance, you can call it directly. + * @param address Address of the account to sign with + * @param data the typed data object + * @return Signature hex string (order: rsv) + */ + public async signTypedDataAsync(address: string, typedData: any): Promise<string> { + if (_.isUndefined(typedData)) { + throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage); + } + assert.isETHAddressHex('address', address); + const privateKeyWallet = this._privateKeyWalletForAddress(address); + const sig = await privateKeyWallet.signTypedDataAsync(address, typedData); + return sig; + } private _privateKeyWalletForAddress(address: string): PrivateKeyWalletSubprovider { const derivedKeyInfo = this._findDerivedKeyInfoForAddress(address); const privateKeyHex = derivedKeyInfo.hdKey.privateKey.toString('hex'); diff --git a/packages/subproviders/src/subproviders/private_key_wallet.ts b/packages/subproviders/src/subproviders/private_key_wallet.ts index dbd51e8d7..51409077d 100644 --- a/packages/subproviders/src/subproviders/private_key_wallet.ts +++ b/packages/subproviders/src/subproviders/private_key_wallet.ts @@ -1,4 +1,5 @@ import { assert } from '@0xproject/assert'; +import { signTypedDataUtils } from '@0xproject/utils'; import EthereumTx = require('ethereumjs-tx'); import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; @@ -84,4 +85,29 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider { const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s); return rpcSig; } + /** + * Sign an EIP712 Typed Data message. The signing address will be calculated from the private key. + * The address must be provided it must match the address calculated from the private key. + * If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData` + * JSON RPC request, and this method will be called auto-magically. + * If you are not using this via a ProviderEngine instance, you can call it directly. + * @param address Address of the account to sign with + * @param data the typed data object + * @return Signature hex string (order: rsv) + */ + public async signTypedDataAsync(address: string, typedData: any): Promise<string> { + if (_.isUndefined(typedData)) { + throw new Error(WalletSubproviderErrors.DataMissingForSignTypedData); + } + assert.isETHAddressHex('address', address); + if (address !== this._address) { + throw new Error( + `Requested to sign message with address: ${address}, instantiated with address: ${this._address}`, + ); + } + const dataBuff = signTypedDataUtils.signTypedDataHash(typedData); + const sig = ethUtil.ecsign(dataBuff, this._privateKeyBuffer); + const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s); + return rpcSig; + } } diff --git a/packages/subproviders/src/types.ts b/packages/subproviders/src/types.ts index fe58bffa5..e8a47ad34 100644 --- a/packages/subproviders/src/types.ts +++ b/packages/subproviders/src/types.ts @@ -107,8 +107,10 @@ export interface ResponseWithTxParams { export enum WalletSubproviderErrors { AddressNotFound = 'ADDRESS_NOT_FOUND', DataMissingForSignPersonalMessage = 'DATA_MISSING_FOR_SIGN_PERSONAL_MESSAGE', + DataMissingForSignTypedData = 'DATA_MISSING_FOR_SIGN_TYPED_DATA', SenderInvalidOrNotSupplied = 'SENDER_INVALID_OR_NOT_SUPPLIED', FromAddressMissingOrInvalid = 'FROM_ADDRESS_MISSING_OR_INVALID', + MethodNotSupported = 'METHOD_NOT_SUPPORTED', } export enum LedgerSubproviderErrors { TooOldLedgerFirmware = 'TOO_OLD_LEDGER_FIRMWARE', diff --git a/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts b/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts index f2bdda3cd..61dcbf6da 100644 --- a/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts +++ b/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts @@ -47,6 +47,13 @@ describe('MnemonicWalletSubprovider', () => { const txHex = await subprovider.signTransactionAsync(txData); expect(txHex).to.be.equal(fixtureData.TX_DATA_ACCOUNT_1_SIGNED_RESULT); }); + it('signs an EIP712 sign typed data message', async () => { + const signature = await subprovider.signTypedDataAsync( + fixtureData.TEST_RPC_ACCOUNT_0, + fixtureData.EIP712_TEST_TYPED_DATA, + ); + expect(signature).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT); + }); }); describe('failure cases', () => { it('throws an error if address is invalid ', async () => { @@ -118,6 +125,20 @@ describe('MnemonicWalletSubprovider', () => { }); provider.sendAsync(payload, callback); }); + it('signs an EIP712 sign typed data message with eth_signTypedData', (done: DoneCallback) => { + const payload = { + jsonrpc: '2.0', + method: 'eth_signTypedData', + params: [fixtureData.TEST_RPC_ACCOUNT_0, fixtureData.EIP712_TEST_TYPED_DATA], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { + expect(err).to.be.a('null'); + expect(response.result).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT); + done(); + }); + provider.sendAsync(payload, callback); + }); }); describe('failure cases', () => { it('should throw if `data` param not hex when calling eth_sign', (done: DoneCallback) => { diff --git a/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts b/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts index 95773145f..4cd70e5ed 100644 --- a/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts +++ b/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts @@ -32,6 +32,13 @@ describe('PrivateKeyWalletSubprovider', () => { const txHex = await subprovider.signTransactionAsync(fixtureData.TX_DATA); expect(txHex).to.be.equal(fixtureData.TX_DATA_SIGNED_RESULT); }); + it('signs an EIP712 sign typed data message', async () => { + const signature = await subprovider.signTypedDataAsync( + fixtureData.TEST_RPC_ACCOUNT_0, + fixtureData.EIP712_TEST_TYPED_DATA, + ); + expect(signature).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT); + }); }); }); describe('calls through a provider', () => { @@ -103,6 +110,20 @@ describe('PrivateKeyWalletSubprovider', () => { }); provider.sendAsync(payload, callback); }); + it('signs an EIP712 sign typed data message with eth_signTypedData', (done: DoneCallback) => { + const payload = { + jsonrpc: '2.0', + method: 'eth_signTypedData', + params: [fixtureData.TEST_RPC_ACCOUNT_0, fixtureData.EIP712_TEST_TYPED_DATA], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { + expect(err).to.be.a('null'); + expect(response.result).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT); + done(); + }); + provider.sendAsync(payload, callback); + }); }); describe('failure cases', () => { it('should throw if `data` param not hex when calling eth_sign', (done: DoneCallback) => { diff --git a/packages/subproviders/test/utils/fixture_data.ts b/packages/subproviders/test/utils/fixture_data.ts index 7cf502c97..3eb4493b5 100644 --- a/packages/subproviders/test/utils/fixture_data.ts +++ b/packages/subproviders/test/utils/fixture_data.ts @@ -30,4 +30,35 @@ export const fixtureData = { '0xf85f8080822710940000000000000000000000000000000000000000808078a0712854c73c69445cc1b22a7c3d7312ff9a97fe4ffba35fd636e8236b211b6e7ca0647cee031615e52d916c7c707025bc64ad525d8f1b9876c3435a863b42743178', TX_DATA_ACCOUNT_1_SIGNED_RESULT: '0xf85f8080822710940000000000000000000000000000000000000000808078a04b02af7ff3f18ce114b601542cc8ebdc50921354f75dd510d31793453a0710e6a0540082a01e475465801b8186a2edc79ec1a2dcf169b9781c25a58a417023c9ca', + EIP712_TEST_TYPED_DATA: { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + ], + Test: [ + { + name: 'testAddress', + type: 'address', + }, + { + name: 'testNumber', + type: 'uint256', + }, + ], + }, + domain: { + name: 'Test', + }, + message: { + testAddress: '0x0000000000000000000000000000000000000000', + testNumber: '12345', + }, + primaryType: 'Test', + }, + EIP712_TEST_TYPED_DATA_HASH: '0xb460d69ca60383293877cd765c0f97bd832d66bca720f7e32222ce1118832493', + EIP712_TEST_TYPED_DATA_SIGNED_RESULT: + '0x20af5b6bfc3658942198d6eeda159b4ed589f90cee6eac3ba117818ffba5fd7e354a353aad93faabd6eb6c66e17921c92bd1cd09c92a770f554470dc3e254ce701', }; diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 6bb6ced70..65dd75101 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "1.2.0", + "changes": [ + { + "note": "Added `EIP712Parameter` `EIP712Types` `EIP712TypedData` for EIP712 signing", + "pr": 1102 + } + ] + }, + { "timestamp": 1538693146, "version": "1.1.4", "changes": [ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2f148f0e6..d57bdfb6f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -589,3 +589,18 @@ export interface Metadata { externalTypeToLink: ExternalTypeToLink; externalExportToLink: ExternalExportToLink; } + +export interface EIP712Parameter { + name: string; + type: string; +} + +export interface EIP712Types { + [key: string]: EIP712Parameter[]; +} +export interface EIP712TypedData { + types: EIP712Types; + domain: any; + message: any; + primaryType: string; +} diff --git a/packages/utils/src/sign_typed_data_utils.ts b/packages/utils/src/sign_typed_data_utils.ts index 902d8530c..b72fd099b 100644 --- a/packages/utils/src/sign_typed_data_utils.ts +++ b/packages/utils/src/sign_typed_data_utils.ts @@ -1,29 +1,30 @@ 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; -} +import { EIP712TypedData, EIP712Types } from '@0xproject/types'; export const signTypedDataUtils = { - findDependencies(primaryType: string, types: EIP712Types, found: string[] = []): string[] { + /** + * Computes the Sign Typed Data hash + * @param typedData An object that conforms to the EIP712TypedData interface + * @return A Buffer containing the hash of the sign typed data. + */ + 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), + ]), + ); + }, + _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)) { + for (const dep of signTypedDataUtils._findDependencies(field.type, types, found)) { if (!found.includes(dep)) { found.push(dep); } @@ -31,8 +32,8 @@ export const signTypedDataUtils = { } return found; }, - encodeType(primaryType: string, types: EIP712Types): string { - let deps = signTypedDataUtils.findDependencies(primaryType, types); + _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 = ''; @@ -41,9 +42,9 @@ export const signTypedDataUtils = { } return result; }, - encodeData(primaryType: string, data: any, types: EIP712Types): string { + _encodeData(primaryType: string, data: any, types: EIP712Types): string { const encodedTypes = ['bytes32']; - const encodedValues = [signTypedDataUtils.typeHash(primaryType, types)]; + const encodedValues = [signTypedDataUtils._typeHash(primaryType, types)]; for (const field of types[primaryType]) { let value = data[field.name]; if (field.type === 'string' || field.type === 'bytes') { @@ -52,7 +53,7 @@ export const signTypedDataUtils = { encodedValues.push(value); } else if (types[field.type] !== undefined) { encodedTypes.push('bytes32'); - value = ethUtil.sha3(signTypedDataUtils.encodeData(field.type, value, types)); + 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'); @@ -63,19 +64,10 @@ export const signTypedDataUtils = { } return ethers.utils.defaultAbiCoder.encode(encodedTypes, encodedValues); }, - typeHash(primaryType: string, types: EIP712Types): Buffer { - return ethUtil.sha3(signTypedDataUtils.encodeType(primaryType, types)); + _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), - ]), - ); + _structHash(primaryType: string, data: any, types: EIP712Types): Buffer { + return ethUtil.sha3(signTypedDataUtils._encodeData(primaryType, data, types)); }, }; diff --git a/packages/utils/test/sign_typed_data_utils_test.ts b/packages/utils/test/sign_typed_data_utils_test.ts index b21ffefa0..e1cb4f6e1 100644 --- a/packages/utils/test/sign_typed_data_utils_test.ts +++ b/packages/utils/test/sign_typed_data_utils_test.ts @@ -7,8 +7,37 @@ const expect = chai.expect; describe('signTypedDataUtils', () => { describe('signTypedDataHash', () => { - const signTypedDataHashHex = '0x55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692'; - const signTypedData = { + const simpleSignTypedDataHashHex = '0xb460d69ca60383293877cd765c0f97bd832d66bca720f7e32222ce1118832493'; + const simpleSignTypedData = { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + ], + Test: [ + { + name: 'testAddress', + type: 'address', + }, + { + name: 'testNumber', + type: 'uint256', + }, + ], + }, + domain: { + name: 'Test', + }, + message: { + testAddress: '0x0000000000000000000000000000000000000000', + testNumber: '12345', + }, + primaryType: 'Test', + }; + const orderSignTypedDataHashHex = '0x55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692'; + const orderSignTypedData = { types: { EIP712Domain: [ { @@ -97,11 +126,15 @@ describe('signTypedDataUtils', () => { }, primaryType: 'Order', }; - it.only('creates a known hash of the sign typed data', () => { - const hash = signTypedDataUtils.signTypedDataHash(signTypedData).toString('hex'); + it('creates a hash of the test sign typed data', () => { + const hash = signTypedDataUtils.signTypedDataHash(simpleSignTypedData).toString('hex'); + const hashHex = `0x${hash}`; + expect(hashHex).to.be.eq(simpleSignTypedDataHashHex); + }); + it('creates a hash of the order sign typed data', () => { + const hash = signTypedDataUtils.signTypedDataHash(orderSignTypedData).toString('hex'); const hashHex = `0x${hash}`; - expect(hashHex).to.be.eq(signTypedDataHashHex); - console.log(hash); + expect(hashHex).to.be.eq(orderSignTypedDataHashHex); }); }); }); |