diff options
-rw-r--r-- | packages/order-utils/src/constants.ts | 1 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 1 | ||||
-rw-r--r-- | packages/order-utils/src/market_utils.ts | 23 | ||||
-rw-r--r-- | packages/order-utils/test/market_utils_test.ts | 146 | ||||
-rw-r--r-- | packages/order-utils/test/utils/test_order_factory.ts | 63 |
5 files changed, 222 insertions, 12 deletions
diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index b18546a6c..5137ff499 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, diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 129eb0a3d..858f500c6 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -32,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 index 4ddcc6ec8..710eddf8a 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -39,10 +39,17 @@ export const marketUtils = { return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; } else { const orderState = orderStates[index]; - const makerAssetAmountAvailable = getMakerAssetAmountAvailable(orderState); + const makerAssetAmountAvailable = orderState.remainingFillableMakerAssetAmount; + // 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: _.concat(resultOrders, order), - remainingFillAmount: remainingFillAmount.minus(makerAssetAmountAvailable), + resultOrders: makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT) + ? _.concat(resultOrders, order) + : resultOrders, + remainingFillAmount: BigNumber.max( + constants.ZERO_AMOUNT, + remainingFillAmount.minus(makerAssetAmountAvailable), + ), }; } }, @@ -83,7 +90,7 @@ export const marketUtils = { signedOrders, (accFees, order, index) => { const orderState = orderStates[index]; - const makerAssetAmountAvailable = getMakerAssetAmountAvailable(orderState); + const makerAssetAmountAvailable = orderState.remainingFillableMakerAssetAmount; const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable .div(order.makerAssetAmount) .mul(order.takerFee); @@ -99,11 +106,3 @@ export const marketUtils = { ); }, }; - -const getMakerAssetAmountAvailable = (orderState: OrderRelevantState) => { - return BigNumber.min( - orderState.makerBalance, - orderState.remainingFillableMakerAssetAmount, - orderState.makerProxyAllowance, - ); -}; 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..93779d035 --- /dev/null +++ b/packages/order-utils/test/market_utils_test.ts @@ -0,0 +1,146 @@ +import { OrderRelevantState, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { constants, marketUtils, orderFactory } 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.only('#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 all completely fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const testOrderCount = 3; + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + testOrderCount, + ); + // generate order states that cover the required fill amount + const inputOrderStates = testOrderFactory.generateTestOrderRelevantStates( + { + remainingFillableMakerAssetAmount: makerAssetAmount, + }, + testOrderCount, + ); + it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => { + // try to fill 30 units of makerAsset + const fillAmount = new BigNumber(30); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + 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 25 units of makerAsset + const fillAmount = new BigNumber(25); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + 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 35 units of makerAsset + const fillAmount = new BigNumber(35); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + inputOrders, + inputOrderStates, + fillAmount, + ); + 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, + inputOrderStates, + 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, + inputOrderStates, + 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 testOrderCount = 3; + const makerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + }, + testOrderCount, + ); + // generate order states that cover different scenarios + // 1. order is completely filled already + // 2. order is partially fillable + // 3. order is completely fillable + const partialOrderStates: Array<Partial<OrderRelevantState>> = [ + { + remainingFillableMakerAssetAmount: constants.ZERO_AMOUNT, + }, + { + remainingFillableMakerAssetAmount: new BigNumber(5), + }, + { + remainingFillableMakerAssetAmount: makerAssetAmount, + }, + ]; + const inputOrderStates: OrderRelevantState[] = _.map( + partialOrderStates, + testOrderFactory.generateTestOrderRelevantState, + ); + it('returns last 2 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, + inputOrderStates, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15)); + }); + }); + }); + describe('#findFeeOrdersThatCoverFeesForTargetOrders', () => {}); +}); 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..2c5d8cf61 --- /dev/null +++ b/packages/order-utils/test/utils/test_order_factory.ts @@ -0,0 +1,63 @@ +import { Order, OrderRelevantState, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants, orderFactory } from '../../src'; + +const BASE_TEST_ORDER: Order = orderFactory.createOrder( + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + constants.NULL_BYTES, + constants.ZERO_AMOUNT, + constants.NULL_BYTES, + constants.NULL_ADDRESS, + constants.NULL_ADDRESS, +); +const BASE_TEST_SIGNED_ORDER: SignedOrder = { + ...BASE_TEST_ORDER, + signature: constants.NULL_BYTES, +}; +const BASE_TEST_ORDER_RELEVANT_STATE: OrderRelevantState = { + makerBalance: constants.ZERO_AMOUNT, + makerProxyAllowance: constants.ZERO_AMOUNT, + makerFeeBalance: constants.ZERO_AMOUNT, + makerFeeProxyAllowance: constants.ZERO_AMOUNT, + filledTakerAssetAmount: constants.ZERO_AMOUNT, + remainingFillableMakerAssetAmount: constants.ZERO_AMOUNT, + remainingFillableTakerAssetAmount: constants.ZERO_AMOUNT, +}; + +export const testOrderFactory = { + generateTestSignedOrder(partialOrder: Partial<SignedOrder>): SignedOrder { + return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder); + }, + generateTestSignedOrders(partialOrder: Partial<SignedOrder>, numOrders: number): SignedOrder[] { + const baseTestOrders = generateArrayOfInput(BASE_TEST_SIGNED_ORDER, numOrders); + return transformObjects(baseTestOrders, partialOrder); + }, + generateTestOrderRelevantState(partialOrderRelevantState: Partial<OrderRelevantState>): OrderRelevantState { + return transformObject(BASE_TEST_ORDER_RELEVANT_STATE, partialOrderRelevantState); + }, + generateTestOrderRelevantStates( + partialOrderRelevantState: Partial<OrderRelevantState>, + numOrderStates: number, + ): OrderRelevantState[] { + const baseTestOrderStates = generateArrayOfInput(BASE_TEST_ORDER_RELEVANT_STATE, numOrderStates); + return transformObjects(baseTestOrderStates, partialOrderRelevantState); + }, +}; + +function generateArrayOfInput<T>(input: T, rangeLength: number): T[] { + return _.map(_.range(rangeLength), () => input); +} +function transformObject<T>(input: T, transformation: Partial<T>): T { + const copy = _.cloneDeep(input); + return _.assign(copy, transformation); +} +function transformObjects<T>(inputs: T[], transformation: Partial<T>): T[] { + return _.map(inputs, input => transformObject(input, transformation)); +} |