import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { assertRoughlyEquals, constants, expect, getRandomFloat, getRandomInteger, Numberish, randomAddress, } from '@0x/contracts-test-utils'; import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; import { SignedOrder } from '@0x/types'; import { BigNumber, hexUtils } from '@0x/utils'; import * as _ from 'lodash'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexSample, ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; const { BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants; // tslint:disable: custom-no-magic-numbers describe('MarketOperationUtils tests', () => { const CHAIN_ID = 1; const contractAddresses = getContractAddressesForChainOrThrow(CHAIN_ID); const ETH2DAI_BRIDGE_ADDRESS = contractAddresses.eth2DaiBridge; const KYBER_BRIDGE_ADDRESS = contractAddresses.kyberBridge; const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge; const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); let originalSamplerOperations: any; before(() => { originalSamplerOperations = DexOrderSampler.ops; }); after(() => { DexOrderSampler.ops = originalSamplerOperations; }); function createOrder(overrides?: Partial): SignedOrder { return { chainId: CHAIN_ID, exchangeAddress: contractAddresses.exchange, makerAddress: constants.NULL_ADDRESS, takerAddress: constants.NULL_ADDRESS, senderAddress: constants.NULL_ADDRESS, feeRecipientAddress: randomAddress(), salt: generatePseudoRandomSalt(), expirationTimeSeconds: getRandomInteger(0, 2 ** 64), makerAssetData: MAKER_ASSET_DATA, takerAssetData: TAKER_ASSET_DATA, makerFeeAssetData: constants.NULL_BYTES, takerFeeAssetData: constants.NULL_BYTES, makerAssetAmount: getRandomInteger(1, 1e18), takerAssetAmount: getRandomInteger(1, 1e18), makerFee: constants.ZERO_AMOUNT, takerFee: constants.ZERO_AMOUNT, signature: hexUtils.random(), ...overrides, }; } function getSourceFromAssetData(assetData: string): ERC20BridgeSource { if (assetData.length === 74) { return ERC20BridgeSource.Native; } const bridgeAddress = hexUtils.slice(assetData, 48, 68).toLowerCase(); switch (bridgeAddress) { case KYBER_BRIDGE_ADDRESS.toLowerCase(): return ERC20BridgeSource.Kyber; case ETH2DAI_BRIDGE_ADDRESS.toLowerCase(): return ERC20BridgeSource.Eth2Dai; case UNISWAP_BRIDGE_ADDRESS.toLowerCase(): return ERC20BridgeSource.Uniswap; default: break; } throw new Error(`Unknown bridge address: ${bridgeAddress}`); } function assertSamePrefix(actual: string, expected: string): void { expect(actual.substr(0, expected.length)).to.eq(expected); } function createOrdersFromSellRates(takerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] { const singleTakerAssetAmount = takerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); return rates.map(r => createOrder({ makerAssetAmount: singleTakerAssetAmount.times(r).integerValue(), takerAssetAmount: singleTakerAssetAmount, }), ); } function createOrdersFromBuyRates(makerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] { const singleMakerAssetAmount = makerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); return rates.map(r => createOrder({ makerAssetAmount: singleMakerAssetAmount, takerAssetAmount: singleMakerAssetAmount.div(r).integerValue(), }), ); } const ORDER_DOMAIN = { exchangeAddress: contractAddresses.exchange, chainId: CHAIN_ID, }; type GetQuotesOperation = (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => BigNumber[]; function createGetQuotesOperationFromSellRates(rates: Numberish[]): GetQuotesOperation { return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { return fillAmounts.map((a, i) => a.times(rates[i]).integerValue()); }; } function createGetQuotesOperationFromBuyRates(rates: Numberish[]): GetQuotesOperation { return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { return fillAmounts.map((a, i) => a.div(rates[i]).integerValue()); }; } type GetMultipleQuotesOperation = ( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[], ) => DexSample[][]; function createGetMultipleQuotesOperationFromSellRates(rates: RatesBySource): GetMultipleQuotesOperation { return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { return sources.map(s => fillAmounts.map((a, i) => ({ source: s, input: a, output: a.times(rates[s][i]).integerValue(), })), ); }; } function createGetMultipleQuotesOperationFromBuyRates(rates: RatesBySource): GetMultipleQuotesOperation { return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { return sources.map(s => fillAmounts.map((a, i) => ({ source: s, input: a, output: a.div(rates[s][i]).integerValue(), })), ); }; } function createDecreasingRates(count: number): BigNumber[] { const rates: BigNumber[] = []; const initialRate = getRandomFloat(1e-3, 1e2); _.times(count, () => getRandomFloat(0.95, 1)).forEach((r, i) => { const prevRate = i === 0 ? initialRate : rates[i - 1]; rates.push(prevRate.times(r)); }); return rates; } const NUM_SAMPLES = 3; interface RatesBySource { [source: string]: Numberish[]; } const DEFAULT_RATES: RatesBySource = { [ERC20BridgeSource.Native]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES), }; function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource { const minSourceRates = Object.keys(rates).map(s => _.last(rates[s]) as BigNumber); const bestSourceRate = BigNumber.max(...minSourceRates); let source = Object.keys(rates)[_.findIndex(minSourceRates, t => bestSourceRate.eq(t))] as ERC20BridgeSource; // Native order rates play by different rules. if (source !== ERC20BridgeSource.Native) { const nativeTotalRate = BigNumber.sum(...rates[ERC20BridgeSource.Native]).div( rates[ERC20BridgeSource.Native].length, ); if (nativeTotalRate.gt(bestSourceRate)) { source = ERC20BridgeSource.Native; } } return source; } const DEFAULT_OPS = { getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] { return orders.map(o => o.takerAssetAmount); }, getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { return orders.map(o => o.makerAssetAmount); }, getKyberSellQuotes: createGetQuotesOperationFromSellRates(DEFAULT_RATES[ERC20BridgeSource.Kyber]), getUniswapSellQuotes: createGetQuotesOperationFromSellRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]), getEth2DaiSellQuotes: createGetQuotesOperationFromSellRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]), getUniswapBuyQuotes: createGetQuotesOperationFromBuyRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]), getEth2DaiBuyQuotes: createGetQuotesOperationFromBuyRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]), getSellQuotes: createGetMultipleQuotesOperationFromSellRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleQuotesOperationFromBuyRates(DEFAULT_RATES), }; function replaceSamplerOps(ops: Partial = {}): void { DexOrderSampler.ops = { ...DEFAULT_OPS, ...ops, } as any; } const MOCK_SAMPLER = ({ async executeAsync(...ops: any[]): Promise { return ops; }, async executeBatchAsync(ops: any[]): Promise { return ops; }, } as any) as DexOrderSampler; describe('MarketOperationUtils', () => { let marketOperationUtils: MarketOperationUtils; before(async () => { marketOperationUtils = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN); }); describe('getMarketSellOrdersAsync()', () => { const FILL_AMOUNT = getRandomInteger(1, 1e18); const ORDERS = createOrdersFromSellRates( FILL_AMOUNT, _.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]), ); const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 }; beforeEach(() => { replaceSamplerOps(); }); it('queries `numSamples` samples', async () => { const numSamples = _.random(1, 16); let actualNumSamples = 0; replaceSamplerOps({ getSellQuotes: (sources, makerToken, takerToken, amounts) => { actualNumSamples = amounts.length; return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples, }); expect(actualNumSamples).eq(numSamples); }); it('polls all DEXes if `excludedSources` is empty', async () => { let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ getSellQuotes: (sources, makerToken, takerToken, amounts) => { sourcesPolled = sources.slice(); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); expect(sourcesPolled.sort()).to.deep.eq(SELL_SOURCES.slice().sort()); }); it('does not poll DEXes in `excludedSources`', async () => { const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ getSellQuotes: (sources, makerToken, takerToken, amounts) => { sourcesPolled = sources.slice(); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, }); expect(sourcesPolled.sort()).to.deep.eq(_.without(SELL_SOURCES, ...excludedSources).sort()); }); it('returns the most cost-effective single source if `runLimit == 0`', async () => { const bestSource = findSourceWithMaxOutput(DEFAULT_RATES); expect(bestSource).to.exist(''); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, runLimit: 0, }); const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData)); expect(uniqueAssetDatas).to.be.length(1); expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource); }); it('generates bridge orders with correct asset data', async () => { const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); const makerAssetDataPrefix = hexUtils.slice( assetDataUtils.encodeERC20BridgeAssetData( MAKER_TOKEN, constants.NULL_ADDRESS, constants.NULL_BYTES, ), 0, 36, ); assertSamePrefix(order.makerAssetData, makerAssetDataPrefix); expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA); } }); it('generates bridge orders with correct taker amount', async () => { const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); const totalTakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAssetAmount)); expect(totalTakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); }); it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, { ...DEFAULT_OPTS, bridgeSlippage }, ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const source = getSourceFromAssetData(order.makerAssetData); const expectedMakerAmount = FILL_AMOUNT.times(_.last(DEFAULT_RATES[source]) as BigNumber); const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber(); assertRoughlyEquals(slippage, bridgeSlippage, 8); } }); it('ignores native orders below `dustFractionThreshold`', async () => { const dustFractionThreshold = 0.01; const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN); const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount))); // Pass in an order with the globally best rate but with a dust input amount. const dustOrder = createOrder({ makerAssetAmount: dustAmount.times(maxRate.plus(0.01)), takerAssetAmount: dustAmount, }); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( _.shuffle([dustOrder, ...ORDERS]), FILL_AMOUNT, // Ignore all DEX sources so only native orders are returned. { ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: SELL_SOURCES }, ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { expect(order.takerAssetAmount).to.bignumber.gt(dustAmount); } }); it('can mix convex sources', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05]; replaceSamplerOps({ getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates), }); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false }, ); expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)); const expectedSources = [ ERC20BridgeSource.Kyber, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Native, ]; expect(orderSources).to.deep.eq(expectedSources); }); it('excludes Kyber when `noConflicts` enabled and Uniswap or Eth2Dai are used first', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.3, 0.2, 0.1, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05]; replaceSamplerOps({ getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates), }); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true }, ); expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Native, ERC20BridgeSource.Native, ]; expect(orderSources).to.deep.eq(expectedSources); }); it('excludes Uniswap and Eth2Dai when `noConflicts` enabled and Kyber is used first', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.1, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.15, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.15, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05]; replaceSamplerOps({ getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates), }); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true }, ); expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)); const expectedSources = [ ERC20BridgeSource.Kyber, ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Native, ]; expect(orderSources).to.deep.eq(expectedSources); }); }); describe('getMarketBuyOrdersAsync()', () => { const FILL_AMOUNT = getRandomInteger(1, 1e18); const ORDERS = createOrdersFromBuyRates( FILL_AMOUNT, _.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]), ); const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 }; beforeEach(() => { replaceSamplerOps(); }); it('queries `numSamples` samples', async () => { const numSamples = _.random(1, 16); let actualNumSamples = 0; replaceSamplerOps({ getBuyQuotes: (sources, makerToken, takerToken, amounts) => { actualNumSamples = amounts.length; return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples, }); expect(actualNumSamples).eq(numSamples); }); it('polls all DEXes if `excludedSources` is empty', async () => { let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ getBuyQuotes: (sources, makerToken, takerToken, amounts) => { sourcesPolled = sources.slice(); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); expect(sourcesPolled).to.deep.eq(BUY_SOURCES); }); it('does not poll DEXes in `excludedSources`', async () => { const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ getBuyQuotes: (sources, makerToken, takerToken, amounts) => { sourcesPolled = sources.slice(); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, }); expect(sourcesPolled).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources)); }); it('returns the most cost-effective single source if `runLimit == 0`', async () => { const bestSource = findSourceWithMaxOutput(DEFAULT_RATES); expect(bestSource).to.exist(''); const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, runLimit: 0, }); const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData)); expect(uniqueAssetDatas).to.be.length(1); expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource); }); it('generates bridge orders with correct asset data', async () => { const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); const makerAssetDataPrefix = hexUtils.slice( assetDataUtils.encodeERC20BridgeAssetData( MAKER_TOKEN, constants.NULL_ADDRESS, constants.NULL_BYTES, ), 0, 36, ); assertSamePrefix(order.makerAssetData, makerAssetDataPrefix); expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA); } }); it('generates bridge orders with correct taker amount', async () => { const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); const totalMakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAssetAmount)); expect(totalMakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); }); it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, { ...DEFAULT_OPTS, bridgeSlippage }, ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const source = getSourceFromAssetData(order.makerAssetData); const expectedTakerAmount = FILL_AMOUNT.div(_.last(DEFAULT_RATES[source]) as BigNumber); const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1; assertRoughlyEquals(slippage, bridgeSlippage, 8); } }); it('Ignores native orders below `dustFractionThreshold`', async () => { const dustFractionThreshold = 0.01; const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN); const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount))); // Pass in an order with the globally best rate but with a dust input amount. const dustOrder = createOrder({ makerAssetAmount: dustAmount, takerAssetAmount: dustAmount.div(maxRate.plus(0.01)).integerValue(BigNumber.ROUND_DOWN), }); const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( _.shuffle([dustOrder, ...ORDERS]), FILL_AMOUNT, // Ignore all DEX sources so only native orders are returned. { ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: BUY_SOURCES }, ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { expect(order.makerAssetAmount).to.bignumber.gt(dustAmount); } }); it('can mix convex sources', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; replaceSamplerOps({ getBuyQuotes: createGetMultipleQuotesOperationFromBuyRates(rates), }); const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 }, ); expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Native, ERC20BridgeSource.Native, ]; expect(orderSources).to.deep.eq(expectedSources); }); }); }); }); // tslint:disable-next-line: max-file-line-count