From 5afe2616a416233d08cfada0ce3662471ca3ec40 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Mon, 27 Jul 2020 15:07:52 +1000 Subject: [PATCH] feat: asset-swapper market depth (#2641) * feat: asset-swapper market depth * split promises into 2 * fix lint and docs * chore: refactor * rebase off development * CHANGELOG --- packages/asset-swapper/CHANGELOG.json | 4 + packages/asset-swapper/src/index.ts | 14 +- packages/asset-swapper/src/swap_quoter.ts | 95 +++++++- .../src/utils/market_operation_utils/index.ts | 225 +++++++++++------- .../utils/market_operation_utils/orders.ts | 19 +- .../src/utils/market_operation_utils/types.ts | 38 ++- 6 files changed, 282 insertions(+), 113 deletions(-) diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 408bc11bb0..dd0ad365b3 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -17,6 +17,10 @@ { "note": "Support more varied curves", "pr": 2633 + }, + { + "note": "Adds `getBidAskLiquidityForMakerTakerAssetPairAsync` to return more detailed sample information", + "pr": 2641 } ] }, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index f7ab148f4e..a7ffc0cf2c 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -64,30 +64,31 @@ export { SwapQuoteInfo, SwapQuoteOrdersBreakdown, SwapQuoteRequestOpts, - SwapQuoterRfqtOpts, SwapQuoterError, SwapQuoterOpts, + SwapQuoterRfqtOpts, } from './types'; -import { ERC20BridgeSource } from './utils/market_operation_utils/types'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { BalancerFillData, CollapsedFill, CurveFillData, + CurveFunctionSelectors, CurveInfo, ERC20BridgeSource, FeeSchedule, FillData, GetMarketOrdersRfqtOpts, + LiquidityProviderFillData, + MarketDepth, + MarketDepthSide, + MultiBridgeFillData, NativeCollapsedFill, NativeFillData, OptimizedMarketOrder, UniswapV2FillData, - CurveFunctionSelectors, } from './utils/market_operation_utils/types'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; -export { QuoteRequestor } from './utils/quote_requestor'; -export { rfqtMocker } from './utils/rfqt_mocker'; export { BridgeReportSource, NativeOrderbookReportSource, @@ -95,4 +96,7 @@ export { QuoteReport, QuoteReportSource, } from './utils/quote_report_generator'; +export { QuoteRequestor } from './utils/quote_requestor'; +export { rfqtMocker } from './utils/rfqt_mocker'; +import { ERC20BridgeSource } from './utils/market_operation_utils/types'; export type Native = ERC20BridgeSource.Native; diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 92225e092b..8bb7281fd7 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -27,7 +27,12 @@ import { calculateLiquidity } from './utils/calculate_liquidity'; import { MarketOperationUtils } from './utils/market_operation_utils'; import { createDummyOrderForSampler } from './utils/market_operation_utils/orders'; import { DexOrderSampler } from './utils/market_operation_utils/sampler'; -import { ERC20BridgeSource } from './utils/market_operation_utils/types'; +import { + ERC20BridgeSource, + MarketDepth, + MarketDepthSide, + MarketSideLiquidity, +} from './utils/market_operation_utils/types'; import { orderPrunerUtils } from './utils/order_prune_utils'; import { OrderStateUtils } from './utils/order_state_utils'; import { ProtocolFeeUtils } from './utils/protocol_fee_utils'; @@ -393,6 +398,94 @@ export class SwapQuoter { return calculateLiquidity(ordersWithFillableAmounts); } + /** + * 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( + makerTokenAddress: string, + takerTokenAddress: string, + takerAssetAmount: BigNumber, + options: Partial = {}, + ): Promise { + assert.isString('makerTokenAddress', makerTokenAddress); + assert.isString('takerTokenAddress', takerTokenAddress); + const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); + const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); + let [sellOrders, buyOrders] = + options.excludedSources && options.excludedSources.includes(ERC20BridgeSource.Native) + ? Promise.resolve([[], []]) + : await Promise.all([ + this.orderbook.getOrdersAsync(makerAssetData, takerAssetData), + this.orderbook.getOrdersAsync(takerAssetData, makerAssetData), + ]); + if (!sellOrders || sellOrders.length === 0) { + sellOrders = [ + { + metaData: {}, + order: createDummyOrderForSampler( + makerAssetData, + takerAssetData, + this._contractAddresses.uniswapBridge, + ), + }, + ]; + } + if (!buyOrders || buyOrders.length === 0) { + buyOrders = [ + { + metaData: {}, + order: createDummyOrderForSampler( + takerAssetData, + makerAssetData, + this._contractAddresses.uniswapBridge, + ), + }, + ]; + } + const getMarketDepthSide = (marketSideLiquidity: MarketSideLiquidity): MarketDepthSide => { + const { dexQuotes, nativeOrders, orderFillableAmounts, side } = marketSideLiquidity; + return [ + ...dexQuotes, + nativeOrders.map((o, i) => { + const scaleFactor = orderFillableAmounts[i].div(o.takerAssetAmount); + return { + input: (side === MarketOperation.Sell ? o.takerAssetAmount : o.makerAssetAmount) + .times(scaleFactor) + .integerValue(), + output: (side === MarketOperation.Sell ? o.makerAssetAmount : o.takerAssetAmount) + .times(scaleFactor) + .integerValue(), + fillData: o, + source: ERC20BridgeSource.Native, + }; + }), + ]; + }; + const [bids, asks] = await Promise.all([ + this._marketOperationUtils.getMarketBuyLiquidityAsync( + (buyOrders || []).map(o => o.order), + takerAssetAmount, + options, + ), + this._marketOperationUtils.getMarketSellLiquidityAsync( + (sellOrders || []).map(o => o.order), + takerAssetAmount, + options, + ), + ]); + return { + bids: getMarketDepthSide(bids), + asks: getMarketDepthSide(asks), + }; + } + /** * Get the asset data of all assets that can be used to purchase makerAssetData in the order provider passed in at init. * diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 13ce139e20..caab906043 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -25,6 +25,7 @@ import { ERC20BridgeSource, FeeSchedule, GetMarketOrdersOpts, + MarketSideLiquidity, OptimizedMarketOrder, OptimizedOrdersAndQuoteReport, OrderDomain, @@ -74,18 +75,17 @@ export class MarketOperationUtils { } /** - * gets the orders required for a market sell operation by (potentially) merging native orders with - * generated bridge orders. + * Gets the liquidity available for a market sell operation * @param nativeOrders Native orders. * @param takerAmount Amount of taker asset to sell. * @param opts Options object. - * @return orders. + * @return MarketSideLiquidity. */ - public async getMarketSellOrdersAsync( + public async getMarketSellLiquidityAsync( nativeOrders: SignedOrder[], takerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { if (nativeOrders.length === 0) { throw new Error(AggregationError.EmptyOrders); } @@ -156,41 +156,40 @@ export class MarketOperationUtils { rfqtIndicativeQuotes, [balancerQuotes], ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); - return this._generateOptimizedOrdersAsync({ - orderFillableAmounts, - nativeOrders, - dexQuotes: dexQuotes.concat(balancerQuotes), - rfqtIndicativeQuotes, - liquidityProviderAddress, - multiBridgeAddress: this._multiBridge, - inputToken: takerToken, - outputToken: makerToken, + + // Attach the LiquidityProvider address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach( + q => (q.fillData = { poolAddress: liquidityProviderAddress }), + ); + // Attach the MultiBridge address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach( + q => (q.fillData = { poolAddress: this._multiBridge }), + ); + return { side: MarketOperation.Sell, inputAmount: takerAmount, + inputToken: takerToken, + outputToken: makerToken, + dexQuotes: dexQuotes.concat(balancerQuotes), + nativeOrders, + orderFillableAmounts, ethToOutputRate: ethToMakerAssetRate, - bridgeSlippage: _opts.bridgeSlippage, - maxFallbackSlippage: _opts.maxFallbackSlippage, - excludedSources: _opts.excludedSources, - feeSchedule: _opts.feeSchedule, - allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, - quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, - }); + rfqtIndicativeQuotes, + }; } /** - * gets the orders required for a market buy operation by (potentially) merging native orders with - * generated bridge orders. + * Gets the liquidity available for a market buy operation * @param nativeOrders Native orders. * @param makerAmount Amount of maker asset to buy. * @param opts Options object. - * @return object with optimized orders and a QuoteReport + * @return MarketSideLiquidity. */ - public async getMarketBuyOrdersAsync( + public async getMarketBuyLiquidityAsync( nativeOrders: SignedOrder[], makerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { if (nativeOrders.length === 0) { throw new Error(AggregationError.EmptyOrders); } @@ -260,19 +259,68 @@ export class MarketOperationUtils { rfqtIndicativeQuotes, [balancerQuotes], ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); - - return this._generateOptimizedOrdersAsync({ - orderFillableAmounts, - nativeOrders, - dexQuotes: dexQuotes.concat(balancerQuotes), - rfqtIndicativeQuotes, - liquidityProviderAddress, - multiBridgeAddress: this._multiBridge, - inputToken: makerToken, - outputToken: takerToken, + // Attach the LiquidityProvider address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach( + q => (q.fillData = { poolAddress: liquidityProviderAddress }), + ); + // Attach the MultiBridge address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach( + q => (q.fillData = { poolAddress: this._multiBridge }), + ); + return { side: MarketOperation.Buy, inputAmount: makerAmount, + inputToken: makerToken, + outputToken: takerToken, + dexQuotes: dexQuotes.concat(balancerQuotes), + nativeOrders, + orderFillableAmounts, ethToOutputRate: ethToTakerAssetRate, + rfqtIndicativeQuotes, + }; + } + + /** + * gets the orders required for a market sell operation by (potentially) merging native orders with + * generated bridge orders. + * @param nativeOrders Native orders. + * @param takerAmount Amount of taker asset to sell. + * @param opts Options object. + * @return object with optimized orders and a QuoteReport + */ + public async getMarketSellOrdersAsync( + nativeOrders: SignedOrder[], + takerAmount: BigNumber, + opts?: Partial, + ): Promise { + const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); + return this._generateOptimizedOrdersAsync(marketSideLiquidity, { + bridgeSlippage: _opts.bridgeSlippage, + maxFallbackSlippage: _opts.maxFallbackSlippage, + excludedSources: _opts.excludedSources, + feeSchedule: _opts.feeSchedule, + allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, + }); + } + + /** + * gets the orders required for a market buy operation by (potentially) merging native orders with + * generated bridge orders. + * @param nativeOrders Native orders. + * @param makerAmount Amount of maker asset to buy. + * @param opts Options object. + * @return object with optimized orders and a QuoteReport + */ + public async getMarketBuyOrdersAsync( + nativeOrders: SignedOrder[], + makerAmount: BigNumber, + opts?: Partial, + ): Promise { + const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); + return this._generateOptimizedOrdersAsync(marketSideLiquidity, { bridgeSlippage: _opts.bridgeSlippage, maxFallbackSlippage: _opts.maxFallbackSlippage, excludedSources: _opts.excludedSources, @@ -351,23 +399,28 @@ export class MarketOperationUtils { const dexQuotes = batchDexQuotes[i]; const makerAmount = makerAmounts[i]; try { - return (await this._generateOptimizedOrdersAsync({ - orderFillableAmounts, - nativeOrders, - dexQuotes, - rfqtIndicativeQuotes: [], - inputToken: makerToken, - outputToken: takerToken, - side: MarketOperation.Buy, - inputAmount: makerAmount, - ethToOutputRate: ethToTakerAssetRate, - bridgeSlippage: _opts.bridgeSlippage, - maxFallbackSlippage: _opts.maxFallbackSlippage, - excludedSources: _opts.excludedSources, - feeSchedule: _opts.feeSchedule, - allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, - })).optimizedOrders; + const { optimizedOrders } = await this._generateOptimizedOrdersAsync( + { + side: MarketOperation.Buy, + nativeOrders, + orderFillableAmounts, + dexQuotes, + inputAmount: makerAmount, + ethToOutputRate: ethToTakerAssetRate, + rfqtIndicativeQuotes: [], + inputToken: makerToken, + outputToken: takerToken, + }, + { + bridgeSlippage: _opts.bridgeSlippage, + maxFallbackSlippage: _opts.maxFallbackSlippage, + excludedSources: _opts.excludedSources, + feeSchedule: _opts.feeSchedule, + allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, + }, + ); + return optimizedOrders; } catch (e) { // It's possible for one of the pairs to have no path // rather than throw NO_OPTIMAL_PATH we return undefined @@ -377,40 +430,42 @@ export class MarketOperationUtils { ); } - private async _generateOptimizedOrdersAsync(opts: { - side: MarketOperation; - inputToken: string; - outputToken: string; - inputAmount: BigNumber; - nativeOrders: SignedOrder[]; - orderFillableAmounts: BigNumber[]; - dexQuotes: DexSample[][]; - rfqtIndicativeQuotes: RFQTIndicativeQuote[]; - runLimit?: number; - ethToOutputRate?: BigNumber; - bridgeSlippage?: number; - maxFallbackSlippage?: number; - excludedSources?: ERC20BridgeSource[]; - feeSchedule?: FeeSchedule; - allowFallback?: boolean; - shouldBatchBridgeOrders?: boolean; - liquidityProviderAddress?: string; - multiBridgeAddress?: string; - quoteRequestor?: QuoteRequestor; - }): Promise { - const { inputToken, outputToken, side, inputAmount } = opts; + private async _generateOptimizedOrdersAsync( + marketSideLiquidity: MarketSideLiquidity, + opts: { + runLimit?: number; + bridgeSlippage?: number; + maxFallbackSlippage?: number; + excludedSources?: ERC20BridgeSource[]; + feeSchedule?: FeeSchedule; + allowFallback?: boolean; + shouldBatchBridgeOrders?: boolean; + quoteRequestor?: QuoteRequestor; + }, + ): Promise { + const { + inputToken, + outputToken, + side, + inputAmount, + nativeOrders, + orderFillableAmounts, + rfqtIndicativeQuotes, + dexQuotes, + ethToOutputRate, + } = marketSideLiquidity; const maxFallbackSlippage = opts.maxFallbackSlippage || 0; // Convert native orders and dex quotes into fill paths. const paths = createFillPaths({ side, // Augment native orders with their fillable amounts. orders: [ - ...createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts), - ...createSignedOrdersFromRfqtIndicativeQuotes(opts.rfqtIndicativeQuotes), + ...createSignedOrdersWithFillableAmounts(side, nativeOrders, orderFillableAmounts), + ...createSignedOrdersFromRfqtIndicativeQuotes(rfqtIndicativeQuotes), ], - dexQuotes: opts.dexQuotes, + dexQuotes, targetInput: inputAmount, - ethToOutputRate: opts.ethToOutputRate, + ethToOutputRate, excludedSources: opts.excludedSources, feeSchedule: opts.feeSchedule, }); @@ -458,15 +513,13 @@ export class MarketOperationUtils { orderDomain: this._orderDomain, contractAddresses: this.contractAddresses, bridgeSlippage: opts.bridgeSlippage || 0, - liquidityProviderAddress: opts.liquidityProviderAddress, - multiBridgeAddress: opts.multiBridgeAddress, shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, }); const quoteReport = new QuoteReportGenerator( - opts.side, - _.flatten(opts.dexQuotes), - opts.nativeOrders, - opts.orderFillableAmounts, + side, + _.flatten(dexQuotes), + nativeOrders, + orderFillableAmounts, _.flatten(optimizedOrders.map(o => o.fills)), opts.quoteRequestor, ).generateReport(); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index abeba32e73..f07a61a0a7 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -24,6 +24,8 @@ import { CurveFillData, ERC20BridgeSource, Fill, + LiquidityProviderFillData, + MultiBridgeFillData, NativeCollapsedFill, OptimizedMarketOrder, OrderDomain, @@ -143,8 +145,6 @@ export interface CreateOrderFromPathOpts { contractAddresses: ContractAddresses; bridgeSlippage: number; shouldBatchBridgeOrders: boolean; - liquidityProviderAddress?: string; - multiBridgeAddress?: string; } // Convert sell fills into orders. @@ -177,7 +177,8 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts return orders; } -function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrderFromPathOpts): string { +function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string { + const source = fill.source; switch (source) { case ERC20BridgeSource.Eth2Dai: return opts.contractAddresses.eth2DaiBridge; @@ -192,15 +193,9 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder case ERC20BridgeSource.Balancer: return opts.contractAddresses.balancerBridge; case ERC20BridgeSource.LiquidityProvider: - if (opts.liquidityProviderAddress === undefined) { - throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.'); - } - return opts.liquidityProviderAddress; + return (fill.fillData as LiquidityProviderFillData).poolAddress; case ERC20BridgeSource.MultiBridge: - if (opts.multiBridgeAddress === undefined) { - throw new Error('Cannot create a MultiBridge order without a MultiBridge address.'); - } - return opts.multiBridgeAddress; + return (fill.fillData as MultiBridgeFillData).poolAddress; default: break; } @@ -209,7 +204,7 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder { const [makerToken, takerToken] = getMakerTakerTokens(opts); - const bridgeAddress = getBridgeAddressFromSource(fill.source, opts); + const bridgeAddress = getBridgeAddressFromFill(fill, opts); let makerAssetData; switch (fill.source) { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 570adc522d..e432f5497e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -1,4 +1,6 @@ import { ERC20BridgeSamplerContract } from '@0x/contract-wrappers'; +import { RFQTIndicativeQuote } from '@0x/quote-server'; +import { MarketOperation, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; @@ -86,6 +88,14 @@ export interface UniswapV2FillData extends FillData { tokenAddressPath: string[]; } +export interface LiquidityProviderFillData extends FillData { + poolAddress: string; +} + +export interface MultiBridgeFillData extends FillData { + poolAddress: string; +} + /** * Represents an individual DEX sample from the sampler contract. */ @@ -256,16 +266,26 @@ export interface SourceQuoteOperation ext fillData?: TFillData; } -/** - * Used in the ERC20BridgeSampler when a source does not natively - * support sampling via a specific buy amount. - */ -export interface FakeBuyOpts { - targetSlippageBps: BigNumber; - maxIterations: BigNumber; -} - export interface OptimizedOrdersAndQuoteReport { optimizedOrders: OptimizedMarketOrder[]; quoteReport: QuoteReport; } + +export type MarketDepthSide = Array>>; + +export interface MarketDepth { + bids: MarketDepthSide; + asks: MarketDepthSide; +} + +export interface MarketSideLiquidity { + side: MarketOperation; + inputAmount: BigNumber; + inputToken: string; + outputToken: string; + dexQuotes: Array>>; + nativeOrders: SignedOrder[]; + orderFillableAmounts: BigNumber[]; + ethToOutputRate: BigNumber; + rfqtIndicativeQuotes: RFQTIndicativeQuote[]; +}