* Extended Quote report for indicative quote * feat: Only save 'full' quotes on quote report * Unify extended quote report
676 lines
27 KiB
TypeScript
676 lines
27 KiB
TypeScript
import { ChainId, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
|
|
import { FillQuoteTransformerOrderType, LimitOrder } from '@0x/protocol-utils';
|
|
import { BigNumber, providerUtils } from '@0x/utils';
|
|
import Axios, { AxiosInstance } from 'axios';
|
|
import { BlockParamLiteral, MethodAbi, SupportedProvider, ZeroExProvider } from 'ethereum-types';
|
|
import { FastABI } from 'fast-abi';
|
|
import { Agent as HttpAgent } from 'http';
|
|
import { Agent as HttpsAgent } from 'https';
|
|
import * as _ from 'lodash';
|
|
|
|
import { artifacts } from './artifacts';
|
|
import { constants, INVALID_SIGNATURE, KEEP_ALIVE_TTL } from './constants';
|
|
import {
|
|
AssetSwapperContractAddresses,
|
|
MarketBuySwapQuote,
|
|
MarketOperation,
|
|
OrderPrunerPermittedFeeTypes,
|
|
RfqRequestOpts,
|
|
SignedNativeOrder,
|
|
SwapQuote,
|
|
SwapQuoteInfo,
|
|
SwapQuoteOrdersBreakdown,
|
|
SwapQuoteRequestOpts,
|
|
SwapQuoterOpts,
|
|
SwapQuoterRfqOpts,
|
|
} from './types';
|
|
import { assert } from './utils/assert';
|
|
import { MarketOperationUtils } from './utils/market_operation_utils';
|
|
import { BancorService } from './utils/market_operation_utils/bancor_service';
|
|
import { SAMPLER_ADDRESS, SOURCE_FLAGS, ZERO_AMOUNT } from './utils/market_operation_utils/constants';
|
|
import { DexOrderSampler } from './utils/market_operation_utils/sampler';
|
|
import { SourceFilters } from './utils/market_operation_utils/source_filters';
|
|
import {
|
|
ERC20BridgeSource,
|
|
FeeSchedule,
|
|
FillData,
|
|
GetMarketOrdersOpts,
|
|
MarketDepth,
|
|
MarketDepthSide,
|
|
MarketSideLiquidity,
|
|
OptimizedMarketOrder,
|
|
OptimizerResultWithReport,
|
|
} from './utils/market_operation_utils/types';
|
|
import { ProtocolFeeUtils } from './utils/protocol_fee_utils';
|
|
import { QuoteRequestor } from './utils/quote_requestor';
|
|
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './utils/quote_simulation';
|
|
import { ERC20BridgeSamplerContract } from './wrappers';
|
|
|
|
export abstract class Orderbook {
|
|
public abstract getOrdersAsync(
|
|
makerToken: string,
|
|
takerToken: string,
|
|
pruneFn?: (o: SignedNativeOrder) => boolean,
|
|
): Promise<SignedNativeOrder[]>;
|
|
public abstract getBatchOrdersAsync(
|
|
makerTokens: string[],
|
|
takerToken: string,
|
|
pruneFn?: (o: SignedNativeOrder) => boolean,
|
|
): Promise<SignedNativeOrder[][]>;
|
|
// tslint:disable-next-line:prefer-function-over-method
|
|
public async destroyAsync(): Promise<void> {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// tslint:disable:max-classes-per-file
|
|
export class SwapQuoter {
|
|
public readonly provider: ZeroExProvider;
|
|
public readonly orderbook: Orderbook;
|
|
public readonly expiryBufferMs: number;
|
|
public readonly chainId: number;
|
|
public readonly permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>;
|
|
private readonly _contractAddresses: AssetSwapperContractAddresses;
|
|
private readonly _protocolFeeUtils: ProtocolFeeUtils;
|
|
private readonly _marketOperationUtils: MarketOperationUtils;
|
|
private readonly _rfqtOptions?: SwapQuoterRfqOpts;
|
|
private readonly _quoteRequestorHttpClient: AxiosInstance;
|
|
private readonly _integratorIdsSet: Set<string>;
|
|
|
|
/**
|
|
* Instantiates a new SwapQuoter instance
|
|
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
|
|
* @param orderbook An object that conforms to Orderbook, see type for definition.
|
|
* @param options Initialization options for the SwapQuoter. See type definition for details.
|
|
*
|
|
* @return An instance of SwapQuoter
|
|
*/
|
|
constructor(supportedProvider: SupportedProvider, orderbook: Orderbook, options: Partial<SwapQuoterOpts> = {}) {
|
|
const {
|
|
chainId,
|
|
expiryBufferMs,
|
|
permittedOrderFeeTypes,
|
|
samplerGasLimit,
|
|
rfqt,
|
|
tokenAdjacencyGraph,
|
|
liquidityProviderRegistry,
|
|
} = { ...constants.DEFAULT_SWAP_QUOTER_OPTS, ...options };
|
|
const provider = providerUtils.standardizeOrThrow(supportedProvider);
|
|
assert.isValidOrderbook('orderbook', orderbook);
|
|
assert.isNumber('chainId', chainId);
|
|
assert.isNumber('expiryBufferMs', expiryBufferMs);
|
|
this.chainId = chainId;
|
|
this.provider = provider;
|
|
this.orderbook = orderbook;
|
|
this.expiryBufferMs = expiryBufferMs;
|
|
this.permittedOrderFeeTypes = permittedOrderFeeTypes;
|
|
|
|
this._rfqtOptions = rfqt;
|
|
this._contractAddresses = options.contractAddresses || {
|
|
...getContractAddressesForChainOrThrow(chainId),
|
|
};
|
|
this._protocolFeeUtils = ProtocolFeeUtils.getInstance(
|
|
constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
|
|
options.ethGasStationUrl,
|
|
);
|
|
// Allow the sampler bytecode to be overwritten using geths override functionality
|
|
const samplerBytecode = _.get(artifacts.ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object');
|
|
// Allow address of the Sampler to be overridden, i.e in Ganache where overrides do not work
|
|
const samplerAddress = (options.samplerOverrides && options.samplerOverrides.to) || SAMPLER_ADDRESS;
|
|
const defaultCodeOverrides = samplerBytecode
|
|
? {
|
|
[samplerAddress]: { code: samplerBytecode },
|
|
}
|
|
: {};
|
|
const samplerOverrides = _.assign(
|
|
{ block: BlockParamLiteral.Latest, overrides: defaultCodeOverrides },
|
|
options.samplerOverrides,
|
|
);
|
|
const fastAbi = new FastABI(ERC20BridgeSamplerContract.ABI() as MethodAbi[], { BigNumber });
|
|
const samplerContract = new ERC20BridgeSamplerContract(
|
|
samplerAddress,
|
|
this.provider,
|
|
{
|
|
gas: samplerGasLimit,
|
|
},
|
|
{},
|
|
undefined,
|
|
{
|
|
encodeInput: (fnName: string, values: any) => fastAbi.encodeInput(fnName, values),
|
|
decodeOutput: (fnName: string, data: string) => fastAbi.decodeOutput(fnName, data),
|
|
},
|
|
);
|
|
|
|
this._marketOperationUtils = new MarketOperationUtils(
|
|
new DexOrderSampler(
|
|
this.chainId,
|
|
samplerContract,
|
|
samplerOverrides,
|
|
undefined, // pools caches for balancer and cream
|
|
tokenAdjacencyGraph,
|
|
liquidityProviderRegistry,
|
|
this.chainId === ChainId.Mainnet // Enable Bancor only on Mainnet
|
|
? async () => BancorService.createAsync(provider)
|
|
: async () => undefined,
|
|
),
|
|
this._contractAddresses,
|
|
{
|
|
chainId,
|
|
exchangeAddress: this._contractAddresses.exchange,
|
|
},
|
|
);
|
|
|
|
this._quoteRequestorHttpClient = Axios.create({
|
|
httpAgent: new HttpAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
|
|
httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
|
|
...(rfqt ? rfqt.axiosInstanceOpts : {}),
|
|
});
|
|
|
|
const integratorIds = this._rfqtOptions?.integratorsWhitelist.map(integrator => integrator.integratorId) || [];
|
|
this._integratorIdsSet = new Set(integratorIds);
|
|
}
|
|
|
|
public async getBatchMarketBuySwapQuoteAsync(
|
|
makerTokens: string[],
|
|
targetTakerToken: string,
|
|
makerTokenBuyAmounts: BigNumber[],
|
|
options: Partial<SwapQuoteRequestOpts> = {},
|
|
): Promise<MarketBuySwapQuote[]> {
|
|
makerTokenBuyAmounts.map((a, i) => assert.isBigNumber(`makerAssetBuyAmounts[${i}]`, a));
|
|
let gasPrice: BigNumber;
|
|
if (!!options.gasPrice) {
|
|
gasPrice = options.gasPrice;
|
|
assert.isBigNumber('gasPrice', gasPrice);
|
|
} else {
|
|
gasPrice = await this.getGasPriceEstimationOrThrowAsync();
|
|
}
|
|
|
|
const allOrders = await this.orderbook.getBatchOrdersAsync(
|
|
makerTokens,
|
|
targetTakerToken,
|
|
this._limitOrderPruningFn,
|
|
);
|
|
|
|
// Orders could be missing from the orderbook, so we create a dummy one as a placeholder
|
|
allOrders.forEach((orders: SignedNativeOrder[], i: number) => {
|
|
if (!orders || orders.length === 0) {
|
|
allOrders[i] = [createDummyOrder(makerTokens[i], targetTakerToken)];
|
|
}
|
|
});
|
|
|
|
const opts = { ...constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, ...options };
|
|
const optimizerResults = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync(
|
|
allOrders,
|
|
makerTokenBuyAmounts,
|
|
opts as GetMarketOrdersOpts,
|
|
);
|
|
|
|
const batchSwapQuotes = await Promise.all(
|
|
optimizerResults.map(async (result, i) => {
|
|
if (result) {
|
|
const { makerToken, takerToken } = allOrders[i][0].order;
|
|
return createSwapQuote(
|
|
result,
|
|
makerToken,
|
|
takerToken,
|
|
MarketOperation.Buy,
|
|
makerTokenBuyAmounts[i],
|
|
gasPrice,
|
|
opts.gasSchedule,
|
|
opts.bridgeSlippage,
|
|
);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}),
|
|
);
|
|
return batchSwapQuotes.filter(x => x !== undefined) as MarketBuySwapQuote[];
|
|
}
|
|
|
|
/**
|
|
* Returns the bids and asks liquidity for the entire market.
|
|
* For certain sources (like AMM's) it is recommended to provide a practical maximum takerAssetAmount.
|
|
* @param makerTokenAddress The address of the maker asset
|
|
* @param takerTokenAddress The address of the taker asset
|
|
* @param takerAssetAmount The amount to sell and buy for the bids and asks.
|
|
*
|
|
* @return An object that conforms to MarketDepth that contains all of the samples and liquidity
|
|
* information for the source.
|
|
*/
|
|
public async getBidAskLiquidityForMakerTakerAssetPairAsync(
|
|
makerToken: string,
|
|
takerToken: string,
|
|
takerAssetAmount: BigNumber,
|
|
options: Partial<SwapQuoteRequestOpts> = {},
|
|
): Promise<MarketDepth> {
|
|
assert.isString('makerToken', makerToken);
|
|
assert.isString('takerToken', takerToken);
|
|
const sourceFilters = new SourceFilters([], options.excludedSources, options.includedSources);
|
|
|
|
let [sellOrders, buyOrders] = !sourceFilters.isAllowed(ERC20BridgeSource.Native)
|
|
? [[], []]
|
|
: await Promise.all([
|
|
this.orderbook.getOrdersAsync(makerToken, takerToken),
|
|
this.orderbook.getOrdersAsync(takerToken, makerToken),
|
|
]);
|
|
if (!sellOrders || sellOrders.length === 0) {
|
|
sellOrders = [createDummyOrder(makerToken, takerToken)];
|
|
}
|
|
if (!buyOrders || buyOrders.length === 0) {
|
|
buyOrders = [createDummyOrder(takerToken, makerToken)];
|
|
}
|
|
|
|
const getMarketDepthSide = (marketSideLiquidity: MarketSideLiquidity): MarketDepthSide => {
|
|
const { dexQuotes, nativeOrders } = marketSideLiquidity.quotes;
|
|
const { side } = marketSideLiquidity;
|
|
|
|
return [
|
|
...dexQuotes,
|
|
nativeOrders.map(o => {
|
|
return {
|
|
input: side === MarketOperation.Sell ? o.fillableTakerAmount : o.fillableMakerAmount,
|
|
output: side === MarketOperation.Sell ? o.fillableMakerAmount : o.fillableTakerAmount,
|
|
fillData: o,
|
|
source: ERC20BridgeSource.Native,
|
|
};
|
|
}),
|
|
];
|
|
};
|
|
const [bids, asks] = await Promise.all([
|
|
this._marketOperationUtils.getMarketBuyLiquidityAsync(buyOrders, takerAssetAmount, options),
|
|
this._marketOperationUtils.getMarketSellLiquidityAsync(sellOrders, takerAssetAmount, options),
|
|
]);
|
|
return {
|
|
bids: getMarketDepthSide(bids),
|
|
asks: getMarketDepthSide(asks),
|
|
makerTokenDecimals: asks.makerTokenDecimals,
|
|
takerTokenDecimals: asks.takerTokenDecimals,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the recommended gas price for a fast transaction
|
|
*/
|
|
public async getGasPriceEstimationOrThrowAsync(): Promise<BigNumber> {
|
|
return this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync();
|
|
}
|
|
|
|
/**
|
|
* Destroys any subscriptions or connections.
|
|
*/
|
|
public async destroyAsync(): Promise<void> {
|
|
await this._protocolFeeUtils.destroyAsync();
|
|
await this.orderbook.destroyAsync();
|
|
}
|
|
|
|
/**
|
|
* Utility function to get Ether token address
|
|
*/
|
|
public getEtherToken(): string {
|
|
return this._contractAddresses.etherToken;
|
|
}
|
|
|
|
/**
|
|
* Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address.
|
|
* You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption.
|
|
* @param makerToken The address of the maker asset
|
|
* @param takerToken The address of the taker asset
|
|
* @param assetFillAmount If a buy, the amount of maker asset to buy. If a sell, the amount of taker asset to sell.
|
|
* @param marketOperation Either a Buy or a Sell quote
|
|
* @param options Options for the request. See type definition for more information.
|
|
*
|
|
* @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information.
|
|
*/
|
|
public async getSwapQuoteAsync(
|
|
makerToken: string,
|
|
takerToken: string,
|
|
assetFillAmount: BigNumber,
|
|
marketOperation: MarketOperation,
|
|
options: Partial<SwapQuoteRequestOpts>,
|
|
): Promise<SwapQuote> {
|
|
assert.isETHAddressHex('makerToken', makerToken);
|
|
assert.isETHAddressHex('takerToken', takerToken);
|
|
assert.isBigNumber('assetFillAmount', assetFillAmount);
|
|
const opts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
|
|
let gasPrice: BigNumber;
|
|
if (!!opts.gasPrice) {
|
|
gasPrice = opts.gasPrice;
|
|
assert.isBigNumber('gasPrice', gasPrice);
|
|
} else {
|
|
gasPrice = await this.getGasPriceEstimationOrThrowAsync();
|
|
}
|
|
|
|
const sourceFilters = new SourceFilters([], opts.excludedSources, opts.includedSources);
|
|
|
|
opts.rfqt = this._validateRfqtOpts(sourceFilters, opts.rfqt);
|
|
const rfqtOptions = this._rfqtOptions;
|
|
|
|
// Get SRA orders (limit orders)
|
|
const shouldSkipOpenOrderbook =
|
|
!sourceFilters.isAllowed(ERC20BridgeSource.Native) ||
|
|
(opts.rfqt && opts.rfqt.nativeExclusivelyRFQ === true);
|
|
const nativeOrders = shouldSkipOpenOrderbook
|
|
? await Promise.resolve([])
|
|
: await this.orderbook.getOrdersAsync(makerToken, takerToken, this._limitOrderPruningFn);
|
|
|
|
// if no native orders, pass in a dummy order for the sampler to have required metadata for sampling
|
|
if (nativeOrders.length === 0) {
|
|
nativeOrders.push(createDummyOrder(makerToken, takerToken));
|
|
}
|
|
|
|
// ** Prepare options for fetching market side liquidity **
|
|
// Scale fees by gas price.
|
|
const cloneOpts = _.omit(opts, 'gasPrice') as GetMarketOrdersOpts;
|
|
const calcOpts: GetMarketOrdersOpts = {
|
|
...cloneOpts,
|
|
gasPrice,
|
|
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData: FillData) =>
|
|
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
|
|
),
|
|
exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)),
|
|
};
|
|
// pass the QuoteRequestor on if rfqt enabled
|
|
if (calcOpts.rfqt !== undefined) {
|
|
calcOpts.rfqt.quoteRequestor = new QuoteRequestor(
|
|
rfqtOptions?.makerAssetOfferings || {},
|
|
{},
|
|
this._quoteRequestorHttpClient,
|
|
rfqtOptions?.altRfqCreds,
|
|
rfqtOptions?.warningLogger,
|
|
rfqtOptions?.infoLogger,
|
|
this.expiryBufferMs,
|
|
rfqtOptions?.metricsProxy,
|
|
);
|
|
}
|
|
|
|
const result: OptimizerResultWithReport = await this._marketOperationUtils.getOptimizerResultAsync(
|
|
nativeOrders,
|
|
assetFillAmount,
|
|
marketOperation,
|
|
calcOpts,
|
|
);
|
|
|
|
const swapQuote = createSwapQuote(
|
|
result,
|
|
makerToken,
|
|
takerToken,
|
|
marketOperation,
|
|
assetFillAmount,
|
|
gasPrice,
|
|
opts.gasSchedule,
|
|
opts.bridgeSlippage,
|
|
);
|
|
|
|
// Use the raw gas, not scaled by gas price
|
|
const exchangeProxyOverhead = opts.exchangeProxyOverhead(result.sourceFlags).toNumber();
|
|
swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead;
|
|
swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead;
|
|
|
|
return swapQuote;
|
|
}
|
|
|
|
private readonly _limitOrderPruningFn = (limitOrder: SignedNativeOrder) => {
|
|
const order = new LimitOrder(limitOrder.order);
|
|
const isOpenOrder = order.taker === constants.NULL_ADDRESS;
|
|
const willOrderExpire = order.willExpire(this.expiryBufferMs / constants.ONE_SECOND_MS); // tslint:disable-line:boolean-naming
|
|
const isFeeTypeAllowed =
|
|
this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
|
|
order.takerTokenFeeAmount.eq(constants.ZERO_AMOUNT);
|
|
return isOpenOrder && !willOrderExpire && isFeeTypeAllowed;
|
|
}; // tslint:disable-line:semicolon
|
|
|
|
private _isIntegratorIdWhitelisted(integratorId: string | undefined): boolean {
|
|
if (!integratorId) {
|
|
return false;
|
|
}
|
|
return this._integratorIdsSet.has(integratorId);
|
|
}
|
|
|
|
private _isTxOriginBlacklisted(txOrigin: string | undefined): boolean {
|
|
if (!txOrigin) {
|
|
return false;
|
|
}
|
|
const blacklistedTxOrigins = this._rfqtOptions ? this._rfqtOptions.txOriginBlacklist : new Set();
|
|
return blacklistedTxOrigins.has(txOrigin.toLowerCase());
|
|
}
|
|
|
|
private _validateRfqtOpts(
|
|
sourceFilters: SourceFilters,
|
|
rfqt: RfqRequestOpts | undefined,
|
|
): RfqRequestOpts | undefined {
|
|
if (!rfqt) {
|
|
return rfqt;
|
|
}
|
|
// tslint:disable-next-line: boolean-naming
|
|
const { integrator, nativeExclusivelyRFQ, intentOnFilling, txOrigin } = rfqt;
|
|
// If RFQ-T is enabled and `nativeExclusivelyRFQ` is set, then `ERC20BridgeSource.Native` should
|
|
// never be excluded.
|
|
if (nativeExclusivelyRFQ === true && !sourceFilters.isAllowed(ERC20BridgeSource.Native)) {
|
|
throw new Error('Native liquidity cannot be excluded if "rfqt.nativeExclusivelyRFQ" is set');
|
|
}
|
|
|
|
// If an integrator ID was provided, but the ID is not whitelisted, raise a warning and disable RFQ
|
|
if (!this._isIntegratorIdWhitelisted(integrator.integratorId)) {
|
|
if (this._rfqtOptions && this._rfqtOptions.warningLogger) {
|
|
this._rfqtOptions.warningLogger(
|
|
{
|
|
...integrator,
|
|
},
|
|
'Attempt at using an RFQ API key that is not whitelisted. Disabling RFQ for the request lifetime.',
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// If the requested tx origin is blacklisted, raise a warning and disable RFQ
|
|
if (this._isTxOriginBlacklisted(txOrigin)) {
|
|
if (this._rfqtOptions && this._rfqtOptions.warningLogger) {
|
|
this._rfqtOptions.warningLogger(
|
|
{
|
|
txOrigin,
|
|
},
|
|
'Attempt at using a tx Origin that is blacklisted. Disabling RFQ for the request lifetime.',
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// Otherwise check other RFQ options
|
|
if (
|
|
intentOnFilling && // The requestor is asking for a firm quote
|
|
this._isIntegratorIdWhitelisted(integrator.integratorId) && // A valid API key was provided
|
|
sourceFilters.isAllowed(ERC20BridgeSource.Native) // Native liquidity is not excluded
|
|
) {
|
|
if (!txOrigin || txOrigin === constants.NULL_ADDRESS) {
|
|
throw new Error('RFQ-T firm quote requests must specify a tx origin');
|
|
}
|
|
}
|
|
|
|
return rfqt;
|
|
}
|
|
}
|
|
// tslint:disable-next-line: max-file-line-count
|
|
|
|
// begin formatting and report generation functions
|
|
function createSwapQuote(
|
|
optimizerResult: OptimizerResultWithReport,
|
|
makerToken: string,
|
|
takerToken: string,
|
|
operation: MarketOperation,
|
|
assetFillAmount: BigNumber,
|
|
gasPrice: BigNumber,
|
|
gasSchedule: FeeSchedule,
|
|
slippage: number,
|
|
): SwapQuote {
|
|
const {
|
|
optimizedOrders,
|
|
quoteReport,
|
|
extendedQuoteReportSources,
|
|
sourceFlags,
|
|
takerAmountPerEth,
|
|
makerAmountPerEth,
|
|
priceComparisonsReport,
|
|
} = optimizerResult;
|
|
const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop];
|
|
|
|
// Calculate quote info
|
|
const { bestCaseQuoteInfo, worstCaseQuoteInfo, sourceBreakdown } = isTwoHop
|
|
? calculateTwoHopQuoteInfo(optimizedOrders, operation, gasSchedule, slippage)
|
|
: calculateQuoteInfo(optimizedOrders, operation, assetFillAmount, gasPrice, gasSchedule, slippage);
|
|
|
|
// Put together the swap quote
|
|
const { makerTokenDecimals, takerTokenDecimals } = optimizerResult.marketSideLiquidity;
|
|
const swapQuote = {
|
|
makerToken,
|
|
takerToken,
|
|
gasPrice,
|
|
orders: optimizedOrders,
|
|
bestCaseQuoteInfo,
|
|
worstCaseQuoteInfo,
|
|
sourceBreakdown,
|
|
makerTokenDecimals,
|
|
takerTokenDecimals,
|
|
takerAmountPerEth,
|
|
makerAmountPerEth,
|
|
quoteReport,
|
|
extendedQuoteReportSources,
|
|
isTwoHop,
|
|
priceComparisonsReport,
|
|
};
|
|
|
|
if (operation === MarketOperation.Buy) {
|
|
return {
|
|
...swapQuote,
|
|
type: MarketOperation.Buy,
|
|
makerTokenFillAmount: assetFillAmount,
|
|
};
|
|
} else {
|
|
return {
|
|
...swapQuote,
|
|
type: MarketOperation.Sell,
|
|
takerTokenFillAmount: assetFillAmount,
|
|
};
|
|
}
|
|
}
|
|
|
|
function calculateQuoteInfo(
|
|
optimizedOrders: OptimizedMarketOrder[],
|
|
operation: MarketOperation,
|
|
assetFillAmount: BigNumber,
|
|
gasPrice: BigNumber,
|
|
gasSchedule: FeeSchedule,
|
|
slippage: number,
|
|
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
|
|
const bestCaseFillResult = simulateBestCaseFill({
|
|
gasPrice,
|
|
orders: optimizedOrders,
|
|
side: operation,
|
|
fillAmount: assetFillAmount,
|
|
opts: { gasSchedule },
|
|
});
|
|
|
|
const worstCaseFillResult = simulateWorstCaseFill({
|
|
gasPrice,
|
|
orders: optimizedOrders,
|
|
side: operation,
|
|
fillAmount: assetFillAmount,
|
|
opts: { gasSchedule, slippage },
|
|
});
|
|
|
|
return {
|
|
bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult),
|
|
worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult),
|
|
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
|
|
};
|
|
}
|
|
|
|
function calculateTwoHopQuoteInfo(
|
|
optimizedOrders: OptimizedMarketOrder[],
|
|
operation: MarketOperation,
|
|
gasSchedule: FeeSchedule,
|
|
slippage: number,
|
|
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
|
|
const [firstHopOrder, secondHopOrder] = optimizedOrders;
|
|
const [firstHopFill] = firstHopOrder.fills;
|
|
const [secondHopFill] = secondHopOrder.fills;
|
|
const gas = new BigNumber(
|
|
gasSchedule[ERC20BridgeSource.MultiHop]!({
|
|
firstHopSource: _.pick(firstHopFill, 'source', 'fillData'),
|
|
secondHopSource: _.pick(secondHopFill, 'source', 'fillData'),
|
|
}),
|
|
).toNumber();
|
|
return {
|
|
bestCaseQuoteInfo: {
|
|
makerAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input,
|
|
takerAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
|
|
totalTakerAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
|
|
feeTakerTokenAmount: constants.ZERO_AMOUNT,
|
|
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
|
|
gas,
|
|
},
|
|
// TODO jacob consolidate this with quote simulation worstCase
|
|
worstCaseQuoteInfo: {
|
|
makerAmount: MarketOperation.Sell
|
|
? secondHopOrder.makerAmount.times(1 - slippage).integerValue()
|
|
: secondHopOrder.makerAmount,
|
|
takerAmount: MarketOperation.Sell
|
|
? firstHopOrder.takerAmount
|
|
: firstHopOrder.takerAmount.times(1 + slippage).integerValue(),
|
|
totalTakerAmount: MarketOperation.Sell
|
|
? firstHopOrder.takerAmount
|
|
: firstHopOrder.takerAmount.times(1 + slippage).integerValue(),
|
|
feeTakerTokenAmount: constants.ZERO_AMOUNT,
|
|
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
|
|
gas,
|
|
},
|
|
sourceBreakdown: {
|
|
[ERC20BridgeSource.MultiHop]: {
|
|
proportion: new BigNumber(1),
|
|
intermediateToken: secondHopOrder.takerToken,
|
|
hops: [firstHopFill.source, secondHopFill.source],
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown {
|
|
const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource));
|
|
const breakdown: SwapQuoteOrdersBreakdown = {};
|
|
Object.entries(fillAmountBySource).forEach(([s, fillAmount]) => {
|
|
const source = s as keyof SwapQuoteOrdersBreakdown;
|
|
if (source === ERC20BridgeSource.MultiHop) {
|
|
// TODO jacob has a different breakdown
|
|
} else {
|
|
breakdown[source] = fillAmount.div(totalFillAmount);
|
|
}
|
|
});
|
|
return breakdown;
|
|
}
|
|
|
|
function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo {
|
|
return {
|
|
makerAmount: fr.totalMakerAssetAmount,
|
|
takerAmount: fr.takerAssetAmount,
|
|
totalTakerAmount: fr.totalTakerAssetAmount,
|
|
feeTakerTokenAmount: fr.takerFeeTakerAssetAmount,
|
|
protocolFeeInWeiAmount: fr.protocolFeeAmount,
|
|
gas: fr.gas,
|
|
};
|
|
}
|
|
|
|
function createDummyOrder(makerToken: string, takerToken: string): SignedNativeOrder {
|
|
return {
|
|
type: FillQuoteTransformerOrderType.Limit,
|
|
order: {
|
|
...new LimitOrder({
|
|
makerToken,
|
|
takerToken,
|
|
makerAmount: ZERO_AMOUNT,
|
|
takerAmount: ZERO_AMOUNT,
|
|
takerTokenFeeAmount: ZERO_AMOUNT,
|
|
}),
|
|
},
|
|
signature: INVALID_SIGNATURE,
|
|
};
|
|
}
|