diff options
author | Brandon Millman <brandon.millman@gmail.com> | 2018-10-10 08:04:54 +0800 |
---|---|---|
committer | Brandon Millman <brandon.millman@gmail.com> | 2018-10-10 08:04:54 +0800 |
commit | 8155d311af04339c105f1a29b74f1ddbced85197 (patch) | |
tree | 9b345f05561e806b6d89cec26b371aadafb109ed /packages | |
parent | cd8e6d9cdf7d6aa1e811bb7d9d7017da342907ed (diff) | |
parent | bd06ebde8d1a5caf138ad3b36f9a5c5f255f2312 (diff) | |
download | dexon-sol-tools-8155d311af04339c105f1a29b74f1ddbced85197.tar dexon-sol-tools-8155d311af04339c105f1a29b74f1ddbced85197.tar.gz dexon-sol-tools-8155d311af04339c105f1a29b74f1ddbced85197.tar.bz2 dexon-sol-tools-8155d311af04339c105f1a29b74f1ddbced85197.tar.lz dexon-sol-tools-8155d311af04339c105f1a29b74f1ddbced85197.tar.xz dexon-sol-tools-8155d311af04339c105f1a29b74f1ddbced85197.tar.zst dexon-sol-tools-8155d311af04339c105f1a29b74f1ddbced85197.zip |
Merge branch 'development' into feature/asset-buyer/api-tweaks
* development:
Define bundlewatch.ci.repoBranchBase
Stop accesing ethers private methods
Fix merge conflicts
Fix ethers build issue
Upgrade ethers.js version
Fix branch name
Add max sizes
Use bundlewatch instead of bundlesize
Move Metamask Error to OrderErrors
Update the exported types for the packages which touch RPC providers
Throw and handle errors from Providers.
Clarifies use of schemas outside of Javascript/TypeScript.
Detect MM on signature validation failure.
Return SignedOrder from signing utils.
Update 0x.js Changelog
Add eth_signTypedData support to our wallet subproviders
Move SignTypedData to utils package
Introduce Metamask Subprovider.
Expose eth_signTypedData functionality for order signing
Diffstat (limited to 'packages')
66 files changed, 1256 insertions, 454 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/package.json b/packages/0x.js/package.json index 6a3074d26..1a57edd45 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -84,7 +84,7 @@ "@0xproject/utils": "^2.0.2", "@0xproject/web3-wrapper": "^3.0.3", "ethereum-types": "^1.0.11", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5", "web3-provider-engine": "14.0.6" }, 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/base-contract/package.json b/packages/base-contract/package.json index 7aac46ab9..4ee471219 100644 --- a/packages/base-contract/package.json +++ b/packages/base-contract/package.json @@ -45,7 +45,7 @@ "@0xproject/utils": "^2.0.2", "@0xproject/web3-wrapper": "^3.0.3", "ethereum-types": "^1.0.11", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5" }, "publishConfig": { diff --git a/packages/base-contract/src/index.ts b/packages/base-contract/src/index.ts index 981e6fca6..a8e4ad2e2 100644 --- a/packages/base-contract/src/index.ts +++ b/packages/base-contract/src/index.ts @@ -17,7 +17,7 @@ import * as _ from 'lodash'; import { formatABIDataItem } from './utils'; export interface EthersInterfaceByFunctionSignature { - [key: string]: ethers.Interface; + [key: string]: ethers.utils.Interface; } const REVERT_ERROR_SELECTOR = '08c379a0'; @@ -101,7 +101,7 @@ export class BaseContract { // if it overflows the corresponding Solidity type, there is a bug in the // encoder, or the encoder performs unsafe type coercion. public static strictArgumentEncodingCheck(inputAbi: DataItem[], args: any[]): void { - const coder = new ethers.AbiCoder(); + const coder = new ethers.utils.AbiCoder(); const params = abiUtils.parseEthersParams(inputAbi); const rawEncoded = coder.encode(inputAbi, args); const rawDecoded = coder.decode(inputAbi, rawEncoded); @@ -117,7 +117,7 @@ export class BaseContract { } } } - protected _lookupEthersInterface(functionSignature: string): ethers.Interface { + protected _lookupEthersInterface(functionSignature: string): ethers.utils.Interface { const ethersInterface = this._ethersInterfacesByFunctionSignature[functionSignature]; if (_.isUndefined(ethersInterface)) { throw new Error(`Failed to lookup method with function signature '${functionSignature}'`); @@ -154,7 +154,7 @@ export class BaseContract { this._ethersInterfacesByFunctionSignature = {}; _.each(methodAbis, methodAbi => { const functionSignature = abiUtils.getFunctionSignature(methodAbi); - this._ethersInterfacesByFunctionSignature[functionSignature] = new ethers.Interface([methodAbi]); + this._ethersInterfacesByFunctionSignature[functionSignature] = new ethers.utils.Interface([methodAbi]); }); } } diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index e83caad97..35f27d77c 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -84,7 +84,7 @@ "ethereum-types": "^1.0.11", "ethereumjs-blockstream": "6.0.0", "ethereumjs-util": "^5.1.1", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "js-sha3": "^0.7.0", "lodash": "^4.17.5", "uuid": "^3.1.0" 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/contract_templates/contract.handlebars b/packages/contract_templates/contract.handlebars index 9ae39f44f..41e5c8630 100644 --- a/packages/contract_templates/contract.handlebars +++ b/packages/contract_templates/contract.handlebars @@ -65,7 +65,7 @@ export class {{contractName}}Contract extends BaseContract { [{{> params inputs=ctor.inputs}}], BaseContract._bigNumberToString, ); - const iface = new ethers.Interface(abi); + const iface = new ethers.utils.Interface(abi); const deployInfo = iface.deployFunction; const txData = deployInfo.encode(bytecode, [{{> params inputs=ctor.inputs}}]); const web3Wrapper = new Web3Wrapper(provider); diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 081e5f8ee..01d04e645 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -84,7 +84,7 @@ "ethereum-types": "^1.0.11", "ethereumjs-abi": "0.6.5", "ethereumjs-util": "^5.1.1", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "js-combinatorics": "^0.5.3", "lodash": "^4.17.5" } 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/fill-scenarios/package.json b/packages/fill-scenarios/package.json index 0616454d6..dd6ede3e0 100644 --- a/packages/fill-scenarios/package.json +++ b/packages/fill-scenarios/package.json @@ -45,7 +45,7 @@ "@0xproject/utils": "^2.0.2", "@0xproject/web3-wrapper": "^3.0.3", "ethereum-types": "^1.0.11", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5" }, "publishConfig": { 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/metacoin/package.json b/packages/metacoin/package.json index 83082d964..58fc0b848 100644 --- a/packages/metacoin/package.json +++ b/packages/metacoin/package.json @@ -41,7 +41,7 @@ "@types/mocha": "^5.2.2", "copyfiles": "^2.0.0", "ethereum-types": "^1.0.11", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5", "run-s": "^0.0.0" }, diff --git a/packages/migrations/package.json b/packages/migrations/package.json index dcae2c562..2d1b7bfc2 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -54,7 +54,7 @@ "@0xproject/web3-wrapper": "^3.0.3", "@ledgerhq/hw-app-eth": "^4.3.0", "ethereum-types": "^1.0.11", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5" }, "optionalDependencies": { 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/package.json b/packages/order-utils/package.json index 23ed9ca12..6471de9f5 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -70,7 +70,7 @@ "ethereum-types": "^1.0.11", "ethereumjs-abi": "0.6.5", "ethereumjs-util": "^5.1.1", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5" }, "publishConfig": { 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/package.json b/packages/order-watcher/package.json index 1b075b8ea..9bdaef5b1 100644 --- a/packages/order-watcher/package.json +++ b/packages/order-watcher/package.json @@ -82,7 +82,7 @@ "bintrees": "^1.0.2", "ethereum-types": "^1.0.11", "ethereumjs-blockstream": "6.0.0", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5" }, "publishConfig": { 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/package.json b/packages/utils/package.json index f1017f84d..20b0f0c86 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -50,7 +50,7 @@ "detect-node": "2.0.3", "ethereum-types": "^1.0.11", "ethereumjs-util": "^5.1.1", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "isomorphic-fetch": "^2.2.1", "js-sha3": "^0.7.0", "lodash": "^4.17.5" diff --git a/packages/utils/src/abi_decoder.ts b/packages/utils/src/abi_decoder.ts index ea8c91d10..ac3e54efb 100644 --- a/packages/utils/src/abi_decoder.ts +++ b/packages/utils/src/abi_decoder.ts @@ -9,7 +9,7 @@ import { RawLog, SolidityTypes, } from 'ethereum-types'; -import * as ethers from 'ethers'; +import { ethers } from 'ethers'; import * as _ from 'lodash'; import { addressUtils } from './address_utils'; @@ -41,7 +41,7 @@ export class AbiDecoder { return log; } const event = this._methodIds[methodId][numIndexedArgs]; - const ethersInterface = new ethers.Interface([event]); + const ethersInterface = new ethers.utils.Interface([event]); const decodedParams: DecodedLogArgs = {}; let topicsIndex = 1; @@ -96,7 +96,7 @@ export class AbiDecoder { if (_.isUndefined(abiArray)) { return; } - const ethersInterface = new ethers.Interface(abiArray); + const ethersInterface = new ethers.utils.Interface(abiArray); _.map(abiArray, (abi: AbiDefinition) => { if (abi.type === AbiType.Event) { const topic = ethersInterface.events[abi.name].topic; 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/package.json b/packages/web3-wrapper/package.json index ef31a68dc..a3f82c87e 100644 --- a/packages/web3-wrapper/package.json +++ b/packages/web3-wrapper/package.json @@ -60,7 +60,7 @@ "@0xproject/utils": "^2.0.2", "ethereum-types": "^1.0.11", "ethereumjs-util": "^5.1.1", - "ethers": "4.0.0-beta.14", + "ethers": "~4.0.4", "lodash": "^4.17.5" }, "publishConfig": { 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/md/docs/json_schemas/1.0.0/introduction.md b/packages/website/md/docs/json_schemas/1.0.0/introduction.md index a27f4b521..5b2e90387 100644 --- a/packages/website/md/docs/json_schemas/1.0.0/introduction.md +++ b/packages/website/md/docs/json_schemas/1.0.0/introduction.md @@ -1,3 +1,3 @@ Welcome to the [@0xproject/json-schemas](https://github.com/0xProject/0x-monorepo/tree/development/packages/json-schemas) documentation! This package provides JSON schemas for validating 0x Protocol & Standard Relayer API data structures. It provides both the raw JSON schemas and a schema validator class to interact with them from a JS project. -If you are not using a Javascript-based language for your project, you can copy-paste the JSON schemas within this package and use them together with a [JSON Schema](http://json-schema.org/) implementation in your [language of choice](http://json-schema.org/implementations.html) (e.g Python, Haskell, Go, C, C++, Rust, Ruby, Scala, etc...). +If you are not using a Javascript-based language for your project, you can use a Javascript environment to render the JSON schemas within this package and use them together with a [JSON Schema](http://json-schema.org/) implementation in your [language of choice](http://json-schema.org/implementations.html) (e.g Python, Haskell, Go, C, C++, Rust, Ruby, Scala, etc...). All the schema files are currently TypeScript that require evaluation in order to be recognized as valid JSON. 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; } |