@0x/asset-swapper: Add gasSchedule option to SwapQuoter.

`@0x/asset-swapper`: Rename `fees` `SwapQuoter` option to `feeSchedule`.
This commit is contained in:
Lawrence Forman 2020-03-09 22:22:57 -04:00
parent cc12ad8d86
commit 05bf55dca8
9 changed files with 52 additions and 25 deletions

View File

@ -169,6 +169,7 @@ export interface MarketBuySwapQuote extends SwapQuoteBase {
* totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees). * totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees).
* makerAssetAmount: The amount of makerAsset that will be acquired through the swap. * makerAssetAmount: The amount of makerAsset that will be acquired through the swap.
* protocolFeeInWeiAmount: The amount of ETH to pay (in WEI) as protocol fee to perform the swap for desired asset. * protocolFeeInWeiAmount: The amount of ETH to pay (in WEI) as protocol fee to perform the swap for desired asset.
* gas: Amount of estimated gas needed to fill the quote.
*/ */
export interface SwapQuoteInfo { export interface SwapQuoteInfo {
feeTakerAssetAmount: BigNumber; feeTakerAssetAmount: BigNumber;
@ -176,6 +177,7 @@ export interface SwapQuoteInfo {
totalTakerAssetAmount: BigNumber; totalTakerAssetAmount: BigNumber;
makerAssetAmount: BigNumber; makerAssetAmount: BigNumber;
protocolFeeInWeiAmount: BigNumber; protocolFeeInWeiAmount: BigNumber;
gas: number;
} }
/** /**

View File

@ -82,6 +82,7 @@ export const assert = {
); );
}, },
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void { isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
sharedAssert.isNumber(`${variableName}.gas`, swapQuoteInfo.gas);
sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount); sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount); sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount); sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount);

View File

@ -30,7 +30,8 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
bridgeSlippage: 0.005, bridgeSlippage: 0.005,
numSamples: 20, numSamples: 20,
sampleDistributionBase: 1.05, sampleDistributionBase: 1.05,
fees: {}, feeSchedule: {},
gasSchedule: {},
allowFallback: true, allowFallback: true,
}; };

View File

@ -26,18 +26,18 @@ export function createFillPaths(opts: {
targetInput?: BigNumber; targetInput?: BigNumber;
ethToOutputRate?: BigNumber; ethToOutputRate?: BigNumber;
excludedSources?: ERC20BridgeSource[]; excludedSources?: ERC20BridgeSource[];
fees?: { [source: string]: BigNumber }; feeSchedule?: { [source: string]: BigNumber };
}): Fill[][] { }): Fill[][] {
const { side } = opts; const { side } = opts;
const excludedSources = opts.excludedSources || []; const excludedSources = opts.excludedSources || [];
const fees = opts.fees || {}; const feeSchedule = opts.feeSchedule || {};
const orders = opts.orders || []; const orders = opts.orders || [];
const dexQuotes = opts.dexQuotes || []; const dexQuotes = opts.dexQuotes || [];
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT;
// Create native fill paths. // Create native fill paths.
const nativePath = nativeOrdersToPath(side, orders, ethToOutputRate, fees); const nativePath = nativeOrdersToPath(side, orders, ethToOutputRate, feeSchedule);
// Create DEX fill paths. // Create DEX fill paths.
const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, fees); const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule);
return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources); return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources);
} }

View File

@ -98,7 +98,7 @@ export class MarketOperationUtils {
ethToOutputRate: ethToMakerAssetRate, ethToOutputRate: ethToMakerAssetRate,
bridgeSlippage: _opts.bridgeSlippage, bridgeSlippage: _opts.bridgeSlippage,
excludedSources: _opts.excludedSources, excludedSources: _opts.excludedSources,
fees: _opts.fees, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
}); });
} }
@ -170,7 +170,7 @@ export class MarketOperationUtils {
ethToOutputRate: ethToTakerAssetRate, ethToOutputRate: ethToTakerAssetRate,
bridgeSlippage: _opts.bridgeSlippage, bridgeSlippage: _opts.bridgeSlippage,
excludedSources: _opts.excludedSources, excludedSources: _opts.excludedSources,
fees: _opts.fees, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
}); });
} }
@ -242,7 +242,7 @@ export class MarketOperationUtils {
ethToOutputRate: ethToTakerAssetRate, ethToOutputRate: ethToTakerAssetRate,
bridgeSlippage: _opts.bridgeSlippage, bridgeSlippage: _opts.bridgeSlippage,
excludedSources: _opts.excludedSources, excludedSources: _opts.excludedSources,
fees: _opts.fees, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
}); });
}); });
@ -260,7 +260,7 @@ export class MarketOperationUtils {
ethToOutputRate?: BigNumber; ethToOutputRate?: BigNumber;
bridgeSlippage?: number; bridgeSlippage?: number;
excludedSources?: ERC20BridgeSource[]; excludedSources?: ERC20BridgeSource[];
fees?: { [source: string]: BigNumber }; feeSchedule?: { [source: string]: BigNumber };
allowFallback?: boolean; allowFallback?: boolean;
liquidityProviderAddress?: string; liquidityProviderAddress?: string;
}): OptimizedMarketOrder[] { }): OptimizedMarketOrder[] {
@ -274,7 +274,7 @@ export class MarketOperationUtils {
targetInput: inputAmount, targetInput: inputAmount,
ethToOutputRate: opts.ethToOutputRate, ethToOutputRate: opts.ethToOutputRate,
excludedSources: opts.excludedSources, excludedSources: opts.excludedSources,
fees: opts.fees, feeSchedule: opts.feeSchedule,
}); });
// Find the optimal path. // Find the optimal path.
const optimalPath = findOptimalPath(side, paths, inputAmount, opts.runLimit); const optimalPath = findOptimalPath(side, paths, inputAmount, opts.runLimit);

View File

@ -167,7 +167,11 @@ export interface GetMarketOrdersOpts {
/** /**
* Fees for each liquidity source, expressed in gas. * Fees for each liquidity source, expressed in gas.
*/ */
fees: { [source: string]: BigNumber }; feeSchedule: { [source: string]: BigNumber };
/**
* Estimated gas consumed by each liquidity source.
*/
gasSchedule: { [source: string]: number };
/** /**
* Whether to pad the quote with a redundant fallback quote using different * Whether to pad the quote with a redundant fallback quote using different
* sources. * sources.

View File

@ -106,6 +106,7 @@ export class SwapQuoteCalculator {
operation, operation,
assetFillAmounts[i], assetFillAmounts[i],
gasPrice, gasPrice,
opts.gasSchedule,
); );
} else { } else {
return undefined; return undefined;
@ -133,7 +134,7 @@ export class SwapQuoteCalculator {
// Scale fees by gas price. // Scale fees by gas price.
const _opts = { const _opts = {
...opts, ...opts,
fees: _.mapValues(opts.fees, (v, k) => v.times(gasPrice)), fees: _.mapValues(opts.feeSchedule, v => v.times(gasPrice)),
}; };
const firstOrderMakerAssetData = !!prunedOrders[0] const firstOrderMakerAssetData = !!prunedOrders[0]
@ -169,6 +170,7 @@ export class SwapQuoteCalculator {
operation, operation,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
opts.gasSchedule,
); );
} }
private async _createSwapQuoteAsync( private async _createSwapQuoteAsync(
@ -178,17 +180,20 @@ export class SwapQuoteCalculator {
operation: MarketOperation, operation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: { [source: string]: number },
): Promise<SwapQuote> { ): Promise<SwapQuote> {
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync( const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
resultOrders, resultOrders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
gasSchedule,
operation, operation,
); );
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync( const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
resultOrders, resultOrders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
gasSchedule,
operation, operation,
true, true,
); );
@ -226,14 +231,16 @@ export class SwapQuoteCalculator {
orders: OptimizedMarketOrder[], orders: OptimizedMarketOrder[],
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: { [source: string]: number },
operation: MarketOperation, operation: MarketOperation,
worstCase: boolean = false, worstCase: boolean = false,
): Promise<SwapQuoteInfo> { ): Promise<SwapQuoteInfo> {
if (operation === MarketOperation.Buy) { return {
return this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase); ...(operation === MarketOperation.Buy
} else { ? await this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)
return this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase); : await this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)),
} gas: getGasUsedByOrders(orders, gasSchedule),
};
} }
private async _calculateMarketSellQuoteInfoAsync( private async _calculateMarketSellQuoteInfoAsync(
@ -327,6 +334,7 @@ export class SwapQuoteCalculator {
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: totalMakerAssetAmount, makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount, protocolFeeInWeiAmount,
gas: 0,
}; };
} }
@ -416,6 +424,7 @@ export class SwapQuoteCalculator {
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: totalMakerAssetAmount, makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount, protocolFeeInWeiAmount,
gas: 0,
}; };
} }
@ -485,3 +494,12 @@ function getTakerAssetAmountBreakDown(
takerAssetAmount: takerAssetAmountWithFees, takerAssetAmount: takerAssetAmountWithFees,
}; };
} }
function getGasUsedByOrders(orders: OptimizedMarketOrder[], gasSchedule: { [source: string]: number }): number {
let totalUsage = 0;
for (const order of orders) {
totalUsage += gasSchedule[order.fill.source] || 0;
}
return totalUsage;
}
// tslint:disable: max-file-line-count

View File

@ -486,7 +486,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate) .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .dividedToIntegerBy(ETH_TO_MAKER_RATE),
@ -498,7 +498,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
@ -521,7 +521,7 @@ describe('MarketOperationUtils tests', () => {
// Effectively [0.8, ~0.5, ~0, ~0] // Effectively [0.8, ~0.5, ~0, ~0]
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate) .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .dividedToIntegerBy(ETH_TO_MAKER_RATE),
@ -533,7 +533,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
@ -828,7 +828,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate) .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .dividedToIntegerBy(ETH_TO_TAKER_RATE),
@ -840,7 +840,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
@ -862,7 +862,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate) .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .dividedToIntegerBy(ETH_TO_TAKER_RATE),
@ -874,7 +874,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [

View File

@ -25,6 +25,7 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
takerAssetAmount: totalTakerAssetAmount, takerAssetAmount: totalTakerAssetAmount,
totalTakerAssetAmount, totalTakerAssetAmount,
protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice), protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice),
gas: 200e3,
}; };
const breakdown = { const breakdown = {