From 9e8031d5e3cf94cabe07685be510397367e90413 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 9 Oct 2018 18:26:13 +1100 Subject: Throw and handle errors from Providers. In web3 wrapper when a response contains an error field we throw this rather than return response.result which is often undefined. In Signature Utils we handle the error thrown when a user rejects the signing dialogue to prevent double signing. Exposed the ZeroExTransaction JSON schema. In Website only use the MetamaskSubprovider if we can detect the provider is Metamask --- packages/0x.js/CHANGELOG.json | 3 +- packages/0x.js/src/index.ts | 1 + .../src/utils/transaction_encoder.ts | 2 +- .../contracts/test/utils/transaction_factory.ts | 2 +- packages/ethereum-types/CHANGELOG.json | 9 ++ packages/ethereum-types/src/index.ts | 6 + packages/json-schemas/schemas/eip712_typed_data.ts | 2 +- .../schemas/zero_ex_transaction_schema.ts | 10 ++ packages/json-schemas/src/schemas.ts | 6 +- packages/order-utils/src/eip712_utils.ts | 8 ++ packages/order-utils/src/order_hash.ts | 2 +- packages/order-utils/src/signature_utils.ts | 26 +++- packages/order-utils/test/eip712_utils_test.ts | 2 +- packages/order-utils/test/signature_utils_test.ts | 149 ++++++++++++--------- packages/subproviders/src/index.ts | 8 +- .../src/subproviders/private_key_wallet.ts | 2 +- packages/utils/src/sign_typed_data_utils.ts | 6 +- packages/utils/test/sign_typed_data_utils_test.ts | 4 +- packages/web3-wrapper/CHANGELOG.json | 14 ++ packages/web3-wrapper/src/web3_wrapper.ts | 5 +- packages/web3-wrapper/test/web3_wrapper_test.ts | 15 ++- packages/website/ts/blockchain.ts | 12 +- 22 files changed, 205 insertions(+), 89 deletions(-) create mode 100644 packages/json-schemas/schemas/zero_ex_transaction_schema.ts (limited to 'packages') diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json index 3a184382c..6dfcc3d33 100644 --- a/packages/0x.js/CHANGELOG.json +++ b/packages/0x.js/CHANGELOG.json @@ -7,7 +7,8 @@ "pr": 1102 }, { - "note": "Added `MetamaskSubprovider` to handle inconsistencies with signing.", + "note": + "Added `MetamaskSubprovider` to handle inconsistencies in Metamask's signing JSON RPC endpoints.", "pr": 1102 }, { diff --git a/packages/0x.js/src/index.ts b/packages/0x.js/src/index.ts index 228bcecdb..6eb1fd8ee 100644 --- a/packages/0x.js/src/index.ts +++ b/packages/0x.js/src/index.ts @@ -90,6 +90,7 @@ export { JSONRPCRequestPayload, JSONRPCResponsePayload, JSONRPCErrorCallback, + JSONRPCResponseError, LogEntry, DecodedLogArgs, LogEntryEvent, diff --git a/packages/contract-wrappers/src/utils/transaction_encoder.ts b/packages/contract-wrappers/src/utils/transaction_encoder.ts index d9735778e..33086944b 100644 --- a/packages/contract-wrappers/src/utils/transaction_encoder.ts +++ b/packages/contract-wrappers/src/utils/transaction_encoder.ts @@ -33,7 +33,7 @@ export class TransactionEncoder { data, }; const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, exchangeAddress); - const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData); + const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(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 6e4cc4b2f..9ed4c5a31 100644 --- a/packages/contracts/test/utils/transaction_factory.ts +++ b/packages/contracts/test/utils/transaction_factory.ts @@ -25,7 +25,7 @@ export class TransactionFactory { }; const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, this._exchangeAddress); - const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData); + const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData); const signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType); const signedTx = { exchangeAddress: this._exchangeAddress, diff --git a/packages/ethereum-types/CHANGELOG.json b/packages/ethereum-types/CHANGELOG.json index e955f4d04..60fb8c806 100644 --- a/packages/ethereum-types/CHANGELOG.json +++ b/packages/ethereum-types/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.1.0", + "changes": [ + { + "note": "Add `JSONRPCResponseError` and error field on `JSONRPCResponsePayload`.", + "pr": 1102 + } + ] + }, { "timestamp": 1538693146, "version": "1.0.11", diff --git a/packages/ethereum-types/src/index.ts b/packages/ethereum-types/src/index.ts index 7e8b9de3e..ddd472010 100644 --- a/packages/ethereum-types/src/index.ts +++ b/packages/ethereum-types/src/index.ts @@ -113,10 +113,16 @@ export interface JSONRPCRequestPayload { jsonrpc: string; } +export interface JSONRPCResponseError { + message: string; + code: number; +} + export interface JSONRPCResponsePayload { result: any; id: number; jsonrpc: string; + error?: JSONRPCResponseError; } export interface AbstractBlock { diff --git a/packages/json-schemas/schemas/eip712_typed_data.ts b/packages/json-schemas/schemas/eip712_typed_data.ts index 371ff8a78..31ad74610 100644 --- a/packages/json-schemas/schemas/eip712_typed_data.ts +++ b/packages/json-schemas/schemas/eip712_typed_data.ts @@ -1,4 +1,4 @@ -export const eip712TypedData = { +export const eip712TypedDataSchema = { id: '/eip712TypedData', type: 'object', properties: { diff --git a/packages/json-schemas/schemas/zero_ex_transaction_schema.ts b/packages/json-schemas/schemas/zero_ex_transaction_schema.ts new file mode 100644 index 000000000..7f729b724 --- /dev/null +++ b/packages/json-schemas/schemas/zero_ex_transaction_schema.ts @@ -0,0 +1,10 @@ +export const zeroExTransactionSchema = { + id: '/zeroExTransactionSchema', + properties: { + data: { $ref: '/hexSchema' }, + signerAddress: { $ref: '/addressSchema' }, + salt: { $ref: '/numberSchema' }, + }, + required: ['data', 'salt', 'signerAddress'], + type: 'object', +}; diff --git a/packages/json-schemas/src/schemas.ts b/packages/json-schemas/src/schemas.ts index 50dc8d091..4eb96092d 100644 --- a/packages/json-schemas/src/schemas.ts +++ b/packages/json-schemas/src/schemas.ts @@ -2,7 +2,7 @@ import { addressSchema, hexSchema, numberSchema } from '../schemas/basic_type_sc import { blockParamSchema, blockRangeSchema } from '../schemas/block_range_schema'; import { callDataSchema } from '../schemas/call_data_schema'; import { ecSignatureParameterSchema, ecSignatureSchema } from '../schemas/ec_signature_schema'; -import { eip712TypedData } from '../schemas/eip712_typed_data'; +import { eip712TypedDataSchema } from '../schemas/eip712_typed_data'; import { indexFilterValuesSchema } from '../schemas/index_filter_values_schema'; import { orderCancellationRequestsSchema } from '../schemas/order_cancel_schema'; import { orderFillOrKillRequestsSchema } from '../schemas/order_fill_or_kill_requests_schema'; @@ -32,6 +32,7 @@ import { relayerApiOrdersSchema } from '../schemas/relayer_api_orders_schema'; import { signedOrdersSchema } from '../schemas/signed_orders_schema'; import { tokenSchema } from '../schemas/token_schema'; import { jsNumber, txDataSchema } from '../schemas/tx_data_schema'; +import { zeroExTransactionSchema } from '../schemas/zero_ex_transaction_schema'; export const schemas = { numberSchema, @@ -40,7 +41,7 @@ export const schemas = { hexSchema, ecSignatureParameterSchema, ecSignatureSchema, - eip712TypedData, + eip712TypedDataSchema, indexFilterValuesSchema, orderCancellationRequestsSchema, orderFillOrKillRequestsSchema, @@ -70,4 +71,5 @@ export const schemas = { relayerApiOrdersChannelUpdateSchema, relayerApiOrdersResponseSchema, relayerApiAssetDataPairsSchema, + zeroExTransactionSchema, }; diff --git a/packages/order-utils/src/eip712_utils.ts b/packages/order-utils/src/eip712_utils.ts index 43b421de7..56f736500 100644 --- a/packages/order-utils/src/eip712_utils.ts +++ b/packages/order-utils/src/eip712_utils.ts @@ -1,3 +1,5 @@ +import { assert } from '@0xproject/assert'; +import { schemas } from '@0xproject/json-schemas'; import { EIP712Object, EIP712TypedData, EIP712Types, Order, ZeroExTransaction } from '@0xproject/types'; import * as _ from 'lodash'; @@ -18,6 +20,8 @@ export const eip712Utils = { message: EIP712Object, exchangeAddress: string, ): EIP712TypedData => { + assert.isETHAddressHex('exchangeAddress', exchangeAddress); + assert.isString('primaryType', primaryType); const typedData = { types: { EIP712Domain: constants.EIP712_DOMAIN_SCHEMA.parameters, @@ -31,6 +35,7 @@ export const eip712Utils = { message, primaryType, }; + assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema); return typedData; }, /** @@ -39,6 +44,7 @@ export const eip712Utils = { * @return A typed data object */ createOrderTypedData: (order: Order): EIP712TypedData => { + assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]); const normalizedOrder = _.mapValues(order, value => { return !_.isString(value) ? value.toString() : value; }); @@ -61,6 +67,8 @@ export const eip712Utils = { zeroExTransaction: ZeroExTransaction, exchangeAddress: string, ): EIP712TypedData => { + assert.isETHAddressHex('exchangeAddress', exchangeAddress); + assert.doesConformToSchema('zeroExTransaction', zeroExTransaction, schemas.zeroExTransactionSchema); const normalizedTransaction = _.mapValues(zeroExTransaction, value => { return !_.isString(value) ? value.toString() : value; }); diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts index a6dd6688c..b523a3523 100644 --- a/packages/order-utils/src/order_hash.ts +++ b/packages/order-utils/src/order_hash.ts @@ -52,7 +52,7 @@ export const orderHashUtils = { */ getOrderHashBuffer(order: SignedOrder | Order): Buffer { const typedData = eip712Utils.createOrderTypedData(order); - const orderHashBuff = signTypedDataUtils.signTypedDataHash(typedData); + const orderHashBuff = signTypedDataUtils.generateTypedDataHash(typedData); return orderHashBuff; }, }; diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index e518b29a0..2605ccd32 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -194,7 +194,7 @@ export const signatureUtils = { } }, /** - * Signs an order and returns its elliptic curve signature and signature type. First `eth_signTypedData` is requested + * Signs an order and returns a SignedOrder. First `eth_signTypedData` is requested * 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 @@ -202,11 +202,18 @@ export const signatureUtils = { * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type. */ async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise { + assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]); try { - const signedOrder = signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress); + const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress); return signedOrder; } catch (err) { - // Fallback to using EthSign when ethSignTypedData is not supported + // HACK: We are unable to handle specific errors thrown since provider is not an object + // under our control. It could be Metamask Web3, Ethers, or any general RPC provider. + // We check for a user denying the signature request in a way that supports Metamask and + // Coinbase Wallet + if (err.message.includes('User denied message signature')) { + throw err; + } const orderHash = orderHashUtils.getOrderHashHex(order); const signatureHex = await signatureUtils.ecSignHashAsync(provider, orderHash, signerAddress); const signedOrder = { @@ -217,7 +224,7 @@ export const signatureUtils = { } }, /** - * Signs an order using `eth_signTypedData` and returns its elliptic curve signature and signature type. + * Signs an order using `eth_signTypedData` and returns a SignedOrder. * @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 supplied Provider. @@ -226,6 +233,7 @@ export const signatureUtils = { async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise { assert.isWeb3Provider('provider', provider); assert.isETHAddressHex('signerAddress', signerAddress); + assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]); const web3Wrapper = new Web3Wrapper(provider); await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); const normalizedSignerAddress = signerAddress.toLowerCase(); @@ -247,14 +255,16 @@ export const signatureUtils = { } catch (err) { // Detect if Metamask to transition users to the MetamaskSubprovider if ((provider as any).isMetaMask) { - throw new Error(`Unsupported Provider, please use MetamaskSubprovider: ${err.message}`); + throw new Error( + `MetaMask provider must be wrapped in a MetamaskSubprovider (from the '@0xproject/subproviders' package) in order to work with this method.`, + ); } else { throw err; } } }, /** - * Signs a hash and returns its elliptic curve signature and signature type. + * Signs a hash using `eth_sign` and returns its elliptic curve signature and signature type. * @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 supplied Provider. @@ -303,7 +313,9 @@ export const signatureUtils = { } // Detect if Metamask to transition users to the MetamaskSubprovider if ((provider as any).isMetaMask) { - throw new Error('Unsupported Provider, please use MetamaskSubprovider.'); + throw new Error( + `MetaMask provider must be wrapped in a MetamaskSubprovider (from the '@0xproject/subproviders' package) in order to work with this method.`, + ); } else { throw new Error(OrderError.InvalidSignature); } diff --git a/packages/order-utils/test/eip712_utils_test.ts b/packages/order-utils/test/eip712_utils_test.ts index dc76595db..d65cabe9c 100644 --- a/packages/order-utils/test/eip712_utils_test.ts +++ b/packages/order-utils/test/eip712_utils_test.ts @@ -33,7 +33,7 @@ describe('EIP712 Utils', () => { { salt: new BigNumber('0'), data: constants.NULL_BYTES, - signerAddress: constants.NULL_BYTES, + signerAddress: constants.NULL_ADDRESS, }, constants.NULL_ADDRESS, ); diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index 7f6987f6a..f2d6790fb 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -17,6 +17,28 @@ chaiSetup.configure(); const expect = chai.expect; describe('Signature utils', () => { + 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), + }; + }); describe('#isValidSignatureAsync', () => { let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; const ethSignSignature = @@ -117,54 +139,54 @@ describe('Signature utils', () => { }); }); describe('#ecSignOrderAsync', () => { - 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 default to eth_sign if eth_signTypedData is unavailable', async () => { + const expectedSignature = + '0x1c3582f06356a1314dbf1c0e534c4d8e92e59b056ee607a7ff5a825f5f2cc5e6151c5cc7fdd420f5608e4d5bef108e42ad90c7a4b408caef32e24374cf387b0d7603'; + + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise { + if (payload.method === 'eth_signTypedData') { + callback(new Error('Internal RPC Error')); + } else if (payload.method === 'eth_sign') { + const [address, message] = payload.params; + const signature = await web3Wrapper.signMessageAsync(address, message); + callback(null, { + id: 42, + jsonrpc: '2.0', + result: signature, + }); + } else { + callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] }); + } + }, }; + const signedOrder = await signatureUtils.ecSignOrderAsync(fakeProvider, order, makerAddress); + expect(signedOrder.signature).to.equal(expectedSignature); }); - it('should result in the same signature as signing order hash without prefix', async () => { - const orderHashHex = orderHashUtils.getOrderHashHex(order); - const sig = ethUtil.ecsign( - ethUtil.toBuffer(orderHashHex), - Buffer.from('F2F48EE19680706196E2E339E5DA3491186E0C4C5030670656B0E0164837257D', 'hex'), - ); - const signatureBuffer = Buffer.concat([ - ethUtil.toBuffer(sig.v), - ethUtil.toBuffer(sig.r), - ethUtil.toBuffer(sig.s), - ethUtil.toBuffer(SignatureType.EIP712), - ]); - const signatureHex = `0x${signatureBuffer.toString('hex')}`; - const signedOrder = await signatureUtils.ecSignOrderAsync(provider, order, makerAddress); - const isValidSignature = await signatureUtils.isValidSignatureAsync( - provider, - orderHashHex, - signedOrder.signature, - makerAddress, + it('should throw if the user denies the signing request', async () => { + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise { + if (payload.method === 'eth_signTypedData') { + callback(new Error('User denied message signature')); + } else if (payload.method === 'eth_sign') { + const [address, message] = payload.params; + const signature = await web3Wrapper.signMessageAsync(address, message); + callback(null, { + id: 42, + jsonrpc: '2.0', + result: signature, + }); + } else { + callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] }); + } + }, + }; + expect(signatureUtils.ecSignOrderAsync(fakeProvider, order, makerAddress)).to.to.be.rejectedWith( + 'User denied message signature', ); - expect(signatureHex).to.eq(signedOrder.signature); - expect(isValidSignature).to.eq(true); }); }); describe('#ecSignHashAsync', () => { - let makerAddress: string; before(async () => { const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); makerAddress = availableAddreses[0]; @@ -239,27 +261,30 @@ describe('Signature utils', () => { }); }); describe('#ecSignTypedDataOrderAsync', () => { - let makerAddress: string; - const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'; - let order: Order; - before(async () => { - const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); - makerAddress = availableAddreses[0]; - order = { + it('should result in the same signature as signing the order hash without an ethereum message prefix', async () => { + // Note: Since order hash is an EIP712 hash the result of a valid EIP712 signature + // of order hash is the same as signing the order without the Ethereum Message prefix. + const orderHashHex = orderHashUtils.getOrderHashHex(order); + const sig = ethUtil.ecsign( + ethUtil.toBuffer(orderHashHex), + Buffer.from('F2F48EE19680706196E2E339E5DA3491186E0C4C5030670656B0E0164837257D', 'hex'), + ); + const signatureBuffer = Buffer.concat([ + ethUtil.toBuffer(sig.v), + ethUtil.toBuffer(sig.r), + ethUtil.toBuffer(sig.s), + ethUtil.toBuffer(SignatureType.EIP712), + ]); + const signatureHex = `0x${signatureBuffer.toString('hex')}`; + const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, makerAddress); + const isValidSignature = await signatureUtils.isValidSignatureAsync( + provider, + orderHashHex, + signedOrder.signature, 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), - }; + ); + expect(signatureHex).to.eq(signedOrder.signature); + expect(isValidSignature).to.eq(true); }); it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => { const expectedSignature = diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts index 6a8100e68..9f4dac58b 100644 --- a/packages/subproviders/src/index.ts +++ b/packages/subproviders/src/index.ts @@ -57,4 +57,10 @@ export { EIP712Parameter, } from '@0xproject/types'; -export { JSONRPCRequestPayload, Provider, JSONRPCResponsePayload, JSONRPCErrorCallback } from 'ethereum-types'; +export { + JSONRPCRequestPayload, + Provider, + JSONRPCResponsePayload, + JSONRPCErrorCallback, + JSONRPCResponseError, +} from 'ethereum-types'; diff --git a/packages/subproviders/src/subproviders/private_key_wallet.ts b/packages/subproviders/src/subproviders/private_key_wallet.ts index 96e4190ed..e89c4c186 100644 --- a/packages/subproviders/src/subproviders/private_key_wallet.ts +++ b/packages/subproviders/src/subproviders/private_key_wallet.ts @@ -106,7 +106,7 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider { `Requested to sign message with address: ${address}, instantiated with address: ${this._address}`, ); } - const dataBuff = signTypedDataUtils.signTypedDataHash(typedData); + const dataBuff = signTypedDataUtils.generateTypedDataHash(typedData); const sig = ethUtil.ecsign(dataBuff, this._privateKeyBuffer); const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s); return rpcSig; diff --git a/packages/utils/src/sign_typed_data_utils.ts b/packages/utils/src/sign_typed_data_utils.ts index 657a59ed0..cd5bcb42f 100644 --- a/packages/utils/src/sign_typed_data_utils.ts +++ b/packages/utils/src/sign_typed_data_utils.ts @@ -6,11 +6,11 @@ import { EIP712Object, EIP712ObjectValue, EIP712TypedData, EIP712Types } from '@ export const signTypedDataUtils = { /** - * Computes the Sign Typed Data hash + * Generates the EIP712 Typed Data hash for signing * @param typedData An object that conforms to the EIP712TypedData interface - * @return A Buffer containing the hash of the sign typed data. + * @return A Buffer containing the hash of the typed data. */ - signTypedDataHash(typedData: EIP712TypedData): Buffer { + generateTypedDataHash(typedData: EIP712TypedData): Buffer { return ethUtil.sha3( Buffer.concat([ Buffer.from('1901', 'hex'), diff --git a/packages/utils/test/sign_typed_data_utils_test.ts b/packages/utils/test/sign_typed_data_utils_test.ts index e1cb4f6e1..dcba08b04 100644 --- a/packages/utils/test/sign_typed_data_utils_test.ts +++ b/packages/utils/test/sign_typed_data_utils_test.ts @@ -127,12 +127,12 @@ describe('signTypedDataUtils', () => { primaryType: 'Order', }; it('creates a hash of the test sign typed data', () => { - const hash = signTypedDataUtils.signTypedDataHash(simpleSignTypedData).toString('hex'); + const hash = signTypedDataUtils.generateTypedDataHash(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 hash = signTypedDataUtils.generateTypedDataHash(orderSignTypedData).toString('hex'); const hashHex = `0x${hash}`; expect(hashHex).to.be.eq(orderSignTypedDataHashHex); }); diff --git a/packages/web3-wrapper/CHANGELOG.json b/packages/web3-wrapper/CHANGELOG.json index 47f054300..be5c1fef6 100644 --- a/packages/web3-wrapper/CHANGELOG.json +++ b/packages/web3-wrapper/CHANGELOG.json @@ -1,4 +1,18 @@ [ + { + "version": "3.1.0", + "changes": [ + { + "note": "Add `signTypedData` to perform EIP712 `eth_signTypedData`.", + "pr": 1102 + }, + { + "note": + "Web3Wrapper now throws when an RPC request contains an error field in the response. Previously errors could be swallowed and undefined returned.", + "pr": 1102 + } + ] + }, { "version": "3.0.3", "changes": [ diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts index 034bafb5f..726246f1a 100644 --- a/packages/web3-wrapper/src/web3_wrapper.ts +++ b/packages/web3-wrapper/src/web3_wrapper.ts @@ -322,7 +322,7 @@ export class Web3Wrapper { */ public async signTypedDataAsync(address: string, typedData: any): Promise { assert.isETHAddressHex('address', address); - assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedData); + assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema); const signData = await this.sendRawPayloadAsync({ method: 'eth_signTypedData', params: [address, typedData], @@ -669,6 +669,9 @@ export class Web3Wrapper { ...payload, }; const response = await promisify(sendAsync)(payloadWithDefaults); + if (response.error) { + throw new Error(response.error.message); + } const result = response.result; return result; } diff --git a/packages/web3-wrapper/test/web3_wrapper_test.ts b/packages/web3-wrapper/test/web3_wrapper_test.ts index 385c469bf..164253777 100644 --- a/packages/web3-wrapper/test/web3_wrapper_test.ts +++ b/packages/web3-wrapper/test/web3_wrapper_test.ts @@ -1,5 +1,5 @@ import * as chai from 'chai'; -import { BlockParamLiteral } from 'ethereum-types'; +import { BlockParamLiteral, JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types'; import * as Ganache from 'ganache-core'; import * as _ from 'lodash'; import 'mocha'; @@ -78,6 +78,19 @@ describe('Web3Wrapper tests', () => { const signatureLength = 132; expect(signature.length).to.be.equal(signatureLength); }); + it('should throw if the provider returns an error', async () => { + const message = '0xdeadbeef'; + const signer = addresses[1]; + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise { + callback(new Error('User denied message signature')); + }, + }; + const errorWeb3Wrapper = new Web3Wrapper(fakeProvider); + expect(errorWeb3Wrapper.signMessageAsync(signer, message)).to.be.rejectedWith( + 'User denied message signature', + ); + }); }); describe('#getBlockNumberAsync', () => { it('get block number', async () => { diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 652f2eb1d..b1181e4c6 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -17,6 +17,7 @@ import { MetamaskSubprovider, RedundantSubprovider, RPCSubprovider, + SignerSubprovider, Web3ProviderEngine, } from '@0xproject/subproviders'; import { SignedOrder, Token as ZeroExToken } from '@0xproject/types'; @@ -27,8 +28,6 @@ import * as _ from 'lodash'; import * as moment from 'moment'; import * as React from 'react'; import contract = require('truffle-contract'); -import { tokenAddressOverrides } from 'ts/utils/token_address_overrides'; - import { BlockchainWatcher } from 'ts/blockchain_watcher'; import { AssetSendCompleted } from 'ts/components/flash_messages/asset_send_completed'; import { TransactionSubmitted } from 'ts/components/flash_messages/transaction_submitted'; @@ -54,6 +53,7 @@ import { backendClient } from 'ts/utils/backend_client'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; +import { tokenAddressOverrides } from 'ts/utils/token_address_overrides'; import { utils } from 'ts/utils/utils'; import FilterSubprovider = require('web3-provider-engine/subproviders/filters'); @@ -161,7 +161,13 @@ export class Blockchain { // We catch all requests involving a users account and send it to the injectedWeb3 // instance. All other requests go to the public hosted node. const provider = new Web3ProviderEngine(); - provider.addProvider(new MetamaskSubprovider(injectedWeb3.currentProvider)); + const providerName = this._getNameGivenProvider(injectedWeb3.currentProvider); + // Wrap Metamask in a compatability wrapper MetamaskSubprovider (to handle inconsistencies) + const signerSubprovider = + providerName === Providers.Metamask + ? new MetamaskSubprovider(injectedWeb3.currentProvider) + : new SignerSubprovider(injectedWeb3.currentProvider); + provider.addProvider(signerSubprovider); provider.addProvider(new FilterSubprovider()); const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => { return new RPCSubprovider(publicNodeUrl); -- cgit