* Remove old JS router and add a FillAdjustor Clean up JS router and unused functions Remove more unused functions, add adjustment of fills Comment on why we use fill over sample update CODEOWNERS lint Clean up Fill removing unused properties Remove CollapsedFills, omit flags bigint Create GasSchedule vs FeeSchedule, return Fill and gas on OptimizedOrder Use Fill Adjustment in Phase2 of routing Fix Limit orders being treated as VIP * Fix case where dex liquidity is empty * Use best gas adjusted pricing for fee sources * CHANGELOG
435 lines
14 KiB
TypeScript
435 lines
14 KiB
TypeScript
import { FillQuoteTransformerOrderType, RfqOrderFields, Signature } from '@0x/protocol-utils';
|
|
import { BigNumber } from '@0x/utils';
|
|
import _ = require('lodash');
|
|
|
|
import { MarketOperation, NativeOrderWithFillableAmounts } from '../types';
|
|
|
|
import {
|
|
DexSample,
|
|
ERC20BridgeSource,
|
|
Fill,
|
|
FillData,
|
|
MultiHopFillData,
|
|
NativeFillData,
|
|
NativeLimitOrderFillData,
|
|
NativeRfqOrderFillData,
|
|
RawQuotes,
|
|
} from './market_operation_utils/types';
|
|
import { QuoteRequestor, V4RFQIndicativeQuoteMM } from './quote_requestor';
|
|
|
|
export interface QuoteReportEntryBase {
|
|
liquiditySource: ERC20BridgeSource;
|
|
makerAmount: BigNumber;
|
|
takerAmount: BigNumber;
|
|
fillData: FillData;
|
|
}
|
|
export interface BridgeQuoteReportEntry extends QuoteReportEntryBase {
|
|
liquiditySource: Exclude<ERC20BridgeSource, ERC20BridgeSource.Native>;
|
|
}
|
|
|
|
export interface MultiHopQuoteReportEntry extends QuoteReportEntryBase {
|
|
liquiditySource: ERC20BridgeSource.MultiHop;
|
|
hopSources: ERC20BridgeSource[];
|
|
}
|
|
|
|
export interface NativeLimitOrderQuoteReportEntry extends QuoteReportEntryBase {
|
|
liquiditySource: ERC20BridgeSource.Native;
|
|
fillData: NativeFillData;
|
|
fillableTakerAmount: BigNumber;
|
|
isRFQ: false;
|
|
}
|
|
|
|
export interface NativeRfqOrderQuoteReportEntry extends QuoteReportEntryBase {
|
|
liquiditySource: ERC20BridgeSource.Native;
|
|
fillData: NativeFillData;
|
|
fillableTakerAmount: BigNumber;
|
|
isRFQ: true;
|
|
nativeOrder: RfqOrderFields;
|
|
makerUri: string;
|
|
comparisonPrice?: number;
|
|
}
|
|
|
|
export interface IndicativeRfqOrderQuoteReportEntry extends QuoteReportEntryBase {
|
|
liquiditySource: ERC20BridgeSource.Native;
|
|
fillableTakerAmount: BigNumber;
|
|
isRFQ: true;
|
|
makerUri?: string;
|
|
comparisonPrice?: number;
|
|
}
|
|
|
|
export type QuoteReportEntry =
|
|
| BridgeQuoteReportEntry
|
|
| MultiHopQuoteReportEntry
|
|
| NativeLimitOrderQuoteReportEntry
|
|
| 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 {
|
|
sourcesConsidered: 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;
|
|
blockNumber: number | undefined;
|
|
estimatedGas: string;
|
|
}
|
|
|
|
export interface PriceComparisonsReport {
|
|
dexSources: BridgeQuoteReportEntry[];
|
|
multiHopSources: MultiHopQuoteReportEntry[];
|
|
nativeSources: Array<NativeLimitOrderQuoteReportEntry | NativeRfqOrderQuoteReportEntry>;
|
|
}
|
|
|
|
/**
|
|
* Generates a report of sources considered while computing the optimized
|
|
* swap quote, and the sources ultimately included in the computed quote.
|
|
*/
|
|
export function generateQuoteReport(
|
|
marketOperation: MarketOperation,
|
|
nativeOrders: NativeOrderWithFillableAmounts[],
|
|
liquidityDelivered: ReadonlyArray<Fill> | DexSample<MultiHopFillData>,
|
|
comparisonPrice?: BigNumber | undefined,
|
|
quoteRequestor?: QuoteRequestor,
|
|
): QuoteReport {
|
|
const nativeOrderSourcesConsidered = nativeOrders.map(order =>
|
|
nativeOrderToReportEntry(order.type, order as any, order.fillableTakerAmount, comparisonPrice, quoteRequestor),
|
|
);
|
|
const sourcesConsidered = [...nativeOrderSourcesConsidered.filter(order => order.isRFQ)];
|
|
|
|
let sourcesDelivered;
|
|
if (Array.isArray(liquidityDelivered)) {
|
|
// create easy way to look up fillable amounts
|
|
const nativeOrderSignaturesToFillableAmounts = _.fromPairs(
|
|
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),
|
|
];
|
|
}
|
|
return {
|
|
sourcesConsidered,
|
|
sourcesDelivered,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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<Fill> | 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 => isDexSampleFilter(quote, 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 {
|
|
const { v, r, s } = data.signature;
|
|
return `${v}${r}${s}`;
|
|
}
|
|
|
|
/**
|
|
* Generates a report sample for a DEX source
|
|
* NOTE: this is used for the QuoteReport and quote price comparison data
|
|
*/
|
|
export function dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeQuoteReportEntry {
|
|
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 (marketOperation === MarketOperation.Buy) {
|
|
return {
|
|
makerAmount: ds.input,
|
|
takerAmount: ds.output,
|
|
liquiditySource,
|
|
fillData: ds.fillData,
|
|
};
|
|
} else if (marketOperation === MarketOperation.Sell) {
|
|
return {
|
|
makerAmount: ds.output,
|
|
takerAmount: ds.input,
|
|
liquiditySource,
|
|
fillData: ds.fillData,
|
|
};
|
|
} else {
|
|
throw new Error(`Unexpected marketOperation ${marketOperation}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 isDexSampleFilter(ds: DexSample, amount: BigNumber): boolean {
|
|
// The entry is for the total amont, not a sampler entry && there was liquidity in the source
|
|
return ds.input.eq(amount) && ds.output.isGreaterThan(0);
|
|
}
|
|
|
|
/**
|
|
* Generates a report sample for a MultiHop source
|
|
* NOTE: this is used for the QuoteReport and quote price comparison data
|
|
*/
|
|
export function multiHopSampleToReportSource(
|
|
ds: DexSample<MultiHopFillData>,
|
|
marketOperation: MarketOperation,
|
|
): MultiHopQuoteReportEntry {
|
|
const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData;
|
|
// input and output map to different values
|
|
// based on the market operation
|
|
if (marketOperation === MarketOperation.Buy) {
|
|
return {
|
|
liquiditySource: ERC20BridgeSource.MultiHop,
|
|
makerAmount: ds.input,
|
|
takerAmount: ds.output,
|
|
fillData: ds.fillData,
|
|
hopSources: [firstHop.source, secondHop.source],
|
|
};
|
|
} else if (marketOperation === MarketOperation.Sell) {
|
|
return {
|
|
liquiditySource: ERC20BridgeSource.MultiHop,
|
|
makerAmount: ds.output,
|
|
takerAmount: ds.input,
|
|
fillData: ds.fillData,
|
|
hopSources: [firstHop.source, secondHop.source],
|
|
};
|
|
} else {
|
|
throw new Error(`Unexpected marketOperation ${marketOperation}`);
|
|
}
|
|
}
|
|
|
|
function _isNativeOrderFromCollapsedFill(cf: Fill): cf is Fill<NativeFillData> {
|
|
const { type } = cf;
|
|
return type === FillQuoteTransformerOrderType.Limit || type === FillQuoteTransformerOrderType.Rfq;
|
|
}
|
|
|
|
/**
|
|
* Generates a report entry for a native order
|
|
* NOTE: this is used for the QuoteReport and quote price comparison data
|
|
*/
|
|
export function nativeOrderToReportEntry(
|
|
type: FillQuoteTransformerOrderType,
|
|
fillData: NativeLimitOrderFillData | NativeRfqOrderFillData,
|
|
fillableAmount: BigNumber,
|
|
comparisonPrice?: BigNumber | undefined,
|
|
quoteRequestor?: QuoteRequestor,
|
|
): NativeRfqOrderQuoteReportEntry | NativeLimitOrderQuoteReportEntry {
|
|
const nativeOrderBase = {
|
|
makerAmount: fillData.order.makerAmount,
|
|
takerAmount: fillData.order.takerAmount,
|
|
fillableTakerAmount: fillableAmount,
|
|
};
|
|
|
|
// if we find this is an rfqt order, label it as such and associate makerUri
|
|
const isRFQ = type === FillQuoteTransformerOrderType.Rfq;
|
|
const rfqtMakerUri =
|
|
isRFQ && quoteRequestor ? quoteRequestor.getMakerUriForSignature(fillData.signature) : undefined;
|
|
|
|
if (isRFQ) {
|
|
const nativeOrder = fillData.order as RfqOrderFields;
|
|
// tslint:disable-next-line: no-object-literal-type-assertion
|
|
return {
|
|
liquiditySource: ERC20BridgeSource.Native,
|
|
...nativeOrderBase,
|
|
isRFQ: true,
|
|
makerUri: rfqtMakerUri || '',
|
|
...(comparisonPrice ? { comparisonPrice: comparisonPrice.toNumber() } : {}),
|
|
nativeOrder,
|
|
fillData,
|
|
};
|
|
} else {
|
|
// tslint:disable-next-line: no-object-literal-type-assertion
|
|
return {
|
|
liquiditySource: ERC20BridgeSource.Native,
|
|
...nativeOrderBase,
|
|
isRFQ: false,
|
|
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;
|
|
}
|
|
}),
|
|
};
|
|
}
|