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
This commit is contained in:
Xianny 2019-05-15 18:01:55 -04:00 committed by GitHub
parent c7f474ada1
commit 05d34616b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 174 additions and 4 deletions

View File

@ -5,6 +5,10 @@
{ {
"note": "Fix decoding bug in `DutchAuctionWrapper.decodeDutchAuctionData`", "note": "Fix decoding bug in `DutchAuctionWrapper.decodeDutchAuctionData`",
"pr": 1815 "pr": 1815
},
{
"note": "Fallback to eth_sign if eth_signedTypedData fails",
"pr": 1817
} }
] ]
}, },

View File

@ -740,7 +740,7 @@ export class CoordinatorWrapper extends ContractWrapper {
data, data,
verifyingContractAddress: this.exchangeAddress, verifyingContractAddress: this.exchangeAddress,
}; };
const signedTransaction = await signatureUtils.ecSignTypedDataTransactionAsync( const signedTransaction = await signatureUtils.ecSignTransactionAsync(
this._web3Wrapper.getProvider(), this._web3Wrapper.getProvider(),
transaction, transaction,
transaction.signerAddress, transaction.signerAddress,

View File

@ -1,4 +1,13 @@
[ [
{
"version": "8.1.0",
"changes": [
{
"note": "Add `ecSignTransactionAsync`",
"pr": 1817
}
]
},
{ {
"version": "8.0.2", "version": "8.0.2",
"changes": [ "changes": [

View File

@ -20,6 +20,7 @@ import * as _ from 'lodash';
import { assert } from './assert'; import { assert } from './assert';
import { eip712Utils } from './eip712_utils'; import { eip712Utils } from './eip712_utils';
import { orderHashUtils } from './order_hash'; import { orderHashUtils } from './order_hash';
import { transactionHashUtils } from './transaction_hash';
import { TypedDataError } from './types'; import { TypedDataError } from './types';
import { utils } from './utils'; 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<SignedZeroExTransaction> {
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. * Signs a ZeroExTransaction using `eth_signTypedData` and returns a SignedZeroExTransaction.
* @param supportedProvider Web3 provider to use for all JSON RPC requests * @param supportedProvider Web3 provider to use for all JSON RPC requests
@ -495,3 +540,4 @@ function parseSignatureHexAsRSV(signatureHex: string): ECSignature {
}; };
return ecSignature; return ecSignature;
} }
// tslint:disable:max-file-line-count

View File

@ -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 { BigNumber } from '@0x/utils';
import * as chai from 'chai'; import * as chai from 'chai';
import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types'; import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types';
@ -6,7 +7,7 @@ import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash'; import * as _ from 'lodash';
import 'mocha'; import 'mocha';
import { generatePseudoRandomSalt, orderHashUtils } from '../src'; import { generatePseudoRandomSalt, orderHashUtils, transactionHashUtils } from '../src';
import { constants } from '../src/constants'; import { constants } from '../src/constants';
import { signatureUtils } from '../src/signature_utils'; import { signatureUtils } from '../src/signature_utils';
@ -16,10 +17,11 @@ import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure(); chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
describe('Signature utils', () => { describe.only('Signature utils', () => {
let makerAddress: string; let makerAddress: string;
const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'; const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48';
let order: Order; let order: Order;
let transaction: ZeroExTransaction;
before(async () => { before(async () => {
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); const availableAddreses = await web3Wrapper.getAvailableAddressesAsync();
makerAddress = availableAddreses[0]; makerAddress = availableAddreses[0];
@ -38,6 +40,12 @@ describe('Signature utils', () => {
takerAssetAmount: new BigNumber(0), takerAssetAmount: new BigNumber(0),
expirationTimeSeconds: new BigNumber(0), expirationTimeSeconds: new BigNumber(0),
}; };
transaction = {
verifyingContractAddress: fakeExchangeContractAddress,
salt: generatePseudoRandomSalt(),
signerAddress: makerAddress,
data: '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0',
};
}); });
describe('#isValidSignatureAsync', () => { describe('#isValidSignatureAsync', () => {
let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; 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<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 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<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.ecSignTransactionAsync(fakeProvider, transaction, makerAddress),
).to.to.be.rejectedWith('User denied message signature');
});
});
describe('#ecSignHashAsync', () => { describe('#ecSignHashAsync', () => {
before(async () => { before(async () => {
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); const availableAddreses = await web3Wrapper.getAvailableAddressesAsync();
@ -319,6 +376,60 @@ describe('Signature utils', () => {
expect(signedOrder.signature).to.equal(expectedSignature); 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<void> {
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', () => { describe('#convertECSignatureToSignatureHex', () => {
const ecSignature: ECSignature = { const ecSignature: ECSignature = {
v: 27, v: 27,