diff options
Diffstat (limited to 'packages/subproviders/src')
9 files changed, 259 insertions, 20 deletions
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', |