protocol/packages/asset-swapper/src/utils/swap_quote_calculator.ts

252 lines
8.7 KiB
TypeScript

import { assetDataUtils } from '@0x/order-utils';
import { AssetProxyId, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import {
CalculateSwapQuoteOpts,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
SwapQuote,
SwapQuoteInfo,
SwapQuoteOrdersBreakdown,
SwapQuoterError,
} from '../types';
import { MarketOperationUtils } from './market_operation_utils';
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
import { isSupportedAssetDataInOrders } from './utils';
import { QuoteReport } from './quote_report_generator';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
export class SwapQuoteCalculator {
private readonly _marketOperationUtils: MarketOperationUtils;
constructor(marketOperationUtils: MarketOperationUtils) {
this._marketOperationUtils = marketOperationUtils;
}
public async calculateMarketSellSwapQuoteAsync(
prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber,
gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<MarketSellSwapQuote> {
return (await this._calculateSwapQuoteAsync(
prunedOrders,
takerAssetFillAmount,
gasPrice,
MarketOperation.Sell,
opts,
)) as MarketSellSwapQuote;
}
public async calculateMarketBuySwapQuoteAsync(
prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber,
gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<MarketBuySwapQuote> {
return (await this._calculateSwapQuoteAsync(
prunedOrders,
takerAssetFillAmount,
gasPrice,
MarketOperation.Buy,
opts,
)) as MarketBuySwapQuote;
}
public async calculateBatchMarketBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][],
takerAssetFillAmounts: BigNumber[],
gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<Array<MarketBuySwapQuote | undefined>> {
return (await this._calculateBatchBuySwapQuoteAsync(
batchPrunedOrders,
takerAssetFillAmounts,
gasPrice,
MarketOperation.Buy,
opts,
)) as Array<MarketBuySwapQuote | undefined>;
}
private async _calculateBatchBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][],
assetFillAmounts: BigNumber[],
gasPrice: BigNumber,
operation: MarketOperation,
opts: CalculateSwapQuoteOpts,
): Promise<Array<SwapQuote | undefined>> {
const batchSignedOrders = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync(
batchPrunedOrders,
assetFillAmounts,
opts,
);
const batchSwapQuotes = await Promise.all(
batchSignedOrders.map(async (orders, i) => {
if (orders) {
const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0];
return createSwapQuote(
makerAssetData,
takerAssetData,
orders,
operation,
assetFillAmounts[i],
gasPrice,
opts.gasSchedule,
);
} else {
return undefined;
}
}),
);
return batchSwapQuotes;
}
private async _calculateSwapQuoteAsync(
prunedOrders: SignedOrder[],
assetFillAmount: BigNumber,
gasPrice: BigNumber,
operation: MarketOperation,
opts: CalculateSwapQuoteOpts,
): Promise<SwapQuote> {
// checks if maker asset is ERC721 or ERC20 and taker asset is ERC20
if (!isSupportedAssetDataInOrders(prunedOrders)) {
throw Error(SwapQuoterError.AssetDataUnsupported);
}
// 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 optimizedOrders: OptimizedMarketOrder[] | undefined;
let quoteReport: QuoteReport | undefined;
{
// Scale fees by gas price.
const _opts: GetMarketOrdersOpts = {
...opts,
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) =>
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
),
};
const firstOrderMakerAssetData = !!prunedOrders[0]
? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData)
: { assetProxyId: '' };
if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) {
// HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable
optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o));
} else {
if (operation === MarketOperation.Buy) {
const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync(
prunedOrders,
assetFillAmount,
_opts,
);
optimizedOrders = buyResult.optimizedOrders;
quoteReport = buyResult.quoteReport;
} else {
const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders,
assetFillAmount,
_opts,
);
optimizedOrders = sellResult.optimizedOrders;
quoteReport = sellResult.quoteReport;
}
}
}
// assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0];
return createSwapQuote(
makerAssetData,
takerAssetData,
optimizedOrders,
operation,
assetFillAmount,
gasPrice,
opts.gasSchedule,
quoteReport,
);
}
}
function createSwapQuote(
makerAssetData: string,
takerAssetData: string,
optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
quoteReport?: QuoteReport,
): SwapQuote {
const bestCaseFillResult = simulateBestCaseFill({
gasPrice,
orders: optimizedOrders,
side: operation,
fillAmount: assetFillAmount,
opts: { gasSchedule },
});
const worstCaseFillResult = simulateWorstCaseFill({
gasPrice,
orders: optimizedOrders,
side: operation,
fillAmount: assetFillAmount,
opts: { gasSchedule },
});
const quoteBase = {
takerAssetData,
makerAssetData,
gasPrice,
bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult),
worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult),
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
orders: optimizedOrders,
quoteReport,
};
if (operation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
quoteReport,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
quoteReport,
};
}
}
function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown {
const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource));
const breakdown: SwapQuoteOrdersBreakdown = {};
Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => {
breakdown[source] = fillAmount.div(totalFillAmount);
});
return breakdown;
}
function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo {
return {
makerAssetAmount: fr.totalMakerAssetAmount,
takerAssetAmount: fr.takerAssetAmount,
totalTakerAssetAmount: fr.totalTakerAssetAmount,
feeTakerAssetAmount: fr.takerFeeTakerAssetAmount,
protocolFeeInWeiAmount: fr.protocolFeeAmount,
gas: fr.gas,
};
}