import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { assertRoughlyEquals, constants, expect, getRandomFloat, getRandomInteger, Numberish, randomAddress, } from '@0x/contracts-test-utils'; import { Web3Wrapper } from '@0x/dev-utils'; import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types'; import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils'; import * as _ from 'lodash'; import * as TypeMoq from 'typemoq'; import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src'; import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants'; import { createFillPaths } from '../src/utils/market_operation_utils/fills'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types'; const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); // tslint:disable: custom-no-magic-numbers promise-function-async describe('MarketOperationUtils tests', () => { const CHAIN_ID = 1; const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS }; 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 bridgeData = assetDataUtils.decodeAssetDataOrThrow(assetData); if (!assetDataUtils.isERC20BridgeAssetData(bridgeData)) { throw new Error('AssetData is not ERC20BridgeAssetData'); } const { bridgeAddress } = bridgeData; switch (bridgeAddress) { case contractAddresses.kyberBridge.toLowerCase(): return ERC20BridgeSource.Kyber; case contractAddresses.eth2DaiBridge.toLowerCase(): return ERC20BridgeSource.Eth2Dai; case contractAddresses.uniswapBridge.toLowerCase(): return ERC20BridgeSource.Uniswap; case contractAddresses.uniswapV2Bridge.toLowerCase(): return ERC20BridgeSource.UniswapV2; case contractAddresses.curveBridge.toLowerCase(): return ERC20BridgeSource.Curve; case contractAddresses.mStableBridge.toLowerCase(): return ERC20BridgeSource.MStable; 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, }; function createSamplesFromRates( source: ERC20BridgeSource, inputs: Numberish[], rates: Numberish[], fillData?: FillData, ): DexSample[] { const samples: DexSample[] = []; inputs.forEach((input, i) => { const rate = rates[i]; samples.push({ source, fillData: fillData || DEFAULT_FILL_DATA[source], input: new BigNumber(input), output: new BigNumber(input) .minus(i === 0 ? 0 : samples[i - 1].input) .times(rate) .plus(i === 0 ? 0 : samples[i - 1].output) .integerValue(), }); }); return samples; } type GetMultipleQuotesOperation = ( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[], wethAddress: string, liquidityProviderAddress?: string, ) => Promise; function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { return ( sources: ERC20BridgeSource[], _makerToken: string, _takerToken: string, fillAmounts: BigNumber[], _wethAddress: string, ) => { return Promise.resolve(sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]))); }; } function callTradeOperationAndRetainLiquidityProviderParams( tradeOperation: (rates: RatesBySource) => GetMultipleQuotesOperation, rates: RatesBySource, ): [{ sources: ERC20BridgeSource[]; liquidityProviderAddress?: string }, GetMultipleQuotesOperation] { const liquidityPoolParams: { sources: ERC20BridgeSource[]; liquidityProviderAddress?: string } = { sources: [], liquidityProviderAddress: undefined, }; const fn = ( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[], wethAddress: string, _balancerPoolsCache?: any, liquidityProviderAddress?: string, ) => { liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress; liquidityPoolParams.sources = liquidityPoolParams.sources.concat(sources); return tradeOperation(rates)( sources, makerToken, takerToken, fillAmounts, wethAddress, liquidityProviderAddress, ); }; return [liquidityPoolParams, fn]; } function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { return ( sources: ERC20BridgeSource[], _makerToken: string, _takerToken: string, fillAmounts: BigNumber[], _wethAddress: string, ) => { return Promise.resolve( sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r)))), ); }; } type GetMedianRateOperation = ( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[], wethAddress: string, liquidityProviderAddress?: string, ) => BigNumber; type GetLiquidityProviderFromRegistryOperation = ( registryAddress: string, takerToken: string, makerToken: string, ) => string; function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation { return ( _sources: ERC20BridgeSource[], _makerToken: string, _takerToken: string, _fillAmounts: BigNumber[], _wethAddress: string, ) => { return new BigNumber(rate); }; } function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation { return (_registryAddress: string, _takerToken: string, _makerToken: string): string => { return NULL_ADDRESS; }; } function getLiquidityProviderFromRegistryAndReturnCallParameters( liquidityProviderAddress: string = NULL_ADDRESS, ): [ { registryAddress?: string; takerToken?: string; makerToken?: string }, GetLiquidityProviderFromRegistryOperation ] { const callArgs: { registryAddress?: string; takerToken?: string; makerToken?: string } = { registryAddress: undefined, takerToken: undefined, makerToken: undefined, }; const fn = (registryAddress: string, takerToken: string, makerToken: string): string => { callArgs.makerToken = makerToken; callArgs.takerToken = takerToken; if (registryAddress !== constants.NULL_ADDRESS) { callArgs.registryAddress = registryAddress; } return liquidityProviderAddress; }; return [callArgs, fn]; } 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), [ERC20BridgeSource.UniswapV2]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.Balancer]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.Bancor]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.Curve]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.MultiBridge]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.MStable]: _.times(NUM_SAMPLES, () => 0), }; interface FillDataBySource { [source: string]: FillData; } const DEFAULT_FILL_DATA: FillDataBySource = { [ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] }, [ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() }, [ERC20BridgeSource.Bancor]: { path: [], networkAddress: randomAddress() }, [ERC20BridgeSource.Curve]: { curve: { poolAddress: randomAddress(), tokens: [TAKER_TOKEN, MAKER_TOKEN], exchangeFunctionSelector: hexUtils.random(4), sellQuoteFunctionSelector: hexUtils.random(4), buyQuoteFunctionSelector: hexUtils.random(4), }, fromTokenIdx: 0, toTokenIdx: 1, }, }; const DEFAULT_OPS = { getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] { return orders.map(o => o.takerAssetAmount); }, getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { return orders.map(o => o.makerAssetAmount); }, getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), getMedianSellRateAsync: createGetMedianSellRate(1), getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(), }; 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('getRfqtIndicativeQuotesAsync', () => { const partialRfqt: RfqtRequestOpts = { apiKey: 'foo', takerAddress: NULL_ADDRESS, isIndicative: true, intentOnFilling: false, }; it('returns an empty array if native liquidity is excluded from the salad', async () => { const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Strict); const result = await getRfqtIndicativeQuotesAsync( MAKER_ASSET_DATA, TAKER_ASSET_DATA, MarketOperation.Sell, new BigNumber('100e18'), { rfqt: { quoteRequestor: requestor.object, ...partialRfqt }, excludedSources: [ERC20BridgeSource.Native], }, ); expect(result.length).to.eql(0); requestor.verify( r => r.requestRfqtIndicativeQuotesAsync( TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), ), TypeMoq.Times.never(), ); }); it('calls RFQT if Native source is not excluded', async () => { const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose); requestor .setup(r => r.requestRfqtIndicativeQuotesAsync( TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), ), ) .returns(() => Promise.resolve([])) .verifiable(TypeMoq.Times.once()); await getRfqtIndicativeQuotesAsync( MAKER_ASSET_DATA, TAKER_ASSET_DATA, MarketOperation.Sell, new BigNumber('100e18'), { rfqt: { quoteRequestor: requestor.object, ...partialRfqt }, excludedSources: [], }, ); requestor.verifyAll(); }); }); describe('MarketOperationUtils', () => { let marketOperationUtils: MarketOperationUtils; before(async () => { marketOperationUtils = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN); }); describe('getMarketSellOrdersAsync()', () => { const FILL_AMOUNT = new BigNumber('100e18'); const ORDERS = createOrdersFromSellRates( FILL_AMOUNT, _.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]), ); const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, sampleDistributionBase: 1, bridgeSlippage: 0, maxFallbackSlippage: 100, excludedSources: [ ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Curve, ERC20BridgeSource.Balancer, ERC20BridgeSource.MStable, ], allowFallback: false, shouldBatchBridgeOrders: false, }; beforeEach(() => { replaceSamplerOps(); }); it('queries `numSamples` samples', async () => { const numSamples = _.random(1, NUM_SAMPLES); let actualNumSamples = 0; replaceSamplerOps({ getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { actualNumSamples = amounts.length; return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); }, }); 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({ getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); expect(sourcesPolled.sort()).to.deep.equals(SELL_SOURCES.slice().sort()); }); it('polls the liquidity provider when the registry is provided in the arguments', async () => { const [args, fn] = callTradeOperationAndRetainLiquidityProviderParams( createGetMultipleSellQuotesOperationFromRates, DEFAULT_RATES, ); replaceSamplerOps({ getSellQuotesAsync: fn, }); const registryAddress = randomAddress(); const newMarketOperationUtils = new MarketOperationUtils( MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN, registryAddress, ); await newMarketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); expect(args.sources.sort()).to.deep.equals( SELL_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(), ); expect(args.liquidityProviderAddress).to.eql(registryAddress); }); it('does not poll DEXes in `excludedSources`', async () => { const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, }); expect(sourcesPolled.sort()).to.deep.equals(_.without(SELL_SOURCES, ...excludedSources).sort()); }); it('generates bridge orders with correct asset data', async () => { const improvedOrdersResponse = 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 improvedOrders = improvedOrdersResponse.optimizedOrders; 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 improvedOrdersResponse = 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 improvedOrders = improvedOrdersResponse.optimizedOrders; 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 improvedOrdersResponse = 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 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const expectedMakerAmount = order.fills[0].output; const slippage = new BigNumber(1).minus(order.makerAssetAmount.div(expectedMakerAmount.plus(1))); assertRoughlyEquals(slippage, bridgeSlippage, 1); } }); 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, 0, 0, 0]; // unused replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Native, ERC20BridgeSource.Native, ]; expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); }); const ETH_TO_MAKER_RATE = 1.5; it('factors in fees for native orders', async () => { // Native orders will have the best rates but have fees, // dropping their effective rates. const nativeFeeRate = 0.06; const rates: RatesBySource = { [ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, 0.93, 0.92, 0.91] [ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], }; const feeSchedule = { [ERC20BridgeSource.Native]: _.constant( FILL_AMOUNT.div(4) .times(nativeFeeRate) .dividedToIntegerBy(ETH_TO_MAKER_RATE), ), }; replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Native, ]; expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); }); it('factors in fees for dexes', async () => { // Kyber will have the best rates but will have fees, // dropping its effective rates. const uniswapFeeRate = 0.2; const rates: RatesBySource = { [ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], // Effectively [0.8, ~0.5, ~0, ~0] [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], }; const feeSchedule = { [ERC20BridgeSource.Uniswap]: _.constant( FILL_AMOUNT.div(4) .times(uniswapFeeRate) .dividedToIntegerBy(ETH_TO_MAKER_RATE), ), }; replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ]; expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); }); it('can mix one concave source', async () => { const rates: RatesBySource = { [ERC20BridgeSource.Kyber]: [0, 0, 0, 0], // Won't use [ERC20BridgeSource.Eth2Dai]: [0.5, 0.85, 0.75, 0.75], // Concave [ERC20BridgeSource.Uniswap]: [0.96, 0.2, 0.1, 0.1], [ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1], }; replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Native, ]; expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); }); it('fallback orders use different sources', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5]; rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = orderSources.slice(0, 4); const secondSources = orderSources.slice(4); expect(_.intersection(firstSources, secondSources)).to.be.length(0); }); it('does not create a fallback if below maxFallbackSlippage', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); }); it('is able to create a order from LiquidityProvider', async () => { const registryAddress = randomAddress(); const liquidityProviderAddress = randomAddress(); const xAsset = randomAddress(); const yAsset = randomAddress(); const toSell = fromTokenUnitAmount(10); const [getSellQuotesParams, getSellQuotesFn] = callTradeOperationAndRetainLiquidityProviderParams( createGetMultipleSellQuotesOperationFromRates, { [ERC20BridgeSource.LiquidityProvider]: createDecreasingRates(5), }, ); const [ getLiquidityProviderParams, getLiquidityProviderFn, ] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress); replaceSamplerOps({ getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT], getSellQuotesAsync: getSellQuotesFn, getLiquidityProviderFromRegistry: getLiquidityProviderFn, }); const sampler = new MarketOperationUtils( MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN, registryAddress, ); const ordersAndReport = await sampler.getMarketSellOrdersAsync( [ createOrder({ makerAssetData: assetDataUtils.encodeERC20AssetData(xAsset), takerAssetData: assetDataUtils.encodeERC20AssetData(yAsset), }), ], Web3Wrapper.toBaseUnitAmount(10, 18), { excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0, shouldBatchBridgeOrders: false }, ); const result = ordersAndReport.optimizedOrders; expect(result.length).to.eql(1); expect(result[0].makerAddress).to.eql(liquidityProviderAddress); // tslint:disable-next-line:no-unnecessary-type-assertion const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow( result[0].makerAssetData, ) as ERC20BridgeAssetData; expect(decodedAssetData.assetProxyId).to.eql(AssetProxyId.ERC20Bridge); expect(decodedAssetData.bridgeAddress).to.eql(liquidityProviderAddress); expect(result[0].takerAssetAmount).to.bignumber.eql(toSell); expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider); expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress); expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress); expect(getLiquidityProviderParams.makerToken).is.eql(yAsset); expect(getLiquidityProviderParams.takerToken).is.eql(xAsset); }); it('batches contiguous bridge sources', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01]; replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, excludedSources: [ ERC20BridgeSource.Kyber, ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve), ], shouldBatchBridgeOrders: true, }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.be.length(3); const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); expect(orderFillSources).to.deep.eq([ [ERC20BridgeSource.Uniswap], [ERC20BridgeSource.Native], [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve], ]); }); }); describe('getMarketBuyOrdersAsync()', () => { const FILL_AMOUNT = new BigNumber('100e18'); const ORDERS = createOrdersFromBuyRates( FILL_AMOUNT, _.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]), ); const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, sampleDistributionBase: 1, bridgeSlippage: 0, maxFallbackSlippage: 100, excludedSources: [ ERC20BridgeSource.Kyber, ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Curve, ERC20BridgeSource.Balancer, ERC20BridgeSource.MStable, ], allowFallback: false, shouldBatchBridgeOrders: false, }; beforeEach(() => { replaceSamplerOps(); }); it('queries `numSamples` samples', async () => { const numSamples = _.random(1, 16); let actualNumSamples = 0; replaceSamplerOps({ getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { actualNumSamples = amounts.length; return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); }, }); 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({ getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); expect(sourcesPolled.sort()).to.deep.equals(BUY_SOURCES.sort()); }); it('polls the liquidity provider when the registry is provided in the arguments', async () => { const [args, fn] = callTradeOperationAndRetainLiquidityProviderParams( createGetMultipleBuyQuotesOperationFromRates, DEFAULT_RATES, ); replaceSamplerOps({ getBuyQuotesAsync: fn, }); const registryAddress = randomAddress(); const newMarketOperationUtils = new MarketOperationUtils( MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN, registryAddress, ); await newMarketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); expect(args.sources.sort()).to.deep.eq( BUY_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(), ); expect(args.liquidityProviderAddress).to.eql(registryAddress); }); it('does not poll DEXes in `excludedSources`', async () => { const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, }); expect(sourcesPolled.sort()).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources).sort()); }); it('generates bridge orders with correct asset data', async () => { const improvedOrdersResponse = 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 improvedOrders = improvedOrdersResponse.optimizedOrders; 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 maker amount', async () => { const improvedOrdersResponse = 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 improvedOrders = improvedOrdersResponse.optimizedOrders; 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 improvedOrdersResponse = 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 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const expectedTakerAmount = order.fills[0].output; const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).minus(1); assertRoughlyEquals(slippage, bridgeSlippage, 1); } }); 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({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Native, ERC20BridgeSource.Native, ]; expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); }); const ETH_TO_TAKER_RATE = 1.5; it('factors in fees for native orders', async () => { // Native orders will have the best rates but have fees, // dropping their effective rates. const nativeFeeRate = 0.06; const rates: RatesBySource = { [ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91] [ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], }; const feeSchedule = { [ERC20BridgeSource.Native]: _.constant( FILL_AMOUNT.div(4) .times(nativeFeeRate) .dividedToIntegerBy(ETH_TO_TAKER_RATE), ), }; replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Native, ERC20BridgeSource.Native, ]; expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); }); it('factors in fees for dexes', async () => { // Uniswap will have the best rates but will have fees, // dropping its effective rates. const uniswapFeeRate = 0.2; const rates: RatesBySource = { [ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1], // Effectively [0.8, ~0.5, ~0, ~0] [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], }; const feeSchedule = { [ERC20BridgeSource.Uniswap]: _.constant( FILL_AMOUNT.div(4) .times(uniswapFeeRate) .dividedToIntegerBy(ETH_TO_TAKER_RATE), ), }; replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ]; expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); }); it('fallback orders use different sources', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5]; rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = orderSources.slice(0, 4); const secondSources = orderSources.slice(4); expect(_.intersection(firstSources, secondSources)).to.be.length(0); }); it('does not create a fallback if below maxFallbackSlippage', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); }); it('batches contiguous bridge sources', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01]; replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, shouldBatchBridgeOrders: true, }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.be.length(2); const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); expect(orderFillSources).to.deep.eq([ [ERC20BridgeSource.Native], [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], ]); }); }); }); describe('createFillPaths', () => { const takerAssetAmount = new BigNumber(5000000); const ethToOutputRate = new BigNumber(0.5); // tslint:disable-next-line:no-object-literal-type-assertion const smallOrder = { chainId: 1, makerAddress: 'SMALL_ORDER', takerAddress: NULL_ADDRESS, takerAssetAmount, makerAssetAmount: takerAssetAmount.times(2), makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, makerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', takerAssetData: '0xf47261b0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', makerFeeAssetData: '0x', takerFeeAssetData: '0x', fillableTakerAssetAmount: takerAssetAmount, fillableMakerAssetAmount: takerAssetAmount.times(2), fillableTakerFeeAmount: ZERO_AMOUNT, } as SignedOrderWithFillableAmounts; const largeOrder = { ...smallOrder, makerAddress: 'LARGE_ORDER', fillableMakerAssetAmount: smallOrder.fillableMakerAssetAmount.times(2), fillableTakerAssetAmount: smallOrder.fillableTakerAssetAmount.times(2), makerAssetAmount: smallOrder.makerAssetAmount.times(2), takerAssetAmount: smallOrder.takerAssetAmount.times(2), }; const orders = [smallOrder, largeOrder]; const feeSchedule = { [ERC20BridgeSource.Native]: _.constant(2e5), }; it('penalizes native fill based on target amount when target is smaller', () => { const path = createFillPaths({ side: MarketOperation.Sell, orders, dexQuotes: [], targetInput: takerAssetAmount.minus(1), ethToOutputRate, feeSchedule, }); expect((path[0][0].fillData as NativeFillData).order.makerAddress).to.eq(smallOrder.makerAddress); expect(path[0][0].input).to.be.bignumber.eq(takerAssetAmount.minus(1)); }); it('penalizes native fill based on available amount when target is larger', () => { const path = createFillPaths({ side: MarketOperation.Sell, orders, dexQuotes: [], targetInput: POSITIVE_INF, ethToOutputRate, feeSchedule, }); expect((path[0][0].fillData as NativeFillData).order.makerAddress).to.eq(largeOrder.makerAddress); expect((path[0][1].fillData as NativeFillData).order.makerAddress).to.eq(smallOrder.makerAddress); }); }); }); // tslint:disable-next-line: max-file-line-count