Files
protocol/packages/asset-swapper/src/utils/quote_report_generator.ts
Jacob Evans d6bc0a3368 fix: [asset-swapper] prevent error when multihop data is not present (#80)
* fix: [asset-swapper] prevent error when multihop is not present for a route

* Update changelog

Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
2021-01-28 13:04:32 -08:00

234 lines
8.0 KiB
TypeScript

import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../types';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
FillData,
MultiHopFillData,
NativeCollapsedFill,
} from './market_operation_utils/types';
import { QuoteRequestor } from './quote_requestor';
export interface BridgeReportSource {
liquiditySource: Exclude<ERC20BridgeSource, ERC20BridgeSource.Native>;
makerAmount: BigNumber;
takerAmount: BigNumber;
fillData?: FillData;
}
export interface MultiHopReportSource {
liquiditySource: ERC20BridgeSource.MultiHop;
makerAmount: BigNumber;
takerAmount: BigNumber;
hopSources: ERC20BridgeSource[];
fillData: FillData;
}
interface NativeReportSourceBase {
liquiditySource: ERC20BridgeSource.Native;
makerAmount: BigNumber;
takerAmount: BigNumber;
nativeOrder: SignedOrder;
fillableTakerAmount: BigNumber;
}
export interface NativeOrderbookReportSource extends NativeReportSourceBase {
isRfqt: false;
}
export interface NativeRFQTReportSource extends NativeReportSourceBase {
isRfqt: true;
makerUri: string;
comparisonPrice?: number;
}
export type QuoteReportSource =
| BridgeReportSource
| NativeOrderbookReportSource
| NativeRFQTReportSource
| MultiHopReportSource;
export interface QuoteReport {
sourcesConsidered: QuoteReportSource[];
sourcesDelivered: QuoteReportSource[];
}
/**
* 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,
dexQuotes: DexSample[],
multiHopQuotes: Array<DexSample<MultiHopFillData>>,
nativeOrders: SignedOrder[],
orderFillableAmounts: BigNumber[],
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor,
): QuoteReport {
const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
const nativeOrderSourcesConsidered = nativeOrders.map((order, idx) =>
_nativeOrderToReportSource(order, orderFillableAmounts[idx], comparisonPrice, quoteRequestor),
);
const multiHopSourcesConsidered = multiHopQuotes.map(quote =>
_multiHopSampleToReportSource(quote, marketOperation),
);
const sourcesConsidered = [
...dexReportSourcesConsidered,
...nativeOrderSourcesConsidered,
...multiHopSourcesConsidered,
];
let sourcesDelivered;
if (Array.isArray(liquidityDelivered)) {
// create easy way to look up fillable amounts
const nativeOrderSignaturesToFillableAmounts = _nativeOrderSignaturesToFillableAmounts(
nativeOrders,
orderFillableAmounts,
);
// map sources delivered
sourcesDelivered = liquidityDelivered.map(collapsedFill => {
const foundNativeOrder = _nativeOrderFromCollapsedFill(collapsedFill);
if (foundNativeOrder) {
return _nativeOrderToReportSource(
foundNativeOrder,
nativeOrderSignaturesToFillableAmounts[foundNativeOrder.signature],
comparisonPrice,
quoteRequestor,
);
} else {
return _dexSampleToReportSource(collapsedFill, marketOperation);
}
});
} else {
sourcesDelivered = [
_multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation),
];
}
return {
sourcesConsidered,
sourcesDelivered,
};
}
function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): 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 (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}`);
}
}
function _multiHopSampleToReportSource(
ds: DexSample<MultiHopFillData>,
marketOperation: MarketOperation,
): MultiHopReportSource {
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 _nativeOrderSignaturesToFillableAmounts(
nativeOrders: SignedOrder[],
fillableAmounts: BigNumber[],
): { [orderSignature: string]: BigNumber } {
// create easy way to look up fillable amounts based on native order signatures
if (fillableAmounts.length !== nativeOrders.length) {
// length mismatch, abort
throw new Error('orderFillableAmounts must be the same length as nativeOrders');
}
const nativeOrderSignaturesToFillableAmounts: { [orderSignature: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
nativeOrderSignaturesToFillableAmounts[nativeOrder.signature] = fillableAmounts[idx];
});
return nativeOrderSignaturesToFillableAmounts;
}
function _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;
}
}
function _nativeOrderToReportSource(
nativeOrder: SignedOrder,
fillableAmount: BigNumber,
comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor,
): NativeRFQTReportSource | NativeOrderbookReportSource {
const nativeOrderBase: NativeReportSourceBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
fillableTakerAmount: fillableAmount,
nativeOrder,
};
// if we find this is an rfqt order, label it as such and associate makerUri
const foundRfqtMakerUri = quoteRequestor && quoteRequestor.getMakerUriForOrderSignature(nativeOrder.signature);
if (foundRfqtMakerUri) {
const rfqtSource: NativeRFQTReportSource = {
...nativeOrderBase,
isRfqt: true,
makerUri: foundRfqtMakerUri,
};
if (comparisonPrice) {
rfqtSource.comparisonPrice = comparisonPrice.toNumber();
}
return rfqtSource;
} else {
// if it's not an rfqt order, treat as normal
const regularNativeOrder: NativeOrderbookReportSource = {
...nativeOrderBase,
isRfqt: false,
};
return regularNativeOrder;
}
}