diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 7f2bff23b0..2aa9d11a30 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "4.2.0", + "changes": [ + { + "note": "Use `batchCall()` version of the `ERC20BridgeSampler` contract", + "pr": 2477 + } + ] + }, { "version": "4.1.1", "changes": [ diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 602e0213e6..063c045ac1 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -21,7 +21,7 @@ import { } from './types'; import { assert } from './utils/assert'; import { calculateLiquidity } from './utils/calculate_liquidity'; -import { MarketOperationUtils } from './utils/market_operation_utils'; +import { DexOrderSampler, MarketOperationUtils } from './utils/market_operation_utils'; import { dummyOrderUtils } from './utils/market_operation_utils/dummy_order_utils'; import { orderPrunerUtils } from './utils/order_prune_utils'; import { OrderStateUtils } from './utils/order_state_utils'; @@ -162,12 +162,12 @@ export class SwapQuoter { this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS); this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); - const samplerContract = new IERC20BridgeSamplerContract( - this._contractAddresses.erc20BridgeSampler, - this.provider, - { gas: samplerGasLimit }, + const sampler = new DexOrderSampler( + new IERC20BridgeSamplerContract(this._contractAddresses.erc20BridgeSampler, this.provider, { + gas: samplerGasLimit, + }), ); - this._marketOperationUtils = new MarketOperationUtils(samplerContract, this._contractAddresses, { + this._marketOperationUtils = new MarketOperationUtils(sampler, this._contractAddresses, { chainId, exchangeAddress: this._contractAddresses.exchange, }); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 620f7e90b8..485e2c89d7 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -4,15 +4,6 @@ import { ERC20BridgeSource, GetMarketOrdersOpts } from './types'; const INFINITE_TIMESTAMP_SEC = new BigNumber(2524604400); -/** - * Convert a source to a canonical address used by the sampler contract. - */ -const SOURCE_TO_ADDRESS: { [key: string]: string } = { - [ERC20BridgeSource.Eth2Dai]: '0x39755357759ce0d7f32dc8dc45414cca409ae24e', - [ERC20BridgeSource.Uniswap]: '0xc0a47dfe034b400b47bdad5fecda2621de6c4d95', - [ERC20BridgeSource.Kyber]: '0x818e6fecd516ecc3849daf6845e3ec868087b755', -}; - /** * Valid sources for market sell. */ @@ -36,7 +27,6 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { export const constants = { INFINITE_TIMESTAMP_SEC, - SOURCE_TO_ADDRESS, SELL_SOURCES, BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index aed438f35e..815431d643 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -1,5 +1,4 @@ import { ContractAddresses } from '@0x/contract-addresses'; -import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers'; import { assetDataUtils, ERC20AssetData, orderCalculationUtils } from '@0x/order-utils'; import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; @@ -11,7 +10,7 @@ import { fillableAmountsUtils } from '../fillable_amounts_utils'; import { constants as marketOperationUtilConstants } from './constants'; import { CreateOrderUtils } from './create_order'; import { comparePathOutputs, FillsOptimizer, getPathOutput } from './fill_optimizer'; -import { DexOrderSampler } from './sampler'; +import { DexOrderSampler, getSampleAmounts } from './sampler'; import { AggregationError, CollapsedFill, @@ -27,22 +26,20 @@ import { OrderDomain, } from './types'; +export { DexOrderSampler } from './sampler'; + const { ZERO_AMOUNT } = constants; const { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, ERC20_PROXY_ID, SELL_SOURCES } = marketOperationUtilConstants; export class MarketOperationUtils { - private readonly _dexSampler: DexOrderSampler; private readonly _createOrderUtils: CreateOrderUtils; - private readonly _orderDomain: OrderDomain; constructor( - samplerContract: IERC20BridgeSamplerContract, + private readonly _sampler: DexOrderSampler, contractAddresses: ContractAddresses, - orderDomain: OrderDomain, + private readonly _orderDomain: OrderDomain, ) { - this._dexSampler = new DexOrderSampler(samplerContract); this._createOrderUtils = new CreateOrderUtils(contractAddresses); - this._orderDomain = orderDomain; } /** @@ -65,10 +62,15 @@ export class MarketOperationUtils { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts, }; - const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketSellAsync( - nativeOrders, - DexOrderSampler.getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase), - difference(SELL_SOURCES, _opts.excludedSources), + const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]); + const [fillableAmounts, dexQuotes] = await this._sampler.executeAsync( + DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders), + DexOrderSampler.ops.getSellQuotes( + difference(SELL_SOURCES, _opts.excludedSources), + makerToken, + takerToken, + getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase), + ), ); const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts( nativeOrders, @@ -134,11 +136,15 @@ export class MarketOperationUtils { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts, }; - - const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketBuyAsync( - nativeOrders, - DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase), - difference(BUY_SOURCES, _opts.excludedSources), + const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]); + const [fillableAmounts, dexQuotes] = await this._sampler.executeAsync( + DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders), + DexOrderSampler.ops.getBuyQuotes( + difference(BUY_SOURCES, _opts.excludedSources), + makerToken, + takerToken, + getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase), + ), ); const signedOrderWithFillableAmounts = this._createBuyOrdersPathFromSamplerResultIfExists( nativeOrders, @@ -174,17 +180,25 @@ export class MarketOperationUtils { ...opts, }; - const batchSampleResults = await this._dexSampler.getBatchFillableAmountsAndSampleMarketBuyAsync( - batchNativeOrders, - makerAmounts.map(makerAmount => DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples)), - difference(BUY_SOURCES, _opts.excludedSources), - ); - return batchSampleResults.map(([fillableAmounts, dexQuotes], i) => + const sources = difference(BUY_SOURCES, _opts.excludedSources); + const ops = [ + ...batchNativeOrders.map(orders => DexOrderSampler.ops.getOrderFillableMakerAmounts(orders)), + ...batchNativeOrders.map((orders, i) => + DexOrderSampler.ops.getBuyQuotes(sources, getOrderTokens(orders[0])[0], getOrderTokens(orders[0])[1], [ + makerAmounts[i], + ]), + ), + ]; + const executeResults = await this._sampler.executeBatchAsync(ops); + const batchFillableAmounts = executeResults.slice(0, batchNativeOrders.length) as BigNumber[][]; + const batchDexQuotes = executeResults.slice(batchNativeOrders.length) as DexSample[][][]; + + return batchFillableAmounts.map((fillableAmounts, i) => this._createBuyOrdersPathFromSamplerResultIfExists( batchNativeOrders[i], makerAmounts[i], fillableAmounts, - dexQuotes, + batchDexQuotes[i], _opts, ), ); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts index 078b5cd9cf..e7a687c55b 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts @@ -2,99 +2,330 @@ import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers'; import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { constants as marketOperationUtilConstants } from './constants'; import { DexSample, ERC20BridgeSource } from './types'; -const { SOURCE_TO_ADDRESS } = marketOperationUtilConstants; +/** + * A composable operation the be run in `DexOrderSampler.executeAsync()`. + */ +export interface BatchedOperation { + encodeCall(contract: IERC20BridgeSamplerContract): string; + handleCallResultsAsync(contract: IERC20BridgeSamplerContract, callResults: string): Promise; +} -export class DexOrderSampler { - private readonly _samplerContract: IERC20BridgeSamplerContract; +/** + * Composable operations that can be batched in a single transaction, + * for use with `DexOrderSampler.executeAsync()`. + */ +const samplerOperations = { + getOrderFillableTakerAmounts(orders: SignedOrder[]): BatchedOperation { + return { + encodeCall: contract => { + return contract + .getOrderFillableTakerAssetAmounts(orders, orders.map(o => o.signature)) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('getOrderFillableTakerAssetAmounts', callResults); + }, + }; + }, + getOrderFillableMakerAmounts(orders: SignedOrder[]): BatchedOperation { + return { + encodeCall: contract => { + return contract + .getOrderFillableMakerAssetAmounts(orders, orders.map(o => o.signature)) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('getOrderFillableMakerAssetAmounts', callResults); + }, + }; + }, + getKyberSellQuotes( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): BatchedOperation { + return { + encodeCall: contract => { + return contract + .sampleSellsFromKyberNetwork(takerToken, makerToken, takerFillAmounts) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('sampleSellsFromKyberNetwork', callResults); + }, + }; + }, + getUniswapSellQuotes( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): BatchedOperation { + return { + encodeCall: contract => { + return contract + .sampleSellsFromUniswap(takerToken, makerToken, takerFillAmounts) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('sampleSellsFromUniswap', callResults); + }, + }; + }, + getEth2DaiSellQuotes( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): BatchedOperation { + return { + encodeCall: contract => { + return contract + .sampleSellsFromEth2Dai(takerToken, makerToken, takerFillAmounts) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('sampleSellsFromEth2Dai', callResults); + }, + }; + }, + getUniswapBuyQuotes( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): BatchedOperation { + return { + encodeCall: contract => { + return contract + .sampleBuysFromUniswap(takerToken, makerToken, makerFillAmounts) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('sampleBuysFromUniswap', callResults); + }, + }; + }, + getEth2DaiBuyQuotes( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): BatchedOperation { + return { + encodeCall: contract => { + return contract + .sampleBuysFromEth2Dai(takerToken, makerToken, makerFillAmounts) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('sampleBuysFromEth2Dai', callResults); + }, + }; + }, + getSellQuotes( + sources: ERC20BridgeSource[], + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): BatchedOperation { + const subOps = sources.map(source => { + if (source === ERC20BridgeSource.Eth2Dai) { + return samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts); + } else if (source === ERC20BridgeSource.Uniswap) { + return samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts); + } else if (source === ERC20BridgeSource.Kyber) { + return samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts); + } else { + throw new Error(`Unsupported sell sample source: ${source}`); + } + }); + return { + encodeCall: contract => { + const subCalls = subOps.map(op => op.encodeCall(contract)); + return contract.batchCall(subCalls).getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + const rawSubCallResults = contract.getABIDecodedReturnData('batchCall', callResults); + const samples = await Promise.all( + subOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])), + ); + return sources.map((source, i) => { + return samples[i].map((output, j) => ({ + source, + output, + input: takerFillAmounts[j], + })); + }); + }, + }; + }, + getBuyQuotes( + sources: ERC20BridgeSource[], + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): BatchedOperation { + const subOps = sources.map(source => { + if (source === ERC20BridgeSource.Eth2Dai) { + return samplerOperations.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts); + } else if (source === ERC20BridgeSource.Uniswap) { + return samplerOperations.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts); + } else { + throw new Error(`Unsupported buy sample source: ${source}`); + } + }); + return { + encodeCall: contract => { + const subCalls = subOps.map(op => op.encodeCall(contract)); + return contract.batchCall(subCalls).getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + const rawSubCallResults = contract.getABIDecodedReturnData('batchCall', callResults); + const samples = await Promise.all( + subOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])), + ); + return sources.map((source, i) => { + return samples[i].map((output, j) => ({ + source, + output, + input: makerFillAmounts[j], + })); + }); + }, + }; + }, +}; + +/** + * Generate sample amounts up to `maxFillAmount`. + */ +export function getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, expBase: number = 1): BigNumber[] { + const distribution = [...Array(numSamples)].map((v, i) => new BigNumber(expBase).pow(i)); + const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution))); + const amounts = stepSizes.map((s, i) => { + return maxFillAmount + .times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)])) + .integerValue(BigNumber.ROUND_UP); + }); + return amounts; +} +type BatchedOperationResult = T extends BatchedOperation ? TResult : never; + +/** + * Encapsulates interactions with the `ERC20BridgeSampler` contract. + */ +export class DexOrderSampler { /** - * Generate sample amounts up to `maxFillAmount`. + * Composable operations that can be batched in a single transaction, + * for use with `DexOrderSampler.executeAsync()`. */ - public static getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, expBase: number = 1): BigNumber[] { - const distribution = [...Array(numSamples)].map((v, i) => new BigNumber(expBase).pow(i)); - const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution))); - const amounts = stepSizes.map((s, i) => { - return maxFillAmount - .times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)])) - .integerValue(BigNumber.ROUND_UP); - }); - return amounts; - } + public static ops = samplerOperations; + private readonly _samplerContract: IERC20BridgeSamplerContract; constructor(samplerContract: IERC20BridgeSamplerContract) { this._samplerContract = samplerContract; } - public async getFillableAmountsAndSampleMarketBuyAsync( - nativeOrders: SignedOrder[], - sampleAmounts: BigNumber[], - sources: ERC20BridgeSource[], - ): Promise<[BigNumber[], DexSample[][]]> { - const signatures = nativeOrders.map(o => o.signature); - const [fillableAmount, rawSamples] = await this._samplerContract - .queryOrdersAndSampleBuys(nativeOrders, signatures, sources.map(s => SOURCE_TO_ADDRESS[s]), sampleAmounts) - .callAsync(); - const quotes = rawSamples.map((rawDexSamples, sourceIdx) => { - const source = sources[sourceIdx]; - return rawDexSamples.map((sample, sampleIdx) => ({ - source, - input: sampleAmounts[sampleIdx], - output: sample, - })); - }); - return [fillableAmount, quotes]; - } + /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */ - public async getBatchFillableAmountsAndSampleMarketBuyAsync( - nativeOrders: SignedOrder[][], - sampleAmounts: BigNumber[][], - sources: ERC20BridgeSource[], - ): Promise> { - const signatures = nativeOrders.map(o => o.map(i => i.signature)); - const fillableAmountsAndSamples = await this._samplerContract - .queryBatchOrdersAndSampleBuys( - nativeOrders, - signatures, - sources.map(s => SOURCE_TO_ADDRESS[s]), - sampleAmounts, - ) - .callAsync(); - const batchFillableAmountsAndQuotes: Array<[BigNumber[], DexSample[][]]> = []; - fillableAmountsAndSamples.forEach((sampleResult, i) => { - const { tokenAmountsBySource, orderFillableAssetAmounts } = sampleResult; - const quotes = tokenAmountsBySource.map((rawDexSamples, sourceIdx) => { - const source = sources[sourceIdx]; - return rawDexSamples.map((sample, sampleIdx) => ({ - source, - input: sampleAmounts[i][sampleIdx], - output: sample, - })); - }); - batchFillableAmountsAndQuotes.push([orderFillableAssetAmounts, quotes]); - }); - return batchFillableAmountsAndQuotes; + // prettier-ignore + public async executeAsync< + T1 + >(...ops: [T1]): Promise<[ + BatchedOperationResult + ]>; + + // prettier-ignore + public async executeAsync< + T1, T2 + >(...ops: [T1, T2]): Promise<[ + BatchedOperationResult, + BatchedOperationResult + ]>; + + // prettier-ignore + public async executeAsync< + T1, T2, T3 + >(...ops: [T1, T2, T3]): Promise<[ + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult + ]>; + + // prettier-ignore + public async executeAsync< + T1, T2, T3, T4 + >(...ops: [T1, T2, T3, T4]): Promise<[ + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult + ]>; + + // prettier-ignore + public async executeAsync< + T1, T2, T3, T4, T5 + >(...ops: [T1, T2, T3, T4, T5]): Promise<[ + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult + ]>; + + // prettier-ignore + public async executeAsync< + T1, T2, T3, T4, T5, T6 + >(...ops: [T1, T2, T3, T4, T5, T6]): Promise<[ + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult + ]>; + + // prettier-ignore + public async executeAsync< + T1, T2, T3, T4, T5, T6, T7 + >(...ops: [T1, T2, T3, T4, T5, T6, T7]): Promise<[ + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult + ]>; + + // prettier-ignore + public async executeAsync< + T1, T2, T3, T4, T5, T6, T7, T8 + >(...ops: [T1, T2, T3, T4, T5, T6, T7, T8]): Promise<[ + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult, + BatchedOperationResult + ]>; + + /** + * Run a series of operations from `DexOrderSampler.ops` in a single transaction. + */ + public async executeAsync(...ops: any[]): Promise { + return this.executeBatchAsync(ops); } - public async getFillableAmountsAndSampleMarketSellAsync( - nativeOrders: SignedOrder[], - sampleAmounts: BigNumber[], - sources: ERC20BridgeSource[], - ): Promise<[BigNumber[], DexSample[][]]> { - const signatures = nativeOrders.map(o => o.signature); - const [fillableAmount, rawSamples] = await this._samplerContract - .queryOrdersAndSampleSells(nativeOrders, signatures, sources.map(s => SOURCE_TO_ADDRESS[s]), sampleAmounts) - .callAsync(); - const quotes = rawSamples.map((rawDexSamples, sourceIdx) => { - const source = sources[sourceIdx]; - return rawDexSamples.map((sample, sampleIdx) => ({ - source, - input: sampleAmounts[sampleIdx], - output: sample, - })); - }); - return [fillableAmount, quotes]; + /** + * Run a series of operations from `DexOrderSampler.ops` in a single transaction. + * Takes an arbitrary length array, but is not typesafe. + */ + public async executeBatchAsync>>(ops: T): Promise { + const callDatas = ops.map(o => o.encodeCall(this._samplerContract)); + const callResults = await this._samplerContract.batchCall(callDatas).callAsync(); + return Promise.all(callResults.map(async (r, i) => ops[i].handleCallResultsAsync(this._samplerContract, r))); } } diff --git a/packages/asset-swapper/test/dex_sampler_test.ts b/packages/asset-swapper/test/dex_sampler_test.ts new file mode 100644 index 0000000000..8b94f34935 --- /dev/null +++ b/packages/asset-swapper/test/dex_sampler_test.ts @@ -0,0 +1,357 @@ +import { constants, expect, getRandomFloat, getRandomInteger, 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 { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; +import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; + +import { MockSamplerContract } from './utils/mock_sampler_contract'; + +const CHAIN_ID = 1; +// tslint:disable: custom-no-magic-numbers +describe('DexSampler tests', () => { + const MAKER_TOKEN = randomAddress(); + const TAKER_TOKEN = randomAddress(); + const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); + const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); + + describe('getSampleAmounts()', () => { + const FILL_AMOUNT = getRandomInteger(1, 1e18); + const NUM_SAMPLES = 16; + + it('generates the correct number of amounts', () => { + const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); + expect(amounts).to.be.length(NUM_SAMPLES); + }); + + it('first amount is nonzero', () => { + const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); + expect(amounts[0]).to.not.bignumber.eq(0); + }); + + it('last amount is the fill amount', () => { + const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); + expect(amounts[NUM_SAMPLES - 1]).to.bignumber.eq(FILL_AMOUNT); + }); + + it('can generate a single amount', () => { + const amounts = getSampleAmounts(FILL_AMOUNT, 1); + expect(amounts).to.be.length(1); + expect(amounts[0]).to.bignumber.eq(FILL_AMOUNT); + }); + + it('generates ascending amounts', () => { + const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); + for (const i of _.times(NUM_SAMPLES).slice(1)) { + const prev = amounts[i - 1]; + const amount = amounts[i]; + expect(prev).to.bignumber.lt(amount); + } + }); + }); + + function createOrder(overrides?: Partial): SignedOrder { + return { + chainId: CHAIN_ID, + exchangeAddress: hexUtils.random(20), + 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, + }; + } + const ORDERS = _.times(4, () => createOrder()); + const SIMPLE_ORDERS = ORDERS.map(o => _.omit(o, ['signature', 'chainId', 'exchangeAddress'])); + + describe('operations', () => { + it('getOrderFillableMakerAmounts()', async () => { + const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); + const sampler = new MockSamplerContract({ + getOrderFillableMakerAssetAmounts: (orders, signatures) => { + expect(orders).to.deep.eq(SIMPLE_ORDERS); + expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); + return expectedFillableAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS), + ); + expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); + }); + + it('getOrderFillableTakerAmounts()', async () => { + const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); + const sampler = new MockSamplerContract({ + getOrderFillableTakerAssetAmounts: (orders, signatures) => { + expect(orders).to.deep.eq(SIMPLE_ORDERS); + expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); + return expectedFillableAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS), + ); + expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); + }); + + it('getKyberSellQuotes()', async () => { + const expectedTakerToken = hexUtils.random(20); + const expectedMakerToken = hexUtils.random(20); + const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const sampler = new MockSamplerContract({ + sampleSellsFromKyberNetwork: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); + return expectedMakerFillAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getKyberSellQuotes( + expectedMakerToken, + expectedTakerToken, + expectedTakerFillAmounts, + ), + ); + expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts); + }); + + it('getEth2DaiSellQuotes()', async () => { + const expectedTakerToken = hexUtils.random(20); + const expectedMakerToken = hexUtils.random(20); + const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const sampler = new MockSamplerContract({ + sampleSellsFromEth2Dai: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); + return expectedMakerFillAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getEth2DaiSellQuotes( + expectedMakerToken, + expectedTakerToken, + expectedTakerFillAmounts, + ), + ); + expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts); + }); + + it('getUniswapSellQuotes()', async () => { + const expectedTakerToken = hexUtils.random(20); + const expectedMakerToken = hexUtils.random(20); + const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const sampler = new MockSamplerContract({ + sampleSellsFromUniswap: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); + return expectedMakerFillAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getUniswapSellQuotes( + expectedMakerToken, + expectedTakerToken, + expectedTakerFillAmounts, + ), + ); + expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts); + }); + + it('getEth2DaiBuyQuotes()', async () => { + const expectedTakerToken = hexUtils.random(20); + const expectedMakerToken = hexUtils.random(20); + const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const sampler = new MockSamplerContract({ + sampleBuysFromEth2Dai: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); + return expectedTakerFillAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getEth2DaiBuyQuotes( + expectedMakerToken, + expectedTakerToken, + expectedMakerFillAmounts, + ), + ); + expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts); + }); + + it('getUniswapBuyQuotes()', async () => { + const expectedTakerToken = hexUtils.random(20); + const expectedMakerToken = hexUtils.random(20); + const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10); + const sampler = new MockSamplerContract({ + sampleBuysFromUniswap: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); + return expectedTakerFillAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getUniswapBuyQuotes( + expectedMakerToken, + expectedTakerToken, + expectedMakerFillAmounts, + ), + ); + expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts); + }); + + interface RatesBySource { + [src: string]: BigNumber; + } + + it('getSellQuotes()', async () => { + const expectedTakerToken = hexUtils.random(20); + const expectedMakerToken = hexUtils.random(20); + const sources = [ERC20BridgeSource.Kyber, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap]; + const ratesBySource: RatesBySource = { + [ERC20BridgeSource.Kyber]: getRandomFloat(0, 100), + [ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100), + [ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100), + }; + const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); + const sampler = new MockSamplerContract({ + sampleSellsFromKyberNetwork: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); + return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Kyber]).integerValue()); + }, + sampleSellsFromUniswap: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); + return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Uniswap]).integerValue()); + }, + sampleSellsFromEth2Dai: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); + return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [quotes] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getSellQuotes( + sources, + expectedMakerToken, + expectedTakerToken, + expectedTakerFillAmounts, + ), + ); + expect(quotes).to.be.length(sources.length); + const expectedQuotes = sources.map(s => + expectedTakerFillAmounts.map(a => ({ + source: s, + input: a, + output: a.times(ratesBySource[s]).integerValue(), + })), + ); + expect(quotes).to.deep.eq(expectedQuotes); + }); + + it('getBuyQuotes()', async () => { + const expectedTakerToken = hexUtils.random(20); + const expectedMakerToken = hexUtils.random(20); + const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap]; + const ratesBySource: RatesBySource = { + [ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100), + [ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100), + }; + const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); + const sampler = new MockSamplerContract({ + sampleBuysFromUniswap: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); + return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Uniswap]).integerValue()); + }, + sampleBuysFromEth2Dai: (takerToken, makerToken, fillAmounts) => { + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); + return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [quotes] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getBuyQuotes( + sources, + expectedMakerToken, + expectedTakerToken, + expectedMakerFillAmounts, + ), + ); + expect(quotes).to.be.length(sources.length); + const expectedQuotes = sources.map(s => + expectedMakerFillAmounts.map(a => ({ + source: s, + input: a, + output: a.times(ratesBySource[s]).integerValue(), + })), + ); + expect(quotes).to.deep.eq(expectedQuotes); + }); + }); + + describe('batched operations', () => { + it('getOrderFillableMakerAmounts(), getOrderFillableTakerAmounts()', async () => { + const expectedFillableTakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); + const expectedFillableMakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); + const sampler = new MockSamplerContract({ + getOrderFillableMakerAssetAmounts: (orders, signatures) => { + expect(orders).to.deep.eq(SIMPLE_ORDERS); + expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); + return expectedFillableMakerAmounts; + }, + getOrderFillableTakerAssetAmounts: (orders, signatures) => { + expect(orders).to.deep.eq(SIMPLE_ORDERS); + expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); + return expectedFillableTakerAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS), + DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS), + ); + expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts); + expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts); + }); + }); +}); +// tslint:disable-next-line: max-file-line-count diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 0145930f09..3db8fed344 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -10,23 +10,20 @@ import { } from '@0x/contracts-test-utils'; import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; -import { Order, SignedOrder } from '@0x/types'; +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 { ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; +import { DexSample, ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; -import { MockSamplerContract, QueryAndSampleResult } from './utils/mock_sampler_contract'; +const { BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants; -const { SOURCE_TO_ADDRESS, BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants; - -// Because the bridges and the DEX sources are only deployed on mainnet, tests will resort to using mainnet addresses -const CHAIN_ID = 1; // 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; @@ -36,10 +33,15 @@ describe('MarketOperationUtils tests', () => { const TAKER_TOKEN = randomAddress(); const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); + let originalSamplerOperations: any; - interface RatesBySource { - [source: string]: Numberish[]; - } + before(() => { + originalSamplerOperations = DexOrderSampler.ops; + }); + + after(() => { + DexOrderSampler.ops = originalSamplerOperations; + }); function createOrder(overrides?: Partial): SignedOrder { return { @@ -82,15 +84,6 @@ describe('MarketOperationUtils tests', () => { throw new Error(`Unknown bridge address: ${bridgeAddress}`); } - function getSourceFromAddress(sourceAddress: string): ERC20BridgeSource { - for (const k of Object.keys(SOURCE_TO_ADDRESS)) { - if (SOURCE_TO_ADDRESS[k].toLowerCase() === sourceAddress.toLowerCase()) { - return k as ERC20BridgeSource; - } - } - throw new Error(`Unknown source address: ${sourceAddress}`); - } - function assertSamePrefix(actual: string, expected: string): void { expect(actual.substr(0, expected.length)).to.eq(expected); } @@ -107,7 +100,7 @@ describe('MarketOperationUtils tests', () => { function createOrdersFromBuyRates(makerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] { const singleMakerAssetAmount = makerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); - return (rates as any).map((r: Numberish) => + return rates.map(r => createOrder({ makerAssetAmount: singleMakerAssetAmount, takerAssetAmount: singleMakerAssetAmount.div(r).integerValue(), @@ -115,272 +108,197 @@ describe('MarketOperationUtils tests', () => { ); } - function createSamplerFromSellRates(rates: RatesBySource): MockSamplerContract { - return new MockSamplerContract({ - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { - const fillableTakerAssetAmounts = orders.map(o => o.takerAssetAmount); - const samplesBySourceIndex = sources.map(s => - fillAmounts.map((fillAmount, idx) => - fillAmount.times(rates[getSourceFromAddress(s)][idx]).integerValue(BigNumber.ROUND_UP), - ), - ); - return [fillableTakerAssetAmounts, samplesBySourceIndex]; - }, - }); - } - - function createSamplerFromBuyRates(rates: RatesBySource): MockSamplerContract { - return new MockSamplerContract({ - queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { - const fillableMakerAssetAmounts = orders.map(o => o.makerAssetAmount); - const samplesBySourceIndex = sources.map(s => - fillAmounts.map((fillAmount, idx) => - fillAmount.div(rates[getSourceFromAddress(s)][idx]).integerValue(BigNumber.ROUND_UP), - ), - ); - return [fillableMakerAssetAmounts, samplesBySourceIndex]; - }, - }); - } - - const DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL = ( - orders: Order[], - signatures: string[], - sources: string[], - fillAmounts: BigNumber[], - ): QueryAndSampleResult => [ - orders.map((order: Order) => order.takerAssetAmount), - sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))), - ]; - - const DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY = ( - orders: Order[], - signatures: string[], - sources: string[], - fillAmounts: BigNumber[], - ): QueryAndSampleResult => [ - orders.map((order: Order) => order.makerAssetAmount), - sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))), - ]; - const ORDER_DOMAIN = { exchangeAddress: contractAddresses.exchange, chainId: CHAIN_ID, }; - describe('DexOrderSampler', () => { - describe('getSampleAmounts()', () => { - const FILL_AMOUNT = getRandomInteger(1, 1e18); - const NUM_SAMPLES = 16; + type GetQuotesOperation = (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => BigNumber[]; - it('generates the correct number of amounts', () => { - const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); - expect(amounts).to.be.length(NUM_SAMPLES); - }); + function createGetQuotesOperationFromSellRates(rates: Numberish[]): GetQuotesOperation { + return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { + return fillAmounts.map((a, i) => a.times(rates[i]).integerValue()); + }; + } - it('first amount is nonzero', () => { - const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); - expect(amounts[0]).to.not.bignumber.eq(0); - }); + function createGetQuotesOperationFromBuyRates(rates: Numberish[]): GetQuotesOperation { + return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { + return fillAmounts.map((a, i) => a.div(rates[i]).integerValue()); + }; + } - it('last amount is the fill amount', () => { - const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); - expect(amounts[NUM_SAMPLES - 1]).to.bignumber.eq(FILL_AMOUNT); - }); + 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(), + })), + ); + }; + } - it('can generate a single amount', () => { - const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, 1); - expect(amounts).to.be.length(1); - expect(amounts[0]).to.bignumber.eq(FILL_AMOUNT); - }); + 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(), + })), + ); + }; + } - it('generates ascending amounts', () => { - const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); - for (const i of _.times(NUM_SAMPLES).slice(1)) { - const prev = amounts[i - 1]; - const amount = amounts[i]; - expect(prev).to.bignumber.lt(amount); - } - }); + 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; + } - describe('getFillableAmountsAndSampleMarketOperationAsync()', () => { - const SAMPLE_AMOUNTS = [100, 500, 1000].map(v => new BigNumber(v)); - const ORDERS = _.times(4, () => createOrder()); - - it('makes an eth_call with the correct arguments for a sell', async () => { - const sampler = new MockSamplerContract({ - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { - expect(orders).to.deep.eq(ORDERS); - expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); - expect(sources).to.deep.eq(SELL_SOURCES.map(s => SOURCE_TO_ADDRESS[s])); - expect(fillAmounts).to.deep.eq(SAMPLE_AMOUNTS); - return [ - orders.map(() => getRandomInteger(1, 1e18)), - sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))), - ]; - }, - }); - const dexOrderSampler = new DexOrderSampler(sampler); - await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync(ORDERS, SAMPLE_AMOUNTS, SELL_SOURCES); - }); + const NUM_SAMPLES = 3; - it('makes an eth_call with the correct arguments for a buy', async () => { - const sampler = new MockSamplerContract({ - queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { - expect(orders).to.deep.eq(ORDERS); - expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); - expect(sources).to.deep.eq(BUY_SOURCES.map(s => SOURCE_TO_ADDRESS[s])); - expect(fillAmounts).to.deep.eq(SAMPLE_AMOUNTS); - return [ - orders.map(() => getRandomInteger(1, 1e18)), - sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))), - ]; - }, - }); - const dexOrderSampler = new DexOrderSampler(sampler); - await dexOrderSampler.getFillableAmountsAndSampleMarketBuyAsync(ORDERS, SAMPLE_AMOUNTS, BUY_SOURCES); - }); + interface RatesBySource { + [source: string]: Numberish[]; + } - it('returns correct fillable amounts', async () => { - const fillableAmounts = _.times(SAMPLE_AMOUNTS.length, () => getRandomInteger(1, 1e18)); - const sampler = new MockSamplerContract({ - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => [ - fillableAmounts, - sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))), - ], - }); - const dexOrderSampler = new DexOrderSampler(sampler); - const [actualFillableAmounts] = await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync( - ORDERS, - SAMPLE_AMOUNTS, - SELL_SOURCES, - ); - expect(actualFillableAmounts).to.deep.eq(fillableAmounts); - }); + const DEFAULT_RATES: RatesBySource = { + [ERC20BridgeSource.Native]: createDecreasingRates(NUM_SAMPLES), + [ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES), + [ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES), + [ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES), + }; - it('converts samples to DEX quotes', async () => { - const quotes = SELL_SOURCES.map(source => - SAMPLE_AMOUNTS.map(s => ({ - source, - input: s, - output: getRandomInteger(1, 1e18), - })), - ); - const sampler = new MockSamplerContract({ - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => [ - orders.map(() => getRandomInteger(1, 1e18)), - quotes.map(q => q.map(s => s.output)), - ], - }); - const dexOrderSampler = new DexOrderSampler(sampler); - const [, actualQuotes] = await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync( - ORDERS, - SAMPLE_AMOUNTS, - SELL_SOURCES, - ); - expect(actualQuotes).to.deep.eq(quotes); - }); - }); - }); + 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; + } - function createRandomRates(numSamples: number = 32): RatesBySource { - const ALL_SOURCES = [ - ERC20BridgeSource.Native, - ERC20BridgeSource.Eth2Dai, - ERC20BridgeSource.Kyber, - ERC20BridgeSource.Uniswap, - ]; - return _.zipObject( - ALL_SOURCES, - _.times(ALL_SOURCES.length, () => _.fill(new Array(numSamples), getRandomFloat(1e-3, 2))), - ); + 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 SOURCE_RATES = createRandomRates(); const ORDERS = createOrdersFromSellRates( FILL_AMOUNT, - _.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]), - ); - const DEFAULT_SAMPLER = createSamplerFromSellRates(SOURCE_RATES); - const DEFAULT_OPTS = { numSamples: 3, runLimit: 0, sampleDistributionBase: 1 }; - const defaultMarketOperationUtils = new MarketOperationUtils( - DEFAULT_SAMPLER, - contractAddresses, - ORDER_DOMAIN, + _.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]), ); + const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 }; - it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => { - let wasCalled = false; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleSells: (...args) => { - wasCalled = true; - return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(...args); - }, - }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); - await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, DEFAULT_OPTS); - expect(wasCalled).to.be.true(); + beforeEach(() => { + replaceSamplerOps(); }); it('queries `numSamples` samples', async () => { const numSamples = _.random(1, 16); - let fillAmountsLength = 0; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { - fillAmountsLength = fillAmounts.length; - return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts); + let actualNumSamples = 0; + replaceSamplerOps({ + getSellQuotes: (sources, makerToken, takerToken, amounts) => { + actualNumSamples = amounts.length; + return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts); }, }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples, }); - expect(fillAmountsLength).eq(numSamples); + expect(actualNumSamples).eq(numSamples); }); it('polls all DEXes if `excludedSources` is empty', async () => { let sourcesPolled: ERC20BridgeSource[] = []; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { - sourcesPolled = sources.map(a => getSourceFromAddress(a)); - return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts); + replaceSamplerOps({ + getSellQuotes: (sources, makerToken, takerToken, amounts) => { + sourcesPolled = sources.slice(); + return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts); }, }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], }); - expect(sourcesPolled).to.deep.eq(SELL_SOURCES); + 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[] = []; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { - sourcesPolled = sources.map(a => getSourceFromAddress(a)); - return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts); + replaceSamplerOps({ + getSellQuotes: (sources, makerToken, takerToken, amounts) => { + sourcesPolled = sources.slice(); + return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts); }, }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, }); - expect(sourcesPolled).to.deep.eq(_.without(SELL_SOURCES, ...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 bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES))); - const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate)); + const bestSource = findSourceWithMaxOutput(DEFAULT_RATES); expect(bestSource).to.exist(''); - const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { + const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, runLimit: 0, }); @@ -390,7 +308,7 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct asset data', async () => { - const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync( + 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, @@ -414,7 +332,7 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct taker amount', async () => { - const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync( + 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, @@ -426,7 +344,7 @@ describe('MarketOperationUtils tests', () => { it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); - const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync( + 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, @@ -435,7 +353,7 @@ describe('MarketOperationUtils tests', () => { expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const source = getSourceFromAssetData(order.makerAssetData); - const expectedMakerAmount = FILL_AMOUNT.times(SOURCE_RATES[source][0]); + 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); } @@ -450,7 +368,7 @@ describe('MarketOperationUtils tests', () => { makerAssetAmount: dustAmount.times(maxRate.plus(0.01)), takerAssetAmount: dustAmount, }); - const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync( + const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( _.shuffle([dustOrder, ...ORDERS]), FILL_AMOUNT, // Ignore all DEX sources so only native orders are returned. @@ -468,11 +386,9 @@ describe('MarketOperationUtils tests', () => { 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]; - const marketOperationUtils = new MarketOperationUtils( - createSamplerFromSellRates(rates), - contractAddresses, - ORDER_DOMAIN, - ); + replaceSamplerOps({ + getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates), + }); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, @@ -495,11 +411,9 @@ describe('MarketOperationUtils tests', () => { 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]; - const marketOperationUtils = new MarketOperationUtils( - createSamplerFromSellRates(rates), - contractAddresses, - ORDER_DOMAIN, - ); + replaceSamplerOps({ + getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates), + }); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, @@ -522,11 +436,9 @@ describe('MarketOperationUtils tests', () => { 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]; - const marketOperationUtils = new MarketOperationUtils( - createSamplerFromSellRates(rates), - contractAddresses, - ORDER_DOMAIN, - ); + replaceSamplerOps({ + getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates), + }); const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, @@ -546,61 +458,40 @@ describe('MarketOperationUtils tests', () => { describe('getMarketBuyOrdersAsync()', () => { const FILL_AMOUNT = getRandomInteger(1, 1e18); - const SOURCE_RATES = _.omit(createRandomRates(), [ERC20BridgeSource.Kyber]); const ORDERS = createOrdersFromBuyRates( FILL_AMOUNT, - _.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]), - ); - const DEFAULT_SAMPLER = createSamplerFromBuyRates(SOURCE_RATES); - const DEFAULT_OPTS = { numSamples: 3, runLimit: 0, sampleDistributionBase: 1 }; - const defaultMarketOperationUtils = new MarketOperationUtils( - DEFAULT_SAMPLER, - contractAddresses, - ORDER_DOMAIN, + _.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]), ); + const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 }; - it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => { - let wasCalled = false; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleBuys: (...args) => { - wasCalled = true; - return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(...args); - }, - }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); - - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, DEFAULT_OPTS); - expect(wasCalled).to.be.true(); + beforeEach(() => { + replaceSamplerOps(); }); it('queries `numSamples` samples', async () => { const numSamples = _.random(1, 16); - let fillAmountsLength = 0; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { - fillAmountsLength = fillAmounts.length; - return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts); + let actualNumSamples = 0; + replaceSamplerOps({ + getBuyQuotes: (sources, makerToken, takerToken, amounts) => { + actualNumSamples = amounts.length; + return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts); }, }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples, }); - expect(fillAmountsLength).eq(numSamples); + expect(actualNumSamples).eq(numSamples); }); it('polls all DEXes if `excludedSources` is empty', async () => { let sourcesPolled: ERC20BridgeSource[] = []; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { - sourcesPolled = sources.map(a => getSourceFromAddress(a)); - return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts); + replaceSamplerOps({ + getBuyQuotes: (sources, makerToken, takerToken, amounts) => { + sourcesPolled = sources.slice(); + return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts); }, }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources: [], @@ -609,16 +500,14 @@ describe('MarketOperationUtils tests', () => { }); it('does not poll DEXes in `excludedSources`', async () => { - const excludedSources = _.sampleSize(BUY_SOURCES, _.random(1, BUY_SOURCES.length)); + const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); let sourcesPolled: ERC20BridgeSource[] = []; - const sampler = new MockSamplerContract({ - queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { - sourcesPolled = sources.map(a => getSourceFromAddress(a)); - return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts); + replaceSamplerOps({ + getBuyQuotes: (sources, makerToken, takerToken, amounts) => { + sourcesPolled = sources.slice(); + return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts); }, }); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN); - await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, excludedSources, @@ -627,10 +516,9 @@ describe('MarketOperationUtils tests', () => { }); it('returns the most cost-effective single source if `runLimit == 0`', async () => { - const bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES))); - const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate)); + const bestSource = findSourceWithMaxOutput(DEFAULT_RATES); expect(bestSource).to.exist(''); - const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { + const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, runLimit: 0, }); @@ -638,8 +526,9 @@ describe('MarketOperationUtils tests', () => { 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 defaultMarketOperationUtils.getMarketBuyOrdersAsync( + 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, @@ -663,7 +552,7 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct taker amount', async () => { - const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync( + 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, @@ -675,7 +564,7 @@ describe('MarketOperationUtils tests', () => { it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); - const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync( + 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, @@ -684,9 +573,7 @@ describe('MarketOperationUtils tests', () => { expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const source = getSourceFromAssetData(order.makerAssetData); - const expectedTakerAmount = FILL_AMOUNT.div(SOURCE_RATES[source][0]).integerValue( - BigNumber.ROUND_UP, - ); + 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); } @@ -701,7 +588,7 @@ describe('MarketOperationUtils tests', () => { makerAssetAmount: dustAmount, takerAssetAmount: dustAmount.div(maxRate.plus(0.01)).integerValue(BigNumber.ROUND_DOWN), }); - const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( _.shuffle([dustOrder, ...ORDERS]), FILL_AMOUNT, // Ignore all DEX sources so only native orders are returned. @@ -718,11 +605,9 @@ describe('MarketOperationUtils tests', () => { 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]; - const marketOperationUtils = new MarketOperationUtils( - createSamplerFromBuyRates(rates), - contractAddresses, - ORDER_DOMAIN, - ); + replaceSamplerOps({ + getBuyQuotes: createGetMultipleQuotesOperationFromBuyRates(rates), + }); const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, diff --git a/packages/asset-swapper/test/swap_quote_calculator_test.ts b/packages/asset-swapper/test/swap_quote_calculator_test.ts index cf8abb4773..d76ec843da 100644 --- a/packages/asset-swapper/test/swap_quote_calculator_test.ts +++ b/packages/asset-swapper/test/swap_quote_calculator_test.ts @@ -8,7 +8,7 @@ import 'mocha'; import { constants } from '../src/constants'; import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types'; -import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; +import { DexOrderSampler, MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants'; import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator'; @@ -43,27 +43,26 @@ const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = { }, }; -const createSamplerFromSignedOrdersWithFillableAmounts = ( +function createSamplerFromSignedOrdersWithFillableAmounts( signedOrders: SignedOrderWithFillableAmounts[], -): MockSamplerContract => { - const sampler = new MockSamplerContract({ - queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { - const fillableAmounts = signatures.map((s: string) => { - const order = (signedOrders.find(o => o.signature === s) as any) as SignedOrderWithFillableAmounts; - return order.fillableMakerAssetAmount; - }); - return [fillableAmounts, sources.map(() => fillAmounts.map(() => constants.ZERO_AMOUNT))]; - }, - queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { - const fillableAmounts = signatures.map((s: string) => { - const order = (signedOrders.find(o => o.signature === s) as any) as SignedOrderWithFillableAmounts; - return order.fillableTakerAssetAmount; - }); - return [fillableAmounts, sources.map(() => fillAmounts.map(() => constants.ZERO_AMOUNT))]; - }, - }); - return sampler; -}; +): DexOrderSampler { + const sampleDexHandler = (takerToken: string, makerToken: string, amounts: BigNumber[]) => { + return amounts.map(() => constants.ZERO_AMOUNT); + }; + return new DexOrderSampler( + new MockSamplerContract({ + getOrderFillableMakerAssetAmounts: (orders, signatures) => + orders.map((o, i) => signedOrders[i].fillableMakerAssetAmount), + getOrderFillableTakerAssetAmounts: (orders, signatures) => + orders.map((o, i) => signedOrders[i].fillableTakerAssetAmount), + sampleSellsFromEth2Dai: sampleDexHandler, + sampleSellsFromKyberNetwork: sampleDexHandler, + sampleSellsFromUniswap: sampleDexHandler, + sampleBuysFromEth2Dai: sampleDexHandler, + sampleBuysFromUniswap: sampleDexHandler, + }), + ); +} // TODO(dorothy-zbornak): Replace these tests entirely with unit tests because // omg they're a nightmare to maintain. diff --git a/packages/asset-swapper/test/utils/mock_sampler_contract.ts b/packages/asset-swapper/test/utils/mock_sampler_contract.ts index 1b7b2515bf..0ddc580b8f 100644 --- a/packages/asset-swapper/test/utils/mock_sampler_contract.ts +++ b/packages/asset-swapper/test/utils/mock_sampler_contract.ts @@ -2,15 +2,25 @@ import { ContractFunctionObj } from '@0x/base-contract'; import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers'; import { constants } from '@0x/contracts-test-utils'; import { Order } from '@0x/types'; -import { BigNumber } from '@0x/utils'; +import { BigNumber, hexUtils } from '@0x/utils'; -export type QueryAndSampleResult = [BigNumber[], BigNumber[][]]; -export type QueryAndSampleHandler = ( +export type GetOrderFillableAssetAmountResult = BigNumber[]; +export type GetOrderFillableAssetAmountHandler = ( orders: Order[], signatures: string[], - sources: string[], - fillAmounts: BigNumber[], -) => QueryAndSampleResult; +) => GetOrderFillableAssetAmountResult; + +export type SampleResults = BigNumber[]; +export type SampleSellsHandler = ( + takerToken: string, + makerToken: string, + takerTokenAmounts: BigNumber[], +) => SampleResults; +export type SampleBuysHandler = ( + takerToken: string, + makerToken: string, + makerTokenAmounts: BigNumber[], +) => SampleResults; const DUMMY_PROVIDER = { sendAsync: (...args: any[]): any => { @@ -18,56 +28,156 @@ const DUMMY_PROVIDER = { }, }; +interface Handlers { + getOrderFillableMakerAssetAmounts: GetOrderFillableAssetAmountHandler; + getOrderFillableTakerAssetAmounts: GetOrderFillableAssetAmountHandler; + sampleSellsFromKyberNetwork: SampleSellsHandler; + sampleSellsFromEth2Dai: SampleSellsHandler; + sampleSellsFromUniswap: SampleSellsHandler; + sampleBuysFromEth2Dai: SampleBuysHandler; + sampleBuysFromUniswap: SampleBuysHandler; +} + export class MockSamplerContract extends IERC20BridgeSamplerContract { - public readonly queryOrdersAndSampleSellsHandler?: QueryAndSampleHandler; - public readonly queryOrdersAndSampleBuysHandler?: QueryAndSampleHandler; - - public constructor( - handlers?: Partial<{ - queryOrdersAndSampleSells: QueryAndSampleHandler; - queryOrdersAndSampleBuys: QueryAndSampleHandler; - }>, - ) { + private readonly _handlers: Partial = {}; + + public constructor(handlers: Partial = {}) { super(constants.NULL_ADDRESS, DUMMY_PROVIDER); - const _handlers = { - queryOrdersAndSampleSells: undefined, - queryOrdersAndSampleBuys: undefined, - ...handlers, + this._handlers = handlers; + } + + public batchCall(callDatas: string[]): ContractFunctionObj { + return { + ...super.batchCall(callDatas), + callAsync: async (...callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)), }; - this.queryOrdersAndSampleSellsHandler = _handlers.queryOrdersAndSampleSells; - this.queryOrdersAndSampleBuysHandler = _handlers.queryOrdersAndSampleBuys; } - public queryOrdersAndSampleSells( + public getOrderFillableMakerAssetAmounts( orders: Order[], signatures: string[], - sources: string[], - fillAmounts: BigNumber[], - ): ContractFunctionObj { - return { - ...super.queryOrdersAndSampleSells(orders, signatures, sources, fillAmounts), - callAsync: async (...args: any[]): Promise => { - if (!this.queryOrdersAndSampleSellsHandler) { - throw new Error('queryOrdersAndSampleSells handler undefined'); - } - return this.queryOrdersAndSampleSellsHandler(orders, signatures, sources, fillAmounts); - }, - }; + ): ContractFunctionObj { + return this._wrapCall( + super.getOrderFillableMakerAssetAmounts, + this._handlers.getOrderFillableMakerAssetAmounts, + orders, + signatures, + ); } - public queryOrdersAndSampleBuys( + public getOrderFillableTakerAssetAmounts( orders: Order[], signatures: string[], - sources: string[], - fillAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj { + return this._wrapCall( + super.getOrderFillableTakerAssetAmounts, + this._handlers.getOrderFillableTakerAssetAmounts, + orders, + signatures, + ); + } + + public sampleSellsFromKyberNetwork( + takerToken: string, + makerToken: string, + takerAssetAmounts: BigNumber[], + ): ContractFunctionObj { + return this._wrapCall( + super.sampleSellsFromKyberNetwork, + this._handlers.sampleSellsFromKyberNetwork, + takerToken, + makerToken, + takerAssetAmounts, + ); + } + + public sampleSellsFromEth2Dai( + takerToken: string, + makerToken: string, + takerAssetAmounts: BigNumber[], + ): ContractFunctionObj { + return this._wrapCall( + super.sampleSellsFromEth2Dai, + this._handlers.sampleSellsFromEth2Dai, + takerToken, + makerToken, + takerAssetAmounts, + ); + } + + public sampleSellsFromUniswap( + takerToken: string, + makerToken: string, + takerAssetAmounts: BigNumber[], + ): ContractFunctionObj { + return this._wrapCall( + super.sampleSellsFromUniswap, + this._handlers.sampleSellsFromUniswap, + takerToken, + makerToken, + takerAssetAmounts, + ); + } + + public sampleBuysFromEth2Dai( + takerToken: string, + makerToken: string, + makerAssetAmounts: BigNumber[], + ): ContractFunctionObj { + return this._wrapCall( + super.sampleBuysFromEth2Dai, + this._handlers.sampleBuysFromEth2Dai, + takerToken, + makerToken, + makerAssetAmounts, + ); + } + + public sampleBuysFromUniswap( + takerToken: string, + makerToken: string, + makerAssetAmounts: BigNumber[], + ): ContractFunctionObj { + return this._wrapCall( + super.sampleBuysFromUniswap, + this._handlers.sampleBuysFromUniswap, + takerToken, + makerToken, + makerAssetAmounts, + ); + } + + private _callEncodedFunction(callData: string): string { + // tslint:disable-next-line: custom-no-magic-numbers + const selector = hexUtils.slice(callData, 0, 4); + for (const [name, handler] of Object.entries(this._handlers)) { + if (handler && this.getSelector(name) === selector) { + const args = this.getABIDecodedTransactionData(name, callData); + const result = (handler as any)(...args); + return this._lookupAbiEncoder(this.getFunctionSignature(name)).encodeReturnValues([result]); + } + } + if (selector === this.getSelector('batchCall')) { + const calls = this.getABIDecodedTransactionData('batchCall', callData); + const results = calls.map(cd => this._callEncodedFunction(cd)); + return this._lookupAbiEncoder(this.getFunctionSignature('batchCall')).encodeReturnValues([results]); + } + throw new Error(`Unkown selector: ${selector}`); + } + + private _wrapCall( + superFn: (this: MockSamplerContract, ...args: TArgs) => ContractFunctionObj, + handler?: (this: MockSamplerContract, ...args: TArgs) => TResult, + // tslint:disable-next-line: trailing-comma + ...args: TArgs + ): ContractFunctionObj { return { - ...super.queryOrdersAndSampleBuys(orders, signatures, sources, fillAmounts), - callAsync: async (...args: any[]): Promise => { - if (!this.queryOrdersAndSampleBuysHandler) { - throw new Error('queryOrdersAndSampleBuys handler undefined'); + ...superFn.call(this, ...args), + callAsync: async (...callArgs: any[]): Promise => { + if (!handler) { + throw new Error(`${superFn.name} handler undefined`); } - return this.queryOrdersAndSampleBuysHandler(orders, signatures, sources, fillAmounts); + return handler.call(this, ...args); }, }; }