@0x/asset-swapper: Add DFB support + refactor swap quote calculation utils
This commit is contained in:
@@ -37,6 +37,10 @@
|
||||
{
|
||||
"note": "Fix `getBatchMarketBuyOrdersAsync` throwing NO_OPTIMAL_PATH",
|
||||
"pr": 2533
|
||||
},
|
||||
{
|
||||
"note": "Add DFB support + refactor swap quote calculator utils",
|
||||
"pr": 2536
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -179,7 +179,7 @@ export class SwapQuoter {
|
||||
},
|
||||
liquidityProviderRegistryAddress,
|
||||
);
|
||||
this._swapQuoteCalculator = new SwapQuoteCalculator(this._protocolFeeUtils, this._marketOperationUtils);
|
||||
this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
|
||||
feeSchedule: {},
|
||||
gasSchedule: {},
|
||||
allowFallback: true,
|
||||
shouldBatchBridgeOrders: true,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -233,27 +233,25 @@ export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_
|
||||
return clipped;
|
||||
}
|
||||
|
||||
export function collapsePath(side: MarketOperation, path: Fill[]): CollapsedFill[] {
|
||||
export function collapsePath(path: Fill[]): CollapsedFill[] {
|
||||
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
|
||||
for (const fill of path) {
|
||||
const makerAssetAmount = side === MarketOperation.Sell ? fill.output : fill.input;
|
||||
const takerAssetAmount = side === MarketOperation.Sell ? fill.input : fill.output;
|
||||
const source = fill.source;
|
||||
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
|
||||
const prevFill = collapsed[collapsed.length - 1];
|
||||
// If the last fill is from the same source, merge them.
|
||||
if (prevFill.source === source) {
|
||||
prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
|
||||
prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
|
||||
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
|
||||
prevFill.input = prevFill.input.plus(fill.input);
|
||||
prevFill.output = prevFill.output.plus(fill.output);
|
||||
prevFill.subFills.push(fill);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
collapsed.push({
|
||||
source: fill.source,
|
||||
totalMakerAssetAmount: makerAssetAmount,
|
||||
totalTakerAssetAmount: takerAssetAmount,
|
||||
subFills: [{ makerAssetAmount, takerAssetAmount }],
|
||||
input: fill.input,
|
||||
output: fill.output,
|
||||
subFills: [fill],
|
||||
nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ export class MarketOperationUtils {
|
||||
excludedSources: _opts.excludedSources,
|
||||
feeSchedule: _opts.feeSchedule,
|
||||
allowFallback: _opts.allowFallback,
|
||||
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,6 +181,7 @@ export class MarketOperationUtils {
|
||||
excludedSources: _opts.excludedSources,
|
||||
feeSchedule: _opts.feeSchedule,
|
||||
allowFallback: _opts.allowFallback,
|
||||
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,6 +256,7 @@ export class MarketOperationUtils {
|
||||
excludedSources: _opts.excludedSources,
|
||||
feeSchedule: _opts.feeSchedule,
|
||||
allowFallback: _opts.allowFallback,
|
||||
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
|
||||
});
|
||||
} catch (e) {
|
||||
// It's possible for one of the pairs to have no path
|
||||
@@ -278,6 +281,7 @@ export class MarketOperationUtils {
|
||||
excludedSources?: ERC20BridgeSource[];
|
||||
feeSchedule?: { [source: string]: BigNumber };
|
||||
allowFallback?: boolean;
|
||||
shouldBatchBridgeOrders?: boolean;
|
||||
liquidityProviderAddress?: string;
|
||||
}): OptimizedMarketOrder[] {
|
||||
const { inputToken, outputToken, side, inputAmount } = opts;
|
||||
@@ -327,6 +331,7 @@ export class MarketOperationUtils {
|
||||
contractAddresses: this.contractAddresses,
|
||||
bridgeSlippage: opts.bridgeSlippage || 0,
|
||||
liquidityProviderAddress: opts.liquidityProviderAddress,
|
||||
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ContractAddresses } from '@0x/contract-addresses';
|
||||
import { DexForwaderBridgeData, dexForwarderBridgeDataEncoder } from '@0x/contracts-asset-proxy';
|
||||
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { AbiEncoder, BigNumber } from '@0x/utils';
|
||||
import { ERC20BridgeAssetData, SignedOrder } from '@0x/types';
|
||||
import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils';
|
||||
|
||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||
|
||||
@@ -71,12 +72,7 @@ export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOr
|
||||
fillableMakerAssetAmount: order.makerAssetAmount,
|
||||
fillableTakerAssetAmount: order.takerAssetAmount,
|
||||
fillableTakerFeeAmount: order.takerFee,
|
||||
fill: {
|
||||
source: ERC20BridgeSource.Native,
|
||||
totalMakerAssetAmount: order.makerAssetAmount,
|
||||
totalTakerAssetAmount: order.takerAssetAmount,
|
||||
subFills: [],
|
||||
},
|
||||
fills: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,18 +115,34 @@ export interface CreateOrderFromPathOpts {
|
||||
orderDomain: OrderDomain;
|
||||
contractAddresses: ContractAddresses;
|
||||
bridgeSlippage: number;
|
||||
shouldBatchBridgeOrders: boolean;
|
||||
liquidityProviderAddress?: string;
|
||||
}
|
||||
|
||||
// Convert sell fills into orders.
|
||||
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
|
||||
const collapsedPath = collapsePath(opts.side, path);
|
||||
const collapsedPath = collapsePath(path);
|
||||
const orders: OptimizedMarketOrder[] = [];
|
||||
for (const fill of collapsedPath) {
|
||||
if (fill.source === ERC20BridgeSource.Native) {
|
||||
orders.push(createNativeOrder(fill));
|
||||
for (let i = 0; i < collapsedPath.length;) {
|
||||
if (collapsedPath[i].source === ERC20BridgeSource.Native) {
|
||||
orders.push(createNativeOrder(collapsedPath[i]));
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
// If there are contiguous bridge orders, we can batch them together.
|
||||
const contiguousBridgeFills = [collapsedPath[i]];
|
||||
for (let j = i + 1; j < collapsedPath.length; ++j) {
|
||||
if (collapsedPath[j].source === ERC20BridgeSource.Native) {
|
||||
break;
|
||||
}
|
||||
contiguousBridgeFills.push(collapsedPath[j]);
|
||||
}
|
||||
if (contiguousBridgeFills.length === 1 || !opts.shouldBatchBridgeOrders) {
|
||||
orders.push(createBridgeOrder(contiguousBridgeFills[0], opts));
|
||||
i += 1;
|
||||
} else {
|
||||
orders.push(createBridgeOrder(fill, opts));
|
||||
orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
|
||||
i += contiguousBridgeFills.length;
|
||||
}
|
||||
}
|
||||
return orders;
|
||||
@@ -161,8 +173,7 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder
|
||||
}
|
||||
|
||||
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
||||
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
||||
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
||||
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
||||
const bridgeAddress = getBridgeAddressFromSource(fill.source, opts);
|
||||
|
||||
let makerAssetData;
|
||||
@@ -182,14 +193,66 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
|
||||
createBridgeData(takerToken),
|
||||
);
|
||||
}
|
||||
const [slippedMakerAssetAmount, slippedTakerAssetAmount] = getSlippedBridgeAssetAmounts(fill, opts);
|
||||
return {
|
||||
makerAddress: bridgeAddress,
|
||||
fills: [fill],
|
||||
makerAssetData,
|
||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
|
||||
...createCommonBridgeOrderFields(fill, opts),
|
||||
makerAddress: bridgeAddress,
|
||||
makerAssetAmount: slippedMakerAssetAmount,
|
||||
takerAssetAmount: slippedTakerAssetAmount,
|
||||
fillableMakerAssetAmount: slippedMakerAssetAmount,
|
||||
fillableTakerAssetAmount: slippedTakerAssetAmount,
|
||||
...createCommonBridgeOrderFields(opts),
|
||||
};
|
||||
}
|
||||
|
||||
function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
||||
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
||||
let totalMakerAssetAmount = ZERO_AMOUNT;
|
||||
let totalTakerAssetAmount = ZERO_AMOUNT;
|
||||
const batchedBridgeData: DexForwaderBridgeData = {
|
||||
inputToken: takerToken,
|
||||
calls: [],
|
||||
};
|
||||
for (const fill of fills) {
|
||||
const bridgeOrder = createBridgeOrder(fill, opts);
|
||||
totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount);
|
||||
totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount);
|
||||
const { bridgeAddress, bridgeData: orderBridgeData } =
|
||||
assetDataUtils.decodeAssetDataOrThrow(bridgeOrder.makerAssetData) as ERC20BridgeAssetData;
|
||||
batchedBridgeData.calls.push({
|
||||
target: bridgeAddress,
|
||||
bridgeData: orderBridgeData,
|
||||
inputTokenAmount: bridgeOrder.takerAssetAmount,
|
||||
outputTokenAmount: bridgeOrder.makerAssetAmount,
|
||||
});
|
||||
}
|
||||
const batchedBridgeAddress = opts.contractAddresses.dexForwarderBridge;
|
||||
const batchedMakerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
|
||||
makerToken,
|
||||
batchedBridgeAddress,
|
||||
dexForwarderBridgeDataEncoder.encode(batchedBridgeData),
|
||||
);
|
||||
return {
|
||||
fills,
|
||||
makerAssetData: batchedMakerAssetData,
|
||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
|
||||
makerAddress: batchedBridgeAddress,
|
||||
makerAssetAmount: totalMakerAssetAmount,
|
||||
takerAssetAmount: totalTakerAssetAmount,
|
||||
fillableMakerAssetAmount: totalMakerAssetAmount,
|
||||
fillableTakerAssetAmount: totalTakerAssetAmount,
|
||||
...createCommonBridgeOrderFields(opts),
|
||||
};
|
||||
}
|
||||
|
||||
function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
|
||||
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
||||
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
||||
return [makerToken, takerToken];
|
||||
}
|
||||
|
||||
function createBridgeData(tokenAddress: string): string {
|
||||
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
|
||||
return encoder.encode({ tokenAddress });
|
||||
@@ -210,22 +273,36 @@ function createCurveBridgeData(
|
||||
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]);
|
||||
}
|
||||
|
||||
function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFromPathOpts): [BigNumber, BigNumber] {
|
||||
return [
|
||||
// Maker asset amount.
|
||||
opts.side === MarketOperation.Sell
|
||||
? fill.output.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN)
|
||||
: fill.input,
|
||||
// Taker asset amount.
|
||||
opts.side === MarketOperation.Sell
|
||||
? fill.input
|
||||
: fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP),
|
||||
];
|
||||
}
|
||||
|
||||
type CommonBridgeOrderFields = Pick<
|
||||
OptimizedMarketOrder,
|
||||
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
|
||||
Exclude<
|
||||
keyof OptimizedMarketOrder,
|
||||
'fills'
|
||||
| 'makerAddress'
|
||||
| 'makerAssetData'
|
||||
| 'takerAssetData'
|
||||
| 'makerAssetAmount'
|
||||
| 'takerAssetAmount'
|
||||
| 'fillableMakerAssetAmount'
|
||||
| 'fillableTakerAssetAmount'
|
||||
>
|
||||
>;
|
||||
|
||||
function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFromPathOpts): CommonBridgeOrderFields {
|
||||
const makerAssetAmountAdjustedWithSlippage =
|
||||
opts.side === MarketOperation.Sell
|
||||
? fill.totalMakerAssetAmount.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN)
|
||||
: fill.totalMakerAssetAmount;
|
||||
const takerAssetAmountAdjustedWithSlippage =
|
||||
opts.side === MarketOperation.Sell
|
||||
? fill.totalTakerAssetAmount
|
||||
: fill.totalTakerAssetAmount.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP);
|
||||
function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBridgeOrderFields {
|
||||
return {
|
||||
fill,
|
||||
takerAddress: NULL_ADDRESS,
|
||||
senderAddress: NULL_ADDRESS,
|
||||
feeRecipientAddress: NULL_ADDRESS,
|
||||
@@ -235,10 +312,6 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro
|
||||
takerFeeAssetData: NULL_BYTES,
|
||||
makerFee: ZERO_AMOUNT,
|
||||
takerFee: ZERO_AMOUNT,
|
||||
makerAssetAmount: makerAssetAmountAdjustedWithSlippage,
|
||||
fillableMakerAssetAmount: makerAssetAmountAdjustedWithSlippage,
|
||||
takerAssetAmount: takerAssetAmountAdjustedWithSlippage,
|
||||
fillableTakerAssetAmount: takerAssetAmountAdjustedWithSlippage,
|
||||
fillableTakerFeeAmount: ZERO_AMOUNT,
|
||||
signature: WALLET_SIGNATURE,
|
||||
...opts.orderDomain,
|
||||
@@ -247,12 +320,7 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro
|
||||
|
||||
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
|
||||
return {
|
||||
fill: {
|
||||
source: fill.source,
|
||||
totalMakerAssetAmount: fill.totalMakerAssetAmount,
|
||||
totalTakerAssetAmount: fill.totalTakerAssetAmount,
|
||||
subFills: fill.subFills,
|
||||
},
|
||||
fills: [fill],
|
||||
...(fill as NativeCollapsedFill).nativeOrder,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,19 +97,19 @@ export interface CollapsedFill {
|
||||
*/
|
||||
source: ERC20BridgeSource;
|
||||
/**
|
||||
* Total maker asset amount.
|
||||
* Total input amount (sum of `subFill`s)
|
||||
*/
|
||||
totalMakerAssetAmount: BigNumber;
|
||||
input: BigNumber;
|
||||
/**
|
||||
* Total taker asset amount.
|
||||
* Total output amount (sum of `subFill`s)
|
||||
*/
|
||||
totalTakerAssetAmount: BigNumber;
|
||||
output: BigNumber;
|
||||
/**
|
||||
* All the fill asset amounts that were collapsed into this node.
|
||||
* Quantities of all the fills that were collapsed.
|
||||
*/
|
||||
subFills: Array<{
|
||||
makerAssetAmount: BigNumber;
|
||||
takerAssetAmount: BigNumber;
|
||||
input: BigNumber;
|
||||
output: BigNumber;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts {
|
||||
/**
|
||||
* The optimized fills that generated this order.
|
||||
*/
|
||||
fill: CollapsedFill;
|
||||
fills: CollapsedFill[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,9 +180,14 @@ export interface GetMarketOrdersOpts {
|
||||
gasSchedule: { [source: string]: number };
|
||||
/**
|
||||
* Whether to pad the quote with a redundant fallback quote using different
|
||||
* sources.
|
||||
* sources. Defaults to `true`.
|
||||
*/
|
||||
allowFallback: boolean;
|
||||
/**
|
||||
* Whether to combine contiguous bridge orders into a single DexForwarderBridge
|
||||
* order. Defaults to `true`.
|
||||
*/
|
||||
shouldBatchBridgeOrders: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,12 +15,6 @@ export class ProtocolFeeUtils {
|
||||
this._initializeHeartBeat();
|
||||
}
|
||||
|
||||
// TODO(dave4506) at some point, we should add a heart beat to the multiplier, or some RPC call to fetch latest multiplier.
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
public async getProtocolFeeMultiplierAsync(): Promise<BigNumber> {
|
||||
return constants.PROTOCOL_FEE_MULTIPLIER;
|
||||
}
|
||||
|
||||
public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise<BigNumber> {
|
||||
if (this.gasPriceEstimation.eq(constants.ZERO_AMOUNT)) {
|
||||
return this._getGasPriceFromGasStationOrThrowAsync();
|
||||
@@ -39,18 +33,6 @@ export class ProtocolFeeUtils {
|
||||
this._gasPriceHeart.kill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates protocol fee with protofol fee multiplier for each fill.
|
||||
*/
|
||||
public async calculateWorstCaseProtocolFeeAsync<T extends Order>(
|
||||
orders: T[],
|
||||
gasPrice: BigNumber,
|
||||
): Promise<BigNumber> {
|
||||
const protocolFeeMultiplier = await this.getProtocolFeeMultiplierAsync();
|
||||
const protocolFee = new BigNumber(orders.length).times(protocolFeeMultiplier).times(gasPrice);
|
||||
return protocolFee;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: prefer-function-over-method
|
||||
private async _getGasPriceFromGasStationOrThrowAsync(): Promise<BigNumber> {
|
||||
try {
|
||||
|
||||
344
packages/asset-swapper/src/utils/quote_simulation.ts
Normal file
344
packages/asset-swapper/src/utils/quote_simulation.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { constants } from '../constants';
|
||||
import { MarketOperation } from '../types';
|
||||
|
||||
import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types';
|
||||
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
|
||||
|
||||
const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants;
|
||||
const { ROUND_DOWN, ROUND_UP } = BigNumber;
|
||||
|
||||
export interface QuoteFillResult {
|
||||
// Maker asset bought.
|
||||
makerAssetAmount: BigNumber;
|
||||
// Taker asset sold.
|
||||
takerAssetAmount: BigNumber;
|
||||
// Taker fees that can be paid with the maker asset.
|
||||
takerFeeMakerAssetAmount: BigNumber;
|
||||
// Taker fees that can be paid with the taker asset.
|
||||
takerFeeTakerAssetAmount: BigNumber;
|
||||
// Total maker asset amount bought (including fees).
|
||||
totalMakerAssetAmount: BigNumber;
|
||||
// Total taker asset amount sold (including fees).
|
||||
totalTakerAssetAmount: BigNumber;
|
||||
// Protocol fees paid.
|
||||
protocolFeeAmount: BigNumber;
|
||||
// (Estimated) gas used.
|
||||
gas: number;
|
||||
// Fill amounts by source.
|
||||
// For sells, this is the taker assets sold.
|
||||
// For buys, this is the maker assets bought.
|
||||
fillAmountBySource: { [source: string]: BigNumber };
|
||||
}
|
||||
|
||||
interface IntermediateQuoteFillResult {
|
||||
// Input tokens filled. Taker asset for sells, maker asset for buys.
|
||||
input: BigNumber;
|
||||
// Output tokens filled. Maker asset for sells, taker asset for buys.
|
||||
output: BigNumber;
|
||||
// Taker fees that can be paid with the output token.
|
||||
outputFee: BigNumber;
|
||||
// Taker fees that can be paid with the input token.
|
||||
inputFee: BigNumber;
|
||||
// Protocol fees paid.
|
||||
protocolFee: BigNumber;
|
||||
// (Estimated) gas used.
|
||||
gas: number;
|
||||
// Input amounts filled by sources.
|
||||
inputBySource: { [source: string]: BigNumber };
|
||||
}
|
||||
|
||||
const EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT = {
|
||||
input: ZERO_AMOUNT,
|
||||
output: ZERO_AMOUNT,
|
||||
outputFee: ZERO_AMOUNT,
|
||||
inputFee: ZERO_AMOUNT,
|
||||
protocolFee: ZERO_AMOUNT,
|
||||
gas: 0,
|
||||
};
|
||||
|
||||
export interface QuoteFillInfo {
|
||||
orders: OptimizedMarketOrder[];
|
||||
fillAmount: BigNumber;
|
||||
gasPrice: BigNumber;
|
||||
side: MarketOperation;
|
||||
opts: Partial<QuoteFillInfoOpts>;
|
||||
}
|
||||
|
||||
export interface QuoteFillInfoOpts {
|
||||
gasSchedule: { [soruce: string]: number };
|
||||
protocolFeeMultiplier: BigNumber;
|
||||
}
|
||||
|
||||
const DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS: QuoteFillInfoOpts = {
|
||||
gasSchedule: {},
|
||||
protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER,
|
||||
};
|
||||
|
||||
export interface QuoteFillOrderCall {
|
||||
order: OptimizedMarketOrder;
|
||||
// Fillable input amount defined in the order.
|
||||
fillableOrderInput: BigNumber;
|
||||
// Fillable fees payable with input token.
|
||||
// Positive for sells, negative for buys.
|
||||
fillableOrderInputFee: BigNumber;
|
||||
// Fillable fees payable with output token.
|
||||
// Negative for sells, positive for buys.
|
||||
fillableOrderOutputFee: BigNumber;
|
||||
}
|
||||
|
||||
// Simulates filling a quote in the best case.
|
||||
export function simulateBestCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult {
|
||||
const opts = {
|
||||
...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS,
|
||||
...quoteInfo.opts,
|
||||
};
|
||||
const result = fillQuoteOrders(
|
||||
createBestCaseFillOrderCalls(quoteInfo),
|
||||
quoteInfo.fillAmount,
|
||||
quoteInfo.gasPrice.times(opts.protocolFeeMultiplier),
|
||||
opts.gasSchedule,
|
||||
);
|
||||
return fromIntermediateQuoteFillResult(result, quoteInfo);
|
||||
}
|
||||
|
||||
// Simulates filling a quote in the worst case.
|
||||
export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult {
|
||||
const opts = {
|
||||
...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS,
|
||||
...quoteInfo.opts,
|
||||
};
|
||||
const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier);
|
||||
const result = {
|
||||
...fillQuoteOrders(
|
||||
createWorstCaseFillOrderCalls(quoteInfo),
|
||||
quoteInfo.fillAmount,
|
||||
protocolFeePerFillOrder,
|
||||
opts.gasSchedule,
|
||||
),
|
||||
// Worst case gas and protocol fee is hitting all orders.
|
||||
gas: getTotalGasUsedBySources(
|
||||
getFlattenedFillsFromOrders(quoteInfo.orders).map(s => s.source),
|
||||
opts.gasSchedule,
|
||||
),
|
||||
protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length),
|
||||
};
|
||||
return fromIntermediateQuoteFillResult(result, quoteInfo);
|
||||
}
|
||||
|
||||
export function fillQuoteOrders(
|
||||
fillOrders: QuoteFillOrderCall[],
|
||||
inputAmount: BigNumber,
|
||||
protocolFeePerFillOrder: BigNumber,
|
||||
gasSchedule: { [source: string]: number },
|
||||
): IntermediateQuoteFillResult {
|
||||
const result: IntermediateQuoteFillResult = {
|
||||
...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT,
|
||||
inputBySource: {},
|
||||
};
|
||||
let remainingInput = inputAmount;
|
||||
for (const fo of fillOrders) {
|
||||
if (remainingInput.lte(0)) {
|
||||
break;
|
||||
}
|
||||
for (const fill of fo.order.fills) {
|
||||
if (remainingInput.lte(0)) {
|
||||
break;
|
||||
}
|
||||
const { source } = fill;
|
||||
result.gas += gasSchedule[source] || 0;
|
||||
result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT;
|
||||
|
||||
// Actual rates are rarely linear, so fill subfills individually to
|
||||
// get a better approximation of fill size.
|
||||
for (const subFill of fill.subFills) {
|
||||
if (remainingInput.lte(0)) {
|
||||
break;
|
||||
}
|
||||
const filledInput = solveForInputFillAmount(
|
||||
remainingInput,
|
||||
subFill.input,
|
||||
fo.fillableOrderInput,
|
||||
fo.fillableOrderInputFee,
|
||||
);
|
||||
const filledOutput = subFill.output.times(filledInput.div(subFill.input));
|
||||
|
||||
result.inputBySource[source] = result.inputBySource[source].plus(filledInput);
|
||||
result.input = result.input
|
||||
.plus(filledInput);
|
||||
result.output = result.input
|
||||
.plus(filledOutput);
|
||||
const orderFillFrac = filledInput.div(fo.fillableOrderInput);
|
||||
result.inputFee = result.inputFee
|
||||
.plus(orderFillFrac.times(fo.fillableOrderInputFee));
|
||||
result.outputFee = result.outputFee
|
||||
.plus(orderFillFrac.times(fo.fillableOrderOutputFee));
|
||||
remainingInput = inputAmount
|
||||
.minus(result.input.plus(result.inputFee));
|
||||
}
|
||||
}
|
||||
result.protocolFee = result.protocolFee.plus(protocolFeePerFillOrder);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function solveForInputFillAmount(
|
||||
remainingInput: BigNumber,
|
||||
fillableInput: BigNumber,
|
||||
fillableOrderInput: BigNumber,
|
||||
fillableOrderInputFee: BigNumber,
|
||||
): BigNumber {
|
||||
// When accounting for input token taker fees, the effective input amount is
|
||||
// given by:
|
||||
// i' = i + f * i / o
|
||||
// where:
|
||||
// i' - The effective input amount, including fees
|
||||
// i - An input amount
|
||||
// f - fillableOrderInputFee
|
||||
// o - fillableOrderInput
|
||||
// Solving for i we get:
|
||||
// i = (i' * o) / (f + o)
|
||||
const denom = fillableOrderInput.plus(fillableOrderInputFee);
|
||||
if (denom.lte(0)) {
|
||||
// A zero denominator would imply an order whose fees are >= the input
|
||||
// token amount.
|
||||
// For sells, takerFeeAmount >= takerAssetAmount (technically OK but really undesirable).
|
||||
// For buys, takerFeeAmount >= makerAssetAmount (losing all your returns to fees).
|
||||
throw new Error(`Cannot solve for input amount with order input ${fillableOrderInput} and order fee ${fillableOrderInputFee}.`);
|
||||
}
|
||||
// i' = remainingInput
|
||||
return BigNumber.min(fillableInput, remainingInput.times(fillableOrderInput).div(denom));
|
||||
}
|
||||
|
||||
function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] {
|
||||
const { orders, side } = quoteInfo;
|
||||
return orders.map(o => ({
|
||||
order: o,
|
||||
...(side === MarketOperation.Sell
|
||||
? {
|
||||
fillableOrderInput: o.fillableTakerAssetAmount,
|
||||
fillableOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o)
|
||||
? o.fillableTakerFeeAmount
|
||||
: ZERO_AMOUNT,
|
||||
fillableOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o)
|
||||
? o.fillableTakerFeeAmount.negated()
|
||||
: ZERO_AMOUNT,
|
||||
}
|
||||
// Buy
|
||||
: {
|
||||
fillableOrderInput: o.fillableMakerAssetAmount,
|
||||
fillableOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o)
|
||||
? o.fillableTakerFeeAmount.negated()
|
||||
: ZERO_AMOUNT,
|
||||
fillableOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o)
|
||||
? o.fillableTakerFeeAmount
|
||||
: ZERO_AMOUNT,
|
||||
}
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function createWorstCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] {
|
||||
// Reuse best case fill orders.
|
||||
return createBestCaseFillOrderCalls(quoteInfo).map(fo => ({
|
||||
...fo,
|
||||
order: {
|
||||
...fo.order,
|
||||
// Apply slippage to order fills and reverse them.
|
||||
fills: getSlippedOrderFills(fo.order, quoteInfo.side).reverse(),
|
||||
},
|
||||
// Reverse the orders.
|
||||
})).reverse();
|
||||
}
|
||||
|
||||
// Apply order slippage to its fill paths.
|
||||
function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation): CollapsedFill[] {
|
||||
const totalInput = BigNumber.sum(...order.fills.map(f => f.input));
|
||||
const totalOutput = BigNumber.sum(...order.fills.map(f => f.output));
|
||||
const inputScaling = side === MarketOperation.Sell
|
||||
? order.fillableTakerAssetAmount.div(totalInput) // Should be 1
|
||||
: order.fillableMakerAssetAmount.div(totalOutput);
|
||||
const outputScaling = side === MarketOperation.Sell
|
||||
? order.fillableMakerAssetAmount.div(totalOutput)
|
||||
: order.fillableTakerAssetAmount.div(totalInput); // Should be 1
|
||||
return order.fills.map(f => ({
|
||||
...f,
|
||||
input: f.input.times(inputScaling),
|
||||
output: f.output.times(outputScaling),
|
||||
subFills: f.subFills.map(sf => ({
|
||||
...sf,
|
||||
input: sf.input.times(inputScaling),
|
||||
output: sf.output.times(outputScaling),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
function fromIntermediateQuoteFillResult(
|
||||
ir: IntermediateQuoteFillResult,
|
||||
quoteInfo: QuoteFillInfo,
|
||||
): QuoteFillResult {
|
||||
const { side } = quoteInfo;
|
||||
// Round to integers.
|
||||
const inputRounding = side === MarketOperation.Sell
|
||||
? ROUND_UP : ROUND_DOWN;
|
||||
const outputRounding = side === MarketOperation.Sell
|
||||
? ROUND_DOWN : ROUND_UP;
|
||||
const _ir = {
|
||||
input: ir.input.integerValue(inputRounding),
|
||||
output: ir.output.integerValue(outputRounding),
|
||||
inputFee: ir.inputFee.integerValue(inputRounding),
|
||||
outputFee: ir.outputFee.integerValue(outputRounding),
|
||||
protocolFee: ir.protocolFee.integerValue(ROUND_UP),
|
||||
gas: Math.ceil(ir.gas),
|
||||
inputBySource: Object.assign(
|
||||
{},
|
||||
...Object.entries(ir.inputBySource)
|
||||
.map(([k, v]) => ({ [k]: v.integerValue(inputRounding) })),
|
||||
),
|
||||
};
|
||||
return {
|
||||
...(side === MarketOperation.Sell
|
||||
// Sell
|
||||
? {
|
||||
makerAssetAmount: _ir.output,
|
||||
takerAssetAmount: _ir.input,
|
||||
takerFeeMakerAssetAmount: _ir.outputFee,
|
||||
takerFeeTakerAssetAmount: _ir.inputFee,
|
||||
totalMakerAssetAmount: _ir.output.plus(_ir.outputFee),
|
||||
totalTakerAssetAmount: _ir.input,
|
||||
}
|
||||
// Buy
|
||||
: {
|
||||
makerAssetAmount: _ir.input,
|
||||
takerAssetAmount: _ir.output,
|
||||
takerFeeMakerAssetAmount: _ir.inputFee,
|
||||
takerFeeTakerAssetAmount: _ir.outputFee,
|
||||
totalMakerAssetAmount: _ir.input,
|
||||
totalTakerAssetAmount: _ir.output.plus(_ir.outputFee),
|
||||
}
|
||||
),
|
||||
protocolFeeAmount: _ir.protocolFee,
|
||||
gas: _ir.gas,
|
||||
fillAmountBySource: _ir.inputBySource,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] {
|
||||
const fills = [];
|
||||
for (const o of orders) {
|
||||
fills.push(...o.fills);
|
||||
}
|
||||
return fills;
|
||||
}
|
||||
|
||||
function getTotalGasUsedBySources(
|
||||
sources: ERC20BridgeSource[],
|
||||
gasSchedule: { [source: string]: number },
|
||||
): number {
|
||||
let gasUsed = 0;
|
||||
for (const s of sources) {
|
||||
gasUsed += gasSchedule[s] || 0;
|
||||
}
|
||||
return gasUsed;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { assetDataUtils, orderCalculationUtils } from '@0x/order-utils';
|
||||
import { assetDataUtils } from '@0x/order-utils';
|
||||
import { AssetProxyId, SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from '../constants';
|
||||
import {
|
||||
CalculateSwapQuoteOpts,
|
||||
MarketBuySwapQuote,
|
||||
@@ -17,24 +16,24 @@ import {
|
||||
SwapQuoterError,
|
||||
} from '../types';
|
||||
|
||||
import { fillableAmountsUtils } from './fillable_amounts_utils';
|
||||
import { MarketOperationUtils } from './market_operation_utils';
|
||||
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
||||
import { ERC20BridgeSource, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
|
||||
import { ProtocolFeeUtils } from './protocol_fee_utils';
|
||||
import { GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
|
||||
import {
|
||||
isOrderTakerFeePayableWithMakerAsset,
|
||||
isOrderTakerFeePayableWithTakerAsset,
|
||||
isSupportedAssetDataInOrders,
|
||||
} from './utils';
|
||||
|
||||
import {
|
||||
QuoteFillResult,
|
||||
simulateBestCaseFill,
|
||||
simulateWorstCaseFill,
|
||||
} from './quote_simulation';
|
||||
|
||||
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
|
||||
export class SwapQuoteCalculator {
|
||||
private readonly _protocolFeeUtils: ProtocolFeeUtils;
|
||||
private readonly _marketOperationUtils: MarketOperationUtils;
|
||||
|
||||
constructor(protocolFeeUtils: ProtocolFeeUtils, marketOperationUtils: MarketOperationUtils) {
|
||||
this._protocolFeeUtils = protocolFeeUtils;
|
||||
constructor(marketOperationUtils: MarketOperationUtils) {
|
||||
this._marketOperationUtils = marketOperationUtils;
|
||||
}
|
||||
|
||||
@@ -99,7 +98,7 @@ export class SwapQuoteCalculator {
|
||||
batchSignedOrders.map(async (orders, i) => {
|
||||
if (orders) {
|
||||
const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0];
|
||||
return this._createSwapQuoteAsync(
|
||||
return createSwapQuote(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
orders,
|
||||
@@ -163,7 +162,7 @@ export class SwapQuoteCalculator {
|
||||
|
||||
// assetData information for the result
|
||||
const { makerAssetData, takerAssetData } = prunedOrders[0];
|
||||
return this._createSwapQuoteAsync(
|
||||
return createSwapQuote(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
resultOrders,
|
||||
@@ -173,324 +172,77 @@ export class SwapQuoteCalculator {
|
||||
opts.gasSchedule,
|
||||
);
|
||||
}
|
||||
private async _createSwapQuoteAsync(
|
||||
makerAssetData: string,
|
||||
takerAssetData: string,
|
||||
resultOrders: OptimizedMarketOrder[],
|
||||
operation: MarketOperation,
|
||||
assetFillAmount: BigNumber,
|
||||
gasPrice: BigNumber,
|
||||
gasSchedule: { [source: string]: number },
|
||||
): Promise<SwapQuote> {
|
||||
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
|
||||
resultOrders,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
gasSchedule,
|
||||
operation,
|
||||
);
|
||||
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
|
||||
resultOrders,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
gasSchedule,
|
||||
operation,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const breakdown = getSwapQuoteOrdersBreakdown(resultOrders, operation);
|
||||
function createSwapQuote(
|
||||
makerAssetData: string,
|
||||
takerAssetData: string,
|
||||
resultOrders: OptimizedMarketOrder[],
|
||||
operation: MarketOperation,
|
||||
assetFillAmount: BigNumber,
|
||||
gasPrice: BigNumber,
|
||||
gasSchedule: { [source: string]: number },
|
||||
): SwapQuote {
|
||||
const bestCaseFillResult = simulateBestCaseFill({
|
||||
gasPrice,
|
||||
orders: resultOrders,
|
||||
side: operation,
|
||||
fillAmount: assetFillAmount,
|
||||
opts: { gasSchedule },
|
||||
});
|
||||
|
||||
const quoteBase: SwapQuoteBase = {
|
||||
takerAssetData,
|
||||
makerAssetData,
|
||||
// Remove fill metadata.
|
||||
orders: resultOrders.map(o => _.omit(o, 'fill')) as SignedOrderWithFillableAmounts[],
|
||||
bestCaseQuoteInfo,
|
||||
worstCaseQuoteInfo,
|
||||
gasPrice,
|
||||
sourceBreakdown: breakdown,
|
||||
};
|
||||
const worstCaseFillResult = simulateWorstCaseFill({
|
||||
gasPrice,
|
||||
orders: resultOrders,
|
||||
side: operation,
|
||||
fillAmount: assetFillAmount,
|
||||
opts: { gasSchedule },
|
||||
});
|
||||
|
||||
if (operation === MarketOperation.Buy) {
|
||||
return {
|
||||
...quoteBase,
|
||||
type: MarketOperation.Buy,
|
||||
makerAssetFillAmount: assetFillAmount,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...quoteBase,
|
||||
type: MarketOperation.Sell,
|
||||
takerAssetFillAmount: assetFillAmount,
|
||||
};
|
||||
}
|
||||
}
|
||||
const quoteBase: SwapQuoteBase = {
|
||||
takerAssetData,
|
||||
makerAssetData,
|
||||
// Remove fill metadata.
|
||||
orders: resultOrders.map(o => _.omit(o, 'fills')) as SignedOrderWithFillableAmounts[],
|
||||
bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult),
|
||||
worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult),
|
||||
gasPrice,
|
||||
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: prefer-function-over-method
|
||||
private async _calculateQuoteInfoAsync(
|
||||
orders: OptimizedMarketOrder[],
|
||||
assetFillAmount: BigNumber,
|
||||
gasPrice: BigNumber,
|
||||
gasSchedule: { [source: string]: number },
|
||||
operation: MarketOperation,
|
||||
worstCase: boolean = false,
|
||||
): Promise<SwapQuoteInfo> {
|
||||
if (operation === MarketOperation.Buy) {
|
||||
return {
|
||||
...(operation === MarketOperation.Buy
|
||||
? await this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)
|
||||
: await this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)),
|
||||
gas: getGasUsedByOrders(orders, gasSchedule),
|
||||
...quoteBase,
|
||||
type: MarketOperation.Buy,
|
||||
makerAssetFillAmount: assetFillAmount,
|
||||
};
|
||||
}
|
||||
|
||||
private async _calculateMarketSellQuoteInfoAsync(
|
||||
orders: OptimizedMarketOrder[],
|
||||
takerAssetSellAmount: BigNumber,
|
||||
gasPrice: BigNumber,
|
||||
worstCase: boolean = false,
|
||||
): Promise<SwapQuoteInfo> {
|
||||
let totalMakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let totalTakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let remainingTakerAssetFillAmount = takerAssetSellAmount;
|
||||
const filledOrders = [] as OptimizedMarketOrder[];
|
||||
const _orders = !worstCase ? orders : orders.slice().reverse();
|
||||
for (const order of _orders) {
|
||||
let makerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let takerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let feeTakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
if (remainingTakerAssetFillAmount.lte(0)) {
|
||||
break;
|
||||
}
|
||||
if (order.fill.source === ERC20BridgeSource.Native) {
|
||||
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
|
||||
order,
|
||||
);
|
||||
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
|
||||
order,
|
||||
);
|
||||
const takerAssetAmountWithFees = BigNumber.min(
|
||||
remainingTakerAssetFillAmount,
|
||||
adjustedFillableTakerAssetAmount,
|
||||
);
|
||||
const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees);
|
||||
takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount;
|
||||
feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount;
|
||||
makerAssetAmount = takerAssetAmountWithFees
|
||||
.div(adjustedFillableTakerAssetAmount)
|
||||
.times(adjustedFillableMakerAssetAmount)
|
||||
.integerValue(BigNumber.ROUND_DOWN);
|
||||
} else {
|
||||
// This is a collapsed bridge order.
|
||||
// Because collapsed bridge orders actually fill at different rates,
|
||||
// we can iterate over the uncollapsed fills to get the actual
|
||||
// asset amounts transfered.
|
||||
// We can also assume there are no fees and the order is not
|
||||
// partially filled.
|
||||
|
||||
// Infer the bridge slippage from the difference between the fill
|
||||
// size and the actual order asset amounts.
|
||||
const makerAssetBridgeSlippage = !worstCase
|
||||
? constants.ONE_AMOUNT
|
||||
: order.makerAssetAmount.div(order.fill.totalMakerAssetAmount);
|
||||
const takerAssetBridgeSlippage = !worstCase
|
||||
? constants.ONE_AMOUNT
|
||||
: order.takerAssetAmount.div(order.fill.totalTakerAssetAmount);
|
||||
// Consecutively fill the subfills in this order.
|
||||
const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse();
|
||||
for (const subFill of subFills) {
|
||||
if (remainingTakerAssetFillAmount.minus(takerAssetAmount).lte(0)) {
|
||||
break;
|
||||
}
|
||||
const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage);
|
||||
const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage);
|
||||
const partialTakerAssetFillAmount = BigNumber.min(
|
||||
partialTakerAssetAmount,
|
||||
remainingTakerAssetFillAmount.minus(takerAssetAmount),
|
||||
);
|
||||
const partialMakerAssetFillAmount = partialTakerAssetFillAmount
|
||||
.div(partialTakerAssetAmount)
|
||||
.times(partialMakerAssetAmount)
|
||||
.integerValue(BigNumber.ROUND_DOWN);
|
||||
takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount);
|
||||
makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount);
|
||||
}
|
||||
}
|
||||
totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount);
|
||||
totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount);
|
||||
totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount);
|
||||
remainingTakerAssetFillAmount = remainingTakerAssetFillAmount
|
||||
.minus(takerAssetAmount)
|
||||
.minus(feeTakerAssetAmount);
|
||||
filledOrders.push(order);
|
||||
}
|
||||
const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
|
||||
!worstCase ? filledOrders : orders,
|
||||
gasPrice,
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
feeTakerAssetAmount: totalFeeTakerAssetAmount,
|
||||
takerAssetAmount: totalTakerAssetAmount,
|
||||
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
|
||||
makerAssetAmount: totalMakerAssetAmount,
|
||||
protocolFeeInWeiAmount,
|
||||
gas: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private async _calculateMarketBuyQuoteInfoAsync(
|
||||
orders: OptimizedMarketOrder[],
|
||||
makerAssetBuyAmount: BigNumber,
|
||||
gasPrice: BigNumber,
|
||||
worstCase: boolean = false,
|
||||
): Promise<SwapQuoteInfo> {
|
||||
let totalMakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let totalTakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let remainingMakerAssetFillAmount = makerAssetBuyAmount;
|
||||
const filledOrders = [] as OptimizedMarketOrder[];
|
||||
const _orders = !worstCase ? orders : orders.slice().reverse();
|
||||
for (const order of _orders) {
|
||||
let makerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let takerAssetAmount = constants.ZERO_AMOUNT;
|
||||
let feeTakerAssetAmount = constants.ZERO_AMOUNT;
|
||||
if (remainingMakerAssetFillAmount.lte(0)) {
|
||||
break;
|
||||
}
|
||||
if (order.fill.source === ERC20BridgeSource.Native) {
|
||||
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
|
||||
order,
|
||||
);
|
||||
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
|
||||
order,
|
||||
);
|
||||
makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);
|
||||
const takerAssetAmountWithFees = makerAssetAmount
|
||||
.div(adjustedFillableMakerAssetAmount)
|
||||
.multipliedBy(adjustedFillableTakerAssetAmount)
|
||||
.integerValue(BigNumber.ROUND_UP);
|
||||
const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees);
|
||||
takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount;
|
||||
feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount;
|
||||
} else {
|
||||
// This is a collapsed bridge order.
|
||||
// Because collapsed bridge orders actually fill at different rates,
|
||||
// we can iterate over the uncollapsed fills to get the actual
|
||||
// asset amounts transfered.
|
||||
// We can also assume there are no fees and the order is not
|
||||
// partially filled.
|
||||
|
||||
// Infer the bridge slippage from the difference between the fill
|
||||
// size and the actual order asset amounts.
|
||||
const makerAssetBridgeSlippage = !worstCase
|
||||
? constants.ONE_AMOUNT
|
||||
: order.makerAssetAmount.div(order.fill.totalMakerAssetAmount);
|
||||
const takerAssetBridgeSlippage = !worstCase
|
||||
? constants.ONE_AMOUNT
|
||||
: order.takerAssetAmount.div(order.fill.totalTakerAssetAmount);
|
||||
// Consecutively fill the subfills in this order.
|
||||
const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse();
|
||||
for (const subFill of subFills) {
|
||||
if (remainingMakerAssetFillAmount.minus(makerAssetAmount).lte(0)) {
|
||||
break;
|
||||
}
|
||||
const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage);
|
||||
const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage);
|
||||
const partialMakerAssetFillAmount = BigNumber.min(
|
||||
partialMakerAssetAmount,
|
||||
remainingMakerAssetFillAmount.minus(makerAssetAmount),
|
||||
);
|
||||
const partialTakerAssetFillAmount = partialMakerAssetFillAmount
|
||||
.div(partialMakerAssetAmount)
|
||||
.times(partialTakerAssetAmount)
|
||||
.integerValue(BigNumber.ROUND_UP);
|
||||
takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount);
|
||||
makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount);
|
||||
}
|
||||
}
|
||||
totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount);
|
||||
totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount);
|
||||
totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount);
|
||||
remainingMakerAssetFillAmount = remainingMakerAssetFillAmount.minus(makerAssetAmount);
|
||||
filledOrders.push(order);
|
||||
}
|
||||
const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
|
||||
!worstCase ? filledOrders : orders,
|
||||
gasPrice,
|
||||
);
|
||||
return {
|
||||
feeTakerAssetAmount: totalFeeTakerAssetAmount,
|
||||
takerAssetAmount: totalTakerAssetAmount,
|
||||
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
|
||||
makerAssetAmount: totalMakerAssetAmount,
|
||||
protocolFeeInWeiAmount,
|
||||
gas: 0,
|
||||
...quoteBase,
|
||||
type: MarketOperation.Sell,
|
||||
takerAssetFillAmount: assetFillAmount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSwapQuoteOrdersBreakdown(
|
||||
orders: OptimizedMarketOrder[],
|
||||
operation: MarketOperation,
|
||||
fillAmountBySource: { [source: string]: BigNumber },
|
||||
): SwapQuoteOrdersBreakdown {
|
||||
const orderAmounts =
|
||||
operation === MarketOperation.Buy
|
||||
? orders.map(o => o.fill.totalMakerAssetAmount)
|
||||
: orders.map(o => o.fill.totalTakerAssetAmount);
|
||||
const amountsBySource: SwapQuoteOrdersBreakdown = {};
|
||||
orders.forEach((o, i) => {
|
||||
const source = o.fill.source;
|
||||
amountsBySource[source] = orderAmounts[i].plus(amountsBySource[source] || 0);
|
||||
});
|
||||
const totalAmount = BigNumber.sum(0, ...orderAmounts);
|
||||
const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource));
|
||||
const breakdown: SwapQuoteOrdersBreakdown = {};
|
||||
for (const [source, amount] of Object.entries(amountsBySource)) {
|
||||
breakdown[source] = amount.div(totalAmount);
|
||||
}
|
||||
Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => {
|
||||
breakdown[source] = fillAmount.div(totalFillAmount);
|
||||
});
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
function getTakerAssetAmountBreakDown(
|
||||
order: SignedOrderWithFillableAmounts,
|
||||
takerAssetAmountWithFees: BigNumber,
|
||||
): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } {
|
||||
if (isOrderTakerFeePayableWithTakerAsset(order)) {
|
||||
const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee);
|
||||
const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount);
|
||||
const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL);
|
||||
return {
|
||||
takerAssetAmount,
|
||||
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
|
||||
};
|
||||
} else if (isOrderTakerFeePayableWithMakerAsset(order)) {
|
||||
if (takerAssetAmountWithFees.isZero()) {
|
||||
return {
|
||||
takerAssetAmount: constants.ZERO_AMOUNT,
|
||||
feeTakerAssetAmount: constants.ZERO_AMOUNT,
|
||||
};
|
||||
}
|
||||
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerAssetAmountWithFees);
|
||||
const makerAssetFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerAssetAmountWithFees);
|
||||
const takerAssetAmount = takerFeeAmount
|
||||
.div(makerAssetFillAmount)
|
||||
.multipliedBy(takerAssetAmountWithFees)
|
||||
.integerValue(BigNumber.ROUND_UP);
|
||||
return {
|
||||
takerAssetAmount,
|
||||
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
|
||||
};
|
||||
}
|
||||
function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo {
|
||||
return {
|
||||
feeTakerAssetAmount: constants.ZERO_AMOUNT,
|
||||
takerAssetAmount: takerAssetAmountWithFees,
|
||||
makerAssetAmount: fr.totalMakerAssetAmount,
|
||||
takerAssetAmount: fr.takerAssetAmount,
|
||||
totalTakerAssetAmount: fr.totalTakerAssetAmount,
|
||||
feeTakerAssetAmount: fr.takerFeeTakerAssetAmount,
|
||||
protocolFeeInWeiAmount: fr.protocolFeeAmount,
|
||||
gas: fr.gas,
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -12,7 +12,6 @@ import { SwapQuote } from '../src';
|
||||
import { constants } from '../src/constants';
|
||||
import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer';
|
||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
||||
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
||||
@@ -60,7 +59,6 @@ const expectMakerAndTakerBalancesAsyncFactory = (
|
||||
};
|
||||
|
||||
describe('ExchangeSwapQuoteConsumer', () => {
|
||||
let protocolFeeUtils: ProtocolFeeUtils;
|
||||
let userAddresses: string[];
|
||||
let erc20MakerTokenContract: ERC20TokenContract;
|
||||
let erc20TakerTokenContract: ERC20TokenContract;
|
||||
@@ -123,7 +121,6 @@ describe('ExchangeSwapQuoteConsumer', () => {
|
||||
};
|
||||
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
||||
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
||||
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
|
||||
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
|
||||
erc20TakerTokenContract,
|
||||
makerAddress,
|
||||
@@ -156,7 +153,6 @@ describe('ExchangeSwapQuoteConsumer', () => {
|
||||
orders,
|
||||
MarketOperation.Sell,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
|
||||
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||
@@ -165,7 +161,6 @@ describe('ExchangeSwapQuoteConsumer', () => {
|
||||
orders,
|
||||
MarketOperation.Buy,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
|
||||
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { SwapQuote } from '../src';
|
||||
import { constants } from '../src/constants';
|
||||
import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer';
|
||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
||||
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
||||
@@ -61,7 +60,6 @@ const expectMakerAndTakerBalancesAsyncFactory = (
|
||||
};
|
||||
|
||||
describe('ForwarderSwapQuoteConsumer', () => {
|
||||
let protocolFeeUtils: ProtocolFeeUtils;
|
||||
let userAddresses: string[];
|
||||
let coinbaseAddress: string;
|
||||
let makerAddress: string;
|
||||
@@ -126,7 +124,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
||||
};
|
||||
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
||||
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
||||
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
|
||||
expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory(
|
||||
erc20TokenContract,
|
||||
makerAddress,
|
||||
@@ -179,7 +176,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
||||
orders,
|
||||
MarketOperation.Sell,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
|
||||
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||
@@ -188,7 +184,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
||||
orders,
|
||||
MarketOperation.Buy,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
|
||||
invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||
@@ -197,7 +192,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
||||
invalidOrders,
|
||||
MarketOperation.Buy,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
|
||||
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, {
|
||||
|
||||
@@ -299,6 +299,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
maxFallbackSlippage: 100,
|
||||
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||
allowFallback: false,
|
||||
shouldBatchBridgeOrders: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -422,7 +423,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
);
|
||||
expect(improvedOrders).to.not.be.length(0);
|
||||
for (const order of improvedOrders) {
|
||||
const expectedMakerAmount = order.fill.totalMakerAssetAmount;
|
||||
const expectedMakerAmount = order.fills[0].output;
|
||||
const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber();
|
||||
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
||||
}
|
||||
@@ -442,7 +443,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
@@ -466,7 +467,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
if (orderSources.includes(ERC20BridgeSource.Kyber)) {
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap);
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai);
|
||||
@@ -501,7 +502,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
@@ -536,7 +537,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
@@ -561,7 +562,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
@@ -584,7 +585,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const firstSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
@@ -610,7 +611,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||
const secondSources: ERC20BridgeSource[] = [];
|
||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||
@@ -672,6 +673,40 @@ describe('MarketOperationUtils tests', () => {
|
||||
expect(getLiquidityProviderParams.makerToken).is.eql(yAsset);
|
||||
expect(getLiquidityProviderParams.takerToken).is.eql(xAsset);
|
||||
});
|
||||
|
||||
it('batches contiguous bridge sources', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.CurveUsdcDai] = [0.48, 0.01, 0.01, 0.01];
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{
|
||||
...DEFAULT_OPTS,
|
||||
numSamples: 4,
|
||||
excludedSources: [
|
||||
ERC20BridgeSource.Kyber,
|
||||
..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.CurveUsdcDai),
|
||||
] as ERC20BridgeSource[],
|
||||
shouldBatchBridgeOrders: true,
|
||||
},
|
||||
);
|
||||
expect(improvedOrders).to.be.length(3);
|
||||
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
|
||||
expect(orderFillSources).to.deep.eq([
|
||||
[ERC20BridgeSource.Uniswap],
|
||||
[ERC20BridgeSource.Native],
|
||||
[
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.CurveUsdcDai,
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMarketBuyOrdersAsync()', () => {
|
||||
@@ -687,6 +722,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
maxFallbackSlippage: 100,
|
||||
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||
allowFallback: false,
|
||||
shouldBatchBridgeOrders: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -789,7 +825,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('generates bridge orders with correct taker amount', async () => {
|
||||
it('generates bridge orders with correct maker amount', async () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
// Pass in empty orders to prevent native orders from being used.
|
||||
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
||||
@@ -810,7 +846,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
);
|
||||
expect(improvedOrders).to.not.be.length(0);
|
||||
for (const order of improvedOrders) {
|
||||
const expectedTakerAmount = order.fill.totalTakerAssetAmount;
|
||||
const expectedTakerAmount = order.fills[0].output;
|
||||
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1;
|
||||
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
||||
}
|
||||
@@ -829,7 +865,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
@@ -865,7 +901,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Uniswap,
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
@@ -899,7 +935,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
@@ -921,7 +957,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const firstSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
@@ -946,12 +982,40 @@ describe('MarketOperationUtils tests', () => {
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||
const secondSources: ERC20BridgeSource[] = [];
|
||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
||||
});
|
||||
|
||||
it('batches contiguous bridge sources', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Uniswap] = [0.48, 0.47, 0.01, 0.01];
|
||||
replaceSamplerOps({
|
||||
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{
|
||||
...DEFAULT_OPTS,
|
||||
numSamples: 4,
|
||||
shouldBatchBridgeOrders: true,
|
||||
},
|
||||
);
|
||||
expect(improvedOrders).to.be.length(2);
|
||||
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
|
||||
expect(orderFillSources).to.deep.eq([
|
||||
[ERC20BridgeSource.Native],
|
||||
[
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'mocha';
|
||||
import { SwapQuote, SwapQuoteConsumer } from '../src';
|
||||
import { constants } from '../src/constants';
|
||||
import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
||||
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
||||
@@ -69,7 +68,6 @@ const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array<Partial<SignedOrderWithFillableA
|
||||
|
||||
describe('swapQuoteConsumerUtils', () => {
|
||||
let wethContract: WETH9Contract;
|
||||
let protocolFeeUtils: ProtocolFeeUtils;
|
||||
let userAddresses: string[];
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
@@ -119,7 +117,6 @@ describe('swapQuoteConsumerUtils', () => {
|
||||
};
|
||||
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
||||
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
||||
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
|
||||
forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams);
|
||||
|
||||
swapQuoteConsumer = new SwapQuoteConsumer(provider, {
|
||||
@@ -128,7 +125,6 @@ describe('swapQuoteConsumerUtils', () => {
|
||||
});
|
||||
after(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
await protocolFeeUtils.destroyAsync();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
@@ -182,7 +178,6 @@ describe('swapQuoteConsumerUtils', () => {
|
||||
forwarderOrders,
|
||||
MarketOperation.Sell,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
|
||||
largeForwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||
@@ -191,7 +186,6 @@ describe('swapQuoteConsumerUtils', () => {
|
||||
largeForwarderOrders,
|
||||
MarketOperation.Sell,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
|
||||
exchangeSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||
@@ -200,7 +194,6 @@ describe('swapQuoteConsumerUtils', () => {
|
||||
exchangeOrders,
|
||||
MarketOperation.Sell,
|
||||
GAS_PRICE,
|
||||
protocolFeeUtils,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -57,10 +57,6 @@ const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orde
|
||||
};
|
||||
|
||||
class ProtocolFeeUtilsClass extends ProtocolFeeUtils {
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
public async getProtocolFeeMultiplierAsync(): Promise<BigNumber> {
|
||||
return new BigNumber(PROTOCOL_FEE_MULTIPLIER);
|
||||
}
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise<BigNumber> {
|
||||
return new BigNumber(devConstants.DEFAULT_GAS_PRICE);
|
||||
|
||||
@@ -4,7 +4,6 @@ import * as _ from 'lodash';
|
||||
import { ERC20BridgeSource } from '../../src';
|
||||
import { constants } from '../../src/constants';
|
||||
import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types';
|
||||
import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils';
|
||||
|
||||
/**
|
||||
* Creates a swap quote given orders.
|
||||
@@ -15,16 +14,16 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||
orders: SignedOrderWithFillableAmounts[],
|
||||
operation: MarketOperation,
|
||||
gasPrice: BigNumber,
|
||||
protocolFeeUtils: ProtocolFeeUtils,
|
||||
): Promise<SwapQuote> {
|
||||
const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]);
|
||||
const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]);
|
||||
const protocolFeePerOrder = constants.PROTOCOL_FEE_MULTIPLIER.times(gasPrice);
|
||||
const quoteInfo = {
|
||||
makerAssetAmount: makerAssetFillAmount,
|
||||
feeTakerAssetAmount: constants.ZERO_AMOUNT,
|
||||
takerAssetAmount: totalTakerAssetAmount,
|
||||
totalTakerAssetAmount,
|
||||
protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice),
|
||||
protocolFeeInWeiAmount: protocolFeePerOrder.times(orders.length),
|
||||
gas: 200e3,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user