@0x/asset-swapper: Use batchCall() version of the ERC20BridgeSampler contract

This commit is contained in:
Lawrence Forman 2020-02-07 20:39:26 -05:00 committed by Lawrence Forman
parent 55d6eddbb2
commit 0c9c68030e
9 changed files with 1095 additions and 500 deletions

View File

@ -1,4 +1,13 @@
[ [
{
"version": "4.2.0",
"changes": [
{
"note": "Use `batchCall()` version of the `ERC20BridgeSampler` contract",
"pr": 2477
}
]
},
{ {
"timestamp": 1581204851, "timestamp": 1581204851,
"version": "4.1.2", "version": "4.1.2",

View File

@ -21,7 +21,7 @@ import {
} from './types'; } from './types';
import { assert } from './utils/assert'; import { assert } from './utils/assert';
import { calculateLiquidity } from './utils/calculate_liquidity'; 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 { dummyOrderUtils } from './utils/market_operation_utils/dummy_order_utils';
import { orderPrunerUtils } from './utils/order_prune_utils'; import { orderPrunerUtils } from './utils/order_prune_utils';
import { OrderStateUtils } from './utils/order_state_utils'; import { OrderStateUtils } from './utils/order_state_utils';
@ -162,12 +162,12 @@ export class SwapQuoter {
this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider);
this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS); this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); this._orderStateUtils = new OrderStateUtils(this._devUtilsContract);
const samplerContract = new IERC20BridgeSamplerContract( const sampler = new DexOrderSampler(
this._contractAddresses.erc20BridgeSampler, new IERC20BridgeSamplerContract(this._contractAddresses.erc20BridgeSampler, this.provider, {
this.provider, gas: samplerGasLimit,
{ gas: samplerGasLimit }, }),
); );
this._marketOperationUtils = new MarketOperationUtils(samplerContract, this._contractAddresses, { this._marketOperationUtils = new MarketOperationUtils(sampler, this._contractAddresses, {
chainId, chainId,
exchangeAddress: this._contractAddresses.exchange, exchangeAddress: this._contractAddresses.exchange,
}); });

View File

@ -4,15 +4,6 @@ import { ERC20BridgeSource, GetMarketOrdersOpts } from './types';
const INFINITE_TIMESTAMP_SEC = new BigNumber(2524604400); 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. * Valid sources for market sell.
*/ */
@ -36,7 +27,6 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
export const constants = { export const constants = {
INFINITE_TIMESTAMP_SEC, INFINITE_TIMESTAMP_SEC,
SOURCE_TO_ADDRESS,
SELL_SOURCES, SELL_SOURCES,
BUY_SOURCES, BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS, DEFAULT_GET_MARKET_ORDERS_OPTS,

View File

@ -1,5 +1,4 @@
import { ContractAddresses } from '@0x/contract-addresses'; import { ContractAddresses } from '@0x/contract-addresses';
import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { assetDataUtils, ERC20AssetData, orderCalculationUtils } from '@0x/order-utils'; import { assetDataUtils, ERC20AssetData, orderCalculationUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
@ -11,7 +10,7 @@ import { fillableAmountsUtils } from '../fillable_amounts_utils';
import { constants as marketOperationUtilConstants } from './constants'; import { constants as marketOperationUtilConstants } from './constants';
import { CreateOrderUtils } from './create_order'; import { CreateOrderUtils } from './create_order';
import { comparePathOutputs, FillsOptimizer, getPathOutput } from './fill_optimizer'; import { comparePathOutputs, FillsOptimizer, getPathOutput } from './fill_optimizer';
import { DexOrderSampler } from './sampler'; import { DexOrderSampler, getSampleAmounts } from './sampler';
import { import {
AggregationError, AggregationError,
CollapsedFill, CollapsedFill,
@ -27,22 +26,20 @@ import {
OrderDomain, OrderDomain,
} from './types'; } from './types';
export { DexOrderSampler } from './sampler';
const { ZERO_AMOUNT } = constants; const { ZERO_AMOUNT } = constants;
const { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, ERC20_PROXY_ID, SELL_SOURCES } = marketOperationUtilConstants; const { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, ERC20_PROXY_ID, SELL_SOURCES } = marketOperationUtilConstants;
export class MarketOperationUtils { export class MarketOperationUtils {
private readonly _dexSampler: DexOrderSampler;
private readonly _createOrderUtils: CreateOrderUtils; private readonly _createOrderUtils: CreateOrderUtils;
private readonly _orderDomain: OrderDomain;
constructor( constructor(
samplerContract: IERC20BridgeSamplerContract, private readonly _sampler: DexOrderSampler,
contractAddresses: ContractAddresses, contractAddresses: ContractAddresses,
orderDomain: OrderDomain, private readonly _orderDomain: OrderDomain,
) { ) {
this._dexSampler = new DexOrderSampler(samplerContract);
this._createOrderUtils = new CreateOrderUtils(contractAddresses); this._createOrderUtils = new CreateOrderUtils(contractAddresses);
this._orderDomain = orderDomain;
} }
/** /**
@ -65,10 +62,15 @@ export class MarketOperationUtils {
...DEFAULT_GET_MARKET_ORDERS_OPTS, ...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts, ...opts,
}; };
const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketSellAsync( const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
nativeOrders, const [fillableAmounts, dexQuotes] = await this._sampler.executeAsync(
DexOrderSampler.getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase), DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders),
DexOrderSampler.ops.getSellQuotes(
difference(SELL_SOURCES, _opts.excludedSources), difference(SELL_SOURCES, _opts.excludedSources),
makerToken,
takerToken,
getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase),
),
); );
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts( const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
nativeOrders, nativeOrders,
@ -134,11 +136,15 @@ export class MarketOperationUtils {
...DEFAULT_GET_MARKET_ORDERS_OPTS, ...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts, ...opts,
}; };
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketBuyAsync( const [fillableAmounts, dexQuotes] = await this._sampler.executeAsync(
nativeOrders, DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders),
DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase), DexOrderSampler.ops.getBuyQuotes(
difference(BUY_SOURCES, _opts.excludedSources), difference(BUY_SOURCES, _opts.excludedSources),
makerToken,
takerToken,
getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase),
),
); );
const signedOrderWithFillableAmounts = this._createBuyOrdersPathFromSamplerResultIfExists( const signedOrderWithFillableAmounts = this._createBuyOrdersPathFromSamplerResultIfExists(
nativeOrders, nativeOrders,
@ -174,17 +180,25 @@ export class MarketOperationUtils {
...opts, ...opts,
}; };
const batchSampleResults = await this._dexSampler.getBatchFillableAmountsAndSampleMarketBuyAsync( const sources = difference(BUY_SOURCES, _opts.excludedSources);
batchNativeOrders, const ops = [
makerAmounts.map(makerAmount => DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples)), ...batchNativeOrders.map(orders => DexOrderSampler.ops.getOrderFillableMakerAmounts(orders)),
difference(BUY_SOURCES, _opts.excludedSources), ...batchNativeOrders.map((orders, i) =>
); DexOrderSampler.ops.getBuyQuotes(sources, getOrderTokens(orders[0])[0], getOrderTokens(orders[0])[1], [
return batchSampleResults.map(([fillableAmounts, dexQuotes], i) => 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( this._createBuyOrdersPathFromSamplerResultIfExists(
batchNativeOrders[i], batchNativeOrders[i],
makerAmounts[i], makerAmounts[i],
fillableAmounts, fillableAmounts,
dexQuotes, batchDexQuotes[i],
_opts, _opts,
), ),
); );

View File

@ -2,18 +2,203 @@ import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { constants as marketOperationUtilConstants } from './constants';
import { DexSample, ERC20BridgeSource } from './types'; import { DexSample, ERC20BridgeSource } from './types';
const { SOURCE_TO_ADDRESS } = marketOperationUtilConstants; /**
* A composable operation the be run in `DexOrderSampler.executeAsync()`.
*/
export interface BatchedOperation<TResult> {
encodeCall(contract: IERC20BridgeSamplerContract): string;
handleCallResultsAsync(contract: IERC20BridgeSamplerContract, callResults: string): Promise<TResult>;
}
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<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.getOrderFillableTakerAssetAmounts(orders, orders.map(o => o.signature))
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('getOrderFillableTakerAssetAmounts', callResults);
},
};
},
getOrderFillableMakerAmounts(orders: SignedOrder[]): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.getOrderFillableMakerAssetAmounts(orders, orders.map(o => o.signature))
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('getOrderFillableMakerAssetAmounts', callResults);
},
};
},
getKyberSellQuotes(
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.sampleSellsFromKyberNetwork(takerToken, makerToken, takerFillAmounts)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('sampleSellsFromKyberNetwork', callResults);
},
};
},
getUniswapSellQuotes(
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.sampleSellsFromUniswap(takerToken, makerToken, takerFillAmounts)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('sampleSellsFromUniswap', callResults);
},
};
},
getEth2DaiSellQuotes(
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.sampleSellsFromEth2Dai(takerToken, makerToken, takerFillAmounts)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('sampleSellsFromEth2Dai', callResults);
},
};
},
getUniswapBuyQuotes(
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.sampleBuysFromUniswap(takerToken, makerToken, makerFillAmounts)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('sampleBuysFromUniswap', callResults);
},
};
},
getEth2DaiBuyQuotes(
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.sampleBuysFromEth2Dai(takerToken, makerToken, makerFillAmounts)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('sampleBuysFromEth2Dai', callResults);
},
};
},
getSellQuotes(
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<DexSample[][]> {
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<string[]>('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<DexSample[][]> {
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<string[]>('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`. * Generate sample amounts up to `maxFillAmount`.
*/ */
public static getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, expBase: number = 1): BigNumber[] { export function getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, expBase: number = 1): BigNumber[] {
const distribution = [...Array<BigNumber>(numSamples)].map((v, i) => new BigNumber(expBase).pow(i)); const distribution = [...Array<BigNumber>(numSamples)].map((v, i) => new BigNumber(expBase).pow(i));
const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution))); const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution)));
const amounts = stepSizes.map((s, i) => { const amounts = stepSizes.map((s, i) => {
@ -24,77 +209,123 @@ export class DexOrderSampler {
return amounts; return amounts;
} }
type BatchedOperationResult<T> = T extends BatchedOperation<infer TResult> ? TResult : never;
/**
* Encapsulates interactions with the `ERC20BridgeSampler` contract.
*/
export class DexOrderSampler {
/**
* Composable operations that can be batched in a single transaction,
* for use with `DexOrderSampler.executeAsync()`.
*/
public static ops = samplerOperations;
private readonly _samplerContract: IERC20BridgeSamplerContract;
constructor(samplerContract: IERC20BridgeSamplerContract) { constructor(samplerContract: IERC20BridgeSamplerContract) {
this._samplerContract = samplerContract; this._samplerContract = samplerContract;
} }
public async getFillableAmountsAndSampleMarketBuyAsync( /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */
nativeOrders: SignedOrder[],
sampleAmounts: BigNumber[], // prettier-ignore
sources: ERC20BridgeSource[], public async executeAsync<
): Promise<[BigNumber[], DexSample[][]]> { T1
const signatures = nativeOrders.map(o => o.signature); >(...ops: [T1]): Promise<[
const [fillableAmount, rawSamples] = await this._samplerContract BatchedOperationResult<T1>
.queryOrdersAndSampleBuys(nativeOrders, signatures, sources.map(s => SOURCE_TO_ADDRESS[s]), sampleAmounts) ]>;
.callAsync();
const quotes = rawSamples.map((rawDexSamples, sourceIdx) => { // prettier-ignore
const source = sources[sourceIdx]; public async executeAsync<
return rawDexSamples.map((sample, sampleIdx) => ({ T1, T2
source, >(...ops: [T1, T2]): Promise<[
input: sampleAmounts[sampleIdx], BatchedOperationResult<T1>,
output: sample, BatchedOperationResult<T2>
})); ]>;
});
return [fillableAmount, quotes]; // prettier-ignore
public async executeAsync<
T1, T2, T3
>(...ops: [T1, T2, T3]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4
>(...ops: [T1, T2, T3, T4]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5
>(...ops: [T1, T2, T3, T4, T5]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5, T6
>(...ops: [T1, T2, T3, T4, T5, T6]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>,
BatchedOperationResult<T6>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5, T6, T7
>(...ops: [T1, T2, T3, T4, T5, T6, T7]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>,
BatchedOperationResult<T6>,
BatchedOperationResult<T7>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5, T6, T7, T8
>(...ops: [T1, T2, T3, T4, T5, T6, T7, T8]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>,
BatchedOperationResult<T6>,
BatchedOperationResult<T7>,
BatchedOperationResult<T8>
]>;
/**
* Run a series of operations from `DexOrderSampler.ops` in a single transaction.
*/
public async executeAsync(...ops: any[]): Promise<any[]> {
return this.executeBatchAsync(ops);
} }
public async getBatchFillableAmountsAndSampleMarketBuyAsync( /**
nativeOrders: SignedOrder[][], * Run a series of operations from `DexOrderSampler.ops` in a single transaction.
sampleAmounts: BigNumber[][], * Takes an arbitrary length array, but is not typesafe.
sources: ERC20BridgeSource[], */
): Promise<Array<[BigNumber[], DexSample[][]]>> { public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> {
const signatures = nativeOrders.map(o => o.map(i => i.signature)); const callDatas = ops.map(o => o.encodeCall(this._samplerContract));
const fillableAmountsAndSamples = await this._samplerContract const callResults = await this._samplerContract.batchCall(callDatas).callAsync();
.queryBatchOrdersAndSampleBuys( return Promise.all(callResults.map(async (r, i) => ops[i].handleCallResultsAsync(this._samplerContract, r)));
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;
}
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];
} }
} }

View File

@ -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>): 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

View File

@ -10,23 +10,20 @@ import {
} from '@0x/contracts-test-utils'; } from '@0x/contracts-test-utils';
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-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 { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants'; import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; 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 // tslint:disable: custom-no-magic-numbers
describe('MarketOperationUtils tests', () => { describe('MarketOperationUtils tests', () => {
const CHAIN_ID = 1;
const contractAddresses = getContractAddressesForChainOrThrow(CHAIN_ID); const contractAddresses = getContractAddressesForChainOrThrow(CHAIN_ID);
const ETH2DAI_BRIDGE_ADDRESS = contractAddresses.eth2DaiBridge; const ETH2DAI_BRIDGE_ADDRESS = contractAddresses.eth2DaiBridge;
const KYBER_BRIDGE_ADDRESS = contractAddresses.kyberBridge; const KYBER_BRIDGE_ADDRESS = contractAddresses.kyberBridge;
@ -36,10 +33,15 @@ describe('MarketOperationUtils tests', () => {
const TAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress();
const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN);
const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
let originalSamplerOperations: any;
interface RatesBySource { before(() => {
[source: string]: Numberish[]; originalSamplerOperations = DexOrderSampler.ops;
} });
after(() => {
DexOrderSampler.ops = originalSamplerOperations;
});
function createOrder(overrides?: Partial<SignedOrder>): SignedOrder { function createOrder(overrides?: Partial<SignedOrder>): SignedOrder {
return { return {
@ -82,15 +84,6 @@ describe('MarketOperationUtils tests', () => {
throw new Error(`Unknown bridge address: ${bridgeAddress}`); 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 { function assertSamePrefix(actual: string, expected: string): void {
expect(actual.substr(0, expected.length)).to.eq(expected); expect(actual.substr(0, expected.length)).to.eq(expected);
} }
@ -107,7 +100,7 @@ describe('MarketOperationUtils tests', () => {
function createOrdersFromBuyRates(makerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] { function createOrdersFromBuyRates(makerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] {
const singleMakerAssetAmount = makerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP); const singleMakerAssetAmount = makerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP);
return (rates as any).map((r: Numberish) => return rates.map(r =>
createOrder({ createOrder({
makerAssetAmount: singleMakerAssetAmount, makerAssetAmount: singleMakerAssetAmount,
takerAssetAmount: singleMakerAssetAmount.div(r).integerValue(), 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 = { const ORDER_DOMAIN = {
exchangeAddress: contractAddresses.exchange, exchangeAddress: contractAddresses.exchange,
chainId: CHAIN_ID, chainId: CHAIN_ID,
}; };
describe('DexOrderSampler', () => { type GetQuotesOperation = (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => BigNumber[];
describe('getSampleAmounts()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const NUM_SAMPLES = 16;
it('generates the correct number of amounts', () => { function createGetQuotesOperationFromSellRates(rates: Numberish[]): GetQuotesOperation {
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES); return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
expect(amounts).to.be.length(NUM_SAMPLES); 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);
});
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);
});
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);
});
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);
} }
});
});
describe('getFillableAmountsAndSampleMarketOperationAsync()', () => { function createGetQuotesOperationFromBuyRates(rates: Numberish[]): GetQuotesOperation {
const SAMPLE_AMOUNTS = [100, 500, 1000].map(v => new BigNumber(v)); return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
const ORDERS = _.times(4, () => createOrder()); return fillAmounts.map((a, i) => a.div(rates[i]).integerValue());
};
}
it('makes an eth_call with the correct arguments for a sell', async () => { type GetMultipleQuotesOperation = (
const sampler = new MockSamplerContract({ sources: ERC20BridgeSource[],
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { makerToken: string,
expect(orders).to.deep.eq(ORDERS); takerToken: string,
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); fillAmounts: BigNumber[],
expect(sources).to.deep.eq(SELL_SOURCES.map(s => SOURCE_TO_ADDRESS[s])); ) => DexSample[][];
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);
});
it('makes an eth_call with the correct arguments for a buy', async () => { function createGetMultipleQuotesOperationFromSellRates(rates: RatesBySource): GetMultipleQuotesOperation {
const sampler = new MockSamplerContract({ return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { return sources.map(s =>
expect(orders).to.deep.eq(ORDERS); fillAmounts.map((a, i) => ({
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); source: s,
expect(sources).to.deep.eq(BUY_SOURCES.map(s => SOURCE_TO_ADDRESS[s])); input: a,
expect(fillAmounts).to.deep.eq(SAMPLE_AMOUNTS); output: a.times(rates[s][i]).integerValue(),
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);
});
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);
});
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 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))),
);
} }
function createGetMultipleQuotesOperationFromBuyRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return sources.map(s =>
fillAmounts.map((a, i) => ({
source: s,
input: a,
output: a.div(rates[s][i]).integerValue(),
})),
);
};
}
function createDecreasingRates(count: number): BigNumber[] {
const rates: BigNumber[] = [];
const initialRate = getRandomFloat(1e-3, 1e2);
_.times(count, () => getRandomFloat(0.95, 1)).forEach((r, i) => {
const prevRate = i === 0 ? initialRate : rates[i - 1];
rates.push(prevRate.times(r));
});
return rates;
}
const NUM_SAMPLES = 3;
interface RatesBySource {
[source: string]: Numberish[];
}
const DEFAULT_RATES: RatesBySource = {
[ERC20BridgeSource.Native]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES),
};
function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource {
const minSourceRates = Object.keys(rates).map(s => _.last(rates[s]) as BigNumber);
const bestSourceRate = BigNumber.max(...minSourceRates);
let source = Object.keys(rates)[_.findIndex(minSourceRates, t => bestSourceRate.eq(t))] as ERC20BridgeSource;
// Native order rates play by different rules.
if (source !== ERC20BridgeSource.Native) {
const nativeTotalRate = BigNumber.sum(...rates[ERC20BridgeSource.Native]).div(
rates[ERC20BridgeSource.Native].length,
);
if (nativeTotalRate.gt(bestSourceRate)) {
source = ERC20BridgeSource.Native;
}
}
return source;
}
const DEFAULT_OPS = {
getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.takerAssetAmount);
},
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.makerAssetAmount);
},
getKyberSellQuotes: createGetQuotesOperationFromSellRates(DEFAULT_RATES[ERC20BridgeSource.Kyber]),
getUniswapSellQuotes: createGetQuotesOperationFromSellRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
getEth2DaiSellQuotes: createGetQuotesOperationFromSellRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
getUniswapBuyQuotes: createGetQuotesOperationFromBuyRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
getEth2DaiBuyQuotes: createGetQuotesOperationFromBuyRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
getSellQuotes: createGetMultipleQuotesOperationFromSellRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleQuotesOperationFromBuyRates(DEFAULT_RATES),
};
function replaceSamplerOps(ops: Partial<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', () => { describe('MarketOperationUtils', () => {
let marketOperationUtils: MarketOperationUtils;
before(async () => {
marketOperationUtils = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN);
});
describe('getMarketSellOrdersAsync()', () => { describe('getMarketSellOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18); const FILL_AMOUNT = getRandomInteger(1, 1e18);
const SOURCE_RATES = createRandomRates();
const ORDERS = createOrdersFromSellRates( const ORDERS = createOrdersFromSellRates(
FILL_AMOUNT, FILL_AMOUNT,
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]), _.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]),
);
const DEFAULT_SAMPLER = createSamplerFromSellRates(SOURCE_RATES);
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0, sampleDistributionBase: 1 };
const defaultMarketOperationUtils = new MarketOperationUtils(
DEFAULT_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
); );
const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 };
it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => { beforeEach(() => {
let wasCalled = false; replaceSamplerOps();
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();
}); });
it('queries `numSamples` samples', async () => { it('queries `numSamples` samples', async () => {
const numSamples = _.random(1, 16); const numSamples = _.random(1, 16);
let fillAmountsLength = 0; let actualNumSamples = 0;
const sampler = new MockSamplerContract({ replaceSamplerOps({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { getSellQuotes: (sources, makerToken, takerToken, amounts) => {
fillAmountsLength = fillAmounts.length; actualNumSamples = amounts.length;
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts);
}, },
}); });
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
numSamples, numSamples,
}); });
expect(fillAmountsLength).eq(numSamples); expect(actualNumSamples).eq(numSamples);
}); });
it('polls all DEXes if `excludedSources` is empty', async () => { it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({ replaceSamplerOps({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { getSellQuotes: (sources, makerToken, takerToken, amounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a)); sourcesPolled = sources.slice();
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts);
}, },
}); });
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources: [], 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 () => { it('does not poll DEXes in `excludedSources`', async () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({ replaceSamplerOps({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => { getSellQuotes: (sources, makerToken, takerToken, amounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a)); sourcesPolled = sources.slice();
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts);
}, },
}); });
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources, 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 () => { it('returns the most cost-effective single source if `runLimit == 0`', async () => {
const bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES))); const bestSource = findSourceWithMaxOutput(DEFAULT_RATES);
const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate));
expect(bestSource).to.exist(''); expect(bestSource).to.exist('');
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
runLimit: 0, runLimit: 0,
}); });
@ -390,7 +308,7 @@ describe('MarketOperationUtils tests', () => {
}); });
it('generates bridge orders with correct asset data', async () => { 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. // Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT, FILL_AMOUNT,
@ -414,7 +332,7 @@ describe('MarketOperationUtils tests', () => {
}); });
it('generates bridge orders with correct taker amount', async () => { 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. // Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT, FILL_AMOUNT,
@ -426,7 +344,7 @@ describe('MarketOperationUtils tests', () => {
it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { it('generates bridge orders with max slippage of `bridgeSlippage`', async () => {
const bridgeSlippage = _.random(0.1, true); 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. // Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT, FILL_AMOUNT,
@ -435,7 +353,7 @@ describe('MarketOperationUtils tests', () => {
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData); 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(); const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber();
assertRoughlyEquals(slippage, bridgeSlippage, 8); assertRoughlyEquals(slippage, bridgeSlippage, 8);
} }
@ -450,7 +368,7 @@ describe('MarketOperationUtils tests', () => {
makerAssetAmount: dustAmount.times(maxRate.plus(0.01)), makerAssetAmount: dustAmount.times(maxRate.plus(0.01)),
takerAssetAmount: dustAmount, takerAssetAmount: dustAmount,
}); });
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
_.shuffle([dustOrder, ...ORDERS]), _.shuffle([dustOrder, ...ORDERS]),
FILL_AMOUNT, FILL_AMOUNT,
// Ignore all DEX sources so only native orders are returned. // 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.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 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]; rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils( replaceSamplerOps({
createSamplerFromSellRates(rates), getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates),
contractAddresses, });
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
@ -495,11 +411,9 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 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.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils( replaceSamplerOps({
createSamplerFromSellRates(rates), getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates),
contractAddresses, });
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
@ -522,11 +436,9 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.15, 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.Eth2Dai] = [0.15, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils( replaceSamplerOps({
createSamplerFromSellRates(rates), getSellQuotes: createGetMultipleQuotesOperationFromSellRates(rates),
contractAddresses, });
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
@ -546,61 +458,40 @@ describe('MarketOperationUtils tests', () => {
describe('getMarketBuyOrdersAsync()', () => { describe('getMarketBuyOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18); const FILL_AMOUNT = getRandomInteger(1, 1e18);
const SOURCE_RATES = _.omit(createRandomRates(), [ERC20BridgeSource.Kyber]);
const ORDERS = createOrdersFromBuyRates( const ORDERS = createOrdersFromBuyRates(
FILL_AMOUNT, FILL_AMOUNT,
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]), _.times(NUM_SAMPLES, () => DEFAULT_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,
); );
const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 };
it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => { beforeEach(() => {
let wasCalled = false; replaceSamplerOps();
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();
}); });
it('queries `numSamples` samples', async () => { it('queries `numSamples` samples', async () => {
const numSamples = _.random(1, 16); const numSamples = _.random(1, 16);
let fillAmountsLength = 0; let actualNumSamples = 0;
const sampler = new MockSamplerContract({ replaceSamplerOps({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { getBuyQuotes: (sources, makerToken, takerToken, amounts) => {
fillAmountsLength = fillAmounts.length; actualNumSamples = amounts.length;
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts);
}, },
}); });
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
numSamples, numSamples,
}); });
expect(fillAmountsLength).eq(numSamples); expect(actualNumSamples).eq(numSamples);
}); });
it('polls all DEXes if `excludedSources` is empty', async () => { it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({ replaceSamplerOps({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { getBuyQuotes: (sources, makerToken, takerToken, amounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a)); sourcesPolled = sources.slice();
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts);
}, },
}); });
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources: [], excludedSources: [],
@ -609,16 +500,14 @@ describe('MarketOperationUtils tests', () => {
}); });
it('does not poll DEXes in `excludedSources`', async () => { 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[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({ replaceSamplerOps({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { getBuyQuotes: (sources, makerToken, takerToken, amounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a)); sourcesPolled = sources.slice();
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts);
}, },
}); });
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
excludedSources, excludedSources,
@ -627,10 +516,9 @@ describe('MarketOperationUtils tests', () => {
}); });
it('returns the most cost-effective single source if `runLimit == 0`', async () => { it('returns the most cost-effective single source if `runLimit == 0`', async () => {
const bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES))); const bestSource = findSourceWithMaxOutput(DEFAULT_RATES);
const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate));
expect(bestSource).to.exist(''); expect(bestSource).to.exist('');
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
runLimit: 0, runLimit: 0,
}); });
@ -638,8 +526,9 @@ describe('MarketOperationUtils tests', () => {
expect(uniqueAssetDatas).to.be.length(1); expect(uniqueAssetDatas).to.be.length(1);
expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource); expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource);
}); });
it('generates bridge orders with correct asset data', async () => { 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. // Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT, FILL_AMOUNT,
@ -663,7 +552,7 @@ describe('MarketOperationUtils tests', () => {
}); });
it('generates bridge orders with correct taker amount', async () => { 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. // Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT, FILL_AMOUNT,
@ -675,7 +564,7 @@ describe('MarketOperationUtils tests', () => {
it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { it('generates bridge orders with max slippage of `bridgeSlippage`', async () => {
const bridgeSlippage = _.random(0.1, true); 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. // Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT, FILL_AMOUNT,
@ -684,9 +573,7 @@ describe('MarketOperationUtils tests', () => {
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData); const source = getSourceFromAssetData(order.makerAssetData);
const expectedTakerAmount = FILL_AMOUNT.div(SOURCE_RATES[source][0]).integerValue( const expectedTakerAmount = FILL_AMOUNT.div(_.last(DEFAULT_RATES[source]) as BigNumber);
BigNumber.ROUND_UP,
);
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1; const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1;
assertRoughlyEquals(slippage, bridgeSlippage, 8); assertRoughlyEquals(slippage, bridgeSlippage, 8);
} }
@ -701,7 +588,7 @@ describe('MarketOperationUtils tests', () => {
makerAssetAmount: dustAmount, makerAssetAmount: dustAmount,
takerAssetAmount: dustAmount.div(maxRate.plus(0.01)).integerValue(BigNumber.ROUND_DOWN), takerAssetAmount: dustAmount.div(maxRate.plus(0.01)).integerValue(BigNumber.ROUND_DOWN),
}); });
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
_.shuffle([dustOrder, ...ORDERS]), _.shuffle([dustOrder, ...ORDERS]),
FILL_AMOUNT, FILL_AMOUNT,
// Ignore all DEX sources so only native orders are returned. // 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.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 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.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils( replaceSamplerOps({
createSamplerFromBuyRates(rates), getBuyQuotes: createGetMultipleQuotesOperationFromBuyRates(rates),
contractAddresses, });
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,

View File

@ -8,7 +8,7 @@ import 'mocha';
import { constants } from '../src/constants'; import { constants } from '../src/constants';
import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types'; 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 { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator'; import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator';
@ -43,27 +43,26 @@ const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = {
}, },
}; };
const createSamplerFromSignedOrdersWithFillableAmounts = ( function createSamplerFromSignedOrdersWithFillableAmounts(
signedOrders: SignedOrderWithFillableAmounts[], signedOrders: SignedOrderWithFillableAmounts[],
): MockSamplerContract => { ): DexOrderSampler {
const sampler = new MockSamplerContract({ const sampleDexHandler = (takerToken: string, makerToken: string, amounts: BigNumber[]) => {
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => { return amounts.map(() => constants.ZERO_AMOUNT);
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;
}; };
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 // TODO(dorothy-zbornak): Replace these tests entirely with unit tests because
// omg they're a nightmare to maintain. // omg they're a nightmare to maintain.

View File

@ -2,15 +2,25 @@ import { ContractFunctionObj } from '@0x/base-contract';
import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers'; import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { constants } from '@0x/contracts-test-utils'; import { constants } from '@0x/contracts-test-utils';
import { Order } from '@0x/types'; import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber, hexUtils } from '@0x/utils';
export type QueryAndSampleResult = [BigNumber[], BigNumber[][]]; export type GetOrderFillableAssetAmountResult = BigNumber[];
export type QueryAndSampleHandler = ( export type GetOrderFillableAssetAmountHandler = (
orders: Order[], orders: Order[],
signatures: string[], signatures: string[],
sources: string[], ) => GetOrderFillableAssetAmountResult;
fillAmounts: BigNumber[],
) => QueryAndSampleResult; 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 = { const DUMMY_PROVIDER = {
sendAsync: (...args: any[]): any => { 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 { export class MockSamplerContract extends IERC20BridgeSamplerContract {
public readonly queryOrdersAndSampleSellsHandler?: QueryAndSampleHandler; private readonly _handlers: Partial<Handlers> = {};
public readonly queryOrdersAndSampleBuysHandler?: QueryAndSampleHandler;
public constructor( public constructor(handlers: Partial<Handlers> = {}) {
handlers?: Partial<{
queryOrdersAndSampleSells: QueryAndSampleHandler;
queryOrdersAndSampleBuys: QueryAndSampleHandler;
}>,
) {
super(constants.NULL_ADDRESS, DUMMY_PROVIDER); super(constants.NULL_ADDRESS, DUMMY_PROVIDER);
const _handlers = { this._handlers = handlers;
queryOrdersAndSampleSells: undefined,
queryOrdersAndSampleBuys: undefined,
...handlers,
};
this.queryOrdersAndSampleSellsHandler = _handlers.queryOrdersAndSampleSells;
this.queryOrdersAndSampleBuysHandler = _handlers.queryOrdersAndSampleBuys;
} }
public queryOrdersAndSampleSells( public batchCall(callDatas: string[]): ContractFunctionObj<string[]> {
orders: Order[],
signatures: string[],
sources: string[],
fillAmounts: BigNumber[],
): ContractFunctionObj<QueryAndSampleResult> {
return { return {
...super.queryOrdersAndSampleSells(orders, signatures, sources, fillAmounts), ...super.batchCall(callDatas),
callAsync: async (...args: any[]): Promise<QueryAndSampleResult> => { callAsync: async (...callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)),
if (!this.queryOrdersAndSampleSellsHandler) {
throw new Error('queryOrdersAndSampleSells handler undefined');
}
return this.queryOrdersAndSampleSellsHandler(orders, signatures, sources, fillAmounts);
},
}; };
} }
public queryOrdersAndSampleBuys( public getOrderFillableMakerAssetAmounts(
orders: Order[], orders: Order[],
signatures: string[], signatures: string[],
sources: string[], ): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
fillAmounts: BigNumber[], return this._wrapCall(
): ContractFunctionObj<QueryAndSampleResult> { super.getOrderFillableMakerAssetAmounts,
return { this._handlers.getOrderFillableMakerAssetAmounts,
...super.queryOrdersAndSampleBuys(orders, signatures, sources, fillAmounts), orders,
callAsync: async (...args: any[]): Promise<QueryAndSampleResult> => { signatures,
if (!this.queryOrdersAndSampleBuysHandler) { );
throw new Error('queryOrdersAndSampleBuys handler undefined');
} }
return this.queryOrdersAndSampleBuysHandler(orders, signatures, sources, fillAmounts);
public getOrderFillableTakerAssetAmounts(
orders: Order[],
signatures: string[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall(
super.getOrderFillableTakerAssetAmounts,
this._handlers.getOrderFillableTakerAssetAmounts,
orders,
signatures,
);
}
public sampleSellsFromKyberNetwork(
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall(
super.sampleSellsFromKyberNetwork,
this._handlers.sampleSellsFromKyberNetwork,
takerToken,
makerToken,
takerAssetAmounts,
);
}
public sampleSellsFromEth2Dai(
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall(
super.sampleSellsFromEth2Dai,
this._handlers.sampleSellsFromEth2Dai,
takerToken,
makerToken,
takerAssetAmounts,
);
}
public sampleSellsFromUniswap(
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall(
super.sampleSellsFromUniswap,
this._handlers.sampleSellsFromUniswap,
takerToken,
makerToken,
takerAssetAmounts,
);
}
public sampleBuysFromEth2Dai(
takerToken: string,
makerToken: string,
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall(
super.sampleBuysFromEth2Dai,
this._handlers.sampleBuysFromEth2Dai,
takerToken,
makerToken,
makerAssetAmounts,
);
}
public sampleBuysFromUniswap(
takerToken: string,
makerToken: string,
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
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<any>(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<string[]>('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<TArgs extends any[], TResult>(
superFn: (this: MockSamplerContract, ...args: TArgs) => ContractFunctionObj<TResult>,
handler?: (this: MockSamplerContract, ...args: TArgs) => TResult,
// tslint:disable-next-line: trailing-comma
...args: TArgs
): ContractFunctionObj<TResult> {
return {
...superFn.call(this, ...args),
callAsync: async (...callArgs: any[]): Promise<TResult> => {
if (!handler) {
throw new Error(`${superFn.name} handler undefined`);
}
return handler.call(this, ...args);
}, },
}; };
} }