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
This commit is contained in:
Jacob Evans 2020-07-27 15:07:52 +10:00 committed by GitHub
parent 72c869649a
commit 5afe2616a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 282 additions and 113 deletions

View File

@ -17,6 +17,10 @@
{
"note": "Support more varied curves",
"pr": 2633
},
{
"note": "Adds `getBidAskLiquidityForMakerTakerAssetPairAsync` to return more detailed sample information",
"pr": 2641
}
]
},

View File

@ -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;

View File

@ -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<SwapQuoteRequestOpts> = {},
): Promise<MarketDepth> {
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.
*

View File

@ -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<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> {
): Promise<MarketSideLiquidity> {
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<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> {
): Promise<MarketSideLiquidity> {
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<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> {
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<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> {
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<OptimizedOrdersAndQuoteReport> {
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<OptimizedOrdersAndQuoteReport> {
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();

View File

@ -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) {

View File

@ -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<TFillData extends FillData = FillData> 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<Array<DexSample<FillData>>>;
export interface MarketDepth {
bids: MarketDepthSide;
asks: MarketDepthSide;
}
export interface MarketSideLiquidity {
side: MarketOperation;
inputAmount: BigNumber;
inputToken: string;
outputToken: string;
dexQuotes: Array<Array<DexSample<FillData>>>;
nativeOrders: SignedOrder[];
orderFillableAmounts: BigNumber[];
ethToOutputRate: BigNumber;
rfqtIndicativeQuotes: RFQTIndicativeQuote[];
}