aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorJacob Evans <dekz@dekz.net>2018-10-09 18:17:11 +0800
committerGitHub <noreply@github.com>2018-10-09 18:17:11 +0800
commit024bcf492ddbdceb033466aeadef220adc145332 (patch)
tree01a364d769f3eede04f4bd1e4a743784d39eea91 /packages
parent3ac9dac4f0a6b3875b7ae0ea6dd1855743768ef2 (diff)
parent75b9e639194e98febf8e378619afef2d578cbc7e (diff)
downloaddexon-0x-contracts-024bcf492ddbdceb033466aeadef220adc145332.tar
dexon-0x-contracts-024bcf492ddbdceb033466aeadef220adc145332.tar.gz
dexon-0x-contracts-024bcf492ddbdceb033466aeadef220adc145332.tar.bz2
dexon-0x-contracts-024bcf492ddbdceb033466aeadef220adc145332.tar.lz
dexon-0x-contracts-024bcf492ddbdceb033466aeadef220adc145332.tar.xz
dexon-0x-contracts-024bcf492ddbdceb033466aeadef220adc145332.tar.zst
dexon-0x-contracts-024bcf492ddbdceb033466aeadef220adc145332.zip
Merge pull request #1102 from 0xProject/feature/0x.js/eip712-sign-typed-data
[0x.js][order-utils][web3-wrapper] Expose eth_signTypedData functionality for order signing
Diffstat (limited to 'packages')
-rw-r--r--packages/0x.js/CHANGELOG.json19
-rw-r--r--packages/0x.js/src/index.ts10
-rw-r--r--packages/contract-wrappers/src/index.ts1
-rw-r--r--packages/contract-wrappers/src/utils/transaction_encoder.ts22
-rw-r--r--packages/contract-wrappers/test/transaction_encoder_test.ts9
-rw-r--r--packages/contracts/test/exchange/libs.ts18
-rw-r--r--packages/contracts/test/exchange/signature_validator.ts12
-rw-r--r--packages/contracts/test/utils/transaction_factory.ts22
-rw-r--r--packages/ethereum-types/CHANGELOG.json9
-rw-r--r--packages/ethereum-types/src/index.ts6
-rw-r--r--packages/json-schemas/schemas/eip712_typed_data.ts28
-rw-r--r--packages/json-schemas/schemas/zero_ex_transaction_schema.ts10
-rw-r--r--packages/json-schemas/src/schemas.ts4
-rw-r--r--packages/order-utils/CHANGELOG.json18
-rw-r--r--packages/order-utils/src/constants.ts35
-rw-r--r--packages/order-utils/src/eip712_utils.ts162
-rw-r--r--packages/order-utils/src/index.ts23
-rw-r--r--packages/order-utils/src/order_factory.ts9
-rw-r--r--packages/order-utils/src/order_hash.ts33
-rw-r--r--packages/order-utils/src/signature_utils.ts163
-rw-r--r--packages/order-utils/src/types.ts19
-rw-r--r--packages/order-utils/test/eip712_utils_test.ts44
-rw-r--r--packages/order-utils/test/order_hash_test.ts14
-rw-r--r--packages/order-utils/test/signature_utils_test.ts221
-rw-r--r--packages/order-watcher/src/index.ts8
-rw-r--r--packages/sol-cov/src/index.ts8
-rw-r--r--packages/subproviders/CHANGELOG.json13
-rw-r--r--packages/subproviders/package.json2
-rw-r--r--packages/subproviders/src/index.ts18
-rw-r--r--packages/subproviders/src/subproviders/base_wallet_subprovider.ts14
-rw-r--r--packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts37
-rw-r--r--packages/subproviders/src/subproviders/ledger.ts10
-rw-r--r--packages/subproviders/src/subproviders/metamask_subprovider.ts126
-rw-r--r--packages/subproviders/src/subproviders/mnemonic_wallet.ts28
-rw-r--r--packages/subproviders/src/subproviders/private_key_wallet.ts29
-rw-r--r--packages/subproviders/src/subproviders/signer.ts15
-rw-r--r--packages/subproviders/src/types.ts2
-rw-r--r--packages/subproviders/test/unit/eth_lightwallet_subprovider_test.ts21
-rw-r--r--packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts21
-rw-r--r--packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts21
-rw-r--r--packages/subproviders/test/utils/fixture_data.ts31
-rw-r--r--packages/types/CHANGELOG.json13
-rw-r--r--packages/types/src/index.ts41
-rw-r--r--packages/utils/src/index.ts1
-rw-r--r--packages/utils/src/sign_typed_data_utils.ts82
-rw-r--r--packages/utils/test/sign_typed_data_utils_test.ts140
-rw-r--r--packages/web3-wrapper/CHANGELOG.json14
-rw-r--r--packages/web3-wrapper/src/index.ts1
-rw-r--r--packages/web3-wrapper/src/web3_wrapper.ts18
-rw-r--r--packages/web3-wrapper/test/web3_wrapper_test.ts15
-rw-r--r--packages/website/ts/blockchain.ts30
51 files changed, 1236 insertions, 434 deletions
diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json
index 1d6f08760..6dfcc3d33 100644
--- a/packages/0x.js/CHANGELOG.json
+++ b/packages/0x.js/CHANGELOG.json
@@ -1,5 +1,24 @@
[
{
+ "version": "2.0.0",
+ "changes": [
+ {
+ "note": "Add support for `eth_signTypedData`.",
+ "pr": 1102
+ },
+ {
+ "note":
+ "Added `MetamaskSubprovider` to handle inconsistencies in Metamask's signing JSON RPC endpoints.",
+ "pr": 1102
+ },
+ {
+ "note":
+ "Removed `SignerType` (including `SignerType.Metamask`). Please use the `MetamaskSubprovider` to wrap `web3.currentProvider`.",
+ "pr": 1102
+ }
+ ]
+ },
+ {
"version": "1.0.8",
"changes": [
{
diff --git a/packages/0x.js/src/index.ts b/packages/0x.js/src/index.ts
index d07bfcfc8..6eb1fd8ee 100644
--- a/packages/0x.js/src/index.ts
+++ b/packages/0x.js/src/index.ts
@@ -53,7 +53,13 @@ export { OrderWatcher, OnOrderStateChangeCallback, OrderWatcherConfig } from '@0
export import Web3ProviderEngine = require('web3-provider-engine');
-export { RPCSubprovider, Callback, JSONRPCRequestPayloadWithMethod, ErrorCallback } from '@0xproject/subproviders';
+export {
+ RPCSubprovider,
+ Callback,
+ JSONRPCRequestPayloadWithMethod,
+ ErrorCallback,
+ MetamaskSubprovider,
+} from '@0xproject/subproviders';
export { AbiDecoder } from '@0xproject/utils';
@@ -68,7 +74,6 @@ export {
OrderStateInvalid,
OrderState,
AssetProxyId,
- SignerType,
ERC20AssetData,
ERC721AssetData,
SignatureType,
@@ -85,6 +90,7 @@ export {
JSONRPCRequestPayload,
JSONRPCResponsePayload,
JSONRPCErrorCallback,
+ JSONRPCResponseError,
LogEntry,
DecodedLogArgs,
LogEntryEvent,
diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts
index 2fcdd2ddb..e8a53170e 100644
--- a/packages/contract-wrappers/src/index.ts
+++ b/packages/contract-wrappers/src/index.ts
@@ -39,6 +39,7 @@ export {
JSONRPCRequestPayload,
JSONRPCResponsePayload,
JSONRPCErrorCallback,
+ JSONRPCResponseError,
AbiDefinition,
LogWithDecodedArgs,
FunctionAbi,
diff --git a/packages/contract-wrappers/src/utils/transaction_encoder.ts b/packages/contract-wrappers/src/utils/transaction_encoder.ts
index 87cbb43fd..33086944b 100644
--- a/packages/contract-wrappers/src/utils/transaction_encoder.ts
+++ b/packages/contract-wrappers/src/utils/transaction_encoder.ts
@@ -1,22 +1,13 @@
import { schemas } from '@0xproject/json-schemas';
-import { EIP712Schema, EIP712Types, eip712Utils } from '@0xproject/order-utils';
+import { eip712Utils } from '@0xproject/order-utils';
import { Order, SignedOrder } from '@0xproject/types';
-import { BigNumber } from '@0xproject/utils';
+import { BigNumber, signTypedDataUtils } from '@0xproject/utils';
import _ = require('lodash');
import { ExchangeContract } from '../contract_wrappers/generated/exchange';
import { assert } from './assert';
-const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
- name: 'ZeroExTransaction',
- parameters: [
- { name: 'salt', type: EIP712Types.Uint256 },
- { name: 'signerAddress', type: EIP712Types.Address },
- { name: 'data', type: EIP712Types.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
@@ -41,12 +32,9 @@ export class TransactionEncoder {
signerAddress,
data,
};
- const executeTransactionHashBuff = eip712Utils.structHash(
- EIP712_ZEROEX_TRANSACTION_SCHEMA,
- executeTransactionData,
- );
- const eip721MessageBuffer = eip712Utils.createEIP712Message(executeTransactionHashBuff, exchangeAddress);
- const messageHex = `0x${eip721MessageBuffer.toString('hex')}`;
+ const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, exchangeAddress);
+ const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
+ const messageHex = `0x${eip712MessageBuffer.toString('hex')}`;
return messageHex;
}
/**
diff --git a/packages/contract-wrappers/test/transaction_encoder_test.ts b/packages/contract-wrappers/test/transaction_encoder_test.ts
index a397e43a8..9da8fe2ca 100644
--- a/packages/contract-wrappers/test/transaction_encoder_test.ts
+++ b/packages/contract-wrappers/test/transaction_encoder_test.ts
@@ -1,7 +1,7 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { FillScenarios } from '@0xproject/fill-scenarios';
import { assetDataUtils, generatePseudoRandomSalt, orderHashUtils, signatureUtils } from '@0xproject/order-utils';
-import { SignedOrder, SignerType } from '@0xproject/types';
+import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import 'mocha';
@@ -80,12 +80,7 @@ describe('TransactionEncoder', () => {
): Promise<void> => {
const salt = generatePseudoRandomSalt();
const encodedTransaction = encoder.getTransactionHex(data, salt, signerAddress);
- const signature = await signatureUtils.ecSignOrderHashAsync(
- provider,
- encodedTransaction,
- signerAddress,
- SignerType.Default,
- );
+ const signature = await signatureUtils.ecSignHashAsync(provider, encodedTransaction, signerAddress);
txHash = await contractWrappers.exchange.executeTransactionAsync(
salt,
signerAddress,
diff --git a/packages/contracts/test/exchange/libs.ts b/packages/contracts/test/exchange/libs.ts
index 37234489e..049b7f37a 100644
--- a/packages/contracts/test/exchange/libs.ts
+++ b/packages/contracts/test/exchange/libs.ts
@@ -1,5 +1,5 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils';
-import { assetDataUtils, eip712Utils, orderHashUtils } from '@0xproject/order-utils';
+import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
@@ -126,22 +126,6 @@ describe('Exchange libs', () => {
});
describe('LibOrder', () => {
- describe('getOrderSchema', () => {
- it('should output the correct order schema hash', async () => {
- const orderSchema = await libs.getOrderSchemaHash.callAsync();
- const schemaHashBuffer = orderHashUtils._getOrderSchemaBuffer();
- const schemaHashHex = `0x${schemaHashBuffer.toString('hex')}`;
- expect(schemaHashHex).to.be.equal(orderSchema);
- });
- });
- describe('getDomainSeparatorSchema', () => {
- it('should output the correct domain separator schema hash', async () => {
- const domainSeparatorSchema = await libs.getDomainSeparatorSchemaHash.callAsync();
- const domainSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer();
- const schemaHashHex = `0x${domainSchemaBuffer.toString('hex')}`;
- expect(schemaHashHex).to.be.equal(domainSeparatorSchema);
- });
- });
describe('getOrderHash', () => {
it('should output the correct orderHash', async () => {
signedOrder = await orderFactory.newSignedOrderAsync();
diff --git a/packages/contracts/test/exchange/signature_validator.ts b/packages/contracts/test/exchange/signature_validator.ts
index 5cc62e777..192ed3ca9 100644
--- a/packages/contracts/test/exchange/signature_validator.ts
+++ b/packages/contracts/test/exchange/signature_validator.ts
@@ -1,6 +1,6 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { assetDataUtils, orderHashUtils, signatureUtils } from '@0xproject/order-utils';
-import { RevertReason, SignatureType, SignedOrder, SignerType } from '@0xproject/types';
+import { RevertReason, SignatureType, SignedOrder } from '@0xproject/types';
import * as chai from 'chai';
import { LogWithDecodedArgs } from 'ethereum-types';
import ethUtil = require('ethereumjs-util');
@@ -231,10 +231,7 @@ describe('MixinSignatureValidator', () => {
it('should return true when SignatureType=EthSign and signature is valid', async () => {
// Create EthSign signature
const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
- const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(
- orderHashHex,
- SignerType.Default,
- );
+ const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
// Create 0x signature from EthSign signature
@@ -257,10 +254,7 @@ describe('MixinSignatureValidator', () => {
it('should return false when SignatureType=EthSign and signature is invalid', async () => {
// Create EthSign signature
const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
- const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(
- orderHashHex,
- SignerType.Default,
- );
+ const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
// Create 0x signature from EthSign signature
diff --git a/packages/contracts/test/utils/transaction_factory.ts b/packages/contracts/test/utils/transaction_factory.ts
index 8465a6a30..9ed4c5a31 100644
--- a/packages/contracts/test/utils/transaction_factory.ts
+++ b/packages/contracts/test/utils/transaction_factory.ts
@@ -1,19 +1,11 @@
-import { EIP712Schema, EIP712Types, eip712Utils, 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';
import { signingUtils } from './signing_utils';
import { SignedTransaction } from './types';
-const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
- name: 'ZeroExTransaction',
- parameters: [
- { name: 'salt', type: EIP712Types.Uint256 },
- { name: 'signerAddress', type: EIP712Types.Address },
- { name: 'data', type: EIP712Types.Bytes },
- ],
-};
-
export class TransactionFactory {
private readonly _signerBuff: Buffer;
private readonly _exchangeAddress: string;
@@ -31,12 +23,10 @@ export class TransactionFactory {
signerAddress,
data,
};
- const executeTransactionHashBuff = eip712Utils.structHash(
- EIP712_ZEROEX_TRANSACTION_SCHEMA,
- executeTransactionData,
- );
- const txHash = eip712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress);
- const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType);
+
+ const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, this._exchangeAddress);
+ const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
+ const signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType);
const signedTx = {
exchangeAddress: this._exchangeAddress,
signature: `0x${signature.toString('hex')}`,
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,5 +1,14 @@
[
{
+ "version": "1.1.0",
+ "changes": [
+ {
+ "note": "Add `JSONRPCResponseError` and error field on `JSONRPCResponsePayload`.",
+ "pr": 1102
+ }
+ ]
+ },
+ {
"timestamp": 1538693146,
"version": "1.0.11",
"changes": [
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
new file mode 100644
index 000000000..31ad74610
--- /dev/null
+++ b/packages/json-schemas/schemas/eip712_typed_data.ts
@@ -0,0 +1,28 @@
+export const eip712TypedDataSchema = {
+ id: '/eip712TypedData',
+ type: 'object',
+ properties: {
+ types: {
+ type: 'object',
+ properties: {
+ EIP712Domain: { type: 'array' },
+ },
+ additionalProperties: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ name: { type: 'string' },
+ type: { type: 'string' },
+ },
+ required: ['name', 'type'],
+ },
+ },
+ required: ['EIP712Domain'],
+ },
+ primaryType: { type: 'string' },
+ domain: { type: 'object' },
+ message: { type: 'object' },
+ },
+ required: ['types', 'primaryType', 'domain', 'message'],
+};
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 3bc37f96b..4eb96092d 100644
--- a/packages/json-schemas/src/schemas.ts
+++ b/packages/json-schemas/src/schemas.ts
@@ -2,6 +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 { 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';
@@ -31,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,
@@ -39,6 +41,7 @@ export const schemas = {
hexSchema,
ecSignatureParameterSchema,
ecSignatureSchema,
+ eip712TypedDataSchema,
indexFilterValuesSchema,
orderCancellationRequestsSchema,
orderFillOrKillRequestsSchema,
@@ -68,4 +71,5 @@ export const schemas = {
relayerApiOrdersChannelUpdateSchema,
relayerApiOrdersResponseSchema,
relayerApiAssetDataPairsSchema,
+ zeroExTransactionSchema,
};
diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json
index 3e841c43c..5a0c0db47 100644
--- a/packages/order-utils/CHANGELOG.json
+++ b/packages/order-utils/CHANGELOG.json
@@ -1,5 +1,23 @@
[
{
+ "version": "2.0.0",
+ "changes": [
+ {
+ "note":
+ "Added `ecSignOrderAsync` to first sign an order using `eth_signTypedData` and fallback to `eth_sign`.",
+ "pr": 1102
+ },
+ {
+ "note": "Added `ecSignTypedDataOrderAsync` to sign an order exclusively using `eth_signTypedData`.",
+ "pr": 1102
+ },
+ {
+ "note": "Rename `ecSignOrderHashAsync` to `ecSignHashAsync` removing `SignerType` parameter.",
+ "pr": 1102
+ }
+ ]
+ },
+ {
"version": "1.0.7",
"changes": [
{
diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts
index c23578c20..7de20a696 100644
--- a/packages/order-utils/src/constants.ts
+++ b/packages/order-utils/src/constants.ts
@@ -13,4 +13,39 @@ export const constants = {
BASE_16: 16,
INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite
ZERO_AMOUNT: new BigNumber(0),
+ 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
index b303c93dc..56f736500 100644
--- a/packages/order-utils/src/eip712_utils.ts
+++ b/packages/order-utils/src/eip712_utils.ts
@@ -1,109 +1,83 @@
-import ethUtil = require('ethereumjs-util');
+import { assert } from '@0xproject/assert';
+import { schemas } from '@0xproject/json-schemas';
+import { EIP712Object, EIP712TypedData, EIP712Types, Order, ZeroExTransaction } from '@0xproject/types';
import * as _ from 'lodash';
-import { crypto } from './crypto';
-import { EIP712Schema, EIP712Types } from './types';
-
-const EIP191_PREFIX = '\x19\x01';
-const EIP712_DOMAIN_NAME = '0x Protocol';
-const EIP712_DOMAIN_VERSION = '2';
-const EIP712_VALUE_LENGTH = 32;
-
-const EIP712_DOMAIN_SCHEMA: EIP712Schema = {
- name: 'EIP712Domain',
- parameters: [
- { name: 'name', type: EIP712Types.String },
- { name: 'version', type: EIP712Types.String },
- { name: 'verifyingContract', type: EIP712Types.Address },
- ],
-};
+import { constants } from './constants';
export const eip712Utils = {
/**
- * Compiles the EIP712Schema and returns the hash of the schema.
- * @param schema The EIP712 schema.
- * @return The hash of the compiled schema
- */
- compileSchema(schema: EIP712Schema): Buffer {
- const eip712Schema = eip712Utils._encodeType(schema);
- const eip712SchemaHashBuffer = crypto.solSHA3([eip712Schema]);
- return eip712SchemaHashBuffer;
- },
- /**
- * Merges the EIP712 hash of a struct with the DomainSeparator for 0x v2.
- * @param hashStruct the EIP712 hash of a struct
- * @param contractAddress the exchange contract address
- * @return The hash of an EIP712 message with domain separator prefixed
- */
- createEIP712Message(hashStruct: Buffer, contractAddress: string): Buffer {
- const domainSeparatorHashBuffer = eip712Utils._getDomainSeparatorHashBuffer(contractAddress);
- const messageBuff = crypto.solSHA3([EIP191_PREFIX, domainSeparatorHashBuffer, hashStruct]);
- return messageBuff;
- },
- /**
- * Pad an address to 32 bytes
- * @param address Address to pad
- * @return padded address
+ * 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
*/
- pad32Address(address: string): Buffer {
- const addressBuffer = ethUtil.toBuffer(address);
- const addressPadded = eip712Utils.pad32Buffer(addressBuffer);
- return addressPadded;
+ createTypedData: (
+ primaryType: string,
+ types: EIP712Types,
+ message: EIP712Object,
+ exchangeAddress: string,
+ ): EIP712TypedData => {
+ assert.isETHAddressHex('exchangeAddress', exchangeAddress);
+ assert.isString('primaryType', primaryType);
+ 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,
+ };
+ assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema);
+ return typedData;
},
/**
- * Pad an buffer to 32 bytes
- * @param buffer Address to pad
- * @return padded buffer
+ * Creates an Order EIP712TypedData object for use with signTypedData.
+ * @param Order the order
+ * @return A typed data object
*/
- pad32Buffer(buffer: Buffer): Buffer {
- const bufferPadded = ethUtil.setLengthLeft(buffer, EIP712_VALUE_LENGTH);
- return bufferPadded;
+ createOrderTypedData: (order: Order): EIP712TypedData => {
+ assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
+ 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;
},
/**
- * Hash together a EIP712 schema with the corresponding data
- * @param schema EIP712-compliant schema
- * @param data Data the complies to the schema
- * @return A buffer containing the SHA256 hash of the schema and encoded data
+ * 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
*/
- structHash(schema: EIP712Schema, data: { [key: string]: any }): Buffer {
- const encodedData = eip712Utils._encodeData(schema, data);
- const schemaHash = eip712Utils.compileSchema(schema);
- const hashBuffer = crypto.solSHA3([schemaHash, ...encodedData]);
- return hashBuffer;
- },
- _getDomainSeparatorSchemaBuffer(): Buffer {
- return eip712Utils.compileSchema(EIP712_DOMAIN_SCHEMA);
- },
- _getDomainSeparatorHashBuffer(exchangeAddress: string): Buffer {
- const domainSeparatorSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer();
- const encodedData = eip712Utils._encodeData(EIP712_DOMAIN_SCHEMA, {
- name: EIP712_DOMAIN_NAME,
- version: EIP712_DOMAIN_VERSION,
- verifyingContract: exchangeAddress,
+ createZeroExTransactionTypedData: (
+ 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;
});
- const domainSeparatorHashBuff2 = crypto.solSHA3([domainSeparatorSchemaBuffer, ...encodedData]);
- return domainSeparatorHashBuff2;
- },
- _encodeType(schema: EIP712Schema): string {
- const namedTypes = _.map(schema.parameters, ({ name, type }) => `${type} ${name}`);
- const namedTypesJoined = namedTypes.join(',');
- const encodedType = `${schema.name}(${namedTypesJoined})`;
- return encodedType;
- },
- _encodeData(schema: EIP712Schema, data: { [key: string]: any }): any {
- const encodedValues = [];
- for (const parameter of schema.parameters) {
- const value = data[parameter.name];
- if (parameter.type === EIP712Types.String || parameter.type === EIP712Types.Bytes) {
- encodedValues.push(crypto.solSHA3([ethUtil.toBuffer(value)]));
- } else if (parameter.type === EIP712Types.Uint256) {
- encodedValues.push(value);
- } else if (parameter.type === EIP712Types.Address) {
- encodedValues.push(eip712Utils.pad32Address(value));
- } else {
- throw new Error(`Unable to encode ${parameter.type}`);
- }
- }
- return encodedValues;
+ 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 1553647c6..dbb782b85 100644
--- a/packages/order-utils/src/index.ts
+++ b/packages/order-utils/src/index.ts
@@ -2,7 +2,6 @@ export { orderHashUtils } from './order_hash';
export { signatureUtils } from './signature_utils';
export { generatePseudoRandomSalt } from './salt';
export { assetDataUtils } from './asset_data_utils';
-export { eip712Utils } from './eip712_utils';
export { marketUtils } from './market_utils';
export { rateUtils } from './rate_utils';
export { sortingUtils } from './sorting_utils';
@@ -19,7 +18,17 @@ 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 { Provider, JSONRPCRequestPayload, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types';
+export { constants } from './constants';
+export { eip712Utils } from './eip712_utils';
+
+export {
+ Provider,
+ JSONRPCRequestPayload,
+ JSONRPCErrorCallback,
+ JSONRPCResponsePayload,
+ JSONRPCResponseError,
+} from 'ethereum-types';
+
export {
SignedOrder,
Order,
@@ -29,17 +38,19 @@ export {
ERC20AssetData,
ERC721AssetData,
AssetProxyId,
- SignerType,
SignatureType,
OrderStateValid,
OrderStateInvalid,
ExchangeContractErrs,
+ EIP712Parameter,
+ EIP712TypedData,
+ EIP712Types,
+ EIP712Object,
+ EIP712ObjectValue,
+ ZeroExTransaction,
} from '@0xproject/types';
export {
OrderError,
- EIP712Parameter,
- EIP712Schema,
- EIP712Types,
TradeSide,
TransferType,
FindFeeOrdersThatCoverFeesForTargetOrdersOpts,
diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts
index b1292903a..0f0cd6046 100644
--- a/packages/order-utils/src/order_factory.ts
+++ b/packages/order-utils/src/order_factory.ts
@@ -1,4 +1,4 @@
-import { Order, SignedOrder, SignerType } from '@0xproject/types';
+import { Order, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
@@ -71,12 +71,7 @@ export const orderFactory = {
createOrderOpts,
);
const orderHash = orderHashUtils.getOrderHashHex(order);
- const signature = await signatureUtils.ecSignOrderHashAsync(
- provider,
- orderHash,
- makerAddress,
- SignerType.Default,
- );
+ const signature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
const signedOrder: SignedOrder = _.assign(order, { signature });
return signedOrder;
},
diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts
index 8e98f8767..b523a3523 100644
--- a/packages/order-utils/src/order_hash.ts
+++ b/packages/order-utils/src/order_hash.ts
@@ -1,31 +1,13 @@
import { schemas, SchemaValidator } from '@0xproject/json-schemas';
import { Order, SignedOrder } from '@0xproject/types';
+import { signTypedDataUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import { assert } from './assert';
import { eip712Utils } from './eip712_utils';
-import { EIP712Schema, EIP712Types } from './types';
const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string';
-const EIP712_ORDER_SCHEMA: EIP712Schema = {
- name: 'Order',
- parameters: [
- { name: 'makerAddress', type: EIP712Types.Address },
- { name: 'takerAddress', type: EIP712Types.Address },
- { name: 'feeRecipientAddress', type: EIP712Types.Address },
- { name: 'senderAddress', type: EIP712Types.Address },
- { name: 'makerAssetAmount', type: EIP712Types.Uint256 },
- { name: 'takerAssetAmount', type: EIP712Types.Uint256 },
- { name: 'makerFee', type: EIP712Types.Uint256 },
- { name: 'takerFee', type: EIP712Types.Uint256 },
- { name: 'expirationTimeSeconds', type: EIP712Types.Uint256 },
- { name: 'salt', type: EIP712Types.Uint256 },
- { name: 'makerAssetData', type: EIP712Types.Bytes },
- { name: 'takerAssetData', type: EIP712Types.Bytes },
- ],
-};
-
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,16 +46,13 @@ 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 orderParamsHashBuff = eip712Utils.structHash(EIP712_ORDER_SCHEMA, order);
- const orderHashBuff = eip712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress);
+ const typedData = eip712Utils.createOrderTypedData(order);
+ const orderHashBuff = signTypedDataUtils.generateTypedDataHash(typedData);
return orderHashBuff;
},
- _getOrderSchemaBuffer(): Buffer {
- return eip712Utils.compileSchema(EIP712_ORDER_SCHEMA);
- },
};
diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts
index 3b656d3fc..372d210d0 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, SignatureType, SignerType, 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,9 +7,11 @@ import * as _ from 'lodash';
import { artifacts } from './artifacts';
import { assert } from './assert';
+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 { orderHashUtils } from './order_hash';
import { OrderError } from './types';
import { utils } from './utils';
@@ -49,7 +51,7 @@ export const signatureUtils = {
case SignatureType.EthSign: {
const ecSignature = signatureUtils.parseECSignature(signature);
- const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.Default);
+ const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data);
return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress);
}
@@ -192,36 +194,90 @@ export const signatureUtils = {
}
},
/**
- * Signs an orderHash and returns it's elliptic curve signature and signature type.
- * This method currently supports TestRPC, Geth and Parity above and below V1.6.6
- * @param orderHash Hex encoded orderHash to sign.
+ * 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
- * must be available via the Provider supplied to 0x.js.
- * @param signerType Different signers add/require different prefixes to be prepended to the message being signed.
- * Since we cannot know ahead of time which signer you are using, you must supply a SignerType.
- * @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 ecSignOrderHashAsync(
- provider: Provider,
- orderHash: string,
- signerAddress: string,
- signerType: SignerType,
- ): Promise<string> {
+ async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> {
+ assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
+ try {
+ const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress);
+ return signedOrder;
+ } catch (err) {
+ // 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. Unfortunately for signers with a different error message,
+ // they will receive two signature requests.
+ 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 = {
+ ...order,
+ signature: signatureHex,
+ };
+ return signedOrder;
+ }
+ },
+ /**
+ * 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.
+ * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type.
+ */
+ async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> {
assert.isWeb3Provider('provider', provider);
- assert.isHexString('orderHash', orderHash);
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();
-
- let msgHashHex = orderHash;
- const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(orderHash, signerType);
- // Metamask incorrectly implements eth_sign and does not prefix the message as per the spec
- // Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
- if (signerType === SignerType.Metamask) {
- msgHashHex = prefixedMsgHashHex;
+ const typedData = eip712Utils.createOrderTypedData(order);
+ try {
+ const signature = await web3Wrapper.signTypedDataAsync(normalizedSignerAddress, typedData);
+ const ecSignatureRSV = parseSignatureHexAsRSV(signature);
+ const signatureBuffer = Buffer.concat([
+ ethUtil.toBuffer(ecSignatureRSV.v),
+ ethUtil.toBuffer(ecSignatureRSV.r),
+ ethUtil.toBuffer(ecSignatureRSV.s),
+ ethUtil.toBuffer(SignatureType.EIP712),
+ ]);
+ const signatureHex = `0x${signatureBuffer.toString('hex')}`;
+ return {
+ ...order,
+ signature: signatureHex,
+ };
+ } catch (err) {
+ // Detect if Metamask to transition users to the MetamaskSubprovider
+ if ((provider as any).isMetaMask) {
+ throw new Error(OrderError.InvalidMetamaskSigner);
+ } else {
+ throw err;
+ }
}
- const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex);
+ },
+ /**
+ * 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.
+ * @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> {
+ assert.isWeb3Provider('provider', provider);
+ assert.isHexString('msgHash', msgHash);
+ assert.isETHAddressHex('signerAddress', signerAddress);
+ const web3Wrapper = new Web3Wrapper(provider);
+ await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
+ const normalizedSignerAddress = signerAddress.toLowerCase();
+ const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHash);
+ const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(msgHash);
// HACK: There is no consensus on whether the signatureHex string should be formatted as
// v + r + s OR r + s + v, and different clients (even different versions of the same client)
@@ -238,10 +294,7 @@ export const signatureUtils = {
normalizedSignerAddress,
);
if (isValidRSVSignature) {
- const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
- ecSignatureRSV,
- signerType,
- );
+ const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(ecSignatureRSV);
return convertedSignatureHex;
}
}
@@ -253,41 +306,30 @@ export const signatureUtils = {
normalizedSignerAddress,
);
if (isValidVRSSignature) {
- const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
- ecSignatureVRS,
- signerType,
- );
+ const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(ecSignatureVRS);
return convertedSignatureHex;
}
}
-
- throw new Error(OrderError.InvalidSignature);
+ // Detect if Metamask to transition users to the MetamaskSubprovider
+ if ((provider as any).isMetaMask) {
+ throw new Error(OrderError.InvalidMetamaskSigner);
+ } else {
+ throw new Error(OrderError.InvalidSignature);
+ }
},
/**
- * Combines ECSignature with V,R,S and the relevant signature type for use in 0x protocol
+ * Combines ECSignature with V,R,S and the EthSign signature type for use in 0x protocol
* @param ecSignature The ECSignature of the signed data
- * @param signerType The SignerType of the signed data
* @return Hex encoded string of signature (v,r,s) with Signature Type
*/
- convertECSignatureToSignatureHex(ecSignature: ECSignature, signerType: SignerType): string {
+ convertECSignatureToSignatureHex(ecSignature: ECSignature): string {
const signatureBuffer = Buffer.concat([
ethUtil.toBuffer(ecSignature.v),
ethUtil.toBuffer(ecSignature.r),
ethUtil.toBuffer(ecSignature.s),
]);
const signatureHex = `0x${signatureBuffer.toString('hex')}`;
- let signatureType;
- switch (signerType) {
- case SignerType.Metamask:
- case SignerType.Ledger:
- case SignerType.Default: {
- signatureType = SignatureType.EthSign;
- break;
- }
- default:
- throw new Error(`Unrecognized SignerType: ${signerType}`);
- }
- const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, signatureType);
+ const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, SignatureType.EthSign);
return signatureWithType;
},
/**
@@ -304,28 +346,17 @@ export const signatureUtils = {
/**
* Adds the relevant prefix to the message being signed.
* @param message Message to sign
- * @param signerType The type of message prefix to add for a given SignerType. Different signers expect
- * specific message prefixes.
* @return Prefixed message
*/
- addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string {
+ addSignedMessagePrefix(message: string): string {
assert.isString('message', message);
- assert.doesBelongToStringEnum('signerType', signerType, SignerType);
- switch (signerType) {
- case SignerType.Metamask:
- case SignerType.Ledger:
- case SignerType.Default: {
- const msgBuff = ethUtil.toBuffer(message);
- const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
- const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
- return prefixedMsgHex;
- }
- default:
- throw new Error(`Unrecognized SignerType: ${signerType}`);
- }
+ const msgBuff = ethUtil.toBuffer(message);
+ const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
+ const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
+ return prefixedMsgHex;
},
/**
- * Parse a 0x protocol hex-encoded signature string into it's ECSignature components
+ * Parse a 0x protocol hex-encoded signature string into its ECSignature components
* @param signature A hex encoded ecSignature 0x Protocol signature
* @return An ECSignature object with r,s,v parameters
*/
diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts
index a843efaa4..5b13dd754 100644
--- a/packages/order-utils/src/types.ts
+++ b/packages/order-utils/src/types.ts
@@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils';
export enum OrderError {
InvalidSignature = 'INVALID_SIGNATURE',
+ InvalidMetamaskSigner = "MetaMask provider must be wrapped in a MetamaskSubprovider (from the '@0xproject/subproviders' package) in order to work with this method.",
}
export enum TradeSide {
@@ -14,24 +15,6 @@ export enum TransferType {
Fee = 'fee',
}
-export interface EIP712Parameter {
- name: string;
- type: EIP712Types;
-}
-
-export interface EIP712Schema {
- name: string;
- parameters: EIP712Parameter[];
-}
-
-export enum EIP712Types {
- Address = 'address',
- Bytes = 'bytes',
- Bytes32 = 'bytes32',
- String = 'string',
- Uint256 = 'uint256',
-}
-
export interface CreateOrderOpts {
takerAddress?: string;
senderAddress?: string;
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..d65cabe9c
--- /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_ADDRESS,
+ },
+ 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 2ca1109a1..f2d6790fb 100644
--- a/packages/order-utils/test/signature_utils_test.ts
+++ b/packages/order-utils/test/signature_utils_test.ts
@@ -1,12 +1,13 @@
-import { SignerType } from '@0xproject/types';
+import { Order, SignatureType } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types';
+import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash';
import 'mocha';
-import * as Sinon from 'sinon';
-import { generatePseudoRandomSalt } from '../src';
+import { generatePseudoRandomSalt, orderHashUtils } from '../src';
+import { constants } from '../src/constants';
import { signatureUtils } from '../src/signature_utils';
import { chaiSetup } from './utils/chai_setup';
@@ -16,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 =
@@ -115,28 +138,64 @@ describe('Signature utils', () => {
expect(salt.lessThan(twoPow256)).to.be.true();
});
});
- describe('#ecSignOrderHashAsync', () => {
- let stubs: Sinon.SinonStub[] = [];
- let makerAddress: string;
+ describe('#ecSignOrderAsync', () => {
+ it('should default to eth_sign if eth_signTypedData is unavailable', async () => {
+ const expectedSignature =
+ '0x1c3582f06356a1314dbf1c0e534c4d8e92e59b056ee607a7ff5a825f5f2cc5e6151c5cc7fdd420f5608e4d5bef108e42ad90c7a4b408caef32e24374cf387b0d7603';
+
+ const fakeProvider = {
+ async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
+ 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 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',
+ );
+ });
+ });
+ describe('#ecSignHashAsync', () => {
before(async () => {
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync();
makerAddress = availableAddreses[0];
});
- afterEach(() => {
- // clean up any stubs after the test has completed
- _.each(stubs, s => s.restore());
- stubs = [];
- });
- it('Should return the correct Signature', async () => {
+ it('should return the correct Signature', async () => {
const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
const expectedSignature =
'0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403';
- const ecSignature = await signatureUtils.ecSignOrderHashAsync(
- provider,
- orderHash,
- makerAddress,
- SignerType.Default,
- );
+ const ecSignature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
expect(ecSignature).to.equal(expectedSignature);
});
it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => {
@@ -162,12 +221,7 @@ describe('Signature utils', () => {
}
},
};
- const ecSignature = await signatureUtils.ecSignOrderHashAsync(
- fakeProvider,
- orderHash,
- makerAddress,
- SignerType.Default,
- );
+ const ecSignature = await signatureUtils.ecSignHashAsync(fakeProvider, orderHash, makerAddress);
expect(ecSignature).to.equal(expectedSignature);
});
it('should return the correct Signature for signatureHex concatenated as V + R + S', async () => {
@@ -190,64 +244,68 @@ describe('Signature utils', () => {
},
};
- const ecSignature = await signatureUtils.ecSignOrderHashAsync(
- fakeProvider,
+ const ecSignature = await signatureUtils.ecSignHashAsync(fakeProvider, orderHash, makerAddress);
+ expect(ecSignature).to.equal(expectedSignature);
+ });
+ it('should return a valid signature', async () => {
+ const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
+ const ecSignature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
+
+ const isValidSignature = await signatureUtils.isValidSignatureAsync(
+ provider,
orderHash,
+ ecSignature,
makerAddress,
- SignerType.Default,
);
- expect(ecSignature).to.equal(expectedSignature);
+ expect(isValidSignature).to.be.true();
});
- // Note this is due to a bug in Metamask where it does not prefix before signing, this is a known issue and is to be fixed in the future
- // Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
- it('should receive a payload modified with a prefix when Metamask is SignerType', async () => {
- const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
- const orderHashPrefixed = '0xae70f31d26096291aa681b26cb7574563956221d0b4213631e1ef9df675d4cba';
+ });
+ describe('#ecSignTypedDataOrderAsync', () => {
+ 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,
+ );
+ 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 =
- '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03';
- // Generated from a MM eth_sign request from 0x5409ed021d9299bf6814279a6a1411a7e866a631 signing 0xae70f31d26096291aa681b26cb7574563956221d0b4213631e1ef9df675d4cba
- const metamaskSignature =
- '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b1b';
+ '0x1cd472c439833774b55d248c31b6585f21aea1b9363ebb4ec58549e46b62eb5a6f696f5781f62de008ee7f77650ef940d99c97ec1dee67b3f5cea1bbfdfeb2eba602';
const fakeProvider = {
async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
- if (payload.method === 'eth_sign') {
- const [, message] = payload.params;
- expect(message).to.equal(orderHashPrefixed);
+ 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: metamaskSignature,
+ result: signature,
});
} else {
callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] });
}
},
};
-
- const ecSignature = await signatureUtils.ecSignOrderHashAsync(
- fakeProvider,
- orderHash,
- makerAddress,
- SignerType.Metamask,
- );
- expect(ecSignature).to.equal(expectedSignature);
- });
- it('should return a valid signature', async () => {
- const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
- const ecSignature = await signatureUtils.ecSignOrderHashAsync(
- provider,
- orderHash,
- makerAddress,
- SignerType.Default,
- );
-
- const isValidSignature = await signatureUtils.isValidSignatureAsync(
- provider,
- orderHash,
- ecSignature,
- makerAddress,
- );
- expect(isValidSignature).to.be.true();
+ const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(fakeProvider, order, makerAddress);
+ expect(signedOrder.signature).to.equal(expectedSignature);
});
});
describe('#convertECSignatureToSignatureHex', () => {
@@ -256,38 +314,11 @@ describe('Signature utils', () => {
r: '0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d64393',
s: '0x46b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf2',
};
- it('should concatenate v,r,s and append the EthSign signature type when SignerType is Default', async () => {
+ it('should concatenate v,r,s and append the EthSign signature type', async () => {
const expectedSignatureWithSignatureType =
'0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
- const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex(
- ecSignature,
- SignerType.Default,
- );
+ const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex(ecSignature);
expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType);
});
- it('should concatenate v,r,s and append the EthSign signature type when SignerType is Ledger', async () => {
- const expectedSignatureWithSignatureType =
- '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
- const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex(
- ecSignature,
- SignerType.Ledger,
- );
- expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType);
- });
- it('should concatenate v,r,s and append the EthSign signature type when SignerType is Metamask', async () => {
- const expectedSignatureWithSignatureType =
- '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
- const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex(
- ecSignature,
- SignerType.Metamask,
- );
- expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType);
- });
- it('should throw if the SignerType is invalid', async () => {
- const expectedMessage = 'Unrecognized SignerType: INVALID_SIGNER';
- expect(() =>
- signatureUtils.convertECSignatureToSignatureHex(ecSignature, 'INVALID_SIGNER' as SignerType),
- ).to.throw(expectedMessage);
- });
});
});
diff --git a/packages/order-watcher/src/index.ts b/packages/order-watcher/src/index.ts
index d7ad4fba7..d2f91eab1 100644
--- a/packages/order-watcher/src/index.ts
+++ b/packages/order-watcher/src/index.ts
@@ -12,4 +12,10 @@ export {
export { OnOrderStateChangeCallback, OrderWatcherConfig } from './types';
export { SignedOrder } from '@0xproject/types';
-export { JSONRPCRequestPayload, JSONRPCErrorCallback, Provider, JSONRPCResponsePayload } from 'ethereum-types';
+export {
+ JSONRPCRequestPayload,
+ JSONRPCErrorCallback,
+ Provider,
+ JSONRPCResponsePayload,
+ JSONRPCResponseError,
+} from 'ethereum-types';
diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts
index 15be86a9d..612d0869a 100644
--- a/packages/sol-cov/src/index.ts
+++ b/packages/sol-cov/src/index.ts
@@ -8,7 +8,13 @@ export { ProfilerSubprovider } from './profiler_subprovider';
export { RevertTraceSubprovider } from './revert_trace_subprovider';
export { ContractData } from './types';
-export { JSONRPCRequestPayload, Provider, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types';
+export {
+ JSONRPCRequestPayload,
+ Provider,
+ JSONRPCErrorCallback,
+ JSONRPCResponsePayload,
+ JSONRPCResponseError,
+} from 'ethereum-types';
export {
JSONRPCRequestPayloadWithMethod,
diff --git a/packages/subproviders/CHANGELOG.json b/packages/subproviders/CHANGELOG.json
index 97f886f64..387207b01 100644
--- a/packages/subproviders/CHANGELOG.json
+++ b/packages/subproviders/CHANGELOG.json
@@ -1,5 +1,18 @@
[
{
+ "version": "2.1.0",
+ "changes": [
+ {
+ "note": "Add `MetamaskSubprovider` to handle inconsistent JSON RPC behaviour",
+ "pr": 1102
+ },
+ {
+ "note": "Add support for `eth_signTypedData` in wallets Mnemonic, Private and EthLightWallet",
+ "pr": 1102
+ }
+ ]
+ },
+ {
"version": "2.0.7",
"changes": [
{
diff --git a/packages/subproviders/package.json b/packages/subproviders/package.json
index fad478349..ff3ab7ed5 100644
--- a/packages/subproviders/package.json
+++ b/packages/subproviders/package.json
@@ -45,7 +45,7 @@
"ethereum-types": "^1.0.11",
"ethereumjs-tx": "^1.3.5",
"ethereumjs-util": "^5.1.1",
- "ganache-core": "0xProject/ganache-core#monorepo-dep",
+ "ganache-core": "^2.2.1",
"hdkey": "^0.7.1",
"json-rpc-error": "2.0.0",
"lodash": "^4.17.5",
diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts
index b5f9b3f90..9f4dac58b 100644
--- a/packages/subproviders/src/index.ts
+++ b/packages/subproviders/src/index.ts
@@ -27,6 +27,7 @@ export { Subprovider } from './subproviders/subprovider';
export { NonceTrackerSubprovider } from './subproviders/nonce_tracker';
export { PrivateKeyWalletSubprovider } from './subproviders/private_key_wallet';
export { MnemonicWalletSubprovider } from './subproviders/mnemonic_wallet';
+export { MetamaskSubprovider } from './subproviders/metamask_subprovider';
export { EthLightwalletSubprovider } from './subproviders/eth_lightwallet_subprovider';
export {
@@ -47,6 +48,19 @@ 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';
+export {
+ JSONRPCRequestPayload,
+ Provider,
+ JSONRPCResponsePayload,
+ JSONRPCErrorCallback,
+ JSONRPCResponseError,
+} from 'ethereum-types';
diff --git a/packages/subproviders/src/subproviders/base_wallet_subprovider.ts b/packages/subproviders/src/subproviders/base_wallet_subprovider.ts
index 4342e47e9..409a0d330 100644
--- a/packages/subproviders/src/subproviders/base_wallet_subprovider.ts
+++ b/packages/subproviders/src/subproviders/base_wallet_subprovider.ts
@@ -23,6 +23,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
public abstract async getAccountsAsync(): Promise<string[]>;
public abstract async signTransactionAsync(txParams: PartialTxParams): Promise<string>;
public abstract async signPersonalMessageAsync(data: string, address: string): Promise<string>;
+ public abstract async signTypedDataAsync(address: string, typedData: any): Promise<string>;
/**
* This method conforms to the web3-provider-engine interface.
@@ -36,6 +37,8 @@ export abstract class BaseWalletSubprovider extends Subprovider {
public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> {
let accounts;
let txParams;
+ let address;
+ let typedData;
switch (payload.method) {
case 'eth_coinbase':
try {
@@ -86,7 +89,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
case 'eth_sign':
case 'personal_sign':
const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0];
- const address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1];
+ address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1];
try {
const ecSignatureHex = await this.signPersonalMessageAsync(data, address);
end(null, ecSignatureHex);
@@ -94,6 +97,15 @@ export abstract class BaseWalletSubprovider extends Subprovider {
end(err);
}
return;
+ case 'eth_signTypedData':
+ [address, typedData] = payload.params;
+ try {
+ const signature = await this.signTypedDataAsync(address, typedData);
+ end(null, signature);
+ } catch (err) {
+ end(err);
+ }
+ return;
default:
next();
diff --git a/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts b/packages/subproviders/src/subproviders/eth_lightwallet_subprovider.ts
index 6afd71422..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 MnemonicWalletSubprovider 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,26 @@ 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;
+ }
+ /**
+ * Sign an EIP712 Typed Data message. The signing address will associated with the provided address.
+ * If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData`
+ * JSON RPC request, and this method will be called auto-magically.
+ * If you are not using this via a ProviderEngine instance, you can call it directly.
+ * @param address Address of the account to sign with
+ * @param data the typed data object
+ * @return Signature hex string (order: rsv)
+ */
+ public async signTypedDataAsync(address: string, typedData: 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/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts
index 6ad5de2e2..ee8edde92 100644
--- a/packages/subproviders/src/subproviders/ledger.ts
+++ b/packages/subproviders/src/subproviders/ledger.ts
@@ -187,6 +187,16 @@ export class LedgerSubprovider extends BaseWalletSubprovider {
throw err;
}
}
+ /**
+ * eth_signTypedData is currently not supported on Ledger devices.
+ * @param address Address of the account to sign with
+ * @param data the typed data object
+ * @return Signature hex string (order: rsv)
+ */
+ // tslint:disable-next-line:prefer-function-over-method
+ public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
+ throw new Error(WalletSubproviderErrors.MethodNotSupported);
+ }
private async _createLedgerClientAsync(): Promise<LedgerEthereumClient> {
await this._connectionLock.acquire();
if (!_.isUndefined(this._ledgerClientIfExists)) {
diff --git a/packages/subproviders/src/subproviders/metamask_subprovider.ts b/packages/subproviders/src/subproviders/metamask_subprovider.ts
new file mode 100644
index 000000000..46fc2a9cd
--- /dev/null
+++ b/packages/subproviders/src/subproviders/metamask_subprovider.ts
@@ -0,0 +1,126 @@
+import { marshaller, Web3Wrapper } from '@0xproject/web3-wrapper';
+import { JSONRPCRequestPayload, Provider } from 'ethereum-types';
+import * as ethUtil from 'ethereumjs-util';
+
+import { Callback, ErrorCallback } from '../types';
+
+import { Subprovider } from './subprovider';
+
+/**
+ * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine)
+ * subprovider interface and the provider sendAsync interface.
+ * It handles inconsistencies with Metamask implementations of various JSON RPC methods.
+ * It forwards JSON RPC requests involving the domain of a signer (getAccounts,
+ * sendTransaction, signMessage etc...) to the provider instance supplied at instantiation. All other requests
+ * are passed onwards for subsequent subproviders to handle.
+ */
+export class MetamaskSubprovider extends Subprovider {
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _provider: Provider;
+ /**
+ * Instantiates a new MetamaskSubprovider
+ * @param provider Web3 provider that should handle all user account related requests
+ */
+ constructor(provider: Provider) {
+ super();
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._provider = provider;
+ }
+ /**
+ * This method conforms to the web3-provider-engine interface.
+ * It is called internally by the ProviderEngine when it is this subproviders
+ * turn to handle a JSON RPC request.
+ * @param payload JSON RPC payload
+ * @param next Callback to call if this subprovider decides not to handle the request
+ * @param end Callback to call if subprovider handled the request and wants to pass back the request.
+ */
+ // tslint:disable-next-line:prefer-function-over-method async-suffix
+ public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> {
+ let message;
+ let address;
+ switch (payload.method) {
+ case 'web3_clientVersion':
+ try {
+ const nodeVersion = await this._web3Wrapper.getNodeVersionAsync();
+ end(null, nodeVersion);
+ } catch (err) {
+ end(err);
+ }
+ return;
+ case 'eth_accounts':
+ try {
+ const accounts = await this._web3Wrapper.getAvailableAddressesAsync();
+ end(null, accounts);
+ } catch (err) {
+ end(err);
+ }
+ return;
+ case 'eth_sendTransaction':
+ const [txParams] = payload.params;
+ try {
+ const txData = marshaller.unmarshalTxData(txParams);
+ const txHash = await this._web3Wrapper.sendTransactionAsync(txData);
+ end(null, txHash);
+ } catch (err) {
+ end(err);
+ }
+ return;
+ case 'eth_sign':
+ [address, message] = payload.params;
+ try {
+ // Metamask incorrectly implements eth_sign and does not prefix the message as per the spec
+ // Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
+ const msgBuff = ethUtil.toBuffer(message);
+ const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
+ const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
+ const signature = await this._web3Wrapper.signMessageAsync(address, prefixedMsgHex);
+ signature ? end(null, signature) : end(new Error('Error performing eth_sign'), null);
+ } catch (err) {
+ end(err);
+ }
+ return;
+ case 'eth_signTypedData':
+ case 'eth_signTypedData_v3':
+ [address, message] = payload.params;
+ try {
+ // 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>({
+ method: 'eth_signTypedData_v3',
+ params: [address, messageJSON],
+ });
+ signature ? end(null, signature) : end(new Error('Error performing eth_signTypedData'), null);
+ } catch (err) {
+ end(err);
+ }
+ return;
+ default:
+ next();
+ return;
+ }
+ }
+ /**
+ * This method conforms to the provider sendAsync interface.
+ * Allowing the MetamaskSubprovider to be used as a generic provider (outside of Web3ProviderEngine) with the
+ * addition of wrapping the inconsistent Metamask behaviour
+ * @param payload JSON RPC payload
+ * @return The contents nested under the result key of the response body
+ */
+ public sendAsync(payload: JSONRPCRequestPayload, callback: ErrorCallback): void {
+ void this.handleRequest(
+ payload,
+ // handleRequest has decided to not handle this, so fall through to the provider
+ () => {
+ const sendAsync = this._provider.sendAsync.bind(this._provider);
+ sendAsync(payload, callback);
+ },
+ // handleRequest has called end and will handle this
+ (err, data) => {
+ err ? callback(err) : callback(null, { ...payload, result: data });
+ },
+ );
+ }
+}
diff --git a/packages/subproviders/src/subproviders/mnemonic_wallet.ts b/packages/subproviders/src/subproviders/mnemonic_wallet.ts
index 1495112b6..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)
@@ -108,6 +109,25 @@ export class MnemonicWalletSubprovider extends BaseWalletSubprovider {
const sig = await privateKeyWallet.signPersonalMessageAsync(data, address);
return sig;
}
+ /**
+ * Sign an EIP712 Typed Data message. The signing account will be the account
+ * associated with the provided address. If you've added this MnemonicWalletSubprovider to
+ * your app's provider, you can simply send an `eth_signTypedData` JSON RPC request, and
+ * this method will be called auto-magically. If you are not using this via a ProviderEngine
+ * instance, you can call it directly.
+ * @param address Address of the account to sign with
+ * @param data the typed data object
+ * @return Signature hex string (order: rsv)
+ */
+ public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> {
+ if (_.isUndefined(typedData)) {
+ throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage);
+ }
+ assert.isETHAddressHex('address', address);
+ const privateKeyWallet = this._privateKeyWalletForAddress(address);
+ const sig = await privateKeyWallet.signTypedDataAsync(address, typedData);
+ return sig;
+ }
private _privateKeyWalletForAddress(address: string): PrivateKeyWalletSubprovider {
const derivedKeyInfo = this._findDerivedKeyInfoForAddress(address);
const privateKeyHex = derivedKeyInfo.hdKey.privateKey.toString('hex');
diff --git a/packages/subproviders/src/subproviders/private_key_wallet.ts b/packages/subproviders/src/subproviders/private_key_wallet.ts
index 9d6fc487e..e89c4c186 100644
--- a/packages/subproviders/src/subproviders/private_key_wallet.ts
+++ b/packages/subproviders/src/subproviders/private_key_wallet.ts
@@ -1,4 +1,6 @@
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';
import * as _ from 'lodash';
@@ -23,7 +25,7 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider {
constructor(privateKey: string) {
assert.isString('privateKey', privateKey);
super();
- this._privateKeyBuffer = new Buffer(privateKey, 'hex');
+ this._privateKeyBuffer = Buffer.from(privateKey, 'hex');
this._address = `0x${ethUtil.privateToAddress(this._privateKeyBuffer).toString('hex')}`;
}
/**
@@ -84,4 +86,29 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider {
const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s);
return rpcSig;
}
+ /**
+ * Sign an EIP712 Typed Data message. The signing address will be calculated from the private key.
+ * The address must be provided it must match the address calculated from the private key.
+ * If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData`
+ * JSON RPC request, and this method will be called auto-magically.
+ * If you are not using this via a ProviderEngine instance, you can call it directly.
+ * @param address Address of the account to sign with
+ * @param data the typed data object
+ * @return Signature hex string (order: rsv)
+ */
+ public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> {
+ if (_.isUndefined(typedData)) {
+ throw new Error(WalletSubproviderErrors.DataMissingForSignTypedData);
+ }
+ assert.isETHAddressHex('address', address);
+ if (address !== this._address) {
+ throw new Error(
+ `Requested to sign message with address: ${address}, instantiated with address: ${this._address}`,
+ );
+ }
+ const dataBuff = signTypedDataUtils.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/subproviders/src/subproviders/signer.ts b/packages/subproviders/src/subproviders/signer.ts
index d5fd86897..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) {
@@ -31,6 +31,8 @@ export class SignerSubprovider extends Subprovider {
*/
// tslint:disable-next-line:prefer-function-over-method async-suffix
public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> {
+ let message;
+ let address;
switch (payload.method) {
case 'web3_clientVersion':
try {
@@ -59,7 +61,7 @@ export class SignerSubprovider extends Subprovider {
}
return;
case 'eth_sign':
- const [address, message] = payload.params;
+ [address, message] = payload.params;
try {
const signature = await this._web3Wrapper.signMessageAsync(address, message);
end(null, signature);
@@ -67,6 +69,15 @@ export class SignerSubprovider extends Subprovider {
end(err);
}
return;
+ case 'eth_signTypedData':
+ [address, message] = payload.params;
+ try {
+ const signature = await this._web3Wrapper.signTypedDataAsync(address, message);
+ end(null, signature);
+ } catch (err) {
+ end(err);
+ }
+ return;
default:
next();
return;
diff --git a/packages/subproviders/src/types.ts b/packages/subproviders/src/types.ts
index fe58bffa5..e8a47ad34 100644
--- a/packages/subproviders/src/types.ts
+++ b/packages/subproviders/src/types.ts
@@ -107,8 +107,10 @@ export interface ResponseWithTxParams {
export enum WalletSubproviderErrors {
AddressNotFound = 'ADDRESS_NOT_FOUND',
DataMissingForSignPersonalMessage = 'DATA_MISSING_FOR_SIGN_PERSONAL_MESSAGE',
+ DataMissingForSignTypedData = 'DATA_MISSING_FOR_SIGN_TYPED_DATA',
SenderInvalidOrNotSupplied = 'SENDER_INVALID_OR_NOT_SUPPLIED',
FromAddressMissingOrInvalid = 'FROM_ADDRESS_MISSING_OR_INVALID',
+ MethodNotSupported = 'METHOD_NOT_SUPPORTED',
}
export enum LedgerSubproviderErrors {
TooOldLedgerFirmware = 'TOO_OLD_LEDGER_FIRMWARE',
diff --git a/packages/subproviders/test/unit/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/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts b/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts
index f2bdda3cd..61dcbf6da 100644
--- a/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts
+++ b/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts
@@ -47,6 +47,13 @@ describe('MnemonicWalletSubprovider', () => {
const txHex = await subprovider.signTransactionAsync(txData);
expect(txHex).to.be.equal(fixtureData.TX_DATA_ACCOUNT_1_SIGNED_RESULT);
});
+ it('signs an EIP712 sign typed data message', async () => {
+ const signature = await subprovider.signTypedDataAsync(
+ fixtureData.TEST_RPC_ACCOUNT_0,
+ fixtureData.EIP712_TEST_TYPED_DATA,
+ );
+ expect(signature).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
+ });
});
describe('failure cases', () => {
it('throws an error if address is invalid ', async () => {
@@ -118,6 +125,20 @@ describe('MnemonicWalletSubprovider', () => {
});
provider.sendAsync(payload, callback);
});
+ it('signs an EIP712 sign typed data message with eth_signTypedData', (done: DoneCallback) => {
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_signTypedData',
+ params: [fixtureData.TEST_RPC_ACCOUNT_0, fixtureData.EIP712_TEST_TYPED_DATA],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
});
describe('failure cases', () => {
it('should throw if `data` param not hex when calling eth_sign', (done: DoneCallback) => {
diff --git a/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts b/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts
index 95773145f..4cd70e5ed 100644
--- a/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts
+++ b/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts
@@ -32,6 +32,13 @@ describe('PrivateKeyWalletSubprovider', () => {
const txHex = await subprovider.signTransactionAsync(fixtureData.TX_DATA);
expect(txHex).to.be.equal(fixtureData.TX_DATA_SIGNED_RESULT);
});
+ it('signs an EIP712 sign typed data message', async () => {
+ const signature = await subprovider.signTypedDataAsync(
+ fixtureData.TEST_RPC_ACCOUNT_0,
+ fixtureData.EIP712_TEST_TYPED_DATA,
+ );
+ expect(signature).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
+ });
});
});
describe('calls through a provider', () => {
@@ -103,6 +110,20 @@ describe('PrivateKeyWalletSubprovider', () => {
});
provider.sendAsync(payload, callback);
});
+ it('signs an EIP712 sign typed data message with eth_signTypedData', (done: DoneCallback) => {
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_signTypedData',
+ params: [fixtureData.TEST_RPC_ACCOUNT_0, fixtureData.EIP712_TEST_TYPED_DATA],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
});
describe('failure cases', () => {
it('should throw if `data` param not hex when calling eth_sign', (done: DoneCallback) => {
diff --git a/packages/subproviders/test/utils/fixture_data.ts b/packages/subproviders/test/utils/fixture_data.ts
index 7cf502c97..3eb4493b5 100644
--- a/packages/subproviders/test/utils/fixture_data.ts
+++ b/packages/subproviders/test/utils/fixture_data.ts
@@ -30,4 +30,35 @@ export const fixtureData = {
'0xf85f8080822710940000000000000000000000000000000000000000808078a0712854c73c69445cc1b22a7c3d7312ff9a97fe4ffba35fd636e8236b211b6e7ca0647cee031615e52d916c7c707025bc64ad525d8f1b9876c3435a863b42743178',
TX_DATA_ACCOUNT_1_SIGNED_RESULT:
'0xf85f8080822710940000000000000000000000000000000000000000808078a04b02af7ff3f18ce114b601542cc8ebdc50921354f75dd510d31793453a0710e6a0540082a01e475465801b8186a2edc79ec1a2dcf169b9781c25a58a417023c9ca',
+ EIP712_TEST_TYPED_DATA: {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ Test: [
+ {
+ name: 'testAddress',
+ type: 'address',
+ },
+ {
+ name: 'testNumber',
+ type: 'uint256',
+ },
+ ],
+ },
+ domain: {
+ name: 'Test',
+ },
+ message: {
+ testAddress: '0x0000000000000000000000000000000000000000',
+ testNumber: '12345',
+ },
+ primaryType: 'Test',
+ },
+ EIP712_TEST_TYPED_DATA_HASH: '0xb460d69ca60383293877cd765c0f97bd832d66bca720f7e32222ce1118832493',
+ EIP712_TEST_TYPED_DATA_SIGNED_RESULT:
+ '0x20af5b6bfc3658942198d6eeda159b4ed589f90cee6eac3ba117818ffba5fd7e354a353aad93faabd6eb6c66e17921c92bd1cd09c92a770f554470dc3e254ce701',
};
diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json
index 6bb6ced70..53e1f3716 100644
--- a/packages/types/CHANGELOG.json
+++ b/packages/types/CHANGELOG.json
@@ -1,5 +1,18 @@
[
{
+ "version": "1.2.0",
+ "changes": [
+ {
+ "note": "Added `EIP712Parameter` `EIP712Types` `EIP712TypedData` for EIP712 signing",
+ "pr": 1102
+ },
+ {
+ "note": "Added `ZeroExTransaction` type for Exchange executeTransaction",
+ "pr": 1102
+ }
+ ]
+ },
+ {
"timestamp": 1538693146,
"version": "1.1.4",
"changes": [
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 3ae0536d5..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 {
@@ -143,16 +152,6 @@ export enum SignatureType {
NSignatureTypes,
}
-/**
- * The type of the Signer implementation. Some signer implementations use different message prefixes or implement different
- * eth_sign behaviour (e.g Metamask). Default assumes a spec compliant `eth_sign`.
- */
-export enum SignerType {
- Default = 'DEFAULT',
- Ledger = 'LEDGER',
- Metamask = 'METAMASK',
-}
-
export enum AssetProxyId {
ERC20 = '0xf47261b0',
ERC721 = '0x02571792',
@@ -599,3 +598,25 @@ export interface Metadata {
externalTypeToLink: ExternalTypeToLink;
externalExportToLink: ExternalExportToLink;
}
+
+export interface EIP712Parameter {
+ name: string;
+ type: string;
+}
+
+export interface EIP712Types {
+ [key: string]: EIP712Parameter[];
+}
+
+export type EIP712ObjectValue = string | number | EIP712Object;
+
+export interface EIP712Object {
+ [key: string]: EIP712ObjectValue;
+}
+
+export interface EIP712TypedData {
+ types: EIP712Types;
+ domain: EIP712Object;
+ message: EIP712Object;
+ primaryType: string;
+}
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 9d01e5bc5..0723e5788 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -9,3 +9,4 @@ export { abiUtils } from './abi_utils';
export { NULL_BYTES } from './constants';
export { errorUtils } from './error_utils';
export { fetchAsync } from './fetch_async';
+export { signTypedDataUtils } from './sign_typed_data_utils';
diff --git a/packages/utils/src/sign_typed_data_utils.ts b/packages/utils/src/sign_typed_data_utils.ts
new file mode 100644
index 000000000..cd5bcb42f
--- /dev/null
+++ b/packages/utils/src/sign_typed_data_utils.ts
@@ -0,0 +1,82 @@
+import * as ethUtil from 'ethereumjs-util';
+import * as ethers from 'ethers';
+import * as _ from 'lodash';
+
+import { EIP712Object, EIP712ObjectValue, EIP712TypedData, EIP712Types } from '@0xproject/types';
+
+export const signTypedDataUtils = {
+ /**
+ * 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 typed data.
+ */
+ generateTypedDataHash(typedData: EIP712TypedData): Buffer {
+ return ethUtil.sha3(
+ Buffer.concat([
+ Buffer.from('1901', 'hex'),
+ signTypedDataUtils._structHash('EIP712Domain', typedData.domain, typedData.types),
+ signTypedDataUtils._structHash(typedData.primaryType, typedData.message, typedData.types),
+ ]),
+ );
+ },
+ _findDependencies(primaryType: string, types: EIP712Types, found: string[] = []): string[] {
+ if (found.includes(primaryType) || types[primaryType] === undefined) {
+ return found;
+ }
+ found.push(primaryType);
+ for (const field of types[primaryType]) {
+ for (const dep of signTypedDataUtils._findDependencies(field.type, types, found)) {
+ if (!found.includes(dep)) {
+ found.push(dep);
+ }
+ }
+ }
+ return found;
+ },
+ _encodeType(primaryType: string, types: EIP712Types): string {
+ let deps = signTypedDataUtils._findDependencies(primaryType, types);
+ deps = deps.filter(d => d !== primaryType);
+ deps = [primaryType].concat(deps.sort());
+ let result = '';
+ for (const dep of deps) {
+ result += `${dep}(${types[dep].map(({ name, type }) => `${type} ${name}`).join(',')})`;
+ }
+ return result;
+ },
+ _encodeData(primaryType: string, data: EIP712Object, types: EIP712Types): string {
+ const encodedTypes = ['bytes32'];
+ const encodedValues: Array<Buffer | EIP712ObjectValue> = [signTypedDataUtils._typeHash(primaryType, types)];
+ for (const field of types[primaryType]) {
+ const value = data[field.name];
+ if (field.type === 'string' || field.type === 'bytes') {
+ const hashValue = ethUtil.sha3(value as string);
+ encodedTypes.push('bytes32');
+ encodedValues.push(hashValue);
+ } else if (types[field.type] !== undefined) {
+ encodedTypes.push('bytes32');
+ 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);
+ 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: EIP712Object, types: EIP712Types): Buffer {
+ return ethUtil.sha3(signTypedDataUtils._encodeData(primaryType, data, types));
+ },
+};
diff --git a/packages/utils/test/sign_typed_data_utils_test.ts b/packages/utils/test/sign_typed_data_utils_test.ts
new file mode 100644
index 000000000..dcba08b04
--- /dev/null
+++ b/packages/utils/test/sign_typed_data_utils_test.ts
@@ -0,0 +1,140 @@
+import * as chai from 'chai';
+import 'mocha';
+
+import { signTypedDataUtils } from '../src/sign_typed_data_utils';
+
+const expect = chai.expect;
+
+describe('signTypedDataUtils', () => {
+ describe('signTypedDataHash', () => {
+ const simpleSignTypedDataHashHex = '0xb460d69ca60383293877cd765c0f97bd832d66bca720f7e32222ce1118832493';
+ const simpleSignTypedData = {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ Test: [
+ {
+ name: 'testAddress',
+ type: 'address',
+ },
+ {
+ name: 'testNumber',
+ type: 'uint256',
+ },
+ ],
+ },
+ domain: {
+ name: 'Test',
+ },
+ message: {
+ testAddress: '0x0000000000000000000000000000000000000000',
+ testNumber: '12345',
+ },
+ primaryType: 'Test',
+ };
+ const orderSignTypedDataHashHex = '0x55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692';
+ const orderSignTypedData = {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ ],
+ Order: [
+ {
+ name: 'makerAddress',
+ type: 'address',
+ },
+ {
+ name: 'takerAddress',
+ type: 'address',
+ },
+ {
+ name: 'feeRecipientAddress',
+ type: 'address',
+ },
+ {
+ name: 'senderAddress',
+ type: 'address',
+ },
+ {
+ name: 'makerAssetAmount',
+ type: 'uint256',
+ },
+ {
+ name: 'takerAssetAmount',
+ type: 'uint256',
+ },
+ {
+ name: 'makerFee',
+ type: 'uint256',
+ },
+ {
+ name: 'takerFee',
+ type: 'uint256',
+ },
+ {
+ name: 'expirationTimeSeconds',
+ type: 'uint256',
+ },
+ {
+ name: 'salt',
+ type: 'uint256',
+ },
+ {
+ name: 'makerAssetData',
+ type: 'bytes',
+ },
+ {
+ name: 'takerAssetData',
+ type: 'bytes',
+ },
+ ],
+ },
+ domain: {
+ name: '0x Protocol',
+ version: '2',
+ verifyingContract: '0x0000000000000000000000000000000000000000',
+ },
+ message: {
+ makerAddress: '0x0000000000000000000000000000000000000000',
+ takerAddress: '0x0000000000000000000000000000000000000000',
+ makerAssetAmount: '1000000000000000000',
+ takerAssetAmount: '1000000000000000000',
+ expirationTimeSeconds: '12345',
+ makerFee: '0',
+ takerFee: '0',
+ feeRecipientAddress: '0x0000000000000000000000000000000000000000',
+ senderAddress: '0x0000000000000000000000000000000000000000',
+ salt: '12345',
+ makerAssetData: '0x0000000000000000000000000000000000000000',
+ takerAssetData: '0x0000000000000000000000000000000000000000',
+ exchangeAddress: '0x0000000000000000000000000000000000000000',
+ },
+ primaryType: 'Order',
+ };
+ it('creates a hash of the test sign typed data', () => {
+ 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.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,5 +1,19 @@
[
{
+ "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/index.ts b/packages/web3-wrapper/src/index.ts
index 7cdd25e55..9bef06fd4 100644
--- a/packages/web3-wrapper/src/index.ts
+++ b/packages/web3-wrapper/src/index.ts
@@ -30,6 +30,7 @@ export {
OpCode,
TxDataPayable,
JSONRPCResponsePayload,
+ JSONRPCResponseError,
RawLogEntry,
DecodedLogEntryEvent,
LogWithDecodedArgs,
diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts
index d52c1cb6e..726246f1a 100644
--- a/packages/web3-wrapper/src/web3_wrapper.ts
+++ b/packages/web3-wrapper/src/web3_wrapper.ts
@@ -315,6 +315,21 @@ export class Web3Wrapper {
return signData;
}
/**
+ * 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 (as RSV)
+ */
+ public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
+ assert.isETHAddressHex('address', address);
+ assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema);
+ const signData = await this.sendRawPayloadAsync<string>({
+ method: 'eth_signTypedData',
+ params: [address, typedData],
+ });
+ return signData;
+ }
+ /**
* Fetches the latest block number
* @returns Block number
*/
@@ -654,6 +669,9 @@ export class Web3Wrapper {
...payload,
};
const response = await promisify<JSONRPCResponsePayload>(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<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', () => {
it('get block number', async () => {
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts
index c420bbf3a..b1181e4c6 100644
--- a/packages/website/ts/blockchain.ts
+++ b/packages/website/ts/blockchain.ts
@@ -9,11 +9,12 @@ import {
ExchangeFillEventArgs,
IndexedFilterValues,
} from '@0xproject/contract-wrappers';
-import { assetDataUtils, orderHashUtils, signatureUtils, SignerType } from '@0xproject/order-utils';
+import { assetDataUtils, orderHashUtils, signatureUtils } from '@0xproject/order-utils';
import { EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared';
import {
ledgerEthereumBrowserClientFactoryAsync,
LedgerSubprovider,
+ MetamaskSubprovider,
RedundantSubprovider,
RPCSubprovider,
SignerSubprovider,
@@ -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 SignerSubprovider(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);
@@ -432,21 +438,7 @@ export class Blockchain {
}
this._showFlashMessageIfLedger();
const provider = this._contractWrappers.getProvider();
- const isLedgerSigner = !_.isUndefined(this._ledgerSubprovider);
- const injectedProvider = Blockchain._getInjectedWeb3().currentProvider;
- const isMetaMaskSigner = utils.getProviderType(injectedProvider) === Providers.Metamask;
- let signerType = SignerType.Default;
- if (isLedgerSigner) {
- signerType = SignerType.Ledger;
- } else if (isMetaMaskSigner) {
- signerType = SignerType.Metamask;
- }
- const ecSignatureString = await signatureUtils.ecSignOrderHashAsync(
- provider,
- orderHash,
- makerAddress,
- signerType,
- );
+ const ecSignatureString = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
this._dispatcher.updateSignature(ecSignatureString);
return ecSignatureString;
}