aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/order-utils')
-rw-r--r--packages/order-utils/CHANGELOG.json14
-rw-r--r--packages/order-utils/src/constants.ts3
-rw-r--r--packages/order-utils/src/index.ts11
-rw-r--r--packages/order-utils/src/market_utils.ts133
-rw-r--r--packages/order-utils/src/order_factory.ts78
-rw-r--r--packages/order-utils/src/types.ts12
-rw-r--r--packages/order-utils/test/market_utils_test.ts280
-rw-r--r--packages/order-utils/test/utils/test_order_factory.ts32
8 files changed, 540 insertions, 23 deletions
diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json
index a399f5ea1..776bd67ec 100644
--- a/packages/order-utils/CHANGELOG.json
+++ b/packages/order-utils/CHANGELOG.json
@@ -1,5 +1,19 @@
[
{
+ "version": "1.0.1-rc.3",
+ "changes": [
+ {
+ "note":
+ "Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters",
+ "pr": 936
+ },
+ {
+ "note": "Added marketUtils",
+ "pr": 937
+ }
+ ]
+ },
+ {
"version": "1.0.1-rc.2",
"changes": [
{
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 76be63bb8..858f500c6 100644
--- a/packages/order-utils/src/index.ts
+++ b/packages/order-utils/src/index.ts
@@ -13,7 +13,15 @@ export { orderFactory } from './order_factory';
export { constants } from './constants';
export { crypto } from './crypto';
export { generatePseudoRandomSalt } from './salt';
-export { OrderError, MessagePrefixType, MessagePrefixOpts, EIP712Parameter, EIP712Schema, EIP712Types } from './types';
+export {
+ CreateOrderOpts,
+ OrderError,
+ MessagePrefixType,
+ MessagePrefixOpts,
+ EIP712Parameter,
+ EIP712Schema,
+ EIP712Types,
+} from './types';
export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
@@ -24,3 +32,4 @@ export { assetDataUtils } from './asset_data_utils';
export { EIP712Utils } from './eip712_utils';
export { OrderValidationUtils } from './order_validation_utils';
export { ExchangeTransferSimulator } from './exchange_transfer_simulator';
+export { marketUtils } from './market_utils';
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 803cb82b1..14727fd97 100644
--- a/packages/order-utils/src/order_factory.ts
+++ b/packages/order-utils/src/order_factory.ts
@@ -1,49 +1,63 @@
-import { ECSignature, SignedOrder } from '@0xproject/types';
+import { ECSignature, Order, SignedOrder } 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 { ecSignOrderHashAsync } from './signature_utils';
-import { MessagePrefixType } from './types';
+import { CreateOrderOpts, MessagePrefixType } 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,
@@ -56,6 +70,26 @@ export const orderFactory = {
},
};
+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,
+ };
+}
+
function getVRSHexString(ecSignature: ECSignature): string {
const ETH_SIGN_SIGNATURE_TYPE = '03';
const vrs = `${intToHex(ecSignature.v)}${ethUtil.stripHexPrefix(ecSignature.r)}${ethUtil.stripHexPrefix(
diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts
index b08e74e71..f44e94349 100644
--- a/packages/order-utils/src/types.ts
+++ b/packages/order-utils/src/types.ts
@@ -1,3 +1,5 @@
+import { BigNumber } from '@0xproject/utils';
+
export enum OrderError {
InvalidSignature = 'INVALID_SIGNATURE',
}
@@ -51,3 +53,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/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);
+}