diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index dd530319af..07ecbd87e5 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add ERC20 Transformer utils and export useful constants.", "pr": 2604 + }, + { + "note": "Add `getOrderHash()`, `getExchangeTransactionHash()`, `getExchangeProxyTransactionHash()`", + "pr": 2610 } ] }, diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index 2f66f35e0e..fe4784ed27 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -64,6 +64,7 @@ }, "dependencies": { "@0x/assert": "^3.0.8", + "@0x/contract-addresses": "^4.10.0", "@0x/contract-wrappers": "^13.7.0", "@0x/json-schemas": "^5.0.8", "@0x/utils": "^5.5.0", diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index 7ef3cec026..e8f52deea8 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -1,3 +1,4 @@ +import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { BigNumber, NULL_ADDRESS, NULL_BYTES } from '@0x/utils'; import { MethodAbi } from 'ethereum-types'; @@ -150,6 +151,27 @@ export const constants = { { name: 'transactionSignature', type: 'bytes' }, ], }, + MAINNET_EXCHANGE_PROXY_DOMAIN: { + name: 'ZeroEx', + version: '1.0.0', + chainId: 1, + verifyingContract: getContractAddressesForChainOrThrow(1).exchangeProxy, + }, + EXCHANGE_PROXY_MTX_SCEHMA: { + name: 'MetaTransactionData', + parameters: [ + { name: 'signer', type: 'address' }, + { name: 'sender', type: 'address' }, + { name: 'minGasPrice', type: 'uint256' }, + { name: 'maxGasPrice', type: 'uint256' }, + { name: 'expirationTime', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'value', type: 'uint256' }, + { name: 'feeToken', type: 'address' }, + { name: 'feeAmount', type: 'uint256' }, + ], + }, ERC20_METHOD_ABI, ERC721_METHOD_ABI, MULTI_ASSET_METHOD_ABI, diff --git a/packages/order-utils/src/eip712_utils.ts b/packages/order-utils/src/eip712_utils.ts index 57fbe89815..089bfc9cb2 100644 --- a/packages/order-utils/src/eip712_utils.ts +++ b/packages/order-utils/src/eip712_utils.ts @@ -5,11 +5,12 @@ import { EIP712Object, EIP712TypedData, EIP712Types, + ExchangeProxyMetaTransaction, Order, SignedZeroExTransaction, ZeroExTransaction, } from '@0x/types'; -import { hexUtils, signTypedDataUtils } from '@0x/utils'; +import { BigNumber, hexUtils, signTypedDataUtils } from '@0x/utils'; import * as _ from 'lodash'; import { constants } from './constants'; @@ -131,4 +132,18 @@ export const eip712Utils = { ); return typedData; }, + createExchangeProxyMetaTransactionTypedData(mtx: ExchangeProxyMetaTransaction): EIP712TypedData { + return eip712Utils.createTypedData( + constants.EXCHANGE_PROXY_MTX_SCEHMA.name, + { + MetaTransactionData: constants.EXCHANGE_PROXY_MTX_SCEHMA.parameters, + }, + _.mapValues( + _.omit(mtx, 'domain'), + // tslint:disable-next-line: custom-no-magic-numbers + v => (BigNumber.isBigNumber(v) ? v.toString(10) : v), + ) as EIP712Object, + _domain, + ); + }, }; diff --git a/packages/order-utils/src/hash_utils.ts b/packages/order-utils/src/hash_utils.ts new file mode 100644 index 0000000000..57859f0843 --- /dev/null +++ b/packages/order-utils/src/hash_utils.ts @@ -0,0 +1,29 @@ +import { ExchangeProxyMetaTransaction, Order, ZeroExTransaction } from '@0x/types'; +import { hexUtils, signTypedDataUtils } from '@0x/utils'; + +import { eip712Utils } from './eip712_utils'; +import { orderHashUtils } from './order_hash_utils'; +import { transactionHashUtils } from './transaction_hash_utils'; + +/** + * Compute the EIP712 hash of an order. + */ +export function getOrderHash(order: Order): string { + return orderHashUtils.getOrderHash(order); +} + +/** + * Compute the EIP712 hash of an Exchange meta-transaction. + */ +export function getExchangeMetaTransactionHash(tx: ZeroExTransaction): string { + return transactionHashUtils.getTransactionHash(tx); +} + +/** + * Compute the EIP712 hash of an Exchange Proxy meta-transaction. + */ +export function getExchangeProxyMetaTransactionHash(mtx: ExchangeProxyMetaTransaction): string { + return hexUtils.toHex( + signTypedDataUtils.generateTypedDataHash(eip712Utils.createExchangeProxyMetaTransactionTypedData(mtx)), + ); +} diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index b35b070c69..bfc0d9506a 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -49,6 +49,8 @@ export { ZeroExTransaction, SignedZeroExTransaction, ValidatorSignature, + ExchangeProxyMetaTransaction, + SignedExchangeProxyMetaTransaction, } from '@0x/types'; export { @@ -77,6 +79,8 @@ export { decodeAffiliateFeeTransformerData, } from './transformer_data_encoders'; +export { getOrderHash, getExchangeMetaTransactionHash, getExchangeProxyMetaTransactionHash } from './hash_utils'; + import { constants } from './constants'; export const NULL_ADDRESS = constants.NULL_ADDRESS; export const NULL_BYTES = constants.NULL_BYTES; diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index a0c55179c9..8058a2cbf2 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -1,14 +1,16 @@ import { schemas } from '@0x/json-schemas'; import { ECSignature, + ExchangeProxyMetaTransaction, Order, SignatureType, + SignedExchangeProxyMetaTransaction, SignedOrder, SignedZeroExTransaction, ValidatorSignature, ZeroExTransaction, } from '@0x/types'; -import { providerUtils } from '@0x/utils'; +import { hexUtils, providerUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { SupportedProvider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; @@ -16,6 +18,7 @@ import * as _ from 'lodash'; import { assert } from './assert'; import { eip712Utils } from './eip712_utils'; +import { getExchangeProxyMetaTransactionHash } from './hash_utils'; import { orderHashUtils } from './order_hash_utils'; import { transactionHashUtils } from './transaction_hash_utils'; import { TypedDataError } from './types'; @@ -187,6 +190,96 @@ export const signatureUtils = { } } }, + /** + * Signs an Exchange Proxy meta-transaction and returns a SignedExchangeProxyMetaTransaction. + * 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 ExchangeProxyMetaTransaction 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 SignedExchangeProxyMetaTransaction containing the order and + * elliptic curve signature with Signature Type. + */ + async ecSignExchangeProxyMetaTransactionAsync( + supportedProvider: SupportedProvider, + transaction: ExchangeProxyMetaTransaction, + signerAddress: string, + ): Promise { + assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]); + try { + const signedTransaction = await signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync( + 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 = getExchangeProxyMetaTransactionHash(transaction); + const signatureHex = await signatureUtils.ecSignHashAsync( + supportedProvider, + transactionHash, + signerAddress, + ); + const signedTransaction = { + ...transaction, + signature: signatureHex, + }; + return signedTransaction; + } + }, + /** + * Signs an Exchange Proxy meta-transaction using `eth_signTypedData` and + * returns a SignedZeroExTransaction. + * @param supportedProvider Web3 provider to use for all JSON RPC requests + * @param transaction The Exchange Proxy transaction 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 SignedExchangeProxyMetaTransaction containing the + * ExchangeProxyMetaTransaction and elliptic curve signature with Signature Type. + */ + async ecSignTypedDataExchangeProxyMetaTransactionAsync( + supportedProvider: SupportedProvider, + transaction: ExchangeProxyMetaTransaction, + signerAddress: string, + ): Promise { + const provider = providerUtils.standardizeOrThrow(supportedProvider); + assert.isETHAddressHex('signerAddress', signerAddress); + assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]); + const web3Wrapper = new Web3Wrapper(provider); + await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); + const normalizedSignerAddress = signerAddress.toLowerCase(); + const typedData = eip712Utils.createExchangeProxyMetaTransactionTypedData(transaction); + try { + const signature = await web3Wrapper.signTypedDataAsync(normalizedSignerAddress, typedData); + const ecSignatureRSV = parseSignatureHexAsRSV(signature); + const signatureHex = hexUtils.concat( + ecSignatureRSV.v, + ecSignatureRSV.r, + ecSignatureRSV.s, + SignatureType.EIP712, + ); + return { + ...transaction, + signature: signatureHex, + }; + } catch (err) { + // Detect if Metamask to transition users to the MetamaskSubprovider + if ((provider as any).isMetaMask) { + throw new Error(TypedDataError.InvalidMetamaskSigner); + } else { + throw err; + } + } + }, /** * Signs a hash using `eth_sign` and returns its elliptic curve signature and signature type. * @param supportedProvider Web3 provider to use for all JSON RPC requests @@ -245,12 +338,7 @@ export const signatureUtils = { * @return Hex encoded string of signature (v,r,s) with Signature Type */ 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')}`; + const signatureHex = hexUtils.concat(ecSignature.v, ecSignature.r, ecSignature.s); const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, SignatureType.EthSign); return signatureWithType; },