From 07926ded6ef194969ffe26e3879d6e86a0eb9c50 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 2 Oct 2018 17:32:28 +1000 Subject: Introduce Metamask Subprovider. MM has a number of inconsistencies with other providers when implementing the JSON RPC interface. This subprovider wraps those nuances so they do not leak into the rest of our code --- packages/subproviders/CHANGELOG.json | 8 ++ packages/subproviders/src/index.ts | 1 + .../src/subproviders/metamask_subprovider.ts | 124 +++++++++++++++++++++ .../src/subproviders/private_key_wallet.ts | 2 +- packages/subproviders/src/subproviders/signer.ts | 13 ++- 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 packages/subproviders/src/subproviders/metamask_subprovider.ts (limited to 'packages/subproviders') diff --git a/packages/subproviders/CHANGELOG.json b/packages/subproviders/CHANGELOG.json index 97f886f64..30887c6fe 100644 --- a/packages/subproviders/CHANGELOG.json +++ b/packages/subproviders/CHANGELOG.json @@ -1,4 +1,12 @@ [ + { + "version": "2.1.0", + "changes": [ + { + "note": "Add Metamask Subprovider to handle inconsistent JSON RPC behaviour" + } + ] + }, { "version": "2.0.7", "changes": [ diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts index b5f9b3f90..8b5446007 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 { diff --git a/packages/subproviders/src/subproviders/metamask_subprovider.ts b/packages/subproviders/src/subproviders/metamask_subprovider.ts new file mode 100644 index 000000000..724edd574 --- /dev/null +++ b/packages/subproviders/src/subproviders/metamask_subprovider.ts @@ -0,0 +1,124 @@ +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 SignerSubprovider + * @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 { + 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 has namespaced signTypedData to v3 for an indeterminate period of time. + // and expects message to be serialised as JSON + const messageJSON = JSON.stringify(message); + const signature = await this._web3Wrapper.sendRawPayloadAsync({ + 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/private_key_wallet.ts b/packages/subproviders/src/subproviders/private_key_wallet.ts index 9d6fc487e..dbd51e8d7 100644 --- a/packages/subproviders/src/subproviders/private_key_wallet.ts +++ b/packages/subproviders/src/subproviders/private_key_wallet.ts @@ -23,7 +23,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')}`; } /** diff --git a/packages/subproviders/src/subproviders/signer.ts b/packages/subproviders/src/subproviders/signer.ts index d5fd86897..6b519865f 100644 --- a/packages/subproviders/src/subproviders/signer.ts +++ b/packages/subproviders/src/subproviders/signer.ts @@ -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 { + 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; -- cgit v1.2.3