@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