diff options
25 files changed, 380 insertions, 192 deletions
diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json index c25acd3bf..3a184382c 100644 --- a/packages/0x.js/CHANGELOG.json +++ b/packages/0x.js/CHANGELOG.json @@ -12,7 +12,7 @@ }, { "note": - "Removed `SignerType` (including `SignerType.Metamask`). Please use the `MetamaskSubprovider` to wrap web3.currentProvider.", + "Removed `SignerType` (including `SignerType.Metamask`). Please use the `MetamaskSubprovider` to wrap `web3.currentProvider`.", "pr": 1102 } ] diff --git a/packages/contract-wrappers/src/utils/transaction_encoder.ts b/packages/contract-wrappers/src/utils/transaction_encoder.ts index 1800f49ad..d9735778e 100644 --- a/packages/contract-wrappers/src/utils/transaction_encoder.ts +++ b/packages/contract-wrappers/src/utils/transaction_encoder.ts @@ -1,5 +1,5 @@ import { schemas } from '@0xproject/json-schemas'; -import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from '@0xproject/order-utils'; +import { eip712Utils } from '@0xproject/order-utils'; import { Order, SignedOrder } from '@0xproject/types'; import { BigNumber, signTypedDataUtils } from '@0xproject/utils'; import _ = require('lodash'); @@ -8,15 +8,6 @@ import { ExchangeContract } from '../contract_wrappers/generated/exchange'; import { assert } from './assert'; -const EIP712_ZEROEX_TRANSACTION_SCHEMA = { - name: 'ZeroExTransaction', - parameters: [ - { name: 'salt', type: 'uint256' }, - { name: 'signerAddress', type: 'address' }, - { name: 'data', type: 'bytes' }, - ], -}; - /** * Transaction Encoder. Transaction messages exist for the purpose of calling methods on the Exchange contract * in the context of another address. For example, UserA can encode and sign a fillOrder transaction and UserB @@ -37,23 +28,11 @@ export class TransactionEncoder { public getTransactionHex(data: string, salt: BigNumber, signerAddress: string): string { const exchangeAddress = this._getExchangeContract().address; const executeTransactionData = { - salt: salt.toString(), + salt, signerAddress, data, }; - 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 typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, exchangeAddress); const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData); const messageHex = `0x${eip712MessageBuffer.toString('hex')}`; return messageHex; diff --git a/packages/contracts/test/utils/transaction_factory.ts b/packages/contracts/test/utils/transaction_factory.ts index 47880cca5..6e4cc4b2f 100644 --- a/packages/contracts/test/utils/transaction_factory.ts +++ b/packages/contracts/test/utils/transaction_factory.ts @@ -1,9 +1,4 @@ -import { - EIP712_DOMAIN_NAME, - EIP712_DOMAIN_SCHEMA, - EIP712_DOMAIN_VERSION, - generatePseudoRandomSalt, -} from '@0xproject/order-utils'; +import { eip712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils'; import { SignatureType } from '@0xproject/types'; import { signTypedDataUtils } from '@0xproject/utils'; import * as ethUtil from 'ethereumjs-util'; @@ -11,15 +6,6 @@ import * as ethUtil from 'ethereumjs-util'; import { signingUtils } from './signing_utils'; import { SignedTransaction } from './types'; -const EIP712_ZEROEX_TRANSACTION_SCHEMA = { - name: 'ZeroExTransaction', - parameters: [ - { name: 'salt', type: 'uint256' }, - { name: 'signerAddress', type: 'address' }, - { name: 'data', type: 'bytes' }, - ], -}; - export class TransactionFactory { private readonly _signerBuff: Buffer; private readonly _exchangeAddress: string; @@ -33,30 +19,18 @@ export class TransactionFactory { const salt = generatePseudoRandomSalt(); const signerAddress = `0x${this._signerBuff.toString('hex')}`; const executeTransactionData = { - salt: salt.toString(), + salt, signerAddress, data, }; - 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 typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, this._exchangeAddress); 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/json-schemas/schemas/eip712_typed_data.ts b/packages/json-schemas/schemas/eip712_typed_data.ts index 4c4664878..371ff8a78 100644 --- a/packages/json-schemas/schemas/eip712_typed_data.ts +++ b/packages/json-schemas/schemas/eip712_typed_data.ts @@ -1,5 +1,5 @@ export const eip712TypedData = { - id: 'eip712TypedData', + id: '/eip712TypedData', type: 'object', properties: { types: { diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index a9d2fde8b..2555ad350 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -3,15 +3,15 @@ "version": "2.0.0", "changes": [ { - "note": "Added ecSignOrderAsync to first sign an order as EIP712 and fallback to EthSign", + "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", + "note": "Added `ecSignTypedDataOrderAsync` to sign an order exclusively as EIP712", "pr": 1102 }, { - "note": "Rename ecSignOrderHashAsync to ecSignHashAsync removing SignerType parameter", + "note": "Rename `ecSignOrderHashAsync` to `ecSignHashAsync` removing `SignerType` parameter", "pr": 1102 } ] diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index 5403606c3..7de20a696 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -13,16 +13,39 @@ export const constants = { BASE_16: 16, 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' }, - ], + EIP712_DOMAIN_NAME: '0x Protocol', + EIP712_DOMAIN_VERSION: '2', + EIP712_DOMAIN_SCHEMA: { + name: 'EIP712Domain', + parameters: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'verifyingContract', type: 'address' }, + ], + }, + EIP712_ORDER_SCHEMA: { + name: 'Order', + parameters: [ + { 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' }, + ], + }, + EIP712_ZEROEX_TRANSACTION_SCHEMA: { + name: 'ZeroExTransaction', + parameters: [ + { name: 'salt', type: 'uint256' }, + { name: 'signerAddress', type: 'address' }, + { name: 'data', type: 'bytes' }, + ], + }, }; diff --git a/packages/order-utils/src/eip712_utils.ts b/packages/order-utils/src/eip712_utils.ts new file mode 100644 index 000000000..43b421de7 --- /dev/null +++ b/packages/order-utils/src/eip712_utils.ts @@ -0,0 +1,75 @@ +import { EIP712Object, EIP712TypedData, EIP712Types, Order, ZeroExTransaction } from '@0xproject/types'; +import * as _ from 'lodash'; + +import { constants } from './constants'; + +export const eip712Utils = { + /** + * Creates a EIP712TypedData object specific to the 0x protocol for use with signTypedData. + * @param primaryType The primary type found in message + * @param types The additional types for the data in message + * @param message The contents of the message + * @param exchangeAddress The address of the exchange contract + * @return A typed data object + */ + createTypedData: ( + primaryType: string, + types: EIP712Types, + message: EIP712Object, + exchangeAddress: string, + ): EIP712TypedData => { + const typedData = { + types: { + EIP712Domain: constants.EIP712_DOMAIN_SCHEMA.parameters, + ...types, + }, + domain: { + name: constants.EIP712_DOMAIN_NAME, + version: constants.EIP712_DOMAIN_VERSION, + verifyingContract: exchangeAddress, + }, + message, + primaryType, + }; + return typedData; + }, + /** + * Creates an Order EIP712TypedData object for use with signTypedData. + * @param Order the order + * @return A typed data object + */ + createOrderTypedData: (order: Order): EIP712TypedData => { + const normalizedOrder = _.mapValues(order, value => { + return !_.isString(value) ? value.toString() : value; + }); + const typedData = eip712Utils.createTypedData( + constants.EIP712_ORDER_SCHEMA.name, + { Order: constants.EIP712_ORDER_SCHEMA.parameters }, + normalizedOrder, + order.exchangeAddress, + ); + return typedData; + }, + /** + * Creates an ExecuteTransaction EIP712TypedData object for use with signTypedData and + * 0x Exchange executeTransaction. + * @param ZeroExTransaction the 0x transaction + * @param exchangeAddress The address of the exchange contract + * @return A typed data object + */ + createZeroExTransactionTypedData: ( + zeroExTransaction: ZeroExTransaction, + exchangeAddress: string, + ): EIP712TypedData => { + const normalizedTransaction = _.mapValues(zeroExTransaction, value => { + return !_.isString(value) ? value.toString() : value; + }); + const typedData = eip712Utils.createTypedData( + constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.name, + { ZeroExTransaction: constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.parameters }, + normalizedTransaction, + exchangeAddress, + ); + return typedData; + }, +}; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 89a843d8f..2e05fdf2b 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -18,7 +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 { constants } from './constants'; +export { eip712Utils } from './eip712_utils'; export { Provider, JSONRPCRequestPayload, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types'; export { @@ -34,6 +35,12 @@ export { OrderStateValid, OrderStateInvalid, ExchangeContractErrs, + EIP712Parameter, + EIP712TypedData, + EIP712Types, + EIP712Object, + EIP712ObjectValue, + ZeroExTransaction, } from '@0xproject/types'; export { OrderError, diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts index 37b9da811..a6dd6688c 100644 --- a/packages/order-utils/src/order_hash.ts +++ b/packages/order-utils/src/order_hash.ts @@ -4,28 +4,10 @@ import { signTypedDataUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import { assert } from './assert'; -import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants'; +import { eip712Utils } from './eip712_utils'; const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string'; -export const EIP712_ORDER_SCHEMA = { - name: 'Order', - parameters: [ - { 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' }, - ], -}; - export const orderHashUtils = { /** * Checks if the supplied hex encoded order hash is valid. @@ -45,7 +27,7 @@ export const orderHashUtils = { /** * Computes the orderHash for a supplied order. * @param order An object that conforms to the Order or SignedOrder interface definitions. - * @return The resulting orderHash from hashing the supplied order. + * @return Hex encoded string orderHash from hashing the supplied order. */ getOrderHashHex(order: SignedOrder | Order): string { try { @@ -64,27 +46,12 @@ export const orderHashUtils = { return orderHashHex; }, /** - * Computes the orderHash for a supplied order and returns it as a Buffer + * Computes the orderHash for a supplied order * @param order An object that conforms to the Order or SignedOrder interface definitions. - * @return The resulting orderHash from hashing the supplied order as a Buffer + * @return A Buffer containing the resulting orderHash from hashing the supplied order */ getOrderHashBuffer(order: SignedOrder | Order): Buffer { - 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 typedData = eip712Utils.createOrderTypedData(order); const orderHashBuff = signTypedDataUtils.signTypedDataHash(typedData); return orderHashBuff; }, diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index 2d7fcfc9e..8c92b87dd 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -1,5 +1,5 @@ import { schemas } from '@0xproject/json-schemas'; -import { ECSignature, Order, SignatureType, ValidatorSignature } from '@0xproject/types'; +import { ECSignature, Order, SignatureType, SignedOrder, ValidatorSignature } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; @@ -7,11 +7,11 @@ import * as _ from 'lodash'; import { artifacts } from './artifacts'; import { assert } from './assert'; -import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants'; +import { eip712Utils } from './eip712_utils'; import { ExchangeContract } from './generated_contract_wrappers/exchange'; import { IValidatorContract } from './generated_contract_wrappers/i_validator'; import { IWalletContract } from './generated_contract_wrappers/i_wallet'; -import { EIP712_ORDER_SCHEMA, orderHashUtils } from './order_hash'; +import { orderHashUtils } from './order_hash'; import { OrderError } from './types'; import { utils } from './utils'; @@ -195,53 +195,45 @@ export const signatureUtils = { }, /** * Signs an order and returns its elliptic curve signature and signature type. First `eth_signTypedData` is requested - * then a fallback to `eth_sign` if not available on this provider. + * then a fallback to `eth_sign` if not available on the supplied provider. * @param order The Order to sign. * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address - * must be available via the Provider supplied to 0x.js. - * @return A hex encoded string containing the Elliptic curve signature generated by signing the orderHash and the Signature Type. + * must be available via the supplied Provider. + * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type. */ - async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<string> { + async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> { try { - const signatureHex = signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress); - return signatureHex; + const signedOrder = signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress); + return signedOrder; } catch (err) { // Fallback to using EthSign when ethSignTypedData is not supported const orderHash = orderHashUtils.getOrderHashHex(order); const signatureHex = await signatureUtils.ecSignHashAsync(provider, orderHash, signerAddress); - return signatureHex; + const signedOrder = { + ...order, + signature: signatureHex, + }; + return signedOrder; } }, /** * Signs an order using `eth_signTypedData` and returns its elliptic curve signature and signature type. * @param order The Order to sign. * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address - * must be available via the Provider supplied to 0x.js. - * @return A hex encoded string containing the Elliptic curve signature generated by signing the order with `eth_signTypedData` - * and the Signature Type. + * must be available via the supplied Provider. + * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type. */ - async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<string> { + async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> { assert.isWeb3Provider('provider', provider); assert.isETHAddressHex('signerAddress', signerAddress); const web3Wrapper = new Web3Wrapper(provider); await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); + // Detect if Metamask to transition users to the MetamaskSubprovider + if ((provider as any).isMetaMask) { + throw new Error('Unsupported Provider, please use MetamaskSubprovider.'); + } const normalizedSignerAddress = signerAddress.toLowerCase(); - 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: 'Order', - }; + const typedData = eip712Utils.createOrderTypedData(order); const signature = await web3Wrapper.signTypedDataAsync(normalizedSignerAddress, typedData); const ecSignatureRSV = parseSignatureHexAsRSV(signature); const signatureBuffer = Buffer.concat([ @@ -251,14 +243,16 @@ export const signatureUtils = { ethUtil.toBuffer(SignatureType.EIP712), ]); const signatureHex = `0x${signatureBuffer.toString('hex')}`; - return signatureHex; + return { + ...order, + signature: signatureHex, + }; }, /** * Signs a hash and returns its elliptic curve signature and signature type. - * This method currently supports TestRPC, Geth and Parity above and below V1.6.6 * @param msgHash Hex encoded message to sign. * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address - * must be available via the Provider supplied to 0x.js. + * must be available via the supplied Provider. * @return A hex encoded string containing the Elliptic curve signature generated by signing the msgHash and the Signature Type. */ async ecSignHashAsync(provider: Provider, msgHash: string, signerAddress: string): Promise<string> { @@ -268,6 +262,10 @@ export const signatureUtils = { const web3Wrapper = new Web3Wrapper(provider); await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); const normalizedSignerAddress = signerAddress.toLowerCase(); + // Detect if Metamask to transition users to the MetamaskSubprovider + if ((provider as any).isMetaMask) { + throw new Error('Unsupported Provider, please use MetamaskSubprovider.'); + } const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHash); const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(msgHash); diff --git a/packages/order-utils/test/eip712_utils_test.ts b/packages/order-utils/test/eip712_utils_test.ts new file mode 100644 index 000000000..dc76595db --- /dev/null +++ b/packages/order-utils/test/eip712_utils_test.ts @@ -0,0 +1,44 @@ +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { constants } from '../src/constants'; +import { eip712Utils } from '../src/eip712_utils'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('EIP712 Utils', () => { + describe('createTypedData', () => { + it('adds in the EIP712DomainSeparator', () => { + const primaryType = 'Test'; + const typedData = eip712Utils.createTypedData( + primaryType, + { Test: [{ name: 'testValue', type: 'uint256' }] }, + { testValue: '1' }, + constants.NULL_ADDRESS, + ); + expect(typedData.domain).to.not.be.undefined(); + expect(typedData.types.EIP712Domain).to.not.be.undefined(); + const domainObject = typedData.domain; + expect(domainObject.name).to.eq(constants.EIP712_DOMAIN_NAME); + expect(typedData.primaryType).to.eq(primaryType); + }); + }); + describe('createTypedData', () => { + it('adds in the EIP712DomainSeparator', () => { + const typedData = eip712Utils.createZeroExTransactionTypedData( + { + salt: new BigNumber('0'), + data: constants.NULL_BYTES, + signerAddress: constants.NULL_BYTES, + }, + constants.NULL_ADDRESS, + ); + expect(typedData.primaryType).to.eq(constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.name); + expect(typedData.types.EIP712Domain).to.not.be.undefined(); + }); + }); +}); diff --git a/packages/order-utils/test/order_hash_test.ts b/packages/order-utils/test/order_hash_test.ts index 3fdbbad21..fe44218d6 100644 --- a/packages/order-utils/test/order_hash_test.ts +++ b/packages/order-utils/test/order_hash_test.ts @@ -35,6 +35,20 @@ describe('Order hashing', () => { const orderHash = orderHashUtils.getOrderHashHex(order); expect(orderHash).to.be.equal(expectedOrderHash); }); + it('calculates the order hash if amounts are strings', async () => { + // It's common for developers using javascript to provide the amounts + // as strings. Since we eventually toString() the BigNumber + // before encoding we should result in the same orderHash in this scenario + // tslint:disable-next-line:no-unnecessary-type-assertion + const orderHash = orderHashUtils.getOrderHashHex({ + ...order, + makerAssetAmount: '0', + takerAssetAmount: '0', + makerFee: '0', + takerFee: '0', + } as any); + expect(orderHash).to.be.equal(expectedOrderHash); + }); it('throws a readable error message if taker format is invalid', async () => { const orderWithInvalidtakerFormat = { ...order, diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index 03354cd65..7f6987f6a 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -152,14 +152,14 @@ describe('Signature utils', () => { ethUtil.toBuffer(SignatureType.EIP712), ]); const signatureHex = `0x${signatureBuffer.toString('hex')}`; - const eip712Signature = await signatureUtils.ecSignOrderAsync(provider, order, makerAddress); + const signedOrder = await signatureUtils.ecSignOrderAsync(provider, order, makerAddress); const isValidSignature = await signatureUtils.isValidSignatureAsync( provider, orderHashHex, - eip712Signature, + signedOrder.signature, makerAddress, ); - expect(signatureHex).to.eq(eip712Signature); + expect(signatureHex).to.eq(signedOrder.signature); expect(isValidSignature).to.eq(true); }); }); @@ -238,6 +238,51 @@ describe('Signature utils', () => { expect(isValidSignature).to.be.true(); }); }); + describe('#ecSignTypedDataOrderAsync', () => { + let makerAddress: string; + const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'; + let order: Order; + before(async () => { + const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); + makerAddress = availableAddreses[0]; + order = { + makerAddress, + takerAddress: constants.NULL_ADDRESS, + senderAddress: constants.NULL_ADDRESS, + feeRecipientAddress: constants.NULL_ADDRESS, + makerAssetData: constants.NULL_ADDRESS, + takerAssetData: constants.NULL_ADDRESS, + exchangeAddress: fakeExchangeContractAddress, + salt: new BigNumber(0), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + makerAssetAmount: new BigNumber(0), + takerAssetAmount: new BigNumber(0), + expirationTimeSeconds: new BigNumber(0), + }; + }); + it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => { + const expectedSignature = + '0x1cd472c439833774b55d248c31b6585f21aea1b9363ebb4ec58549e46b62eb5a6f696f5781f62de008ee7f77650ef940d99c97ec1dee67b3f5cea1bbfdfeb2eba602'; + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> { + if (payload.method === 'eth_signTypedData') { + const [address, typedData] = payload.params; + const signature = await web3Wrapper.signTypedDataAsync(address, typedData); + callback(null, { + id: 42, + jsonrpc: '2.0', + result: signature, + }); + } else { + callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] }); + } + }, + }; + const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(fakeProvider, order, makerAddress); + expect(signedOrder.signature).to.equal(expectedSignature); + }); + }); describe('#convertECSignatureToSignatureHex', () => { const ecSignature: ECSignature = { v: 27, diff --git a/packages/subproviders/CHANGELOG.json b/packages/subproviders/CHANGELOG.json index 6a6f7848b..387207b01 100644 --- a/packages/subproviders/CHANGELOG.json +++ b/packages/subproviders/CHANGELOG.json @@ -3,11 +3,11 @@ "version": "2.1.0", "changes": [ { - "note": "Add Metamask Subprovider to handle inconsistent JSON RPC behaviour", + "note": "Add `MetamaskSubprovider` to handle inconsistent JSON RPC behaviour", "pr": 1102 }, { - "note": "Add support for eth_signTypedData in Mnemonic, Private and EthLightWallet", + "note": "Add support for `eth_signTypedData` in wallets Mnemonic, Private and EthLightWallet", "pr": 1102 } ] diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts index 8b5446007..6a8100e68 100644 --- a/packages/subproviders/src/index.ts +++ b/packages/subproviders/src/index.ts @@ -48,6 +48,13 @@ export { LedgerGetAddressResult, } from './types'; -export { ECSignature } from '@0xproject/types'; +export { + ECSignature, + EIP712Object, + EIP712ObjectValue, + EIP712TypedData, + EIP712Types, + EIP712Parameter, +} from '@0xproject/types'; export { JSONRPCRequestPayload, Provider, JSONRPCResponsePayload, JSONRPCErrorCallback } from 'ethereum-types'; diff --git a/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts b/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts index e3afeff1b..a1d93ac49 100644 --- a/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts +++ b/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts @@ -1,3 +1,4 @@ +import { EIP712TypedData } from '@0xproject/types'; import * as lightwallet from 'eth-lightwallet'; import { PartialTxParams } from '../types'; @@ -48,16 +49,16 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider { // Lightwallet loses the chain id information when hex encoding the transaction // this results in a different signature on certain networks. PrivateKeyWallet // respects this as it uses the parameters passed in - let privKey = this._keystore.exportPrivateKey(txParams.from, this._pwDerivedKey); - const privKeyWallet = new PrivateKeyWalletSubprovider(privKey); - privKey = ''; - const privKeySignature = await privKeyWallet.signTransactionAsync(txParams); - return privKeySignature; + let privateKey = this._keystore.exportPrivateKey(txParams.from, this._pwDerivedKey); + const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey); + privateKey = ''; + const privateKeySignature = await privateKeyWallet.signTransactionAsync(txParams); + return privateKeySignature; } /** * Sign a personal Ethereum signed message. The signing account will be the account * associated with the provided address. - * If you've added the this Subprovider to your app's provider, you can simply send an `eth_sign` + * If you've added 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 @@ -65,10 +66,10 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider { * @return Signature hex string (order: rsv) */ public async signPersonalMessageAsync(data: string, address: string): Promise<string> { - let privKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey); - const privKeyWallet = new PrivateKeyWalletSubprovider(privKey); - privKey = ''; - const result = privKeyWallet.signPersonalMessageAsync(data, address); + let privateKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey); + const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey); + privateKey = ''; + const result = privateKeyWallet.signPersonalMessageAsync(data, address); return result; } /** @@ -80,11 +81,11 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider { * @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); + public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> { + let privateKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey); + const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey); + privateKey = ''; + const result = privateKeyWallet.signTypedDataAsync(address, typedData); return result; } } diff --git a/packages/subproviders/src/subproviders/metamask_subprovider.ts b/packages/subproviders/src/subproviders/metamask_subprovider.ts index 724edd574..46fc2a9cd 100644 --- a/packages/subproviders/src/subproviders/metamask_subprovider.ts +++ b/packages/subproviders/src/subproviders/metamask_subprovider.ts @@ -18,7 +18,7 @@ export class MetamaskSubprovider extends Subprovider { private readonly _web3Wrapper: Web3Wrapper; private readonly _provider: Provider; /** - * Instantiates a new SignerSubprovider + * Instantiates a new MetamaskSubprovider * @param provider Web3 provider that should handle all user account related requests */ constructor(provider: Provider) { @@ -83,7 +83,9 @@ export class MetamaskSubprovider extends Subprovider { case 'eth_signTypedData_v3': [address, message] = payload.params; try { - // Metamask has namespaced signTypedData to v3 for an indeterminate period of time. + // Metamask supports multiple versions and has namespaced signTypedData to v3 for an indeterminate period of time. + // eth_signTypedData is mapped to an older implementation before the spec was finalized. + // Source: https://github.com/MetaMask/metamask-extension/blob/c49d854b55b3efd34c7fd0414b76f7feaa2eec7c/app/scripts/metamask-controller.js#L1262 // and expects message to be serialised as JSON const messageJSON = JSON.stringify(message); const signature = await this._web3Wrapper.sendRawPayloadAsync<string>({ diff --git a/packages/subproviders/src/subproviders/mnemonic_wallet.ts b/packages/subproviders/src/subproviders/mnemonic_wallet.ts index de99b632a..04a11c7be 100644 --- a/packages/subproviders/src/subproviders/mnemonic_wallet.ts +++ b/packages/subproviders/src/subproviders/mnemonic_wallet.ts @@ -1,4 +1,5 @@ import { assert } from '@0xproject/assert'; +import { EIP712TypedData } from '@0xproject/types'; import { addressUtils } from '@0xproject/utils'; import * as bip39 from 'bip39'; import HDNode = require('hdkey'); @@ -90,10 +91,10 @@ export class MnemonicWalletSubprovider 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` - * 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. + * associated with the provided address. If you've added the MnemonicWalletSubprovider 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 * @param address Address of the account to sign with * @return Signature hex string (order: rsv) @@ -109,16 +110,16 @@ export class MnemonicWalletSubprovider extends BaseWalletSubprovider { 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. + * 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> { + public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> { if (_.isUndefined(typedData)) { throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage); } diff --git a/packages/subproviders/src/subproviders/private_key_wallet.ts b/packages/subproviders/src/subproviders/private_key_wallet.ts index 51409077d..96e4190ed 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 { EIP712TypedData } from '@0xproject/types'; import { signTypedDataUtils } from '@0xproject/utils'; import EthereumTx = require('ethereumjs-tx'); import * as ethUtil from 'ethereumjs-util'; @@ -95,7 +96,7 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider { * @param data the typed data object * @return Signature hex string (order: rsv) */ - public async signTypedDataAsync(address: string, typedData: any): Promise<string> { + public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> { if (_.isUndefined(typedData)) { throw new Error(WalletSubproviderErrors.DataMissingForSignTypedData); } diff --git a/packages/subproviders/src/subproviders/signer.ts b/packages/subproviders/src/subproviders/signer.ts index 6b519865f..eda7db42e 100644 --- a/packages/subproviders/src/subproviders/signer.ts +++ b/packages/subproviders/src/subproviders/signer.ts @@ -14,7 +14,7 @@ import { Subprovider } from './subprovider'; export class SignerSubprovider extends Subprovider { private readonly _web3Wrapper: Web3Wrapper; /** - * Instantiates a new SignerSubprovider + * Instantiates a new SignerSubprovider. * @param provider Web3 provider that should handle all user account related requests */ constructor(provider: Provider) { diff --git a/packages/subproviders/test/unit/eth_lightwallet_subprovider_test.ts b/packages/subproviders/test/unit/eth_lightwallet_subprovider_test.ts index 063817a95..49698ce9e 100644 --- a/packages/subproviders/test/unit/eth_lightwallet_subprovider_test.ts +++ b/packages/subproviders/test/unit/eth_lightwallet_subprovider_test.ts @@ -73,6 +73,13 @@ describe('EthLightwalletSubprovider', () => { const txHex = await ethLightwalletSubprovider.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 ethLightwalletSubprovider.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', () => { @@ -129,6 +136,20 @@ describe('EthLightwalletSubprovider', () => { }); 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/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 65dd75101..53e1f3716 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Added `EIP712Parameter` `EIP712Types` `EIP712TypedData` for EIP712 signing", "pr": 1102 + }, + { + "note": "Added `ZeroExTransaction` type for Exchange executeTransaction", + "pr": 1102 } ] }, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d57bdfb6f..6bc966ba1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -42,6 +42,15 @@ export interface SignedOrder extends Order { } /** + * ZeroExTransaction for use with 0x Exchange executeTransaction + */ +export interface ZeroExTransaction { + salt: BigNumber; + signerAddress: string; + data: string; +} + +/** * Elliptic Curve signature */ export interface ECSignature { @@ -598,9 +607,16 @@ export interface EIP712Parameter { export interface EIP712Types { [key: string]: EIP712Parameter[]; } + +export type EIP712ObjectValue = string | number | EIP712Object; + +export interface EIP712Object { + [key: string]: EIP712ObjectValue; +} + export interface EIP712TypedData { types: EIP712Types; - domain: any; - message: any; + domain: EIP712Object; + message: EIP712Object; primaryType: string; } diff --git a/packages/utils/src/sign_typed_data_utils.ts b/packages/utils/src/sign_typed_data_utils.ts index b72fd099b..657a59ed0 100644 --- a/packages/utils/src/sign_typed_data_utils.ts +++ b/packages/utils/src/sign_typed_data_utils.ts @@ -1,7 +1,8 @@ import * as ethUtil from 'ethereumjs-util'; import * as ethers from 'ethers'; +import * as _ from 'lodash'; -import { EIP712TypedData, EIP712Types } from '@0xproject/types'; +import { EIP712Object, EIP712ObjectValue, EIP712TypedData, EIP712Types } from '@0xproject/types'; export const signTypedDataUtils = { /** @@ -42,32 +43,40 @@ export const signTypedDataUtils = { } return result; }, - _encodeData(primaryType: string, data: any, types: EIP712Types): string { + _encodeData(primaryType: string, data: EIP712Object, types: EIP712Types): string { const encodedTypes = ['bytes32']; - const encodedValues = [signTypedDataUtils._typeHash(primaryType, types)]; + const encodedValues: Array<Buffer | EIP712ObjectValue> = [signTypedDataUtils._typeHash(primaryType, types)]; for (const field of types[primaryType]) { - let value = data[field.name]; + const value = data[field.name]; if (field.type === 'string' || field.type === 'bytes') { - value = ethUtil.sha3(value); + const hashValue = ethUtil.sha3(value as string); encodedTypes.push('bytes32'); - encodedValues.push(value); + encodedValues.push(hashValue); } else if (types[field.type] !== undefined) { encodedTypes.push('bytes32'); - value = ethUtil.sha3(signTypedDataUtils._encodeData(field.type, value, types)); - encodedValues.push(value); + const hashValue = ethUtil.sha3( + // tslint:disable-next-line:no-unnecessary-type-assertion + signTypedDataUtils._encodeData(field.type, value as EIP712Object, types), + ); + encodedValues.push(hashValue); } 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); + const normalizedValue = signTypedDataUtils._normalizeValue(field.type, value); + encodedValues.push(normalizedValue); } } return ethers.utils.defaultAbiCoder.encode(encodedTypes, encodedValues); }, + _normalizeValue(type: string, value: any): EIP712ObjectValue { + const normalizedValue = type === 'uint256' && _.isObject(value) && value.isBigNumber ? value.toString() : value; + return normalizedValue; + }, _typeHash(primaryType: string, types: EIP712Types): Buffer { return ethUtil.sha3(signTypedDataUtils._encodeType(primaryType, types)); }, - _structHash(primaryType: string, data: any, types: EIP712Types): Buffer { + _structHash(primaryType: string, data: EIP712Object, types: EIP712Types): Buffer { return ethUtil.sha3(signTypedDataUtils._encodeData(primaryType, data, types)); }, }; diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts index df6879a48..034bafb5f 100644 --- a/packages/web3-wrapper/src/web3_wrapper.ts +++ b/packages/web3-wrapper/src/web3_wrapper.ts @@ -318,9 +318,9 @@ export class Web3Wrapper { * Sign an EIP712 typed data message with a specific address's private key (`eth_signTypedData`) * @param address Address of signer * @param typedData Typed data message to sign - * @returns Signature string (might be VRS or RSV depending on the Signer) + * @returns Signature string (as RSV) */ - public async signTypedDataAsync(address: string, typedData: object): Promise<string> { + public async signTypedDataAsync(address: string, typedData: any): Promise<string> { assert.isETHAddressHex('address', address); assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedData); const signData = await this.sendRawPayloadAsync<string>({ |