Merge pull request #2627 from 0xProject/feature/new-order-reporter

asset-swapper: QuoteReport response
This commit is contained in:
Steve Klebanoff 2020-07-23 17:50:48 -07:00 committed by GitHub
commit 72c869649a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 726 additions and 116 deletions

View File

@ -2,6 +2,10 @@
{ {
"version": "4.7.0", "version": "4.7.0",
"changes": [ "changes": [
{
"note": "Return quoteReport from SwapQuoter functions",
"pr": 2627
},
{ {
"note": "Allow an empty override for sampler overrides", "note": "Allow an empty override for sampler overrides",
"pr": 2637 "pr": 2637

View File

@ -64,9 +64,11 @@ export {
SwapQuoteInfo, SwapQuoteInfo,
SwapQuoteOrdersBreakdown, SwapQuoteOrdersBreakdown,
SwapQuoteRequestOpts, SwapQuoteRequestOpts,
SwapQuoterRfqtOpts,
SwapQuoterError, SwapQuoterError,
SwapQuoterOpts, SwapQuoterOpts,
} from './types'; } from './types';
import { ERC20BridgeSource } from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { export {
BalancerFillData, BalancerFillData,
@ -86,3 +88,11 @@ export {
export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export { QuoteRequestor } from './utils/quote_requestor'; export { QuoteRequestor } from './utils/quote_requestor';
export { rfqtMocker } from './utils/rfqt_mocker'; export { rfqtMocker } from './utils/rfqt_mocker';
export {
BridgeReportSource,
NativeOrderbookReportSource,
NativeRFQTReportSource,
QuoteReport,
QuoteReportSource,
} from './utils/quote_report_generator';
export type Native = ERC20BridgeSource.Native;

View File

@ -20,6 +20,7 @@ import {
SwapQuote, SwapQuote,
SwapQuoteRequestOpts, SwapQuoteRequestOpts,
SwapQuoterOpts, SwapQuoterOpts,
SwapQuoterRfqtOpts,
} 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';
@ -46,8 +47,7 @@ export class SwapQuoter {
private readonly _devUtilsContract: DevUtilsContract; private readonly _devUtilsContract: DevUtilsContract;
private readonly _marketOperationUtils: MarketOperationUtils; private readonly _marketOperationUtils: MarketOperationUtils;
private readonly _orderStateUtils: OrderStateUtils; private readonly _orderStateUtils: OrderStateUtils;
private readonly _quoteRequestor: QuoteRequestor; private readonly _rfqtOptions?: SwapQuoterRfqtOpts;
private readonly _rfqtTakerApiKeyWhitelist: string[];
/** /**
* Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders. * Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders.
@ -168,7 +168,8 @@ export class SwapQuoter {
this.orderbook = orderbook; this.orderbook = orderbook;
this.expiryBufferMs = expiryBufferMs; this.expiryBufferMs = expiryBufferMs;
this.permittedOrderFeeTypes = permittedOrderFeeTypes; this.permittedOrderFeeTypes = permittedOrderFeeTypes;
this._rfqtTakerApiKeyWhitelist = rfqt ? rfqt.takerApiKeyWhitelist || [] : [];
this._rfqtOptions = rfqt;
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider);
this._protocolFeeUtils = ProtocolFeeUtils.getInstance( this._protocolFeeUtils = ProtocolFeeUtils.getInstance(
@ -176,12 +177,6 @@ export class SwapQuoter {
options.ethGasStationUrl, options.ethGasStationUrl,
); );
this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); this._orderStateUtils = new OrderStateUtils(this._devUtilsContract);
this._quoteRequestor = new QuoteRequestor(
rfqt ? rfqt.makerAssetOfferings || {} : {},
rfqt ? rfqt.warningLogger : undefined,
rfqt ? rfqt.infoLogger : undefined,
expiryBufferMs,
);
// Allow the sampler bytecode to be overwritten using geths override functionality // Allow the sampler bytecode to be overwritten using geths override functionality
const samplerBytecode = _.get(ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object'); const samplerBytecode = _.get(ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object');
const defaultCodeOverrides = samplerBytecode const defaultCodeOverrides = samplerBytecode
@ -569,24 +564,34 @@ export class SwapQuoter {
// get batches of orders from different sources, awaiting sources in parallel // get batches of orders from different sources, awaiting sources in parallel
const orderBatchPromises: Array<Promise<SignedOrder[]>> = []; const orderBatchPromises: Array<Promise<SignedOrder[]>> = [];
orderBatchPromises.push(
// Don't fetch Open Orderbook orders from the DB if Native has been excluded, or if `nativeExclusivelyRFQT` has been set. const skipOpenOrderbook =
opts.excludedSources.includes(ERC20BridgeSource.Native) || opts.excludedSources.includes(ERC20BridgeSource.Native) ||
(opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true) (opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true);
? Promise.resolve([]) if (!skipOpenOrderbook) {
: this._getSignedOrdersAsync(makerAssetData, takerAssetData), orderBatchPromises.push(this._getSignedOrdersAsync(makerAssetData, takerAssetData)); // order book
}
const rfqtOptions = this._rfqtOptions;
const quoteRequestor = new QuoteRequestor(
rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {},
rfqtOptions ? rfqtOptions.warningLogger : undefined,
rfqtOptions ? rfqtOptions.infoLogger : undefined,
this.expiryBufferMs,
); );
if ( if (
opts.rfqt && // This is an RFQT-enabled API request opts.rfqt && // This is an RFQT-enabled API request
opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote
!opts.excludedSources.includes(ERC20BridgeSource.Native) && // Native liquidity is not excluded opts.rfqt.apiKey &&
this._rfqtTakerApiKeyWhitelist.includes(opts.rfqt.apiKey) // A valid API key was provided this._isApiKeyWhitelisted(opts.rfqt.apiKey) && // A valid API key was provided
!opts.excludedSources.includes(ERC20BridgeSource.Native) // Native liquidity is not excluded
) { ) {
if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) {
throw new Error('RFQ-T requests must specify a taker address'); throw new Error('RFQ-T requests must specify a taker address');
} }
orderBatchPromises.push( orderBatchPromises.push(
this._quoteRequestor quoteRequestor
.requestRfqtFirmQuotesAsync( .requestRfqtFirmQuotesAsync(
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
@ -600,7 +605,7 @@ export class SwapQuoter {
const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises); const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises);
const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch)); const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch), []);
const orders = sortingUtils.sortOrders(unsortedOrders); const orders = sortingUtils.sortOrders(unsortedOrders);
@ -615,8 +620,8 @@ export class SwapQuoter {
const calcOpts: CalculateSwapQuoteOpts = opts; const calcOpts: CalculateSwapQuoteOpts = opts;
if (calcOpts.rfqt !== undefined && this._shouldEnableIndicativeRfqt(calcOpts.rfqt, marketOperation)) { if (calcOpts.rfqt !== undefined) {
calcOpts.rfqt.quoteRequestor = this._quoteRequestor; calcOpts.rfqt.quoteRequestor = quoteRequestor;
} }
if (marketOperation === MarketOperation.Buy) { if (marketOperation === MarketOperation.Buy) {
@ -637,13 +642,9 @@ export class SwapQuoter {
return swapQuote; return swapQuote;
} }
private _shouldEnableIndicativeRfqt(opts: CalculateSwapQuoteOpts['rfqt'], op: MarketOperation): boolean { private _isApiKeyWhitelisted(apiKey: string): boolean {
return ( const whitelistedApiKeys = this._rfqtOptions ? this._rfqtOptions.takerApiKeyWhitelist : [];
opts !== undefined && return whitelistedApiKeys.includes(apiKey);
opts.isIndicative !== undefined &&
opts.isIndicative &&
this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey)
);
} }
} }
// tslint:disable-next-line: max-file-line-count // tslint:disable-next-line: max-file-line-count

View File

@ -3,6 +3,7 @@ import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { GetMarketOrdersOpts, OptimizedMarketOrder } from './utils/market_operation_utils/types'; import { GetMarketOrdersOpts, OptimizedMarketOrder } from './utils/market_operation_utils/types';
import { QuoteReport } from './utils/quote_report_generator';
import { LogFunction } from './utils/quote_requestor'; import { LogFunction } from './utils/quote_requestor';
/** /**
@ -155,6 +156,7 @@ export interface SwapQuoteBase {
bestCaseQuoteInfo: SwapQuoteInfo; bestCaseQuoteInfo: SwapQuoteInfo;
worstCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo;
sourceBreakdown: SwapQuoteOrdersBreakdown; sourceBreakdown: SwapQuoteOrdersBreakdown;
quoteReport?: QuoteReport;
} }
/** /**
@ -236,6 +238,13 @@ export interface RfqtMakerAssetOfferings {
export { LogFunction } from './utils/quote_requestor'; export { LogFunction } from './utils/quote_requestor';
export interface SwapQuoterRfqtOpts {
takerApiKeyWhitelist: string[];
makerAssetOfferings: RfqtMakerAssetOfferings;
warningLogger?: LogFunction;
infoLogger?: LogFunction;
}
/** /**
* chainId: The ethereum chain id. Defaults to 1 (mainnet). * chainId: The ethereum chain id. Defaults to 1 (mainnet).
* orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). * orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s).
@ -252,12 +261,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
liquidityProviderRegistryAddress?: string; liquidityProviderRegistryAddress?: string;
multiBridgeAddress?: string; multiBridgeAddress?: string;
ethGasStationUrl?: string; ethGasStationUrl?: string;
rfqt?: { rfqt?: SwapQuoterRfqtOpts;
takerApiKeyWhitelist: string[];
makerAssetOfferings: RfqtMakerAssetOfferings;
warningLogger?: LogFunction;
infoLogger?: LogFunction;
};
samplerOverrides?: SamplerOverrides; samplerOverrides?: SamplerOverrides;
} }

View File

@ -2,10 +2,13 @@ import { ContractAddresses } from '@0x/contract-addresses';
import { RFQTIndicativeQuote } from '@0x/quote-server'; import { RFQTIndicativeQuote } from '@0x/quote-server';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
import { QuoteRequestor } from '../quote_requestor';
import { difference } from '../utils'; import { difference } from '../utils';
import { QuoteReportGenerator } from './../quote_report_generator';
import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants'; import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants';
import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills';
import { import {
@ -23,6 +26,7 @@ import {
FeeSchedule, FeeSchedule,
GetMarketOrdersOpts, GetMarketOrdersOpts,
OptimizedMarketOrder, OptimizedMarketOrder,
OptimizedOrdersAndQuoteReport,
OrderDomain, OrderDomain,
} from './types'; } from './types';
@ -81,7 +85,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
takerAmount: BigNumber, takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizedMarketOrder[]> { ): Promise<OptimizedOrdersAndQuoteReport> {
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
@ -170,6 +174,7 @@ export class MarketOperationUtils {
feeSchedule: _opts.feeSchedule, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined,
}); });
} }
@ -179,13 +184,13 @@ export class MarketOperationUtils {
* @param nativeOrders Native orders. * @param nativeOrders Native orders.
* @param makerAmount Amount of maker asset to buy. * @param makerAmount Amount of maker asset to buy.
* @param opts Options object. * @param opts Options object.
* @return orders. * @return object with optimized orders and a QuoteReport
*/ */
public async getMarketBuyOrdersAsync( public async getMarketBuyOrdersAsync(
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
makerAmount: BigNumber, makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizedMarketOrder[]> { ): Promise<OptimizedOrdersAndQuoteReport> {
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
@ -274,6 +279,7 @@ export class MarketOperationUtils {
feeSchedule: _opts.feeSchedule, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined,
}); });
} }
@ -345,7 +351,7 @@ export class MarketOperationUtils {
const dexQuotes = batchDexQuotes[i]; const dexQuotes = batchDexQuotes[i];
const makerAmount = makerAmounts[i]; const makerAmount = makerAmounts[i];
try { try {
return await this._generateOptimizedOrdersAsync({ return (await this._generateOptimizedOrdersAsync({
orderFillableAmounts, orderFillableAmounts,
nativeOrders, nativeOrders,
dexQuotes, dexQuotes,
@ -361,7 +367,7 @@ export class MarketOperationUtils {
feeSchedule: _opts.feeSchedule, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
}); })).optimizedOrders;
} catch (e) { } catch (e) {
// It's possible for one of the pairs to have no path // It's possible for one of the pairs to have no path
// rather than throw NO_OPTIMAL_PATH we return undefined // rather than throw NO_OPTIMAL_PATH we return undefined
@ -390,7 +396,8 @@ export class MarketOperationUtils {
shouldBatchBridgeOrders?: boolean; shouldBatchBridgeOrders?: boolean;
liquidityProviderAddress?: string; liquidityProviderAddress?: string;
multiBridgeAddress?: string; multiBridgeAddress?: string;
}): Promise<OptimizedMarketOrder[]> { quoteRequestor?: QuoteRequestor;
}): Promise<OptimizedOrdersAndQuoteReport> {
const { inputToken, outputToken, side, inputAmount } = opts; const { inputToken, outputToken, side, inputAmount } = opts;
const maxFallbackSlippage = opts.maxFallbackSlippage || 0; const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
// Convert native orders and dex quotes into fill paths. // Convert native orders and dex quotes into fill paths.
@ -444,7 +451,7 @@ export class MarketOperationUtils {
optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath]; optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath];
} }
} }
return createOrdersFromPath(optimalPath, { const optimizedOrders = createOrdersFromPath(optimalPath, {
side, side,
inputToken, inputToken,
outputToken, outputToken,
@ -455,6 +462,15 @@ export class MarketOperationUtils {
multiBridgeAddress: opts.multiBridgeAddress, multiBridgeAddress: opts.multiBridgeAddress,
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
}); });
const quoteReport = new QuoteReportGenerator(
opts.side,
_.flatten(opts.dexQuotes),
opts.nativeOrders,
opts.orderFillableAmounts,
_.flatten(optimizedOrders.map(o => o.fills)),
opts.quoteRequestor,
).generateReport();
return { optimizedOrders, quoteReport };
} }
private _optionalSources(): ERC20BridgeSource[] { private _optionalSources(): ERC20BridgeSource[] {

View File

@ -3,6 +3,7 @@ import { BigNumber } from '@0x/utils';
import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types';
import { QuoteRequestor } from '../../utils/quote_requestor'; import { QuoteRequestor } from '../../utils/quote_requestor';
import { QuoteReport } from '../quote_report_generator';
/** /**
* Order domain keys: chainId and exchange * Order domain keys: chainId and exchange
@ -254,3 +255,17 @@ export interface SourceQuoteOperation<TFillData extends FillData = FillData> ext
source: ERC20BridgeSource; source: ERC20BridgeSource;
fillData?: TFillData; fillData?: TFillData;
} }
/**
* Used in the ERC20BridgeSampler when a source does not natively
* support sampling via a specific buy amount.
*/
export interface FakeBuyOpts {
targetSlippageBps: BigNumber;
maxIterations: BigNumber;
}
export interface OptimizedOrdersAndQuoteReport {
optimizedOrders: OptimizedMarketOrder[];
quoteReport: QuoteReport;
}

View File

@ -0,0 +1,161 @@
import { orderHashUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { ERC20BridgeSource, SignedOrder } from '..';
import { MarketOperation } from '../types';
import { CollapsedFill, DexSample, NativeCollapsedFill } from './market_operation_utils/types';
import { QuoteRequestor } from './quote_requestor';
export interface BridgeReportSource {
liquiditySource: Exclude<ERC20BridgeSource, ERC20BridgeSource.Native>;
makerAmount: BigNumber;
takerAmount: BigNumber;
}
interface NativeReportSourceBase {
liquiditySource: ERC20BridgeSource.Native;
makerAmount: BigNumber;
takerAmount: BigNumber;
orderHash: string;
nativeOrder: SignedOrder;
fillableTakerAmount: BigNumber;
}
export interface NativeOrderbookReportSource extends NativeReportSourceBase {
isRfqt: false;
}
export interface NativeRFQTReportSource extends NativeReportSourceBase {
isRfqt: true;
makerUri: string;
}
export type QuoteReportSource = BridgeReportSource | NativeOrderbookReportSource | NativeRFQTReportSource;
export interface QuoteReport {
sourcesConsidered: QuoteReportSource[];
sourcesDelivered: QuoteReportSource[];
}
const nativeOrderFromCollapsedFill = (cf: CollapsedFill): SignedOrder | undefined => {
// Cast as NativeCollapsedFill and then check
// if it really is a NativeCollapsedFill
const possibleNativeCollapsedFill = cf as NativeCollapsedFill;
if (possibleNativeCollapsedFill.fillData && possibleNativeCollapsedFill.fillData.order) {
return possibleNativeCollapsedFill.fillData.order;
} else {
return undefined;
}
};
export class QuoteReportGenerator {
private readonly _dexQuotes: DexSample[];
private readonly _nativeOrders: SignedOrder[];
private readonly _orderHashesToFillableAmounts: { [orderHash: string]: BigNumber };
private readonly _marketOperation: MarketOperation;
private readonly _collapsedFills: CollapsedFill[];
private readonly _quoteRequestor?: QuoteRequestor;
constructor(
marketOperation: MarketOperation,
dexQuotes: DexSample[],
nativeOrders: SignedOrder[],
orderFillableAmounts: BigNumber[],
collapsedFills: CollapsedFill[],
quoteRequestor?: QuoteRequestor,
) {
this._dexQuotes = dexQuotes;
this._nativeOrders = nativeOrders;
this._marketOperation = marketOperation;
this._quoteRequestor = quoteRequestor;
this._collapsedFills = collapsedFills;
// convert order fillable amount array to easy to look up hash
if (orderFillableAmounts.length !== nativeOrders.length) {
// length mismatch, abort
this._orderHashesToFillableAmounts = {};
return;
}
const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx];
});
this._orderHashesToFillableAmounts = orderHashesToFillableAmounts;
}
public generateReport(): QuoteReport {
const dexReportSourcesConsidered = this._dexQuotes.map(dq => this._dexSampleToReportSource(dq));
const nativeOrderSourcesConsidered = this._nativeOrders.map(no => this._nativeOrderToReportSource(no));
const sourcesConsidered = [...dexReportSourcesConsidered, ...nativeOrderSourcesConsidered];
const sourcesDelivered = this._collapsedFills.map(collapsedFill => {
const foundNativeOrder = nativeOrderFromCollapsedFill(collapsedFill);
if (foundNativeOrder) {
return this._nativeOrderToReportSource(foundNativeOrder);
} else {
return this._dexSampleToReportSource(collapsedFill);
}
});
return {
sourcesConsidered,
sourcesDelivered,
};
}
private _dexSampleToReportSource(ds: DexSample): BridgeReportSource {
const liquiditySource = ds.source;
if (liquiditySource === ERC20BridgeSource.Native) {
throw new Error(`Unexpected liquidity source Native`);
}
// input and output map to different values
// based on the market operation
if (this._marketOperation === MarketOperation.Buy) {
return {
makerAmount: ds.input,
takerAmount: ds.output,
liquiditySource,
};
} else if (this._marketOperation === MarketOperation.Sell) {
return {
makerAmount: ds.output,
takerAmount: ds.input,
liquiditySource,
};
} else {
throw new Error(`Unexpected marketOperation ${this._marketOperation}`);
}
}
private _nativeOrderToReportSource(nativeOrder: SignedOrder): NativeRFQTReportSource | NativeOrderbookReportSource {
const orderHash = orderHashUtils.getOrderHash(nativeOrder);
const nativeOrderBase: NativeReportSourceBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
fillableTakerAmount: this._orderHashesToFillableAmounts[orderHash],
nativeOrder,
orderHash,
};
// if we find this is an rfqt order, label it as such and associate makerUri
const foundRfqtMakerUri = this._quoteRequestor && this._quoteRequestor.getMakerUriForOrderHash(orderHash);
if (foundRfqtMakerUri) {
const rfqtSource: NativeRFQTReportSource = {
...nativeOrderBase,
isRfqt: true,
makerUri: foundRfqtMakerUri,
};
return rfqtSource;
} else {
// if it's not an rfqt order, treat as normal
const regularNativeOrder: NativeOrderbookReportSource = {
...nativeOrderBase,
isRfqt: false,
};
return regularNativeOrder;
}
}
}

View File

@ -1,9 +1,9 @@
import { schemas, SchemaValidator } from '@0x/json-schemas'; import { schemas, SchemaValidator } from '@0x/json-schemas';
import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; import { assetDataUtils, orderCalculationUtils, orderHashUtils, SignedOrder } from '@0x/order-utils';
import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server'; import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server';
import { ERC20AssetData } from '@0x/types'; import { ERC20AssetData } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils'; import { BigNumber, logUtils } from '@0x/utils';
import Axios, { AxiosResponse } from 'axios'; import Axios from 'axios';
import { constants } from '../constants'; import { constants } from '../constants';
import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types';
@ -85,6 +85,7 @@ export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void;
export class QuoteRequestor { export class QuoteRequestor {
private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); private readonly _schemaValidator: SchemaValidator = new SchemaValidator();
private readonly _orderHashToMakerUri: { [orderHash: string]: string } = {};
constructor( constructor(
private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings,
@ -105,7 +106,7 @@ export class QuoteRequestor {
const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options };
assertTakerAddressOrThrow(_opts.takerAddress); assertTakerAddressOrThrow(_opts.takerAddress);
const firmQuotes = await this._getQuotesAsync<RFQTFirmQuote>( // not yet BigNumber const firmQuoteResponses = await this._getQuotesAsync<RFQTFirmQuote>( // not yet BigNumber
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
assetFillAmount, assetFillAmount,
@ -114,41 +115,38 @@ export class QuoteRequestor {
'firm', 'firm',
); );
const ordersWithStringInts = firmQuotes.map(quote => quote.signedOrder); const result: RFQTFirmQuote[] = [];
firmQuoteResponses.forEach(firmQuoteResponse => {
const orderWithStringInts = firmQuoteResponse.response.signedOrder;
const validatedOrdersWithStringInts = ordersWithStringInts.filter(order => {
try { try {
const hasValidSchema = this._schemaValidator.isValid(order, schemas.signedOrderSchema); const hasValidSchema = this._schemaValidator.isValid(orderWithStringInts, schemas.signedOrderSchema);
if (!hasValidSchema) { if (!hasValidSchema) {
throw new Error('order not valid'); throw new Error('Order not valid');
} }
} catch (err) { } catch (err) {
this._warningLogger(order, `Invalid RFQ-t order received, filtering out. ${err.message}`); this._warningLogger(orderWithStringInts, `Invalid RFQ-t order received, filtering out. ${err.message}`);
return false; return;
} }
if ( if (
!hasExpectedAssetData( !hasExpectedAssetData(
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
order.makerAssetData.toLowerCase(), orderWithStringInts.makerAssetData.toLowerCase(),
order.takerAssetData.toLowerCase(), orderWithStringInts.takerAssetData.toLowerCase(),
) )
) { ) {
this._warningLogger(order, 'Unexpected asset data in RFQ-T order, filtering out'); this._warningLogger(orderWithStringInts, 'Unexpected asset data in RFQ-T order, filtering out');
return false; return;
} }
if (order.takerAddress.toLowerCase() !== _opts.takerAddress.toLowerCase()) { if (orderWithStringInts.takerAddress.toLowerCase() !== _opts.takerAddress.toLowerCase()) {
this._warningLogger(order, 'Unexpected takerAddress in RFQ-T order, filtering out'); this._warningLogger(orderWithStringInts, 'Unexpected takerAddress in RFQ-T order, filtering out');
return false; return;
} }
return true; const orderWithBigNumberInts: SignedOrder = {
});
const validatedOrders: SignedOrder[] = validatedOrdersWithStringInts.map(orderWithStringInts => {
return {
...orderWithStringInts, ...orderWithStringInts,
makerAssetAmount: new BigNumber(orderWithStringInts.makerAssetAmount), makerAssetAmount: new BigNumber(orderWithStringInts.makerAssetAmount),
takerAssetAmount: new BigNumber(orderWithStringInts.takerAssetAmount), takerAssetAmount: new BigNumber(orderWithStringInts.takerAssetAmount),
@ -157,17 +155,25 @@ export class QuoteRequestor {
expirationTimeSeconds: new BigNumber(orderWithStringInts.expirationTimeSeconds), expirationTimeSeconds: new BigNumber(orderWithStringInts.expirationTimeSeconds),
salt: new BigNumber(orderWithStringInts.salt), salt: new BigNumber(orderWithStringInts.salt),
}; };
});
const orders = validatedOrders.filter(order => { if (
if (orderCalculationUtils.willOrderExpire(order, this._expiryBufferMs / constants.ONE_SECOND_MS)) { orderCalculationUtils.willOrderExpire(
this._warningLogger(order, 'Expiry too soon in RFQ-T order, filtering out'); orderWithBigNumberInts,
return false; this._expiryBufferMs / constants.ONE_SECOND_MS,
)
) {
this._warningLogger(orderWithBigNumberInts, 'Expiry too soon in RFQ-T order, filtering out');
return;
} }
return true;
});
return orders.map(order => ({ signedOrder: order })); // Store makerUri for looking up later
this._orderHashToMakerUri[orderHashUtils.getOrderHash(orderWithBigNumberInts)] = firmQuoteResponse.makerUri;
// Passed all validation, add it to result
result.push({ signedOrder: orderWithBigNumberInts });
return;
});
return result;
} }
public async requestRfqtIndicativeQuotesAsync( public async requestRfqtIndicativeQuotesAsync(
@ -189,7 +195,8 @@ export class QuoteRequestor {
'indicative', 'indicative',
); );
const validResponsesWithStringInts = responsesWithStringInts.filter(response => { const validResponsesWithStringInts = responsesWithStringInts.filter(result => {
const response = result.response;
if (!this._isValidRfqtIndicativeQuoteResponse(response)) { if (!this._isValidRfqtIndicativeQuoteResponse(response)) {
this._warningLogger(response, 'Invalid RFQ-T indicative quote received, filtering out'); this._warningLogger(response, 'Invalid RFQ-T indicative quote received, filtering out');
return false; return false;
@ -203,7 +210,8 @@ export class QuoteRequestor {
return true; return true;
}); });
const validResponses = validResponsesWithStringInts.map(response => { const validResponses = validResponsesWithStringInts.map(result => {
const response = result.response;
return { return {
...response, ...response,
makerAssetAmount: new BigNumber(response.makerAssetAmount), makerAssetAmount: new BigNumber(response.makerAssetAmount),
@ -223,6 +231,13 @@ export class QuoteRequestor {
return responses; return responses;
} }
/**
* Given an order hash, returns the makerUri that the order originated from
*/
public getMakerUriForOrderHash(orderHash: string): string | undefined {
return this._orderHashToMakerUri[orderHash];
}
private _isValidRfqtIndicativeQuoteResponse(response: RFQTIndicativeQuote): boolean { private _isValidRfqtIndicativeQuoteResponse(response: RFQTIndicativeQuote): boolean {
const hasValidMakerAssetAmount = const hasValidMakerAssetAmount =
response.makerAssetAmount !== undefined && response.makerAssetAmount !== undefined &&
@ -278,10 +293,9 @@ export class QuoteRequestor {
marketOperation: MarketOperation, marketOperation: MarketOperation,
options: RfqtRequestOpts, options: RfqtRequestOpts,
quoteType: 'firm' | 'indicative', quoteType: 'firm' | 'indicative',
): Promise<ResponseT[]> { ): Promise<Array<{ response: ResponseT; makerUri: string }>> {
// create an array of promises for quote responses, using "undefined" const result: Array<{ response: ResponseT; makerUri: string }> = [];
// as a placeholder for failed requests. await Promise.all(
const responsesIfDefined: Array<undefined | AxiosResponse<ResponseT>> = await Promise.all(
Object.keys(this._rfqtAssetOfferings).map(async url => { Object.keys(this._rfqtAssetOfferings).map(async url => {
if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) {
const requestParamsWithBigNumbers = { const requestParamsWithBigNumbers = {
@ -330,7 +344,7 @@ export class QuoteRequestor {
}, },
}, },
}); });
return response; result.push({ response: response.data, makerUri: url });
} catch (err) { } catch (err) {
this._infoLogger({ this._infoLogger({
rfqtMakerInteraction: { rfqtMakerInteraction: {
@ -347,17 +361,10 @@ export class QuoteRequestor {
options.apiKey options.apiKey
} for taker address ${options.takerAddress}`, } for taker address ${options.takerAddress}`,
); );
return undefined;
} }
} }
return undefined;
}), }),
); );
return result;
const responses = responsesIfDefined.filter(
(respIfDefd): respIfDefd is AxiosResponse<ResponseT> => respIfDefd !== undefined,
);
return responses.map(response => response.data);
} }
} }

View File

@ -9,7 +9,6 @@ import {
MarketOperation, MarketOperation,
MarketSellSwapQuote, MarketSellSwapQuote,
SwapQuote, SwapQuote,
SwapQuoteBase,
SwapQuoteInfo, SwapQuoteInfo,
SwapQuoteOrdersBreakdown, SwapQuoteOrdersBreakdown,
SwapQuoterError, SwapQuoterError,
@ -17,9 +16,16 @@ import {
import { MarketOperationUtils } from './market_operation_utils'; import { MarketOperationUtils } from './market_operation_utils';
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; import {
FeeSchedule,
FillData,
GetMarketOrdersOpts,
OptimizedMarketOrder,
OptimizedOrdersAndQuoteReport,
} from './market_operation_utils/types';
import { isSupportedAssetDataInOrders } from './utils'; import { isSupportedAssetDataInOrders } from './utils';
import { QuoteReport } from './quote_report_generator';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
@ -87,6 +93,7 @@ export class SwapQuoteCalculator {
assetFillAmounts, assetFillAmounts,
opts, opts,
); );
const batchSwapQuotes = await Promise.all( const batchSwapQuotes = await Promise.all(
batchSignedOrders.map(async (orders, i) => { batchSignedOrders.map(async (orders, i) => {
if (orders) { if (orders) {
@ -120,7 +127,8 @@ export class SwapQuoteCalculator {
} }
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled // since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
let resultOrders: OptimizedMarketOrder[] = []; let optimizedOrders: OptimizedMarketOrder[] | undefined;
let quoteReport: QuoteReport | undefined;
{ {
// Scale fees by gas price. // Scale fees by gas price.
@ -137,20 +145,24 @@ export class SwapQuoteCalculator {
if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) { if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) {
// HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable // HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable
resultOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o)); optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o));
} else { } else {
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync( const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync(
prunedOrders, prunedOrders,
assetFillAmount, assetFillAmount,
_opts, _opts,
); );
optimizedOrders = buyResult.optimizedOrders;
quoteReport = buyResult.quoteReport;
} else { } else {
resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync( const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders, prunedOrders,
assetFillAmount, assetFillAmount,
_opts, _opts,
); );
optimizedOrders = sellResult.optimizedOrders;
quoteReport = sellResult.quoteReport;
} }
} }
} }
@ -160,11 +172,12 @@ export class SwapQuoteCalculator {
return createSwapQuote( return createSwapQuote(
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
resultOrders, optimizedOrders,
operation, operation,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
opts.gasSchedule, opts.gasSchedule,
quoteReport,
); );
} }
} }
@ -172,15 +185,16 @@ export class SwapQuoteCalculator {
function createSwapQuote( function createSwapQuote(
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
resultOrders: OptimizedMarketOrder[], optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation, operation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: FeeSchedule, gasSchedule: FeeSchedule,
quoteReport?: QuoteReport,
): SwapQuote { ): SwapQuote {
const bestCaseFillResult = simulateBestCaseFill({ const bestCaseFillResult = simulateBestCaseFill({
gasPrice, gasPrice,
orders: resultOrders, orders: optimizedOrders,
side: operation, side: operation,
fillAmount: assetFillAmount, fillAmount: assetFillAmount,
opts: { gasSchedule }, opts: { gasSchedule },
@ -188,20 +202,21 @@ function createSwapQuote(
const worstCaseFillResult = simulateWorstCaseFill({ const worstCaseFillResult = simulateWorstCaseFill({
gasPrice, gasPrice,
orders: resultOrders, orders: optimizedOrders,
side: operation, side: operation,
fillAmount: assetFillAmount, fillAmount: assetFillAmount,
opts: { gasSchedule }, opts: { gasSchedule },
}); });
const quoteBase: SwapQuoteBase = { const quoteBase = {
takerAssetData, takerAssetData,
makerAssetData, makerAssetData,
gasPrice, gasPrice,
bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult), bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult),
worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult), worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult),
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
orders: resultOrders, orders: optimizedOrders,
quoteReport,
}; };
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {
@ -209,12 +224,14 @@ function createSwapQuote(
...quoteBase, ...quoteBase,
type: MarketOperation.Buy, type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount, makerAssetFillAmount: assetFillAmount,
quoteReport,
}; };
} else { } else {
return { return {
...quoteBase, ...quoteBase,
type: MarketOperation.Sell, type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount, takerAssetFillAmount: assetFillAmount,
quoteReport,
}; };
} }
} }

View File

@ -507,12 +507,13 @@ describe('MarketOperationUtils tests', () => {
}); });
it('generates bridge orders with correct asset data', async () => { it('generates bridge orders with correct asset data', async () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = 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,
DEFAULT_OPTS, DEFAULT_OPTS,
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); expect(getSourceFromAssetData(order.makerAssetData)).to.exist('');
@ -531,24 +532,26 @@ describe('MarketOperationUtils tests', () => {
}); });
it('generates bridge orders with correct taker amount', async () => { it('generates bridge orders with correct taker amount', async () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = 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,
DEFAULT_OPTS, DEFAULT_OPTS,
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const totalTakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAssetAmount)); const totalTakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAssetAmount));
expect(totalTakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); expect(totalTakerAssetAmount).to.bignumber.gte(FILL_AMOUNT);
}); });
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 marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = 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,
{ ...DEFAULT_OPTS, bridgeSlippage }, { ...DEFAULT_OPTS, bridgeSlippage },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
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 expectedMakerAmount = order.fills[0].output; const expectedMakerAmount = order.fills[0].output;
@ -566,11 +569,12 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4 }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
@ -604,11 +608,12 @@ describe('MarketOperationUtils tests', () => {
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
@ -641,11 +646,12 @@ describe('MarketOperationUtils tests', () => {
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
@ -666,11 +672,12 @@ describe('MarketOperationUtils tests', () => {
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4 }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
@ -689,11 +696,12 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const firstSources = [ const firstSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
@ -715,11 +723,12 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = []; const secondSources: ERC20BridgeSource[] = [];
@ -756,7 +765,7 @@ describe('MarketOperationUtils tests', () => {
ORDER_DOMAIN, ORDER_DOMAIN,
registryAddress, registryAddress,
); );
const result = await sampler.getMarketSellOrdersAsync( const ordersAndReport = await sampler.getMarketSellOrdersAsync(
[ [
createOrder({ createOrder({
makerAssetData: assetDataUtils.encodeERC20AssetData(xAsset), makerAssetData: assetDataUtils.encodeERC20AssetData(xAsset),
@ -766,6 +775,7 @@ describe('MarketOperationUtils tests', () => {
Web3Wrapper.toBaseUnitAmount(10, 18), Web3Wrapper.toBaseUnitAmount(10, 18),
{ excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0, shouldBatchBridgeOrders: false }, { excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0, shouldBatchBridgeOrders: false },
); );
const result = ordersAndReport.optimizedOrders;
expect(result.length).to.eql(1); expect(result.length).to.eql(1);
expect(result[0].makerAddress).to.eql(liquidityProviderAddress); expect(result[0].makerAddress).to.eql(liquidityProviderAddress);
@ -792,7 +802,7 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ {
@ -805,6 +815,7 @@ describe('MarketOperationUtils tests', () => {
shouldBatchBridgeOrders: true, shouldBatchBridgeOrders: true,
}, },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(3); expect(improvedOrders).to.be.length(3);
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
expect(orderFillSources).to.deep.eq([ expect(orderFillSources).to.deep.eq([
@ -913,12 +924,13 @@ describe('MarketOperationUtils tests', () => {
}); });
it('generates bridge orders with correct asset data', async () => { it('generates bridge orders with correct asset data', async () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = 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,
DEFAULT_OPTS, DEFAULT_OPTS,
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); expect(getSourceFromAssetData(order.makerAssetData)).to.exist('');
@ -937,24 +949,26 @@ describe('MarketOperationUtils tests', () => {
}); });
it('generates bridge orders with correct maker amount', async () => { it('generates bridge orders with correct maker amount', async () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = 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,
DEFAULT_OPTS, DEFAULT_OPTS,
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const totalMakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAssetAmount)); const totalMakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAssetAmount));
expect(totalMakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); expect(totalMakerAssetAmount).to.bignumber.gte(FILL_AMOUNT);
}); });
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 marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = 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,
{ ...DEFAULT_OPTS, bridgeSlippage }, { ...DEFAULT_OPTS, bridgeSlippage },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
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 expectedTakerAmount = order.fills[0].output; const expectedTakerAmount = order.fills[0].output;
@ -971,11 +985,12 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4 }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
@ -1009,11 +1024,12 @@ describe('MarketOperationUtils tests', () => {
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
@ -1045,11 +1061,12 @@ describe('MarketOperationUtils tests', () => {
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
@ -1067,11 +1084,12 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const firstSources = [ const firstSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
@ -1092,11 +1110,12 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.fills[0].source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = []; const secondSources: ERC20BridgeSource[] = [];
@ -1112,7 +1131,7 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ {
@ -1121,6 +1140,7 @@ describe('MarketOperationUtils tests', () => {
shouldBatchBridgeOrders: true, shouldBatchBridgeOrders: true,
}, },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(2); expect(improvedOrders).to.be.length(2);
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
expect(orderFillSources).to.deep.eq([ expect(orderFillSources).to.deep.eq([

View File

@ -0,0 +1,354 @@
// tslint:disable:custom-no-magic-numbers
import { orderHashUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import * as TypeMoq from 'typemoq';
import { MarketOperation } from '../src/types';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
NativeCollapsedFill,
} from '../src/utils/market_operation_utils/types';
import { QuoteRequestor } from '../src/utils/quote_requestor';
import {
BridgeReportSource,
NativeOrderbookReportSource,
NativeRFQTReportSource,
QuoteReportGenerator,
QuoteReportSource,
} from './../src/utils/quote_report_generator';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
chaiSetup.configure();
const expect = chai.expect;
const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill => {
return {
source: ERC20BridgeSource.Native,
input: order.takerAssetAmount,
output: order.makerAssetAmount,
fillData: {
order: {
...order,
fillableMakerAssetAmount: new BigNumber(1),
fillableTakerAssetAmount: new BigNumber(1),
fillableTakerFeeAmount: new BigNumber(1),
},
},
subFills: [],
};
};
describe('QuoteReportGenerator', async () => {
describe('generateReport', async () => {
it('should generate report properly for sell', () => {
const marketOperation: MarketOperation = MarketOperation.Sell;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
};
const kyberSample2: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10003),
output: new BigNumber(10004),
};
const uniswapSample1: DexSample = {
source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10003),
output: new BigNumber(10004),
};
const uniswapSample2: DexSample = {
source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10005),
output: new BigNumber(10006),
};
const dexQuotes: DexSample[] = [kyberSample1, kyberSample2, uniswapSample1, uniswapSample2];
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount,
});
const orderbookOrder2FillableAmount = new BigNumber(99);
const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder2',
takerAssetAmount: orderbookOrder2FillableAmount.plus(99),
});
const rfqtOrder1FillableAmount = new BigNumber(100);
const rfqtOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'rfqtOrder1',
takerAssetAmount: rfqtOrder1FillableAmount,
});
const rfqtOrder2FillableAmount = new BigNumber(1001);
const rfqtOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'rfqtOrder2',
takerAssetAmount: rfqtOrder2FillableAmount.plus(100),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, rfqtOrder1, rfqtOrder2, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [
orderbookOrder1FillableAmount,
rfqtOrder1FillableAmount,
rfqtOrder2FillableAmount,
orderbookOrder2FillableAmount,
];
// generate path
const uniswap2Fill: CollapsedFill = { ...uniswapSample2, subFills: [] };
const kyber2Fill: CollapsedFill = { ...kyberSample2, subFills: [] };
const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2);
const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2);
const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, kyber2Fill];
// quote generator mock
const quoteRequestor = TypeMoq.Mock.ofType<QuoteRequestor>();
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(orderbookOrder2)))
.returns(() => {
return undefined;
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder1)))
.returns(() => {
return 'https://rfqt1.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder2)))
.returns(() => {
return 'https://rfqt2.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
const orderReport = new QuoteReportGenerator(
marketOperation,
dexQuotes,
nativeOrders,
orderFillableAmounts,
pathGenerated,
quoteRequestor.object,
).generateReport();
const rfqtOrder1Source: NativeRFQTReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder1.makerAssetAmount,
takerAmount: rfqtOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(rfqtOrder1),
nativeOrder: rfqtOrder1,
fillableTakerAmount: rfqtOrder1FillableAmount,
isRfqt: true,
makerUri: 'https://rfqt1.provider.club',
};
const rfqtOrder2Source: NativeRFQTReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder2.makerAssetAmount,
takerAmount: rfqtOrder2.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(rfqtOrder2),
nativeOrder: rfqtOrder2,
fillableTakerAmount: rfqtOrder2FillableAmount,
isRfqt: true,
makerUri: 'https://rfqt2.provider.club',
};
const orderbookOrder1Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder1),
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
isRfqt: false,
};
const orderbookOrder2Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.makerAssetAmount,
takerAmount: orderbookOrder2.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder2),
nativeOrder: orderbookOrder2,
fillableTakerAmount: orderbookOrder2FillableAmount,
isRfqt: false,
};
const uniswap1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.output,
takerAmount: uniswapSample1.input,
};
const uniswap2Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample2.output,
takerAmount: uniswapSample2.input,
};
const kyber1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
};
const kyber2Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample2.output,
takerAmount: kyberSample2.input,
};
const expectedSourcesConsidered: QuoteReportSource[] = [
kyber1Source,
kyber2Source,
uniswap1Source,
uniswap2Source,
orderbookOrder1Source,
rfqtOrder1Source,
rfqtOrder2Source,
orderbookOrder2Source,
];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesDelivered: QuoteReportSource[] = [
rfqtOrder2Source,
orderbookOrder2Source,
uniswap2Source,
kyber2Source,
];
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) {
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(
expectedSourceDelivered,
`sourceDelivered incorrect at index ${idx}`,
);
});
quoteRequestor.verifyAll();
});
it('should handle properly for buy without quoteRequestor', () => {
const marketOperation: MarketOperation = MarketOperation.Buy;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
};
const uniswapSample1: DexSample = {
source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10003),
output: new BigNumber(10004),
};
const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1];
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount.plus(101),
});
const orderbookOrder2FillableAmount = new BigNumber(5000);
const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder2',
takerAssetAmount: orderbookOrder2FillableAmount.plus(101),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [orderbookOrder1FillableAmount, orderbookOrder2FillableAmount];
// generate path
const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1);
const uniswap1Fill: CollapsedFill = { ...uniswapSample1, subFills: [] };
const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [] };
const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill];
const orderReport = new QuoteReportGenerator(
marketOperation,
dexQuotes,
nativeOrders,
orderFillableAmounts,
pathGenerated,
).generateReport();
const orderbookOrder1Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder1),
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
isRfqt: false,
};
const orderbookOrder2Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.makerAssetAmount,
takerAmount: orderbookOrder2.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder2),
nativeOrder: orderbookOrder2,
fillableTakerAmount: orderbookOrder2FillableAmount,
isRfqt: false,
};
const uniswap1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.input,
takerAmount: uniswapSample1.output,
};
const kyber1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.input,
takerAmount: kyberSample1.output,
};
const expectedSourcesConsidered: QuoteReportSource[] = [
kyber1Source,
uniswap1Source,
orderbookOrder1Source,
orderbookOrder2Source,
];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source];
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) {
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(
expectedSourceDelivered,
`sourceDelivered incorrect at index ${idx}`,
);
});
});
});
});

View File

@ -28,6 +28,7 @@ export const docGenConfigs: DocGenConfigs = {
TFillData: true, TFillData: true,
IterableIterator: true, IterableIterator: true,
Set: true, Set: true,
Exclude: true,
}, },
// Some types are not explicitly part of the public interface like params, return values, etc... But we still // Some types are not explicitly part of the public interface like params, return values, etc... But we still
// want them exported. E.g error enum types that can be thrown by methods. These must be manually added to this // want them exported. E.g error enum types that can be thrown by methods. These must be manually added to this