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
This commit is contained in:
Jacob Evans 2018-10-09 18:26:13 +11:00
parent e1236a4846
commit 9e8031d5e3
No known key found for this signature in database
GPG Key ID: 2036DA2ADDFB0842
23 changed files with 208 additions and 150 deletions

View File

@ -7,7 +7,8 @@
"pr": 1102 "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 "pr": 1102
}, },
{ {

View File

@ -90,6 +90,7 @@ export {
JSONRPCRequestPayload, JSONRPCRequestPayload,
JSONRPCResponsePayload, JSONRPCResponsePayload,
JSONRPCErrorCallback, JSONRPCErrorCallback,
JSONRPCResponseError,
LogEntry, LogEntry,
DecodedLogArgs, DecodedLogArgs,
LogEntryEvent, LogEntryEvent,

View File

@ -33,7 +33,7 @@ export class TransactionEncoder {
data, data,
}; };
const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, exchangeAddress); const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, exchangeAddress);
const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData); const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
const messageHex = `0x${eip712MessageBuffer.toString('hex')}`; const messageHex = `0x${eip712MessageBuffer.toString('hex')}`;
return messageHex; return messageHex;
} }

View File

@ -25,7 +25,7 @@ export class TransactionFactory {
}; };
const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, this._exchangeAddress); 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 signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType);
const signedTx = { const signedTx = {
exchangeAddress: this._exchangeAddress, exchangeAddress: this._exchangeAddress,

View File

@ -1,4 +1,13 @@
[ [
{
"version": "1.1.0",
"changes": [
{
"note": "Add `JSONRPCResponseError` and error field on `JSONRPCResponsePayload`.",
"pr": 1102
}
]
},
{ {
"timestamp": 1538693146, "timestamp": 1538693146,
"version": "1.0.11", "version": "1.0.11",

View File

@ -113,10 +113,16 @@ export interface JSONRPCRequestPayload {
jsonrpc: string; jsonrpc: string;
} }
export interface JSONRPCResponseError {
message: string;
code: number;
}
export interface JSONRPCResponsePayload { export interface JSONRPCResponsePayload {
result: any; result: any;
id: number; id: number;
jsonrpc: string; jsonrpc: string;
error?: JSONRPCResponseError;
} }
export interface AbstractBlock { export interface AbstractBlock {

View File

@ -1,4 +1,4 @@
export const eip712TypedData = { export const eip712TypedDataSchema = {
id: '/eip712TypedData', id: '/eip712TypedData',
type: 'object', type: 'object',
properties: { properties: {

View File

@ -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',
};

View File

@ -2,7 +2,7 @@ import { addressSchema, hexSchema, numberSchema } from '../schemas/basic_type_sc
import { blockParamSchema, blockRangeSchema } from '../schemas/block_range_schema'; import { blockParamSchema, blockRangeSchema } from '../schemas/block_range_schema';
import { callDataSchema } from '../schemas/call_data_schema'; import { callDataSchema } from '../schemas/call_data_schema';
import { ecSignatureParameterSchema, ecSignatureSchema } from '../schemas/ec_signature_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 { indexFilterValuesSchema } from '../schemas/index_filter_values_schema';
import { orderCancellationRequestsSchema } from '../schemas/order_cancel_schema'; import { orderCancellationRequestsSchema } from '../schemas/order_cancel_schema';
import { orderFillOrKillRequestsSchema } from '../schemas/order_fill_or_kill_requests_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 { signedOrdersSchema } from '../schemas/signed_orders_schema';
import { tokenSchema } from '../schemas/token_schema'; import { tokenSchema } from '../schemas/token_schema';
import { jsNumber, txDataSchema } from '../schemas/tx_data_schema'; import { jsNumber, txDataSchema } from '../schemas/tx_data_schema';
import { zeroExTransactionSchema } from '../schemas/zero_ex_transaction_schema';
export const schemas = { export const schemas = {
numberSchema, numberSchema,
@ -40,7 +41,7 @@ export const schemas = {
hexSchema, hexSchema,
ecSignatureParameterSchema, ecSignatureParameterSchema,
ecSignatureSchema, ecSignatureSchema,
eip712TypedData, eip712TypedDataSchema,
indexFilterValuesSchema, indexFilterValuesSchema,
orderCancellationRequestsSchema, orderCancellationRequestsSchema,
orderFillOrKillRequestsSchema, orderFillOrKillRequestsSchema,
@ -70,4 +71,5 @@ export const schemas = {
relayerApiOrdersChannelUpdateSchema, relayerApiOrdersChannelUpdateSchema,
relayerApiOrdersResponseSchema, relayerApiOrdersResponseSchema,
relayerApiAssetDataPairsSchema, relayerApiAssetDataPairsSchema,
zeroExTransactionSchema,
}; };

View File

@ -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 { EIP712Object, EIP712TypedData, EIP712Types, Order, ZeroExTransaction } from '@0xproject/types';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -18,6 +20,8 @@ export const eip712Utils = {
message: EIP712Object, message: EIP712Object,
exchangeAddress: string, exchangeAddress: string,
): EIP712TypedData => { ): EIP712TypedData => {
assert.isETHAddressHex('exchangeAddress', exchangeAddress);
assert.isString('primaryType', primaryType);
const typedData = { const typedData = {
types: { types: {
EIP712Domain: constants.EIP712_DOMAIN_SCHEMA.parameters, EIP712Domain: constants.EIP712_DOMAIN_SCHEMA.parameters,
@ -31,6 +35,7 @@ export const eip712Utils = {
message, message,
primaryType, primaryType,
}; };
assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema);
return typedData; return typedData;
}, },
/** /**
@ -39,6 +44,7 @@ export const eip712Utils = {
* @return A typed data object * @return A typed data object
*/ */
createOrderTypedData: (order: Order): EIP712TypedData => { createOrderTypedData: (order: Order): EIP712TypedData => {
assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
const normalizedOrder = _.mapValues(order, value => { const normalizedOrder = _.mapValues(order, value => {
return !_.isString(value) ? value.toString() : value; return !_.isString(value) ? value.toString() : value;
}); });
@ -61,6 +67,8 @@ export const eip712Utils = {
zeroExTransaction: ZeroExTransaction, zeroExTransaction: ZeroExTransaction,
exchangeAddress: string, exchangeAddress: string,
): EIP712TypedData => { ): EIP712TypedData => {
assert.isETHAddressHex('exchangeAddress', exchangeAddress);
assert.doesConformToSchema('zeroExTransaction', zeroExTransaction, schemas.zeroExTransactionSchema);
const normalizedTransaction = _.mapValues(zeroExTransaction, value => { const normalizedTransaction = _.mapValues(zeroExTransaction, value => {
return !_.isString(value) ? value.toString() : value; return !_.isString(value) ? value.toString() : value;
}); });

View File

@ -52,7 +52,7 @@ export const orderHashUtils = {
*/ */
getOrderHashBuffer(order: SignedOrder | Order): Buffer { getOrderHashBuffer(order: SignedOrder | Order): Buffer {
const typedData = eip712Utils.createOrderTypedData(order); const typedData = eip712Utils.createOrderTypedData(order);
const orderHashBuff = signTypedDataUtils.signTypedDataHash(typedData); const orderHashBuff = signTypedDataUtils.generateTypedDataHash(typedData);
return orderHashBuff; return orderHashBuff;
}, },
}; };

View File

@ -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. * then a fallback to `eth_sign` if not available on the supplied provider.
* @param order The Order to sign. * @param order The Order to sign.
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * @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. * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type.
*/ */
async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> { async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> {
assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
try { try {
const signedOrder = signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress); const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress);
return signedOrder; return signedOrder;
} catch (err) { } 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 orderHash = orderHashUtils.getOrderHashHex(order);
const signatureHex = await signatureUtils.ecSignHashAsync(provider, orderHash, signerAddress); const signatureHex = await signatureUtils.ecSignHashAsync(provider, orderHash, signerAddress);
const signedOrder = { 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 order The Order to sign.
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
* must be available via the supplied Provider. * must be available via the supplied Provider.
@ -226,6 +233,7 @@ export const signatureUtils = {
async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> { async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> {
assert.isWeb3Provider('provider', provider); assert.isWeb3Provider('provider', provider);
assert.isETHAddressHex('signerAddress', signerAddress); assert.isETHAddressHex('signerAddress', signerAddress);
assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
const web3Wrapper = new Web3Wrapper(provider); const web3Wrapper = new Web3Wrapper(provider);
await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
const normalizedSignerAddress = signerAddress.toLowerCase(); const normalizedSignerAddress = signerAddress.toLowerCase();
@ -247,14 +255,16 @@ export const signatureUtils = {
} catch (err) { } catch (err) {
// Detect if Metamask to transition users to the MetamaskSubprovider // Detect if Metamask to transition users to the MetamaskSubprovider
if ((provider as any).isMetaMask) { 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 { } else {
throw err; 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 msgHash Hex encoded message to sign.
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
* must be available via the supplied Provider. * must be available via the supplied Provider.
@ -303,7 +313,9 @@ export const signatureUtils = {
} }
// Detect if Metamask to transition users to the MetamaskSubprovider // Detect if Metamask to transition users to the MetamaskSubprovider
if ((provider as any).isMetaMask) { 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 { } else {
throw new Error(OrderError.InvalidSignature); throw new Error(OrderError.InvalidSignature);
} }

View File

@ -33,7 +33,7 @@ describe('EIP712 Utils', () => {
{ {
salt: new BigNumber('0'), salt: new BigNumber('0'),
data: constants.NULL_BYTES, data: constants.NULL_BYTES,
signerAddress: constants.NULL_BYTES, signerAddress: constants.NULL_ADDRESS,
}, },
constants.NULL_ADDRESS, constants.NULL_ADDRESS,
); );

View File

@ -17,6 +17,28 @@ chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
describe('Signature utils', () => { 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', () => { describe('#isValidSignatureAsync', () => {
let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
const ethSignSignature = const ethSignSignature =
@ -117,54 +139,54 @@ describe('Signature utils', () => {
}); });
}); });
describe('#ecSignOrderAsync', () => { describe('#ecSignOrderAsync', () => {
let makerAddress: string; it('should default to eth_sign if eth_signTypedData is unavailable', async () => {
const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'; const expectedSignature =
let order: Order; '0x1c3582f06356a1314dbf1c0e534c4d8e92e59b056ee607a7ff5a825f5f2cc5e6151c5cc7fdd420f5608e4d5bef108e42ad90c7a4b408caef32e24374cf387b0d7603';
before(async () => {
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); const fakeProvider = {
makerAddress = availableAddreses[0]; async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
order = { if (payload.method === 'eth_signTypedData') {
makerAddress, callback(new Error('Internal RPC Error'));
takerAddress: constants.NULL_ADDRESS, } else if (payload.method === 'eth_sign') {
senderAddress: constants.NULL_ADDRESS, const [address, message] = payload.params;
feeRecipientAddress: constants.NULL_ADDRESS, const signature = await web3Wrapper.signMessageAsync(address, message);
makerAssetData: constants.NULL_ADDRESS, callback(null, {
takerAssetData: constants.NULL_ADDRESS, id: 42,
exchangeAddress: fakeExchangeContractAddress, jsonrpc: '2.0',
salt: new BigNumber(0), result: signature,
makerFee: new BigNumber(0),
takerFee: new BigNumber(0),
makerAssetAmount: new BigNumber(0),
takerAssetAmount: new BigNumber(0),
expirationTimeSeconds: new BigNumber(0),
};
}); });
it('should result in the same signature as signing order hash without prefix', async () => { } else {
const orderHashHex = orderHashUtils.getOrderHashHex(order); callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] });
const sig = ethUtil.ecsign( }
ethUtil.toBuffer(orderHashHex), },
Buffer.from('F2F48EE19680706196E2E339E5DA3491186E0C4C5030670656B0E0164837257D', 'hex'), };
const signedOrder = await signatureUtils.ecSignOrderAsync(fakeProvider, order, makerAddress);
expect(signedOrder.signature).to.equal(expectedSignature);
});
it('should throw if the user denies the signing request', async () => {
const fakeProvider = {
async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
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',
); );
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,
);
expect(signatureHex).to.eq(signedOrder.signature);
expect(isValidSignature).to.eq(true);
}); });
}); });
describe('#ecSignHashAsync', () => { describe('#ecSignHashAsync', () => {
let makerAddress: string;
before(async () => { before(async () => {
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); const availableAddreses = await web3Wrapper.getAvailableAddressesAsync();
makerAddress = availableAddreses[0]; makerAddress = availableAddreses[0];
@ -239,27 +261,30 @@ describe('Signature utils', () => {
}); });
}); });
describe('#ecSignTypedDataOrderAsync', () => { describe('#ecSignTypedDataOrderAsync', () => {
let makerAddress: string; it('should result in the same signature as signing the order hash without an ethereum message prefix', async () => {
const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'; // Note: Since order hash is an EIP712 hash the result of a valid EIP712 signature
let order: Order; // of order hash is the same as signing the order without the Ethereum Message prefix.
before(async () => { const orderHashHex = orderHashUtils.getOrderHashHex(order);
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); const sig = ethUtil.ecsign(
makerAddress = availableAddreses[0]; ethUtil.toBuffer(orderHashHex),
order = { 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, makerAddress,
takerAddress: constants.NULL_ADDRESS, );
senderAddress: constants.NULL_ADDRESS, expect(signatureHex).to.eq(signedOrder.signature);
feeRecipientAddress: constants.NULL_ADDRESS, expect(isValidSignature).to.eq(true);
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 () => { it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => {
const expectedSignature = const expectedSignature =

View File

@ -57,4 +57,10 @@ export {
EIP712Parameter, EIP712Parameter,
} from '@0xproject/types'; } from '@0xproject/types';
export { JSONRPCRequestPayload, Provider, JSONRPCResponsePayload, JSONRPCErrorCallback } from 'ethereum-types'; export {
JSONRPCRequestPayload,
Provider,
JSONRPCResponsePayload,
JSONRPCErrorCallback,
JSONRPCResponseError,
} from 'ethereum-types';

View File

@ -106,7 +106,7 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider {
`Requested to sign message with address: ${address}, instantiated with address: ${this._address}`, `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 sig = ethUtil.ecsign(dataBuff, this._privateKeyBuffer);
const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s); const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s);
return rpcSig; return rpcSig;

View File

@ -6,11 +6,11 @@ import { EIP712Object, EIP712ObjectValue, EIP712TypedData, EIP712Types } from '@
export const signTypedDataUtils = { 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 * @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( return ethUtil.sha3(
Buffer.concat([ Buffer.concat([
Buffer.from('1901', 'hex'), Buffer.from('1901', 'hex'),

View File

@ -127,12 +127,12 @@ describe('signTypedDataUtils', () => {
primaryType: 'Order', primaryType: 'Order',
}; };
it('creates a hash of the test sign typed data', () => { 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}`; const hashHex = `0x${hash}`;
expect(hashHex).to.be.eq(simpleSignTypedDataHashHex); expect(hashHex).to.be.eq(simpleSignTypedDataHashHex);
}); });
it('creates a hash of the order sign typed data', () => { 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}`; const hashHex = `0x${hash}`;
expect(hashHex).to.be.eq(orderSignTypedDataHashHex); expect(hashHex).to.be.eq(orderSignTypedDataHashHex);
}); });

View File

@ -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", "version": "3.0.3",
"changes": [ "changes": [

View File

@ -322,7 +322,7 @@ export class Web3Wrapper {
*/ */
public async signTypedDataAsync(address: string, typedData: any): Promise<string> { public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
assert.isETHAddressHex('address', address); assert.isETHAddressHex('address', address);
assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedData); assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema);
const signData = await this.sendRawPayloadAsync<string>({ const signData = await this.sendRawPayloadAsync<string>({
method: 'eth_signTypedData', method: 'eth_signTypedData',
params: [address, typedData], params: [address, typedData],
@ -669,6 +669,9 @@ export class Web3Wrapper {
...payload, ...payload,
}; };
const response = await promisify<JSONRPCResponsePayload>(sendAsync)(payloadWithDefaults); const response = await promisify<JSONRPCResponsePayload>(sendAsync)(payloadWithDefaults);
if (response.error) {
throw new Error(response.error.message);
}
const result = response.result; const result = response.result;
return result; return result;
} }

View File

@ -1,5 +1,5 @@
import * as chai from 'chai'; 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 Ganache from 'ganache-core';
import * as _ from 'lodash'; import * as _ from 'lodash';
import 'mocha'; import 'mocha';
@ -78,6 +78,19 @@ describe('Web3Wrapper tests', () => {
const signatureLength = 132; const signatureLength = 132;
expect(signature.length).to.be.equal(signatureLength); 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<void> {
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', () => { describe('#getBlockNumberAsync', () => {
it('get block number', async () => { it('get block number', async () => {

View File

@ -17,6 +17,7 @@ import {
MetamaskSubprovider, MetamaskSubprovider,
RedundantSubprovider, RedundantSubprovider,
RPCSubprovider, RPCSubprovider,
SignerSubprovider,
Web3ProviderEngine, Web3ProviderEngine,
} from '@0xproject/subproviders'; } from '@0xproject/subproviders';
import { SignedOrder, Token as ZeroExToken } from '@0xproject/types'; import { SignedOrder, Token as ZeroExToken } from '@0xproject/types';
@ -27,8 +28,6 @@ import * as _ from 'lodash';
import * as moment from 'moment'; import * as moment from 'moment';
import * as React from 'react'; import * as React from 'react';
import contract = require('truffle-contract'); import contract = require('truffle-contract');
import { tokenAddressOverrides } from 'ts/utils/token_address_overrides';
import { BlockchainWatcher } from 'ts/blockchain_watcher'; import { BlockchainWatcher } from 'ts/blockchain_watcher';
import { AssetSendCompleted } from 'ts/components/flash_messages/asset_send_completed'; import { AssetSendCompleted } from 'ts/components/flash_messages/asset_send_completed';
import { TransactionSubmitted } from 'ts/components/flash_messages/transaction_submitted'; 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 { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants'; import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter'; import { errorReporter } from 'ts/utils/error_reporter';
import { tokenAddressOverrides } from 'ts/utils/token_address_overrides';
import { utils } from 'ts/utils/utils'; import { utils } from 'ts/utils/utils';
import FilterSubprovider = require('web3-provider-engine/subproviders/filters'); 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 // We catch all requests involving a users account and send it to the injectedWeb3
// instance. All other requests go to the public hosted node. // instance. All other requests go to the public hosted node.
const provider = new Web3ProviderEngine(); 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()); provider.addProvider(new FilterSubprovider());
const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => { const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => {
return new RPCSubprovider(publicNodeUrl); return new RPCSubprovider(publicNodeUrl);

View File

@ -5606,19 +5606,6 @@ ethereumjs-wallet@0.6.0:
utf8 "^2.1.1" utf8 "^2.1.1"
uuid "^2.0.1" uuid "^2.0.1"
ethereumjs-wallet@~0.6.0:
version "0.6.2"
resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda"
dependencies:
aes-js "^3.1.1"
bs58check "^2.1.2"
ethereumjs-util "^5.2.0"
hdkey "^1.0.0"
safe-buffer "^5.1.2"
scrypt.js "^0.2.0"
utf8 "^3.0.0"
uuid "^3.3.2"
ethers@3.0.22: ethers@3.0.22:
version "3.0.22" version "3.0.22"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-3.0.22.tgz#7fab1ea16521705837aa43c15831877b2716b436" resolved "https://registry.yarnpkg.com/ethers/-/ethers-3.0.22.tgz#7fab1ea16521705837aa43c15831877b2716b436"
@ -6432,7 +6419,7 @@ ganache-core@0xProject/ganache-core#monorepo-dep:
ethereumjs-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default" ethereumjs-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default"
ethereumjs-util "^5.2.0" ethereumjs-util "^5.2.0"
ethereumjs-vm "2.3.5" ethereumjs-vm "2.3.5"
ethereumjs-wallet "0.6.0" ethereumjs-wallet "~0.6.0"
fake-merkle-patricia-tree "~1.0.1" fake-merkle-patricia-tree "~1.0.1"
heap "~0.2.6" heap "~0.2.6"
js-scrypt "^0.2.0" js-scrypt "^0.2.0"
@ -6797,7 +6784,7 @@ globby@^6.1.0:
pify "^2.0.0" pify "^2.0.0"
pinkie-promise "^2.0.0" pinkie-promise "^2.0.0"
globby@^8.0.0, globby@^8.0.1: globby@^8.0.1:
version "8.0.1" version "8.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50"
dependencies: dependencies:
@ -8576,10 +8563,6 @@ jsesc@~0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@ -9357,7 +9340,7 @@ lodash@^4.13.1, lodash@^4.15.0:
version "4.17.11" version "4.17.11"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: lodash@^4.14.0, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1:
version "4.17.5" version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
@ -10882,10 +10865,6 @@ p-is-promise@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
p-lazy@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-lazy/-/p-lazy-1.0.0.tgz#ec53c802f2ee3ac28f166cc82d0b2b02de27a835"
p-limit@^1.1.0: p-limit@^1.1.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
@ -15834,12 +15813,6 @@ webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
webpack-addons@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/webpack-addons/-/webpack-addons-1.1.5.tgz#2b178dfe873fb6e75e40a819fa5c26e4a9bc837a"
dependencies:
jscodeshift "^0.4.0"
webpack-cli@3.1.2, webpack-cli@^3.1.1: webpack-cli@3.1.2, webpack-cli@^3.1.1:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.1.2.tgz#17d7e01b77f89f884a2bbf9db545f0f6a648e746" resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.1.2.tgz#17d7e01b77f89f884a2bbf9db545f0f6a648e746"
@ -15855,37 +15828,6 @@ webpack-cli@3.1.2, webpack-cli@^3.1.1:
v8-compile-cache "^2.0.2" v8-compile-cache "^2.0.2"
yargs "^12.0.2" yargs "^12.0.2"
webpack-cli@^2.0.9:
version "2.1.3"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-2.1.3.tgz#65d166851abaa56067ef3f716b02a97ba6bbe84d"
dependencies:
chalk "^2.3.2"
cross-spawn "^6.0.5"
diff "^3.5.0"
enhanced-resolve "^4.0.0"
envinfo "^4.4.2"
glob-all "^3.1.0"
global-modules "^1.0.0"
got "^8.2.0"
import-local "^1.0.0"
inquirer "^5.1.0"
interpret "^1.0.4"
jscodeshift "^0.5.0"
listr "^0.13.0"
loader-utils "^1.1.0"
lodash "^4.17.5"
log-symbols "^2.2.0"
mkdirp "^0.5.1"
p-each-series "^1.0.0"
p-lazy "^1.0.0"
prettier "^1.5.3"
supports-color "^5.3.0"
v8-compile-cache "^1.1.2"
webpack-addons "^1.1.5"
yargs "^11.1.0"
yeoman-environment "^2.0.0"
yeoman-generator "^2.0.4"
webpack-dev-middleware@3.4.0: webpack-dev-middleware@3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890" resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890"