930 lines
44 KiB
TypeScript
930 lines
44 KiB
TypeScript
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 { constants as assetSwapperConstants } from '../src/constants';
|
|
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 CURVE_BRIDGE_ADDRESS = contractAddresses.curveBridge;
|
|
|
|
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>): 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;
|
|
case CURVE_BRIDGE_ADDRESS.toLowerCase():
|
|
const curveSource = Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS).filter(
|
|
k => assetData.indexOf(assetSwapperConstants.DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
|
|
);
|
|
return curveSource[0] as ERC20BridgeSource;
|
|
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 createGetSellQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
|
|
return (...args) => {
|
|
const fillAmounts = args.pop() as BigNumber[];
|
|
return fillAmounts.map((a, i) => a.times(rates[i]).integerValue());
|
|
};
|
|
}
|
|
|
|
function createGetBuyQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
|
|
return (...args) => {
|
|
const fillAmounts = args.pop() as BigNumber[];
|
|
return fillAmounts.map((a, i) => a.div(rates[i]).integerValue());
|
|
};
|
|
}
|
|
|
|
type GetMultipleQuotesOperation = (
|
|
sources: ERC20BridgeSource[],
|
|
makerToken: string,
|
|
takerToken: string,
|
|
fillAmounts: BigNumber[],
|
|
liquidityProviderAddress?: string,
|
|
) => DexSample[][];
|
|
|
|
function createGetMultipleSellQuotesOperationFromRates(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 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[],
|
|
liquidityProviderAddress?: string,
|
|
) => {
|
|
liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress;
|
|
liquidityPoolParams.sources = sources;
|
|
return tradeOperation(rates)(sources, makerToken, takerToken, fillAmounts, liquidityProviderAddress);
|
|
};
|
|
return [liquidityPoolParams, fn];
|
|
}
|
|
|
|
function createGetMultipleBuyQuotesOperationFromRates(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(),
|
|
})),
|
|
);
|
|
};
|
|
}
|
|
|
|
type GetMedianRateOperation = (
|
|
sources: ERC20BridgeSource[],
|
|
makerToken: string,
|
|
takerToken: string,
|
|
fillAmounts: BigNumber[],
|
|
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[]) => {
|
|
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;
|
|
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.CurveUsdcDai]: _.times(NUM_SAMPLES, () => 0),
|
|
[ERC20BridgeSource.CurveUsdcDaiUsdt]: _.times(NUM_SAMPLES, () => 0),
|
|
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: _.times(NUM_SAMPLES, () => 0),
|
|
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: _.times(NUM_SAMPLES, () => 0),
|
|
[ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0),
|
|
};
|
|
|
|
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: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Kyber]),
|
|
getUniswapSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
|
|
getEth2DaiSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
|
|
getUniswapBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
|
|
getEth2DaiBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
|
|
getCurveSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.CurveUsdcDai]),
|
|
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
|
|
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
|
|
getMedianSellRate: createGetMedianSellRate(1),
|
|
getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(),
|
|
};
|
|
|
|
function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void {
|
|
DexOrderSampler.ops = {
|
|
...DEFAULT_OPS,
|
|
...ops,
|
|
} as any;
|
|
}
|
|
|
|
const MOCK_SAMPLER = ({
|
|
async executeAsync(...ops: any[]): Promise<any[]> {
|
|
return ops;
|
|
},
|
|
async executeBatchAsync(ops: any[]): Promise<any[]> {
|
|
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,
|
|
bridgeSlippage: 0,
|
|
excludedSources: Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
|
};
|
|
|
|
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('polls the liquidity provider when the registry is provided in the arguments', async () => {
|
|
const [args, fn] = callTradeOperationAndRetainLiquidityProviderParams(
|
|
createGetMultipleSellQuotesOperationFromRates,
|
|
DEFAULT_RATES,
|
|
);
|
|
replaceSamplerOps({
|
|
getSellQuotes: 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.eq(
|
|
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({
|
|
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('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: createGetMultipleSellQuotesOperationFromRates(rates),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
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: createGetMultipleSellQuotesOperationFromRates(rates),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
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: createGetMultipleSellQuotesOperationFromRates(rates),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Kyber,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
|
|
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 fees = {
|
|
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
|
|
.times(nativeFeeRate)
|
|
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
|
|
};
|
|
replaceSamplerOps({
|
|
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
|
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Uniswap,
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
|
|
it('factors in fees for dexes', async () => {
|
|
// Kyber will have the best rates but will have fees,
|
|
// dropping its effective rates.
|
|
const kyberFeeRate = 0.2;
|
|
const rates: RatesBySource = {
|
|
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
|
|
[ERC20BridgeSource.Uniswap]: [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.Kyber]: [1, 0.7, 0.2, 0.2],
|
|
};
|
|
const fees = {
|
|
[ERC20BridgeSource.Kyber]: FILL_AMOUNT.div(4)
|
|
.times(kyberFeeRate)
|
|
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
|
|
};
|
|
replaceSamplerOps({
|
|
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
|
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
const expectedSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
|
|
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,
|
|
excludedSources: Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
|
};
|
|
|
|
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('polls the liquidity provider when the registry is provided in the arguments', async () => {
|
|
const [args, fn] = callTradeOperationAndRetainLiquidityProviderParams(
|
|
createGetMultipleBuyQuotesOperationFromRates,
|
|
DEFAULT_RATES,
|
|
);
|
|
replaceSamplerOps({
|
|
getBuyQuotes: 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({
|
|
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(
|
|
_.omit(
|
|
DEFAULT_RATES,
|
|
ERC20BridgeSource.Kyber,
|
|
ERC20BridgeSource.CurveUsdcDai,
|
|
ERC20BridgeSource.CurveUsdcDaiUsdt,
|
|
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
|
|
),
|
|
);
|
|
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('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: createGetMultipleBuyQuotesOperationFromRates(rates),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
|
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Uniswap,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
|
|
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 fees = {
|
|
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
|
|
.times(nativeFeeRate)
|
|
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
|
|
};
|
|
replaceSamplerOps({
|
|
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
|
|
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
|
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Uniswap,
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
|
|
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 fees = {
|
|
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
|
|
.times(uniswapFeeRate)
|
|
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
|
|
};
|
|
replaceSamplerOps({
|
|
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
|
|
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
|
|
});
|
|
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
|
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
|
|
);
|
|
const orderSources = improvedOrders.map(o => o.fill.source);
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Uniswap,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
|
|
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],
|
|
getSellQuotes: getSellQuotesFn,
|
|
getLiquidityProviderFromRegistry: getLiquidityProviderFn,
|
|
});
|
|
|
|
const sampler = new MarketOperationUtils(
|
|
MOCK_SAMPLER,
|
|
contractAddresses,
|
|
ORDER_DOMAIN,
|
|
registryAddress,
|
|
);
|
|
const result = await sampler.getMarketSellOrdersAsync(
|
|
[
|
|
createOrder({
|
|
makerAssetData: assetDataUtils.encodeERC20AssetData(xAsset),
|
|
takerAssetData: assetDataUtils.encodeERC20AssetData(yAsset),
|
|
}),
|
|
],
|
|
Web3Wrapper.toBaseUnitAmount(10, 18),
|
|
{ excludedSources: SELL_SOURCES, numSamples: 4 },
|
|
);
|
|
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(xAsset);
|
|
expect(getLiquidityProviderParams.takerToken).is.eql(yAsset);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
// tslint:disable-next-line: max-file-line-count
|