diff options
author | Fabio Berger <me@fabioberger.com> | 2018-08-15 05:21:47 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-08-15 05:21:47 +0800 |
commit | 2f2582a0da3095d61a99ef09744dc0995677558e (patch) | |
tree | 54989565919038c42d495dafb7426d4148605e84 /packages/order-utils | |
parent | 8169155a6547fb0283cd0f5362aad3c0b173b00b (diff) | |
parent | fadd292ecf367e42154856509d0ea0c20b23f2f1 (diff) | |
download | dexon-sol-tools-2f2582a0da3095d61a99ef09744dc0995677558e.tar dexon-sol-tools-2f2582a0da3095d61a99ef09744dc0995677558e.tar.gz dexon-sol-tools-2f2582a0da3095d61a99ef09744dc0995677558e.tar.bz2 dexon-sol-tools-2f2582a0da3095d61a99ef09744dc0995677558e.tar.lz dexon-sol-tools-2f2582a0da3095d61a99ef09744dc0995677558e.tar.xz dexon-sol-tools-2f2582a0da3095d61a99ef09744dc0995677558e.tar.zst dexon-sol-tools-2f2582a0da3095d61a99ef09744dc0995677558e.zip |
Merge development
Diffstat (limited to 'packages/order-utils')
-rw-r--r-- | packages/order-utils/CHANGELOG.json | 23 | ||||
-rw-r--r-- | packages/order-utils/CHANGELOG.md | 11 | ||||
-rw-r--r-- | packages/order-utils/package.json | 30 | ||||
-rw-r--r-- | packages/order-utils/src/constants.ts | 3 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 4 | ||||
-rw-r--r-- | packages/order-utils/src/market_utils.ts | 133 | ||||
-rw-r--r-- | packages/order-utils/src/order_factory.ts | 97 | ||||
-rw-r--r-- | packages/order-utils/src/signature_utils.ts | 128 | ||||
-rw-r--r-- | packages/order-utils/src/types.ts | 34 | ||||
-rw-r--r-- | packages/order-utils/test/market_utils_test.ts | 280 | ||||
-rw-r--r-- | packages/order-utils/test/signature_utils_test.ts | 142 | ||||
-rw-r--r-- | packages/order-utils/test/utils/test_order_factory.ts | 32 |
12 files changed, 755 insertions, 162 deletions
diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index a399f5ea1..a81eb077d 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,5 +1,28 @@ [ { + "version": "1.0.1-rc.3", + "changes": [ + { + "pr": 914, + "note": + "Update ecSignOrderHashAsync to return signature string with signature type byte. Removes messagePrefixOpts." + }, + { + "note": + "Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters", + "pr": 936 + }, + { + "note": "Added marketUtils", + "pr": 937 + }, + { + "note": "Dependencies updated" + } + ], + "timestamp": 1534210131 + }, + { "version": "1.0.1-rc.2", "changes": [ { diff --git a/packages/order-utils/CHANGELOG.md b/packages/order-utils/CHANGELOG.md index 15b7e5ca6..0df2a6a75 100644 --- a/packages/order-utils/CHANGELOG.md +++ b/packages/order-utils/CHANGELOG.md @@ -5,6 +5,13 @@ Edit the package's CHANGELOG.json file only. CHANGELOG +## v1.0.1-rc.3 - _August 13, 2018_ + + * Update ecSignOrderHashAsync to return signature string with signature type byte. Removes messagePrefixOpts. (#914) + * Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters (#936) + * Added marketUtils (#937) + * Dependencies updated + ## v1.0.1-rc.2 - _July 26, 2018_ * Dependencies updated @@ -21,7 +28,7 @@ CHANGELOG * Upgrade ethereumjs-abi dep including a fix so that addresses starting with 0 are properly decoded by `decodeERC20AssetData` -## v1.0.0-rc.1 - _July 20, 2018_ +## v1.0.0-rc.1 - _July 19, 2018_ * Refactor to work with V2 of 0x protocol (#636) * Export parseECSignature method (#684) @@ -47,7 +54,7 @@ CHANGELOG * Add orderStateUtils, a module for computing order state needed to decide if an order is still valid -## v0.0.4 - _May 5, 2018_ +## v0.0.4 - _May 4, 2018_ * Dependencies updated diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index c49abfdae..bcfb0611f 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/order-utils", - "version": "1.0.1-rc.2", + "version": "1.0.1-rc.3", "engines": { "node": ">=6.12" }, @@ -44,8 +44,8 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md", "devDependencies": { - "@0xproject/dev-utils": "^1.0.3", - "@0xproject/tslint-config": "^1.0.4", + "@0xproject/dev-utils": "^1.0.4", + "@0xproject/tslint-config": "^1.0.5", "@types/bn.js": "^4.11.0", "@types/lodash": "4.14.104", "chai": "^4.0.1", @@ -54,30 +54,30 @@ "copyfiles": "^1.2.0", "dirty-chai": "^2.0.1", "make-promises-safe": "^1.1.0", - "mocha": "^4.0.1", + "mocha": "^4.1.0", "npm-run-all": "^4.1.2", "shx": "^0.2.2", "sinon": "^4.0.0", "tslint": "5.11.0", "typedoc": "0xProject/typedoc", - "typescript": "2.7.1" + "typescript": "2.9.2" }, "dependencies": { - "@0xproject/assert": "^1.0.4", - "@0xproject/base-contract": "^1.0.4", - "@0xproject/json-schemas": "^1.0.1-rc.3", - "@0xproject/sol-compiler": "^1.0.4", - "@0xproject/types": "^1.0.1-rc.3", - "@0xproject/typescript-typings": "^1.0.3", - "@0xproject/utils": "^1.0.4", - "@0xproject/web3-wrapper": "^1.1.2", + "@0xproject/assert": "^1.0.5", + "@0xproject/base-contract": "^2.0.0-rc.1", + "@0xproject/json-schemas": "^1.0.1-rc.4", + "@0xproject/sol-compiler": "^1.0.5", + "@0xproject/types": "^1.0.1-rc.4", + "@0xproject/typescript-typings": "^1.0.4", + "@0xproject/utils": "^1.0.5", + "@0xproject/web3-wrapper": "^1.2.0", "@types/node": "^8.0.53", "bn.js": "^4.11.8", - "ethereum-types": "^1.0.3", + "ethereum-types": "^1.0.4", "ethereumjs-abi": "0.6.5", "ethereumjs-util": "^5.1.1", "ethers": "3.0.22", - "lodash": "^4.17.4" + "lodash": "^4.17.5" }, "publishConfig": { "access": "public" diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index bb7482184..c23578c20 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + NULL_BYTES: '0x', // tslint:disable-next-line:custom-no-magic-numbers UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), TESTRPC_NETWORK_ID: 50, @@ -10,4 +11,6 @@ export const constants = { ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53, SELECTOR_LENGTH: 4, BASE_16: 16, + INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite + ZERO_AMOUNT: new BigNumber(0), }; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index f3b00a583..638126af6 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -3,6 +3,7 @@ export { signatureUtils } from './signature_utils'; export { generatePseudoRandomSalt } from './salt'; export { assetDataUtils } from './asset_data_utils'; export { eip712Utils } from './eip712_utils'; +export { marketUtils } from './market_utils'; export { OrderStateUtils } from './order_state_utils'; export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; @@ -25,9 +26,8 @@ export { AssetProxyId, } from '@0xproject/types'; export { + CreateOrderOpts, OrderError, - MessagePrefixType, - MessagePrefixOpts, EIP712Parameter, EIP712Schema, EIP712Types, diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts new file mode 100644 index 000000000..681059ddf --- /dev/null +++ b/packages/order-utils/src/market_utils.ts @@ -0,0 +1,133 @@ +import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { assert } from './assert'; +import { constants } from './constants'; + +export const marketUtils = { + /** + * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, + * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. + * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. + * All orders should specify WETH as the takerAsset. + * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param slippageBufferAmount An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fill amount that could not be covered by the input. + */ + findOrdersThatCoverMakerAssetFillAmount( + signedOrders: SignedOrder[], + remainingFillableMakerAssetAmounts: BigNumber[], + makerAssetFillAmount: BigNumber, + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), + ); + assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); + assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + ); + // calculate total amount of makerAsset needed to be filled + const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); + // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount + const result = _.reduce( + signedOrders, + ({ resultOrders, remainingFillAmount }, order, index) => { + if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { + return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; + } else { + const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; + // if there is no makerAssetAmountAvailable do not append order to resultOrders + // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 + return { + resultOrders: makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT) + ? _.concat(resultOrders, order) + : resultOrders, + remainingFillAmount: BigNumber.max( + constants.ZERO_AMOUNT, + remainingFillAmount.minus(makerAssetAmountAvailable), + ), + }; + } + }, + { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount }, + ); + return result; + }, + /** + * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account + * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a + * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of + * feeOrders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param slippageBufferAmount An additional amount of fee to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fee amount that could not be covered by the input. + */ + findFeeOrdersThatCoverFeesForTargetOrders( + signedOrders: SignedOrder[], + remainingFillableMakerAssetAmounts: BigNumber[], + signedFeeOrders: SignedOrder[], + remainingFillableFeeAmounts: BigNumber[], + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } { + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), + ); + assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); + _.forEach(remainingFillableFeeAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount), + ); + assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + ); + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length', + ); + // calculate total amount of ZRX needed to fill signedOrders + const totalFeeAmount = _.reduce( + signedOrders, + (accFees, order, index) => { + const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; + const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable + .mul(order.takerFee) + .div(order.makerAssetAmount); + return accFees.plus(feeToFillMakerAssetAmountAvailable); + }, + constants.ZERO_AMOUNT, + ); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + signedFeeOrders, + remainingFillableFeeAmounts, + totalFeeAmount, + slippageBufferAmount, + ); + return { + resultOrders, + remainingFeeAmount: remainingFillAmount, + }; + // TODO: add more orders here to cover rounding + // https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx + }, +}; diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts index 14122f353..46a69ae4d 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -1,75 +1,90 @@ -import { ECSignature, SignedOrder } from '@0xproject/types'; +import { Order, SignedOrder, SignerType } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Provider } from 'ethereum-types'; -import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; +import { constants } from './constants'; import { orderHashUtils } from './order_hash'; import { generatePseudoRandomSalt } from './salt'; import { signatureUtils } from './signature_utils'; -import { MessagePrefixType } from './types'; +import { CreateOrderOpts } from './types'; export const orderFactory = { - async createSignedOrderAsync( - provider: Provider, + createOrder( makerAddress: string, - takerAddress: string, - senderAddress: string, - makerFee: BigNumber, - takerFee: BigNumber, makerAssetAmount: BigNumber, makerAssetData: string, takerAssetAmount: BigNumber, takerAssetData: string, exchangeAddress: string, - feeRecipientAddress: string, - expirationTimeSecondsIfExists?: BigNumber, - ): Promise<SignedOrder> { - const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite - const expirationTimeSeconds = _.isUndefined(expirationTimeSecondsIfExists) - ? defaultExpirationUnixTimestampSec - : expirationTimeSecondsIfExists; + createOrderOpts: CreateOrderOpts = generateDefaultCreateOrderOpts(), + ): Order { + const defaultCreateOrderOpts = generateDefaultCreateOrderOpts(); const order = { makerAddress, - takerAddress, - senderAddress, - makerFee, - takerFee, makerAssetAmount, takerAssetAmount, makerAssetData, takerAssetData, - salt: generatePseudoRandomSalt(), exchangeAddress, - feeRecipientAddress, - expirationTimeSeconds, + takerAddress: createOrderOpts.takerAddress || defaultCreateOrderOpts.takerAddress, + senderAddress: createOrderOpts.senderAddress || defaultCreateOrderOpts.senderAddress, + makerFee: createOrderOpts.makerFee || defaultCreateOrderOpts.makerFee, + takerFee: createOrderOpts.takerFee || defaultCreateOrderOpts.takerFee, + feeRecipientAddress: createOrderOpts.feeRecipientAddress || defaultCreateOrderOpts.feeRecipientAddress, + salt: createOrderOpts.salt || defaultCreateOrderOpts.salt, + expirationTimeSeconds: + createOrderOpts.expirationTimeSeconds || defaultCreateOrderOpts.expirationTimeSeconds, }; + return order; + }, + async createSignedOrderAsync( + provider: Provider, + makerAddress: string, + makerAssetAmount: BigNumber, + makerAssetData: string, + takerAssetAmount: BigNumber, + takerAssetData: string, + exchangeAddress: string, + createOrderOpts?: CreateOrderOpts, + ): Promise<SignedOrder> { + const order = orderFactory.createOrder( + makerAddress, + makerAssetAmount, + makerAssetData, + takerAssetAmount, + takerAssetData, + exchangeAddress, + createOrderOpts, + ); const orderHash = orderHashUtils.getOrderHashHex(order); - const messagePrefixOpts = { - prefixType: MessagePrefixType.EthSign, - shouldAddPrefixBeforeCallingEthSign: false, - }; - const ecSignature = await signatureUtils.ecSignOrderHashAsync( + const signature = await signatureUtils.ecSignOrderHashAsync( provider, orderHash, makerAddress, - messagePrefixOpts, + SignerType.Default, ); - const signature = getVRSHexString(ecSignature); const signedOrder: SignedOrder = _.assign(order, { signature }); return signedOrder; }, }; -function getVRSHexString(ecSignature: ECSignature): string { - const ETH_SIGN_SIGNATURE_TYPE = '03'; - const vrs = `${intToHex(ecSignature.v)}${ethUtil.stripHexPrefix(ecSignature.r)}${ethUtil.stripHexPrefix( - ecSignature.s, - )}${ETH_SIGN_SIGNATURE_TYPE}`; - return vrs; -} - -function intToHex(i: number): string { - const hex = ethUtil.bufferToHex(ethUtil.toBuffer(i)); - return hex; +function generateDefaultCreateOrderOpts(): { + takerAddress: string; + senderAddress: string; + makerFee: BigNumber; + takerFee: BigNumber; + feeRecipientAddress: string; + salt: BigNumber; + expirationTimeSeconds: BigNumber; +} { + return { + takerAddress: constants.NULL_ADDRESS, + senderAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + salt: generatePseudoRandomSalt(), + expirationTimeSeconds: constants.INFINITE_TIMESTAMP_SEC, + }; } diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index 2dc465256..d466bb00d 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -1,5 +1,5 @@ import { schemas } from '@0xproject/json-schemas'; -import { ECSignature, SignatureType, ValidatorSignature } from '@0xproject/types'; +import { ECSignature, SignatureType, SignerType, ValidatorSignature } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; @@ -10,13 +10,12 @@ import { assert } from './assert'; import { ExchangeContract } from './generated_contract_wrappers/exchange'; import { IValidatorContract } from './generated_contract_wrappers/i_validator'; import { IWalletContract } from './generated_contract_wrappers/i_wallet'; -import { MessagePrefixOpts, MessagePrefixType, OrderError } from './types'; +import { OrderError } from './types'; import { utils } from './utils'; export const signatureUtils = { /** * Verifies that the provided signature is valid according to the 0x Protocol smart contracts - * @param provider Web3 provider to use for all JSON RPC requests * @param data The hex encoded data signed by the supplied signature. * @param signature A hex encoded 0x Protocol signature made up of: [TypeSpecificData][SignatureType]. * E.g [vrs][SignatureType.EIP712] @@ -50,7 +49,7 @@ export const signatureUtils = { case SignatureType.EthSign: { const ecSignature = signatureUtils.parseECSignature(signature); - const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, MessagePrefixType.EthSign); + const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.EthSign); return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); } @@ -84,7 +83,7 @@ export const signatureUtils = { } case SignatureType.Trezor: { - const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, MessagePrefixType.Trezor); + const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.Trezor); const ecSignature = signatureUtils.parseECSignature(signature); return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); } @@ -202,23 +201,21 @@ export const signatureUtils = { } }, /** - * Signs an orderHash and returns it's elliptic curve signature. + * Signs an orderHash and returns it's elliptic curve signature and signature type. * This method currently supports TestRPC, Geth and Parity above and below V1.6.6 - * @param provider The provider to use for JSON RPC calls - * @param orderHash Hex encoded orderHash to sign. - * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address + * @param orderHash Hex encoded orderHash to sign. + * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * must be available via the Provider supplied to 0x.js. - * @param messagePrefixOpts Different signers add/require different prefixes be appended to the message being signed. - * Since we cannot know ahead of time which signer you are using, you must supply both a prefixType and - * whether it must be added before calling `eth_sign` (some signers add it themselves) - * @return An object containing the Elliptic curve signature parameters generated by signing the orderHash. + * @param signerType Different signers add/require different prefixes to be prepended to the message being signed. + * Since we cannot know ahead of time which signer you are using, you must supply a SignerType. + * @return A hex encoded string containing the Elliptic curve signature generated by signing the orderHash and the Signature Type. */ async ecSignOrderHashAsync( provider: Provider, orderHash: string, signerAddress: string, - messagePrefixOpts: MessagePrefixOpts, - ): Promise<ECSignature> { + signerType: SignerType, + ): Promise<string> { assert.isWeb3Provider('provider', provider); assert.isHexString('orderHash', orderHash); assert.isETHAddressHex('signerAddress', signerAddress); @@ -227,8 +224,10 @@ export const signatureUtils = { const normalizedSignerAddress = signerAddress.toLowerCase(); let msgHashHex = orderHash; - const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(orderHash, messagePrefixOpts.prefixType); - if (messagePrefixOpts.shouldAddPrefixBeforeCallingEthSign) { + const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(orderHash, signerType); + // Metamask incorrectly implements eth_sign and does not prefix the message as per the spec + // Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e + if (signerType === SignerType.Metamask) { msgHashHex = prefixedMsgHashHex; } const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex); @@ -237,8 +236,24 @@ export const signatureUtils = { // v + r + s OR r + s + v, and different clients (even different versions of the same client) // return the signature params in different orders. In order to support all client implementations, // we parse the signature in both ways, and evaluate if either one is a valid signature. + // r + s + v is the most prevalent format from eth_sign, so we attempt this first. // tslint:disable-next-line:custom-no-magic-numbers const validVParamValues = [27, 28]; + const ecSignatureRSV = parseSignatureHexAsRSV(signature); + if (_.includes(validVParamValues, ecSignatureRSV.v)) { + const isValidRSVSignature = signatureUtils.isValidECSignature( + prefixedMsgHashHex, + ecSignatureRSV, + normalizedSignerAddress, + ); + if (isValidRSVSignature) { + const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex( + ecSignatureRSV, + signerType, + ); + return convertedSignatureHex; + } + } const ecSignatureVRS = parseSignatureHexAsVRS(signature); if (_.includes(validVParamValues, ecSignatureVRS.v)) { const isValidVRSSignature = signatureUtils.isValidECSignature( @@ -247,54 +262,85 @@ export const signatureUtils = { normalizedSignerAddress, ); if (isValidVRSSignature) { - return ecSignatureVRS; + const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex( + ecSignatureVRS, + signerType, + ); + return convertedSignatureHex; } } - const ecSignatureRSV = parseSignatureHexAsRSV(signature); - if (_.includes(validVParamValues, ecSignatureRSV.v)) { - const isValidRSVSignature = signatureUtils.isValidECSignature( - prefixedMsgHashHex, - ecSignatureRSV, - normalizedSignerAddress, - ); - if (isValidRSVSignature) { - return ecSignatureRSV; + throw new Error(OrderError.InvalidSignature); + }, + /** + * Combines ECSignature with V,R,S and the relevant signature type for use in 0x protocol + * @param ecSignature The ECSignature of the signed data + * @param signerType The SignerType of the signed data + * @return Hex encoded string of signature (v,r,s) with Signature Type + */ + convertECSignatureToSignatureHex(ecSignature: ECSignature, signerType: SignerType): string { + const signatureBuffer = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ethUtil.toBuffer(ecSignature.r), + ethUtil.toBuffer(ecSignature.s), + ]); + const signatureHex = `0x${signatureBuffer.toString('hex')}`; + let signatureType; + switch (signerType) { + case SignerType.Metamask: + case SignerType.Ledger: + case SignerType.Default: { + signatureType = SignatureType.EthSign; + break; } + case SignerType.Trezor: { + signatureType = SignatureType.Trezor; + break; + } + default: + throw new Error(`Unrecognized SignerType: ${signerType}`); } - - throw new Error(OrderError.InvalidSignature); + const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, signatureType); + return signatureWithType; + }, + /** + * Combines the signature proof and the Signature Type. + * @param signature The hex encoded signature proof + * @param signatureType The signature type, i.e EthSign, Trezor, Wallet etc. + * @return Hex encoded string of signature proof with Signature Type + */ + convertToSignatureWithType(signature: string, signatureType: SignatureType): string { + const signatureBuffer = Buffer.concat([ethUtil.toBuffer(signature), ethUtil.toBuffer(signatureType)]); + const signatureHex = `0x${signatureBuffer.toString('hex')}`; + return signatureHex; }, /** * Adds the relevant prefix to the message being signed. * @param message Message to sign - * @param messagePrefixType The type of message prefix to add. Different signers expect + * @param signerType The type of message prefix to add for a given SignerType. Different signers expect * specific message prefixes. * @return Prefixed message */ - addSignedMessagePrefix(message: string, messagePrefixType: MessagePrefixType): string { + addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string { assert.isString('message', message); - assert.doesBelongToStringEnum('messagePrefixType', messagePrefixType, MessagePrefixType); - switch (messagePrefixType) { - case MessagePrefixType.None: - return message; - - case MessagePrefixType.EthSign: { + assert.doesBelongToStringEnum('signerType', signerType, SignerType); + switch (signerType) { + case SignerType.Metamask: + case SignerType.Ledger: + case SignerType.Default: { const msgBuff = ethUtil.toBuffer(message); const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff); const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff); return prefixedMsgHex; } - - case MessagePrefixType.Trezor: { + case SignerType.Trezor: { const msgBuff = ethUtil.toBuffer(message); const prefixedMsgBuff = hashTrezorPersonalMessage(msgBuff); const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff); return prefixedMsgHex; } - default: - throw new Error(`Unrecognized MessagePrefixType: ${messagePrefixType}`); + throw new Error(`Unrecognized SignerType: ${signerType}`); } }, /** diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index b08e74e71..1fbd8cf7b 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -1,29 +1,9 @@ +import { BigNumber } from '@0xproject/utils'; + export enum OrderError { InvalidSignature = 'INVALID_SIGNATURE', } -/** - * The requisite message prefix (is any) to add to an `eth_sign` request. - */ -export enum MessagePrefixType { - None = 'NONE', - EthSign = 'ETH_SIGN', - Trezor = 'TREZOR', -} - -/** - * Options related to message prefixing of messages sent to `eth_sign` - * Some signers prepend a message prefix (e.g Parity Signer, Ledger, TestRPC), while - * others require it already be prepended (e.g Metamask). In addition, different signers - * expect slightly different prefixes (See: https://github.com/ethereum/go-ethereum/issues/14794). - * Depending on the signer that will receive your signing request, you must specify the - * desired prefix and whether it should be added before making the `eth_sign` request. - */ -export interface MessagePrefixOpts { - prefixType: MessagePrefixType; - shouldAddPrefixBeforeCallingEthSign: boolean; -} - export enum TradeSide { Maker = 'maker', Taker = 'taker', @@ -51,3 +31,13 @@ export enum EIP712Types { String = 'string', Uint256 = 'uint256', } + +export interface CreateOrderOpts { + takerAddress?: string; + senderAddress?: string; + makerFee?: BigNumber; + takerFee?: BigNumber; + feeRecipientAddress?: string; + salt?: BigNumber; + expirationTimeSeconds?: BigNumber; +} diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts new file mode 100644 index 000000000..21c0a4802 --- /dev/null +++ b/packages/order-utils/test/market_utils_test.ts @@ -0,0 +1,280 @@ +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { constants, marketUtils } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +// tslint:disable: no-unused-expression +describe('marketUtils', () => { + describe('#findOrdersThatCoverMakerAssetFillAmount', () => { + describe('no orders', () => { + it('returns empty and unchanged remainingFillAmount', async () => { + const fillAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + [], + [], + fillAmount, + ); + expect(resultOrders).to.be.empty; + expect(remainingFillAmount).to.be.bignumber.equal(fillAmount); + }); + }); + describe('orders are completely fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => { + // try to fill 20 units of makerAsset + // include 10 units of slippageBufferAmount + const fillAmount = new BigNumber(20); + const slippageBufferAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + remainingFillableMakerAssetAmounts, + fillAmount, + slippageBufferAmount, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => { + // try to fill 15 units of makerAsset + // include 10 units of slippageBufferAmount + const fillAmount = new BigNumber(15); + const slippageBufferAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + remainingFillableMakerAssetAmounts, + fillAmount, + slippageBufferAmount, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => { + // try to fill 30 units of makerAsset + // include 5 units of slippageBufferAmount + const fillAmount = new BigNumber(30); + const slippageBufferAmount = new BigNumber(5); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + remainingFillableMakerAssetAmounts, + fillAmount, + slippageBufferAmount, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5)); + }); + it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => { + // try to fill 10 units of makerAsset + const fillAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + remainingFillableMakerAssetAmounts, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[0]]); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => { + // try to fill 15 units of makerAsset + const fillAmount = new BigNumber(15); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + remainingFillableMakerAssetAmounts, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('orders are partially fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios + // 1. order is completely filled already + // 2. order is partially fillable + // 3. order is completely fillable + const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount]; + it('returns last two orders and non-zero remainingFillAmount when trying to fill original makerAssetAmounts', async () => { + // try to fill 30 units of makerAsset + const fillAmount = new BigNumber(30); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + remainingFillableMakerAssetAmounts, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15)); + }); + }); + }); + describe('#findFeeOrdersThatCoverFeesForTargetOrders', () => { + // generate three signed fee orders each with 10 units of ZRX, 30 total + const zrxAmount = new BigNumber(10); + const inputFeeOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount: zrxAmount, + }, + 3, + ); + // generate remainingFillableFeeAmounts that equal the zrxAmount + const remainingFillableFeeAmounts = [zrxAmount, zrxAmount, zrxAmount]; + describe('no target orders', () => { + it('returns empty and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + [], + [], + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.empty; + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('no fee orders', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 10 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns empty and non-zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + [], + [], + ); + expect(resultOrders).to.be.empty; + expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); + }); + }); + describe('target orders have no fees', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns empty and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.empty; + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('target orders require fees and are completely fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 10 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns input fee orders and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.deep.equal(inputFeeOrders); + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('target orders require fees and are partially fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 10 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios + // 1. order is completely filled already + // 2. order is partially fillable + // 3. order is completely fillable + const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount]; + it('returns first two input fee orders and zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]); + expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('target orders require more fees than available', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + // each signed order requires 20 units of takerFee + const makerAssetAmount = new BigNumber(10); + const takerFee = new BigNumber(20); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerFee, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount + const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; + it('returns input fee orders and non-zero remainingFeeAmount', async () => { + const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + inputOrders, + remainingFillableMakerAssetAmounts, + inputFeeOrders, + remainingFillableFeeAmounts, + ); + expect(resultOrders).to.be.deep.equal(inputFeeOrders); + expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); + }); + }); + }); +}); diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index 2f36386ad..06ff86515 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -1,3 +1,4 @@ +import { SignerType } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types'; @@ -5,8 +6,8 @@ import * as _ from 'lodash'; import 'mocha'; import * as Sinon from 'sinon'; -import { generatePseudoRandomSalt, MessagePrefixType } from '../src'; -import { signatureUtils } from '../src/signature_utils'; +import { signatureUtils, generatePseudoRandomSalt } from '../src'; +import { convertECSignatureToSignatureHex, isValidSignatureAsync } from '../src/signature_utils'; import { chaiSetup } from './utils/chai_setup'; import { provider, web3Wrapper } from './utils/web3_wrapper'; @@ -22,8 +23,9 @@ describe('Signature utils', () => { let address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; it("should return false if the data doesn't pertain to the signature & address", async () => { + const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; expect( - await signatureUtils.isValidSignatureAsync(provider, '0x0', ethSignSignature, address), + await signatureUtils.isValidSignatureAsync(provider, bytes32Zeros, ethSignSignature, address), ).to.be.false(); }); it("should return false if the address doesn't pertain to the signature & data", async () => { @@ -139,37 +141,28 @@ describe('Signature utils', () => { _.each(stubs, s => s.restore()); stubs = []; }); - it('Should return the correct ECSignature', async () => { + it('Should return the correct Signature', async () => { const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; - const expectedECSignature = { - v: 27, - r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', - s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', - }; - const messagePrefixOpts = { - prefixType: MessagePrefixType.EthSign, - shouldAddPrefixBeforeCallingEthSign: false, - }; + const expectedSignature = + '0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403'; const ecSignature = await signatureUtils.ecSignOrderHashAsync( provider, orderHash, makerAddress, - messagePrefixOpts, + SignerType.Default, ); - expect(ecSignature).to.deep.equal(expectedECSignature); + expect(ecSignature).to.equal(expectedSignature); }); - it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => { + it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => { const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; - const expectedECSignature = { - v: 27, - r: '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113', - s: '0x7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b', - }; + const expectedSignature = + '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03'; const fakeProvider = { async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> { if (payload.method === 'eth_sign') { const [address, message] = payload.params; + expect(message).to.equal(orderHash); const signature = await web3Wrapper.signMessageAsync(address, message); // tslint:disable-next-line:custom-no-magic-numbers const rsvHex = `0x${signature.substr(130)}${signature.substr(2, 128)}`; @@ -183,26 +176,18 @@ describe('Signature utils', () => { } }, }; - - const messagePrefixOpts = { - prefixType: MessagePrefixType.EthSign, - shouldAddPrefixBeforeCallingEthSign: false, - }; const ecSignature = await signatureUtils.ecSignOrderHashAsync( fakeProvider, orderHash, makerAddress, - messagePrefixOpts, + SignerType.Default, ); - expect(ecSignature).to.deep.equal(expectedECSignature); + expect(ecSignature).to.equal(expectedSignature); }); - it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => { + it('should return the correct Signature for signatureHex concatenated as V + R + S', async () => { const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; - const expectedECSignature = { - v: 27, - r: '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113', - s: '0x7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b', - }; + const expectedSignature = + '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03'; const fakeProvider = { async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> { if (payload.method === 'eth_sign') { @@ -219,17 +204,96 @@ describe('Signature utils', () => { }, }; - const messagePrefixOpts = { - prefixType: MessagePrefixType.EthSign, - shouldAddPrefixBeforeCallingEthSign: false, + const ecSignature = await signatureUtils.ecSignOrderHashAsync( + fakeProvider, + orderHash, + makerAddress, + SignerType.Default, + ); + expect(ecSignature).to.equal(expectedSignature); + }); + // Note this is due to a bug in Metamask where it does not prefix before signing, this is a known issue and is to be fixed in the future + // Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e + it('should receive a payload modified with a prefix when Metamask is SignerType', async () => { + const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; + const orderHashPrefixed = '0xae70f31d26096291aa681b26cb7574563956221d0b4213631e1ef9df675d4cba'; + const expectedSignature = + '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03'; + // Generated from a MM eth_sign request from 0x5409ed021d9299bf6814279a6a1411a7e866a631 signing 0xae70f31d26096291aa681b26cb7574563956221d0b4213631e1ef9df675d4cba + const metamaskSignature = + '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b1b'; + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> { + if (payload.method === 'eth_sign') { + const [, message] = payload.params; + expect(message).to.equal(orderHashPrefixed); + callback(null, { + id: 42, + jsonrpc: '2.0', + result: metamaskSignature, + }); + } else { + callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] }); + } + }, }; + const ecSignature = await signatureUtils.ecSignOrderHashAsync( fakeProvider, orderHash, makerAddress, - messagePrefixOpts, + SignerType.Metamask, + ); + expect(ecSignature).to.equal(expectedSignature); + }); + it('should return a valid signature', async () => { + const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; + const ecSignature = await signatureUtils.ecSignOrderHashAsync( + provider, + orderHash, + makerAddress, + SignerType.Default, + ); + + const isValidSignature = await isValidSignatureAsync(provider, orderHash, ecSignature, makerAddress); + expect(isValidSignature).to.be.true(); + }); + }); + describe('#convertECSignatureToSignatureHex', () => { + const ecSignature: ECSignature = { + v: 27, + r: '0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d64393', + s: '0x46b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf2', + }; + it('should concatenate v,r,s and append the Trezor signature type', async () => { + const expectedSignatureWithSignatureType = + '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf208'; + const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Trezor); + expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType); + }); + it('should concatenate v,r,s and append the EthSign signature type when SignerType is Default', async () => { + const expectedSignatureWithSignatureType = + '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203'; + const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Default); + expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType); + }); + it('should concatenate v,r,s and append the EthSign signature type when SignerType is Ledger', async () => { + const expectedSignatureWithSignatureType = + '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203'; + const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Ledger); + expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType); + }); + it('should concatenate v,r,s and append the EthSign signature type when SignerType is Metamask', async () => { + const expectedSignatureWithSignatureType = + '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203'; + const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Metamask); + expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType); + }); + it('should throw if the SignerType is invalid', async () => { + const expectedMessage = 'Unrecognized SignerType: INVALID_SIGNER'; + expect(() => convertECSignatureToSignatureHex(ecSignature, 'INVALID_SIGNER' as SignerType)).to.throw( + expectedMessage, ); - expect(ecSignature).to.deep.equal(expectedECSignature); }); }); }); diff --git a/packages/order-utils/test/utils/test_order_factory.ts b/packages/order-utils/test/utils/test_order_factory.ts new file mode 100644 index 000000000..75dc6f1f2 --- /dev/null +++ b/packages/order-utils/test/utils/test_order_factory.ts @@ -0,0 +1,32 @@ +import { Order, SignedOrder } from '@0xproject/types'; +import * as _ from 'lodash'; + +import { constants, orderFactory } from '../../src'; + +const BASE_TEST_ORDER: Order = orderFactory.createOrder( + constants.NULL_ADDRESS, + constants.ZERO_AMOUNT, + constants.NULL_ADDRESS, + constants.ZERO_AMOUNT, + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, +); +const BASE_TEST_SIGNED_ORDER: SignedOrder = { + ...BASE_TEST_ORDER, + signature: constants.NULL_BYTES, +}; + +export const testOrderFactory = { + generateTestSignedOrder(partialOrder: Partial<SignedOrder>): SignedOrder { + return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder); + }, + generateTestSignedOrders(partialOrder: Partial<SignedOrder>, numOrders: number): SignedOrder[] { + const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_SIGNED_ORDER); + return _.map(baseTestOrders, order => transformObject(order, partialOrder)); + }, +}; + +function transformObject<T>(input: T, transformation: Partial<T>): T { + const copy = _.cloneDeep(input); + return _.assign(copy, transformation); +} |