From 05d34616b72d70ea8a76a208c633da0c5ed1166f Mon Sep 17 00:00:00 2001 From: Xianny <8582774+xianny@users.noreply.github.com> Date: Wed, 15 May 2019 18:01:55 -0400 Subject: [PATCH] fallback to eth_sign if eth_signTypedData fails (#1817) * fallback to eth_sign if eth_signTypedData fails lots of wallets/web3providers still don't support the new signing methods --- packages/contract-wrappers/CHANGELOG.json | 4 + .../contract_wrappers/coordinator_wrapper.ts | 2 +- packages/order-utils/CHANGELOG.json | 9 ++ packages/order-utils/src/signature_utils.ts | 46 +++++++ .../order-utils/test/signature_utils_test.ts | 117 +++++++++++++++++- 5 files changed, 174 insertions(+), 4 deletions(-) diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index e4e997812b..cfa1a59249 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Fix decoding bug in `DutchAuctionWrapper.decodeDutchAuctionData`", "pr": 1815 + }, + { + "note": "Fallback to eth_sign if eth_signedTypedData fails", + "pr": 1817 } ] }, diff --git a/packages/contract-wrappers/src/contract_wrappers/coordinator_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/coordinator_wrapper.ts index 44b88a99e1..2290f7727d 100644 --- a/packages/contract-wrappers/src/contract_wrappers/coordinator_wrapper.ts +++ b/packages/contract-wrappers/src/contract_wrappers/coordinator_wrapper.ts @@ -740,7 +740,7 @@ export class CoordinatorWrapper extends ContractWrapper { data, verifyingContractAddress: this.exchangeAddress, }; - const signedTransaction = await signatureUtils.ecSignTypedDataTransactionAsync( + const signedTransaction = await signatureUtils.ecSignTransactionAsync( this._web3Wrapper.getProvider(), transaction, transaction.signerAddress, diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 06f2f36d3f..b8fdd7f923 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "8.1.0", + "changes": [ + { + "note": "Add `ecSignTransactionAsync`", + "pr": 1817 + } + ] + }, { "version": "8.0.2", "changes": [ diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index bc393f3cf7..b95583f1d6 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -20,6 +20,7 @@ import * as _ from 'lodash'; import { assert } from './assert'; import { eip712Utils } from './eip712_utils'; import { orderHashUtils } from './order_hash'; +import { transactionHashUtils } from './transaction_hash'; import { TypedDataError } from './types'; import { utils } from './utils'; @@ -292,6 +293,50 @@ export const signatureUtils = { } } }, + /** + * Signs a transaction and returns a SignedZeroExTransaction. First `eth_signTypedData` is requested + * then a fallback to `eth_sign` if not available on the supplied provider. + * @param supportedProvider Web3 provider to use for all JSON RPC requests + * @param transaction The ZeroExTransaction 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 SignedTransaction containing the order and Elliptic curve signature with Signature Type. + */ + async ecSignTransactionAsync( + supportedProvider: SupportedProvider, + transaction: ZeroExTransaction, + signerAddress: string, + ): Promise { + assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]); + try { + const signedTransaction = await signatureUtils.ecSignTypedDataTransactionAsync( + supportedProvider, + transaction, + signerAddress, + ); + return signedTransaction; + } 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 transactionHash = transactionHashUtils.getTransactionHashHex(transaction); + const signatureHex = await signatureUtils.ecSignHashAsync( + supportedProvider, + transactionHash, + signerAddress, + ); + const signedTransaction = { + ...transaction, + signature: signatureHex, + }; + return signedTransaction; + } + }, /** * Signs a ZeroExTransaction using `eth_signTypedData` and returns a SignedZeroExTransaction. * @param supportedProvider Web3 provider to use for all JSON RPC requests @@ -495,3 +540,4 @@ function parseSignatureHexAsRSV(signatureHex: string): ECSignature { }; return ecSignature; } +// tslint:disable:max-file-line-count diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index 44aa729b3a..375e8251f0 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -1,4 +1,5 @@ -import { Order, SignatureType } from '@0x/types'; +import { assert } from '@0x/assert'; +import { Order, SignatureType, ZeroExTransaction } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types'; @@ -6,7 +7,7 @@ import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import 'mocha'; -import { generatePseudoRandomSalt, orderHashUtils } from '../src'; +import { generatePseudoRandomSalt, orderHashUtils, transactionHashUtils } from '../src'; import { constants } from '../src/constants'; import { signatureUtils } from '../src/signature_utils'; @@ -16,10 +17,11 @@ import { provider, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; -describe('Signature utils', () => { +describe.only('Signature utils', () => { let makerAddress: string; const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'; let order: Order; + let transaction: ZeroExTransaction; before(async () => { const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); makerAddress = availableAddreses[0]; @@ -38,6 +40,12 @@ describe('Signature utils', () => { takerAssetAmount: new BigNumber(0), expirationTimeSeconds: new BigNumber(0), }; + transaction = { + verifyingContractAddress: fakeExchangeContractAddress, + salt: generatePseudoRandomSalt(), + signerAddress: makerAddress, + data: '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0', + }; }); describe('#isValidSignatureAsync', () => { let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; @@ -197,6 +205,55 @@ describe('Signature utils', () => { ); }); }); + describe('#ecSignTransactionAsync', () => { + it('should default to eth_sign if eth_signTypedData is unavailable', async () => { + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise { + 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 signedTransaction = await signatureUtils.ecSignTransactionAsync( + fakeProvider, + transaction, + makerAddress, + ); + assert.isHexString('signedTransaction.signature', signedTransaction.signature); + }); + it('should throw if the user denies the signing request', async () => { + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise { + 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.ecSignTransactionAsync(fakeProvider, transaction, makerAddress), + ).to.to.be.rejectedWith('User denied message signature'); + }); + }); describe('#ecSignHashAsync', () => { before(async () => { const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); @@ -319,6 +376,60 @@ describe('Signature utils', () => { expect(signedOrder.signature).to.equal(expectedSignature); }); }); + describe('#ecSignTypedDataTransactionAsync', () => { + 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 transactionHashHex = transactionHashUtils.getTransactionHashHex(transaction); + const sig = ethUtil.ecsign( + ethUtil.toBuffer(transactionHashHex), + 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 signedTransaction = await signatureUtils.ecSignTypedDataTransactionAsync( + provider, + transaction, + makerAddress, + ); + const isValidSignature = await signatureUtils.isValidSignatureAsync( + provider, + transactionHashHex, + signedTransaction.signature, + makerAddress, + ); + expect(signatureHex).to.eq(signedTransaction.signature); + expect(isValidSignature).to.eq(true); + }); + it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => { + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise { + 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: signature, + }); + } else { + callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] }); + } + }, + }; + const signedTransaction = await signatureUtils.ecSignTypedDataTransactionAsync( + fakeProvider, + transaction, + makerAddress, + ); + assert.isHexString('signedTransaction.signature', signedTransaction.signature); + }); + }); describe('#convertECSignatureToSignatureHex', () => { const ecSignature: ECSignature = { v: 27,