feat: Extended Quote Report

* Extended Quote report for indicative quote

* feat: Only save 'full' quotes on quote report

* Unify extended quote report
This commit is contained in:
Jorge Pérez 2021-11-09 13:05:01 -06:00 committed by GitHub
parent 10b0d7f363
commit b7adc5a889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 341 additions and 60 deletions

View File

@ -1,5 +1,14 @@
[ [
{ {
"version": "16.32.0",
"changes": [
{
"note": "Extended Quote Report",
"pr": 361
}
],
"timestamp": 1636480845
},
"timestamp": 1635903615, "timestamp": 1635903615,
"version": "16.31.0", "version": "16.31.0",
"changes": [ "changes": [

View File

@ -162,14 +162,20 @@ export {
export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export { export {
BridgeQuoteReportEntry, BridgeQuoteReportEntry,
jsonifyFillData,
MultiHopQuoteReportEntry, MultiHopQuoteReportEntry,
NativeLimitOrderQuoteReportEntry, NativeLimitOrderQuoteReportEntry,
NativeRfqOrderQuoteReportEntry, NativeRfqOrderQuoteReportEntry,
QuoteReport, QuoteReport,
QuoteReportEntry, QuoteReportEntry,
ExtendedQuoteReport,
ExtendedQuoteReportSources,
ExtendedQuoteReportEntry,
ExtendedQuoteReportIndexedEntry,
ExtendedQuoteReportIndexedEntryOutbound,
PriceComparisonsReport, PriceComparisonsReport,
} from './utils/quote_report_generator'; } from './utils/quote_report_generator';
export { QuoteRequestor } from './utils/quote_requestor'; export { QuoteRequestor, V4RFQIndicativeQuoteMM } from './utils/quote_requestor';
export { ERC20BridgeSamplerContract, BalanceCheckerContract, FakeTakerContract } from './wrappers'; export { ERC20BridgeSamplerContract, BalanceCheckerContract, FakeTakerContract } from './wrappers';
import { ERC20BridgeSource } from './utils/market_operation_utils/types'; import { ERC20BridgeSource } from './utils/market_operation_utils/types';
export type Native = ERC20BridgeSource.Native; export type Native = ERC20BridgeSource.Native;

View File

@ -505,6 +505,7 @@ function createSwapQuote(
const { const {
optimizedOrders, optimizedOrders,
quoteReport, quoteReport,
extendedQuoteReportSources,
sourceFlags, sourceFlags,
takerAmountPerEth, takerAmountPerEth,
makerAmountPerEth, makerAmountPerEth,
@ -532,6 +533,7 @@ function createSwapQuote(
takerAmountPerEth, takerAmountPerEth,
makerAmountPerEth, makerAmountPerEth,
quoteReport, quoteReport,
extendedQuoteReportSources,
isTwoHop, isTwoHop,
priceComparisonsReport, priceComparisonsReport,
}; };

View File

@ -19,7 +19,7 @@ import {
OptimizedMarketOrder, OptimizedMarketOrder,
TokenAdjacencyGraph, TokenAdjacencyGraph,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
import { PriceComparisonsReport, QuoteReport } from './utils/quote_report_generator'; import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from './utils/quote_report_generator';
import { MetricsProxy } from './utils/quote_requestor'; import { MetricsProxy } from './utils/quote_requestor';
/** /**
@ -171,6 +171,7 @@ export interface SwapQuoteBase {
worstCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo;
sourceBreakdown: SwapQuoteOrdersBreakdown; sourceBreakdown: SwapQuoteOrdersBreakdown;
quoteReport?: QuoteReport; quoteReport?: QuoteReport;
extendedQuoteReportSources?: ExtendedQuoteReportSources;
priceComparisonsReport?: PriceComparisonsReport; priceComparisonsReport?: PriceComparisonsReport;
isTwoHop: boolean; isTwoHop: boolean;
makerTokenDecimals: number; makerTokenDecimals: number;

View File

@ -18,12 +18,15 @@ import {
import { import {
dexSampleToReportSource, dexSampleToReportSource,
ExtendedQuoteReportSources,
generateExtendedQuoteReportSources,
generateQuoteReport, generateQuoteReport,
multiHopSampleToReportSource, multiHopSampleToReportSource,
nativeOrderToReportEntry, nativeOrderToReportEntry,
PriceComparisonsReport, PriceComparisonsReport,
QuoteReport, QuoteReport,
} from './../quote_report_generator'; } from './../quote_report_generator';
import { getComparisonPrices } from './comparison_price'; import { getComparisonPrices } from './comparison_price';
import { import {
BUY_SOURCE_FILTER_BY_CHAIN_ID, BUY_SOURCE_FILTER_BY_CHAIN_ID,
@ -78,6 +81,25 @@ export class MarketOperationUtils {
return generateQuoteReport(side, quotes.nativeOrders, liquidityDelivered, comparisonPrice, quoteRequestor); return generateQuoteReport(side, quotes.nativeOrders, liquidityDelivered, comparisonPrice, quoteRequestor);
} }
private static _computeExtendedQuoteReportSources(
quoteRequestor: QuoteRequestor | undefined,
marketSideLiquidity: MarketSideLiquidity,
amount: BigNumber,
optimizerResult: OptimizerResult,
comparisonPrice?: BigNumber | undefined,
): ExtendedQuoteReportSources {
const { side, quotes } = marketSideLiquidity;
const { liquidityDelivered } = optimizerResult;
return generateExtendedQuoteReportSources(
side,
quotes,
liquidityDelivered,
amount,
comparisonPrice,
quoteRequestor,
);
}
private static _computePriceComparisonsReport( private static _computePriceComparisonsReport(
quoteRequestor: QuoteRequestor | undefined, quoteRequestor: QuoteRequestor | undefined,
marketSideLiquidity: MarketSideLiquidity, marketSideLiquidity: MarketSideLiquidity,
@ -702,6 +724,16 @@ export class MarketOperationUtils {
); );
} }
// Always compute the Extended Quote Report
let extendedQuoteReportSources: ExtendedQuoteReportSources | undefined;
extendedQuoteReportSources = MarketOperationUtils._computeExtendedQuoteReportSources(
_opts.rfqt ? _opts.rfqt.quoteRequestor : undefined,
marketSideLiquidity,
amount,
optimizerResult,
wholeOrderPrice,
);
let priceComparisonsReport: PriceComparisonsReport | undefined; let priceComparisonsReport: PriceComparisonsReport | undefined;
if (_opts.shouldIncludePriceComparisonsReport) { if (_opts.shouldIncludePriceComparisonsReport) {
priceComparisonsReport = MarketOperationUtils._computePriceComparisonsReport( priceComparisonsReport = MarketOperationUtils._computePriceComparisonsReport(
@ -710,7 +742,7 @@ export class MarketOperationUtils {
wholeOrderPrice, wholeOrderPrice,
); );
} }
return { ...optimizerResult, quoteReport, priceComparisonsReport }; return { ...optimizerResult, quoteReport, extendedQuoteReportSources, priceComparisonsReport };
} }
private async _refreshPoolCacheIfRequiredAsync(takerToken: string, makerToken: string): Promise<void> { private async _refreshPoolCacheIfRequiredAsync(takerToken: string, makerToken: string): Promise<void> {

View File

@ -3,13 +3,12 @@ import {
FillQuoteTransformerOrderType, FillQuoteTransformerOrderType,
FillQuoteTransformerRfqOrderInfo, FillQuoteTransformerRfqOrderInfo,
} from '@0x/protocol-utils'; } from '@0x/protocol-utils';
import { V4RFQIndicativeQuote } from '@0x/quote-server';
import { MarketOperation } from '@0x/types'; import { MarketOperation } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { NativeOrderWithFillableAmounts, RfqFirmQuoteValidator, RfqRequestOpts } from '../../types'; import { NativeOrderWithFillableAmounts, RfqFirmQuoteValidator, RfqRequestOpts } from '../../types';
import { QuoteRequestor } from '../../utils/quote_requestor'; import { QuoteRequestor, V4RFQIndicativeQuoteMM } from '../../utils/quote_requestor';
import { PriceComparisonsReport, QuoteReport } from '../quote_report_generator'; import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from '../quote_report_generator';
import { CollapsedPath } from './path'; import { CollapsedPath } from './path';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
@ -491,6 +490,7 @@ export interface OptimizerResult {
export interface OptimizerResultWithReport extends OptimizerResult { export interface OptimizerResultWithReport extends OptimizerResult {
quoteReport?: QuoteReport; quoteReport?: QuoteReport;
extendedQuoteReportSources?: ExtendedQuoteReportSources;
priceComparisonsReport?: PriceComparisonsReport; priceComparisonsReport?: PriceComparisonsReport;
} }
@ -519,7 +519,7 @@ export interface MarketSideLiquidity {
export interface RawQuotes { export interface RawQuotes {
nativeOrders: NativeOrderWithFillableAmounts[]; nativeOrders: NativeOrderWithFillableAmounts[];
rfqtIndicativeQuotes: V4RFQIndicativeQuote[]; rfqtIndicativeQuotes: V4RFQIndicativeQuoteMM[];
twoHopQuotes: Array<DexSample<MultiHopFillData>>; twoHopQuotes: Array<DexSample<MultiHopFillData>>;
dexQuotes: Array<Array<DexSample<FillData>>>; dexQuotes: Array<Array<DexSample<FillData>>>;
} }

View File

@ -14,8 +14,9 @@ import {
NativeFillData, NativeFillData,
NativeLimitOrderFillData, NativeLimitOrderFillData,
NativeRfqOrderFillData, NativeRfqOrderFillData,
RawQuotes,
} from './market_operation_utils/types'; } from './market_operation_utils/types';
import { QuoteRequestor } from './quote_requestor'; import { QuoteRequestor, V4RFQIndicativeQuoteMM } from './quote_requestor';
export interface QuoteReportEntryBase { export interface QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource; liquiditySource: ERC20BridgeSource;
@ -36,30 +37,77 @@ export interface NativeLimitOrderQuoteReportEntry extends QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource.Native; liquiditySource: ERC20BridgeSource.Native;
fillData: NativeFillData; fillData: NativeFillData;
fillableTakerAmount: BigNumber; fillableTakerAmount: BigNumber;
isRfqt: false; isRFQ: false;
} }
export interface NativeRfqOrderQuoteReportEntry extends QuoteReportEntryBase { export interface NativeRfqOrderQuoteReportEntry extends QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource.Native; liquiditySource: ERC20BridgeSource.Native;
fillData: NativeFillData; fillData: NativeFillData;
fillableTakerAmount: BigNumber; fillableTakerAmount: BigNumber;
isRfqt: true; isRFQ: true;
nativeOrder: RfqOrderFields; nativeOrder: RfqOrderFields;
makerUri: string; makerUri: string;
comparisonPrice?: number; comparisonPrice?: number;
} }
export interface IndicativeRfqOrderQuoteReportEntry extends QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource.Native;
fillableTakerAmount: BigNumber;
isRFQ: true;
makerUri?: string;
comparisonPrice?: number;
}
export type QuoteReportEntry = export type QuoteReportEntry =
| BridgeQuoteReportEntry | BridgeQuoteReportEntry
| MultiHopQuoteReportEntry | MultiHopQuoteReportEntry
| NativeLimitOrderQuoteReportEntry | NativeLimitOrderQuoteReportEntry
| NativeRfqOrderQuoteReportEntry; | NativeRfqOrderQuoteReportEntry;
export type ExtendedQuoteReportEntry =
| BridgeQuoteReportEntry
| MultiHopQuoteReportEntry
| NativeLimitOrderQuoteReportEntry
| NativeRfqOrderQuoteReportEntry
| IndicativeRfqOrderQuoteReportEntry;
export type ExtendedQuoteReportIndexedEntry = ExtendedQuoteReportEntry & {
quoteEntryIndex: number;
isDelivered: boolean;
};
export type ExtendedQuoteReportIndexedEntryOutbound = Omit<ExtendedQuoteReportIndexedEntry, 'fillData'> & {
fillData?: string;
};
export interface QuoteReport { export interface QuoteReport {
sourcesConsidered: QuoteReportEntry[]; sourcesConsidered: QuoteReportEntry[];
sourcesDelivered: QuoteReportEntry[]; sourcesDelivered: QuoteReportEntry[];
} }
export interface ExtendedQuoteReportSources {
sourcesConsidered: ExtendedQuoteReportIndexedEntry[];
sourcesDelivered: ExtendedQuoteReportIndexedEntry[] | undefined;
}
export interface ExtendedQuoteReport {
quoteId?: string;
taker?: string;
timestamp: number;
firmQuoteReport: boolean;
submissionBy: 'taker' | 'metaTxn' | 'rfqm';
buyAmount?: string;
sellAmount?: string;
buyTokenAddress: string;
sellTokenAddress: string;
integratorId?: string;
slippageBips?: number;
zeroExTransactionHash?: string;
decodedUniqueId?: string;
sourcesConsidered: ExtendedQuoteReportIndexedEntryOutbound[];
sourcesDelivered: ExtendedQuoteReportIndexedEntryOutbound[] | undefined;
}
export interface PriceComparisonsReport { export interface PriceComparisonsReport {
dexSources: BridgeQuoteReportEntry[]; dexSources: BridgeQuoteReportEntry[];
multiHopSources: MultiHopQuoteReportEntry[]; multiHopSources: MultiHopQuoteReportEntry[];
@ -80,7 +128,7 @@ export function generateQuoteReport(
const nativeOrderSourcesConsidered = nativeOrders.map(order => const nativeOrderSourcesConsidered = nativeOrders.map(order =>
nativeOrderToReportEntry(order.type, order as any, order.fillableTakerAmount, comparisonPrice, quoteRequestor), nativeOrderToReportEntry(order.type, order as any, order.fillableTakerAmount, comparisonPrice, quoteRequestor),
); );
const sourcesConsidered = [...nativeOrderSourcesConsidered.filter(order => order.isRfqt)]; const sourcesConsidered = [...nativeOrderSourcesConsidered.filter(order => order.isRFQ)];
let sourcesDelivered; let sourcesDelivered;
if (Array.isArray(liquidityDelivered)) { if (Array.isArray(liquidityDelivered)) {
@ -116,6 +164,105 @@ export function generateQuoteReport(
}; };
} }
/**
* Generates a report of sources considered while computing the optimized
* swap quote, the sources ultimately included in the computed quote. This
* extende version incudes all considered quotes, not only native liquidity.
*/
export function generateExtendedQuoteReportSources(
marketOperation: MarketOperation,
quotes: RawQuotes,
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
amount: BigNumber,
comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor,
): ExtendedQuoteReportSources {
const sourcesConsidered: ExtendedQuoteReportEntry[] = [];
// NativeOrders
sourcesConsidered.push(
...quotes.nativeOrders.map(order =>
nativeOrderToReportEntry(
order.type,
order as any,
order.fillableTakerAmount,
comparisonPrice,
quoteRequestor,
),
),
);
// IndicativeQuotes
sourcesConsidered.push(
...quotes.rfqtIndicativeQuotes.map(order => indicativeQuoteToReportEntry(order, comparisonPrice)),
);
// MultiHop
sourcesConsidered.push(...quotes.twoHopQuotes.map(quote => multiHopSampleToReportSource(quote, marketOperation)));
// Dex Quotes
sourcesConsidered.push(
..._.flatten(
quotes.dexQuotes.map(dex =>
dex
.filter(quote => isDexSampleForTotalAmount(quote, marketOperation, amount))
.map(quote => dexSampleToReportSource(quote, marketOperation)),
),
),
);
const sourcesConsideredIndexed = sourcesConsidered.map(
(quote, index): ExtendedQuoteReportIndexedEntry => {
return {
...quote,
quoteEntryIndex: index,
isDelivered: false,
};
},
);
let sourcesDelivered;
if (Array.isArray(liquidityDelivered)) {
// create easy way to look up fillable amounts
const nativeOrderSignaturesToFillableAmounts = _.fromPairs(
quotes.nativeOrders.map(o => {
return [_nativeDataToId(o), o.fillableTakerAmount];
}),
);
// map sources delivered
sourcesDelivered = liquidityDelivered.map(collapsedFill => {
if (_isNativeOrderFromCollapsedFill(collapsedFill)) {
return nativeOrderToReportEntry(
collapsedFill.type,
collapsedFill.fillData,
nativeOrderSignaturesToFillableAmounts[_nativeDataToId(collapsedFill.fillData)],
comparisonPrice,
quoteRequestor,
);
} else {
return dexSampleToReportSource(collapsedFill, marketOperation);
}
});
} else {
sourcesDelivered = [
// tslint:disable-next-line: no-unnecessary-type-assertion
multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation),
];
}
const sourcesDeliveredIndexed = sourcesDelivered.map(
(quote, index): ExtendedQuoteReportIndexedEntry => {
return {
...quote,
quoteEntryIndex: index,
isDelivered: false,
};
},
);
return {
sourcesConsidered: sourcesConsideredIndexed,
sourcesDelivered: sourcesDeliveredIndexed,
};
}
function _nativeDataToId(data: { signature: Signature }): string { function _nativeDataToId(data: { signature: Signature }): string {
const { v, r, s } = data.signature; const { v, r, s } = data.signature;
return `${v}${r}${s}`; return `${v}${r}${s}`;
@ -153,6 +300,22 @@ export function dexSampleToReportSource(ds: DexSample, marketOperation: MarketOp
} }
} }
/**
* Checks if a DEX sample is the one that represents the whole amount requested by taker
* NOTE: this is used for the QuoteReport to filter samples
*/
function isDexSampleForTotalAmount(ds: DexSample, marketOperation: MarketOperation, amount: BigNumber): boolean {
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
return ds.input === amount;
} else if (marketOperation === MarketOperation.Sell) {
return ds.output === amount;
} else {
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
}
/** /**
* Generates a report sample for a MultiHop source * Generates a report sample for a MultiHop source
* NOTE: this is used for the QuoteReport and quote price comparison data * NOTE: this is used for the QuoteReport and quote price comparison data
@ -208,17 +371,17 @@ export function nativeOrderToReportEntry(
}; };
// if we find this is an rfqt order, label it as such and associate makerUri // if we find this is an rfqt order, label it as such and associate makerUri
const isRfqt = type === FillQuoteTransformerOrderType.Rfq; const isRFQ = type === FillQuoteTransformerOrderType.Rfq;
const rfqtMakerUri = const rfqtMakerUri =
isRfqt && quoteRequestor ? quoteRequestor.getMakerUriForSignature(fillData.signature) : undefined; isRFQ && quoteRequestor ? quoteRequestor.getMakerUriForSignature(fillData.signature) : undefined;
if (isRfqt) { if (isRFQ) {
const nativeOrder = fillData.order as RfqOrderFields; const nativeOrder = fillData.order as RfqOrderFields;
// tslint:disable-next-line: no-object-literal-type-assertion // tslint:disable-next-line: no-object-literal-type-assertion
return { return {
liquiditySource: ERC20BridgeSource.Native, liquiditySource: ERC20BridgeSource.Native,
...nativeOrderBase, ...nativeOrderBase,
isRfqt: true, isRFQ: true,
makerUri: rfqtMakerUri || '', makerUri: rfqtMakerUri || '',
...(comparisonPrice ? { comparisonPrice: comparisonPrice.toNumber() } : {}), ...(comparisonPrice ? { comparisonPrice: comparisonPrice.toNumber() } : {}),
nativeOrder, nativeOrder,
@ -229,8 +392,49 @@ export function nativeOrderToReportEntry(
return { return {
liquiditySource: ERC20BridgeSource.Native, liquiditySource: ERC20BridgeSource.Native,
...nativeOrderBase, ...nativeOrderBase,
isRfqt: false, isRFQ: false,
fillData, fillData,
}; };
} }
} }
/**
* Generates a report entry for an indicative RFQ Quote
* NOTE: this is used for the QuoteReport and quote price comparison data
*/
export function indicativeQuoteToReportEntry(
order: V4RFQIndicativeQuoteMM,
comparisonPrice?: BigNumber | undefined,
): IndicativeRfqOrderQuoteReportEntry {
const nativeOrderBase = {
makerAmount: order.makerAmount,
takerAmount: order.takerAmount,
fillableTakerAmount: order.takerAmount,
};
// tslint:disable-next-line: no-object-literal-type-assertion
return {
liquiditySource: ERC20BridgeSource.Native,
...nativeOrderBase,
isRFQ: true,
makerUri: order.makerUri,
fillData: {},
...(comparisonPrice ? { comparisonPrice: comparisonPrice.toNumber() } : {}),
};
}
/**
* For the extended quote report, we output the filldata as JSON
*/
export function jsonifyFillData(source: ExtendedQuoteReportIndexedEntry): ExtendedQuoteReportIndexedEntryOutbound {
return {
...source,
fillData: JSON.stringify(source.fillData, (key: string, value: any) => {
if (key === '_samplerContract') {
return {};
} else {
return value;
}
}),
};
}

View File

@ -39,6 +39,10 @@ interface RfqQuote<T> {
makerUri: string; makerUri: string;
} }
export interface V4RFQIndicativeQuoteMM extends V4RFQIndicativeQuote {
makerUri: string;
}
export interface MetricsProxy { export interface MetricsProxy {
/** /**
* Increments a counter that is tracking valid Firm Quotes that are dropped due to low expiration. * Increments a counter that is tracking valid Firm Quotes that are dropped due to low expiration.
@ -343,7 +347,7 @@ export class QuoteRequestor {
marketOperation: MarketOperation, marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined, comparisonPrice: BigNumber | undefined,
options: RfqmRequestOptions, options: RfqmRequestOptions,
): Promise<V4RFQIndicativeQuote[]> { ): Promise<V4RFQIndicativeQuoteMM[]> {
const _opts: RfqRequestOpts = { const _opts: RfqRequestOpts = {
...constants.DEFAULT_RFQT_REQUEST_OPTS, ...constants.DEFAULT_RFQT_REQUEST_OPTS,
...options, ...options,
@ -367,7 +371,7 @@ export class QuoteRequestor {
marketOperation: MarketOperation, marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined, comparisonPrice: BigNumber | undefined,
options: RfqRequestOpts, options: RfqRequestOpts,
): Promise<V4RFQIndicativeQuote[]> { ): Promise<V4RFQIndicativeQuoteMM[]> {
const _opts: RfqRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; const _opts: RfqRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options };
// Originally a takerAddress was required for indicative quotes, but // Originally a takerAddress was required for indicative quotes, but
// now we've eliminated that requirement. @0x/quote-server, however, // now we've eliminated that requirement. @0x/quote-server, however,
@ -398,8 +402,8 @@ export class QuoteRequestor {
return this._orderSignatureToMakerUri[nativeDataToId({ signature })]; return this._orderSignatureToMakerUri[nativeDataToId({ signature })];
} }
private _isValidRfqtIndicativeQuoteResponse(response: V4RFQIndicativeQuote): boolean { private _isValidRfqtIndicativeQuoteResponse(response: V4RFQIndicativeQuoteMM): boolean {
const requiredKeys: Array<keyof V4RFQIndicativeQuote> = [ const requiredKeys: Array<keyof V4RFQIndicativeQuoteMM> = [
'makerAmount', 'makerAmount',
'takerAmount', 'takerAmount',
'makerToken', 'makerToken',
@ -545,7 +549,10 @@ export class QuoteRequestor {
}, },
}); });
rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= timeoutMs); rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= timeoutMs);
return { response: response.data, makerUri: typedMakerUrl.url }; return {
response: { ...response.data, makerUri: typedMakerUrl.url },
makerUri: typedMakerUrl.url,
};
} else { } else {
if (this._altRfqCreds === undefined) { if (this._altRfqCreds === undefined) {
throw new Error(`don't have credentials for alt MM`); throw new Error(`don't have credentials for alt MM`);
@ -694,7 +701,6 @@ export class QuoteRequestor {
} else { } else {
const secondsRemaining = msRemainingUntilExpiration.div(ONE_SECOND_MS); const secondsRemaining = msRemainingUntilExpiration.div(ONE_SECOND_MS);
this._metrics?.measureExpirationForValidOrder(isLastLook, order.maker, secondsRemaining); this._metrics?.measureExpirationForValidOrder(isLastLook, order.maker, secondsRemaining);
const takerAmount = new BigNumber(order.takerAmount); const takerAmount = new BigNumber(order.takerAmount);
const fillRatio = takerAmount.div(assetFillAmount); const fillRatio = takerAmount.div(assetFillAmount);
if (fillRatio.lt(1) && fillRatio.gte(FILL_RATIO_WARNING_LEVEL)) { if (fillRatio.lt(1) && fillRatio.gte(FILL_RATIO_WARNING_LEVEL)) {
@ -744,9 +750,9 @@ export class QuoteRequestor {
comparisonPrice: BigNumber | undefined, comparisonPrice: BigNumber | undefined,
options: RfqRequestOpts, options: RfqRequestOpts,
assetOfferings: RfqMakerAssetOfferings, assetOfferings: RfqMakerAssetOfferings,
): Promise<V4RFQIndicativeQuote[]> { ): Promise<V4RFQIndicativeQuoteMM[]> {
// fetch quotes // fetch quotes
const rawQuotes = await this._getQuotesAsync<V4RFQIndicativeQuote>( const rawQuotes = await this._getQuotesAsync<V4RFQIndicativeQuoteMM>(
makerToken, makerToken,
takerToken, takerToken,
assetFillAmount, assetFillAmount,
@ -758,7 +764,7 @@ export class QuoteRequestor {
); );
// validate // validate
const validationFunction = (o: V4RFQIndicativeQuote) => this._isValidRfqtIndicativeQuoteResponse(o); const validationFunction = (o: V4RFQIndicativeQuoteMM) => this._isValidRfqtIndicativeQuoteResponse(o);
const validQuotes = rawQuotes.filter(result => { const validQuotes = rawQuotes.filter(result => {
const order = result.response; const order = result.response;
if (!validationFunction(order)) { if (!validationFunction(order)) {

View File

@ -159,7 +159,11 @@ describe('MarketOperationUtils tests', () => {
} else { } else {
requestor requestor
.setup(r => r.requestRfqtIndicativeQuotesAsync(...args)) .setup(r => r.requestRfqtIndicativeQuotesAsync(...args))
.returns(async () => results.map(r => r.order)) .returns(async () =>
results.map(r => {
return { ...r.order, makerUri: 'https://foo.bar/' };
}),
)
.verifiable(verifiable); .verifiable(verifiable);
} }
return requestor; return requestor;

View File

@ -155,7 +155,7 @@ describe('generateQuoteReport', async () => {
makerAmount: rfqtOrder1.order.makerAmount, makerAmount: rfqtOrder1.order.makerAmount,
takerAmount: rfqtOrder1.order.takerAmount, takerAmount: rfqtOrder1.order.takerAmount,
fillableTakerAmount: rfqtOrder1.fillableTakerAmount, fillableTakerAmount: rfqtOrder1.fillableTakerAmount,
isRfqt: true, isRFQ: true,
makerUri: 'https://rfqt1.provider.club', makerUri: 'https://rfqt1.provider.club',
nativeOrder: rfqtOrder1.order, nativeOrder: rfqtOrder1.order,
fillData: { fillData: {
@ -167,7 +167,7 @@ describe('generateQuoteReport', async () => {
makerAmount: rfqtOrder2.order.makerAmount, makerAmount: rfqtOrder2.order.makerAmount,
takerAmount: rfqtOrder2.order.takerAmount, takerAmount: rfqtOrder2.order.takerAmount,
fillableTakerAmount: rfqtOrder2.fillableTakerAmount, fillableTakerAmount: rfqtOrder2.fillableTakerAmount,
isRfqt: true, isRFQ: true,
makerUri: 'https://rfqt2.provider.club', makerUri: 'https://rfqt2.provider.club',
nativeOrder: rfqtOrder2.order, nativeOrder: rfqtOrder2.order,
fillData: { fillData: {
@ -179,7 +179,7 @@ describe('generateQuoteReport', async () => {
makerAmount: orderbookOrder2.order.makerAmount, makerAmount: orderbookOrder2.order.makerAmount,
takerAmount: orderbookOrder2.order.takerAmount, takerAmount: orderbookOrder2.order.takerAmount,
fillableTakerAmount: orderbookOrder2.fillableTakerAmount, fillableTakerAmount: orderbookOrder2.fillableTakerAmount,
isRfqt: false, isRFQ: false,
fillData: { fillData: {
order: orderbookOrder2.order, order: orderbookOrder2.order,
} as NativeLimitOrderFillData, } as NativeLimitOrderFillData,
@ -263,7 +263,7 @@ describe('generateQuoteReport', async () => {
makerAmount: orderbookOrder1.order.makerAmount, makerAmount: orderbookOrder1.order.makerAmount,
takerAmount: orderbookOrder1.order.takerAmount, takerAmount: orderbookOrder1.order.takerAmount,
fillableTakerAmount: orderbookOrder1.fillableTakerAmount, fillableTakerAmount: orderbookOrder1.fillableTakerAmount,
isRfqt: false, isRFQ: false,
fillData: { fillData: {
order: orderbookOrder1.order, order: orderbookOrder1.order,
} as NativeLimitOrderFillData, } as NativeLimitOrderFillData,

View File

@ -494,15 +494,18 @@ describe('QuoteRequestor', async () => {
expiry: makeThreeMinuteExpiry(), expiry: makeThreeMinuteExpiry(),
}; };
const goodMMUri1 = 'https://1337.0.0.1';
const goodMMUri2 = 'https://37.0.0.1';
mockedRequests.push({ mockedRequests.push({
...mockedDefaults, ...mockedDefaults,
endpoint: 'https://1337.0.0.1', endpoint: goodMMUri1,
responseData: successfulQuote1, responseData: successfulQuote1,
}); });
// [GOOD] Another Successful response // [GOOD] Another Successful response
mockedRequests.push({ mockedRequests.push({
...mockedDefaults, ...mockedDefaults,
endpoint: 'https://37.0.0.1', endpoint: goodMMUri2,
responseData: successfulQuote1, responseData: successfulQuote1,
}); });
@ -532,6 +535,16 @@ describe('QuoteRequestor', async () => {
responseData: { ...successfulQuote1, takerToken: otherToken1 }, responseData: { ...successfulQuote1, takerToken: otherToken1 },
}); });
const assetOfferings: { [k: string]: [[string, string]] } = {
'https://420.0.0.1': [[makerToken, takerToken]],
'https://421.0.0.1': [[makerToken, takerToken]],
'https://422.0.0.1': [[makerToken, takerToken]],
'https://423.0.0.1': [[makerToken, takerToken]],
'https://424.0.0.1': [[makerToken, takerToken]],
};
assetOfferings[goodMMUri1] = [[makerToken, takerToken]];
assetOfferings[goodMMUri2] = [[makerToken, takerToken]];
return testHelpers.withMockedRfqQuotes( return testHelpers.withMockedRfqQuotes(
mockedRequests, mockedRequests,
[], [],
@ -539,15 +552,7 @@ describe('QuoteRequestor', async () => {
async () => { async () => {
const qr = new QuoteRequestor( const qr = new QuoteRequestor(
{}, // No RFQ-T asset offerings {}, // No RFQ-T asset offerings
{ assetOfferings,
'https://1337.0.0.1': [[makerToken, takerToken]],
'https://37.0.0.1': [[makerToken, takerToken]],
'https://420.0.0.1': [[makerToken, takerToken]],
'https://421.0.0.1': [[makerToken, takerToken]],
'https://422.0.0.1': [[makerToken, takerToken]],
'https://423.0.0.1': [[makerToken, takerToken]],
'https://424.0.0.1': [[makerToken, takerToken]],
},
quoteRequestorHttpClient, quoteRequestorHttpClient,
); );
const resp = await qr.requestRfqmIndicativeQuotesAsync( const resp = await qr.requestRfqmIndicativeQuotesAsync(
@ -572,7 +577,12 @@ describe('QuoteRequestor', async () => {
}, },
}, },
); );
expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort()); expect(resp.sort()).to.eql(
[
{ ...successfulQuote1, makerUri: goodMMUri1 },
{ ...successfulQuote1, makerUri: goodMMUri2 },
].sort(),
);
}, },
quoteRequestorHttpClient, quoteRequestorHttpClient,
); );
@ -622,9 +632,12 @@ describe('QuoteRequestor', async () => {
expiry: makeThreeMinuteExpiry(), expiry: makeThreeMinuteExpiry(),
}; };
const goodMMUri1 = 'https://1337.0.0.1';
const goodMMUri2 = 'https://37.0.0.1';
mockedRequests.push({ mockedRequests.push({
...mockedDefaults, ...mockedDefaults,
endpoint: 'https://1337.0.0.1', endpoint: goodMMUri1,
responseData: successfulQuote1, responseData: successfulQuote1,
}); });
// Test out a bad response code, ensure it doesnt cause throw // Test out a bad response code, ensure it doesnt cause throw
@ -655,28 +668,26 @@ describe('QuoteRequestor', async () => {
// Another Successful response // Another Successful response
mockedRequests.push({ mockedRequests.push({
...mockedDefaults, ...mockedDefaults,
endpoint: 'https://37.0.0.1', endpoint: goodMMUri2,
responseData: successfulQuote1, responseData: successfulQuote1,
}); });
const assetOfferings: { [k: string]: [[string, string]] } = {
'https://420.0.0.1': [[makerToken, takerToken]],
'https://421.0.0.1': [[makerToken, takerToken]],
'https://422.0.0.1': [[makerToken, takerToken]],
'https://423.0.0.1': [[makerToken, takerToken]],
'https://424.0.0.1': [[makerToken, takerToken]],
};
assetOfferings[goodMMUri1] = [[makerToken, takerToken]];
assetOfferings[goodMMUri2] = [[makerToken, takerToken]];
return testHelpers.withMockedRfqQuotes( return testHelpers.withMockedRfqQuotes(
mockedRequests, mockedRequests,
[], [],
RfqQuoteEndpoint.Indicative, RfqQuoteEndpoint.Indicative,
async () => { async () => {
const qr = new QuoteRequestor( const qr = new QuoteRequestor(assetOfferings, {}, quoteRequestorHttpClient);
{
'https://1337.0.0.1': [[makerToken, takerToken]],
'https://420.0.0.1': [[makerToken, takerToken]],
'https://421.0.0.1': [[makerToken, takerToken]],
'https://422.0.0.1': [[makerToken, takerToken]],
'https://423.0.0.1': [[makerToken, takerToken]],
'https://424.0.0.1': [[makerToken, takerToken]],
'https://37.0.0.1': [[makerToken, takerToken]],
},
{},
quoteRequestorHttpClient,
);
const resp = await qr.requestRfqtIndicativeQuotesAsync( const resp = await qr.requestRfqtIndicativeQuotesAsync(
makerToken, makerToken,
takerToken, takerToken,
@ -693,7 +704,12 @@ describe('QuoteRequestor', async () => {
intentOnFilling: true, intentOnFilling: true,
}, },
); );
expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort()); expect(resp.sort()).to.eql(
[
{ ...successfulQuote1, makerUri: goodMMUri1 },
{ ...successfulQuote1, makerUri: goodMMUri2 },
].sort(),
);
}, },
quoteRequestorHttpClient, quoteRequestorHttpClient,
); );
@ -784,7 +800,7 @@ describe('QuoteRequestor', async () => {
makerEndpointMaxResponseTimeMs: maxTimeoutMs, makerEndpointMaxResponseTimeMs: maxTimeoutMs,
}, },
); );
expect(resp.sort()).to.eql([successfulQuote1].sort()); // notice only one result, despite two requests made expect(resp.sort()).to.eql([{ ...successfulQuote1, makerUri: 'https://1337.0.0.1' }].sort()); // notice only one result, despite two requests made
}, },
quoteRequestorHttpClient, quoteRequestorHttpClient,
); );
@ -847,7 +863,7 @@ describe('QuoteRequestor', async () => {
intentOnFilling: true, intentOnFilling: true,
}, },
); );
expect(resp.sort()).to.eql([successfulQuote1].sort()); expect(resp.sort()).to.eql([{ ...successfulQuote1, makerUri: 'https://1337.0.0.1' }].sort());
}, },
quoteRequestorHttpClient, quoteRequestorHttpClient,
); );

View File

@ -6402,6 +6402,7 @@ fake-merkle-patricia-tree@^1.0.1:
fast-abi@^0.0.2: fast-abi@^0.0.2:
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/fast-abi/-/fast-abi-0.0.2.tgz#da5f796fd7c7b0c966d916ee21daae3eca61c07c" resolved "https://registry.yarnpkg.com/fast-abi/-/fast-abi-0.0.2.tgz#da5f796fd7c7b0c966d916ee21daae3eca61c07c"
integrity sha512-k/2s63SkFf6jU2LyF6oQC5/N+L90q6VD1wkp2NXo+DSHoTeOJD2Q6Egpcs+bTPODik0CHxjb7lORgsG+QCRq/Q==
dependencies: dependencies:
"@mapbox/node-pre-gyp" "^1.0.4" "@mapbox/node-pre-gyp" "^1.0.4"
neon-cli "^0.8.0" neon-cli "^0.8.0"