Update asset-swapper to support MultiplexFeature
This commit is contained in:
		@@ -45,6 +45,7 @@ export {
 | 
				
			|||||||
    ITransformERC20FeatureContract,
 | 
					    ITransformERC20FeatureContract,
 | 
				
			||||||
    IZeroExContract,
 | 
					    IZeroExContract,
 | 
				
			||||||
    LogMetadataTransformerContract,
 | 
					    LogMetadataTransformerContract,
 | 
				
			||||||
 | 
					    MultiplexFeatureContract,
 | 
				
			||||||
    PayTakerTransformerContract,
 | 
					    PayTakerTransformerContract,
 | 
				
			||||||
    PositiveSlippageFeeTransformerContract,
 | 
					    PositiveSlippageFeeTransformerContract,
 | 
				
			||||||
    WethTransformerContract,
 | 
					    WethTransformerContract,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,6 +61,8 @@
 | 
				
			|||||||
        "@0x/base-contract": "^6.2.18",
 | 
					        "@0x/base-contract": "^6.2.18",
 | 
				
			||||||
        "@0x/contract-addresses": "^5.11.0",
 | 
					        "@0x/contract-addresses": "^5.11.0",
 | 
				
			||||||
        "@0x/contract-wrappers": "^13.13.0",
 | 
					        "@0x/contract-wrappers": "^13.13.0",
 | 
				
			||||||
 | 
					        "@0x/contracts-erc20": "^3.3.4",
 | 
				
			||||||
 | 
					        "@0x/contracts-zero-ex": "^0.19.0",
 | 
				
			||||||
        "@0x/dev-utils": "^4.2.1",
 | 
					        "@0x/dev-utils": "^4.2.1",
 | 
				
			||||||
        "@0x/json-schemas": "^5.4.1",
 | 
					        "@0x/json-schemas": "^5.4.1",
 | 
				
			||||||
        "@0x/protocol-utils": "^1.3.0",
 | 
					        "@0x/protocol-utils": "^1.3.0",
 | 
				
			||||||
@@ -93,7 +95,6 @@
 | 
				
			|||||||
        "@0x/contracts-gen": "^2.0.32",
 | 
					        "@0x/contracts-gen": "^2.0.32",
 | 
				
			||||||
        "@0x/contracts-test-utils": "^5.3.22",
 | 
					        "@0x/contracts-test-utils": "^5.3.22",
 | 
				
			||||||
        "@0x/contracts-utils": "^4.7.4",
 | 
					        "@0x/contracts-utils": "^4.7.4",
 | 
				
			||||||
        "@0x/contracts-zero-ex": "^0.19.0",
 | 
					 | 
				
			||||||
        "@0x/mesh-rpc-client": "^9.4.2",
 | 
					        "@0x/mesh-rpc-client": "^9.4.2",
 | 
				
			||||||
        "@0x/migrations": "^7.0.0",
 | 
					        "@0x/migrations": "^7.0.0",
 | 
				
			||||||
        "@0x/sol-compiler": "^4.6.1",
 | 
					        "@0x/sol-compiler": "^4.6.1",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import { ContractAddresses } from '@0x/contract-addresses';
 | 
					import { ContractAddresses } from '@0x/contract-addresses';
 | 
				
			||||||
import { IZeroExContract } from '@0x/contract-wrappers';
 | 
					import { WETH9Contract } from '@0x/contracts-erc20';
 | 
				
			||||||
 | 
					import { IZeroExContract, MultiplexFeatureContract } from '@0x/contracts-zero-ex';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    encodeAffiliateFeeTransformerData,
 | 
					    encodeAffiliateFeeTransformerData,
 | 
				
			||||||
    encodeCurveLiquidityProviderData,
 | 
					    encodeCurveLiquidityProviderData,
 | 
				
			||||||
@@ -8,12 +9,13 @@ import {
 | 
				
			|||||||
    encodePositiveSlippageFeeTransformerData,
 | 
					    encodePositiveSlippageFeeTransformerData,
 | 
				
			||||||
    encodeWethTransformerData,
 | 
					    encodeWethTransformerData,
 | 
				
			||||||
    ETH_TOKEN_ADDRESS,
 | 
					    ETH_TOKEN_ADDRESS,
 | 
				
			||||||
    FillQuoteTransformerData,
 | 
					 | 
				
			||||||
    FillQuoteTransformerOrderType,
 | 
					    FillQuoteTransformerOrderType,
 | 
				
			||||||
    FillQuoteTransformerSide,
 | 
					    FillQuoteTransformerSide,
 | 
				
			||||||
    findTransformerNonce,
 | 
					    findTransformerNonce,
 | 
				
			||||||
 | 
					    RfqOrder,
 | 
				
			||||||
 | 
					    SIGNATURE_ABI,
 | 
				
			||||||
} from '@0x/protocol-utils';
 | 
					} from '@0x/protocol-utils';
 | 
				
			||||||
import { BigNumber, providerUtils } from '@0x/utils';
 | 
					import { AbiEncoder, BigNumber, providerUtils } from '@0x/utils';
 | 
				
			||||||
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
 | 
					import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
 | 
				
			||||||
import * as _ from 'lodash';
 | 
					import * as _ from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,7 +25,6 @@ import {
 | 
				
			|||||||
    CalldataInfo,
 | 
					    CalldataInfo,
 | 
				
			||||||
    ExchangeProxyContractOpts,
 | 
					    ExchangeProxyContractOpts,
 | 
				
			||||||
    MarketBuySwapQuote,
 | 
					    MarketBuySwapQuote,
 | 
				
			||||||
    MarketOperation,
 | 
					 | 
				
			||||||
    MarketSellSwapQuote,
 | 
					    MarketSellSwapQuote,
 | 
				
			||||||
    SwapQuote,
 | 
					    SwapQuote,
 | 
				
			||||||
    SwapQuoteConsumerBase,
 | 
					    SwapQuoteConsumerBase,
 | 
				
			||||||
@@ -36,28 +37,49 @@ import {
 | 
				
			|||||||
    CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID,
 | 
					    CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID,
 | 
				
			||||||
    MOONISWAP_LIQUIDITY_PROVIDER_BY_CHAIN_ID,
 | 
					    MOONISWAP_LIQUIDITY_PROVIDER_BY_CHAIN_ID,
 | 
				
			||||||
} from '../utils/market_operation_utils/constants';
 | 
					} from '../utils/market_operation_utils/constants';
 | 
				
			||||||
import {
 | 
					import { poolEncoder } from '../utils/market_operation_utils/orders';
 | 
				
			||||||
    createBridgeDataForBridgeOrder,
 | 
					 | 
				
			||||||
    getERC20BridgeSourceToBridgeSource,
 | 
					 | 
				
			||||||
    poolEncoder,
 | 
					 | 
				
			||||||
} from '../utils/market_operation_utils/orders';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    CurveFillData,
 | 
					    CurveFillData,
 | 
				
			||||||
    ERC20BridgeSource,
 | 
					    ERC20BridgeSource,
 | 
				
			||||||
    LiquidityProviderFillData,
 | 
					    LiquidityProviderFillData,
 | 
				
			||||||
    MooniswapFillData,
 | 
					    MooniswapFillData,
 | 
				
			||||||
    NativeLimitOrderFillData,
 | 
					 | 
				
			||||||
    NativeRfqOrderFillData,
 | 
					 | 
				
			||||||
    OptimizedMarketBridgeOrder,
 | 
					    OptimizedMarketBridgeOrder,
 | 
				
			||||||
    OptimizedMarketOrder,
 | 
					 | 
				
			||||||
    OptimizedMarketOrderBase,
 | 
					 | 
				
			||||||
    UniswapV2FillData,
 | 
					    UniswapV2FillData,
 | 
				
			||||||
} from '../utils/market_operation_utils/types';
 | 
					} from '../utils/market_operation_utils/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    getFQTTransformerDataFromOptimizedOrders,
 | 
				
			||||||
 | 
					    isBuyQuote,
 | 
				
			||||||
 | 
					    isDirectSwapCompatible,
 | 
				
			||||||
 | 
					    isMultiplexBatchFillCompatible,
 | 
				
			||||||
 | 
					    isMultiplexMultiHopFillCompatible,
 | 
				
			||||||
 | 
					} from './quote_consumer_utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tslint:disable-next-line:custom-no-magic-numbers
 | 
					// tslint:disable-next-line:custom-no-magic-numbers
 | 
				
			||||||
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
 | 
					const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
 | 
				
			||||||
const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants;
 | 
					const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const transformERC20Encoder = AbiEncoder.create([
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        name: 'transformations',
 | 
				
			||||||
 | 
					        type: 'tuple[]',
 | 
				
			||||||
 | 
					        components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    { name: 'ethValue', type: 'uint256' },
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					const rfqDataEncoder = AbiEncoder.create([
 | 
				
			||||||
 | 
					    { name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI },
 | 
				
			||||||
 | 
					    { name: 'signature', type: 'tuple', components: SIGNATURE_ABI },
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					const uniswapDataEncoder = AbiEncoder.create([
 | 
				
			||||||
 | 
					    { name: 'tokens', type: 'address[]' },
 | 
				
			||||||
 | 
					    { name: 'isSushi', type: 'bool' },
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					const plpDataEncoder = AbiEncoder.create([
 | 
				
			||||||
 | 
					    { name: 'provider', type: 'address' },
 | 
				
			||||||
 | 
					    { name: 'auxiliaryData', type: 'bytes' },
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
					export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
				
			||||||
    public readonly provider: ZeroExProvider;
 | 
					    public readonly provider: ZeroExProvider;
 | 
				
			||||||
    public readonly chainId: number;
 | 
					    public readonly chainId: number;
 | 
				
			||||||
@@ -229,6 +251,25 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isMultiplexBatchFillCompatible(quote, optsWithDefaults)) {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                calldataHexString: this._encodeMultiplexBatchFillCalldata(quote),
 | 
				
			||||||
 | 
					                ethAmount,
 | 
				
			||||||
 | 
					                toAddress: this._exchangeProxy.address,
 | 
				
			||||||
 | 
					                allowanceTarget: this._exchangeProxy.address,
 | 
				
			||||||
 | 
					                gasOverhead: ZERO_AMOUNT,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (isMultiplexMultiHopFillCompatible(quote, optsWithDefaults)) {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                calldataHexString: this._encodeMultiplexMultiHopFillCalldata(quote, optsWithDefaults),
 | 
				
			||||||
 | 
					                ethAmount,
 | 
				
			||||||
 | 
					                toAddress: this._exchangeProxy.address,
 | 
				
			||||||
 | 
					                allowanceTarget: this._exchangeProxy.address,
 | 
				
			||||||
 | 
					                gasOverhead: ZERO_AMOUNT,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Build up the transforms.
 | 
					        // Build up the transforms.
 | 
				
			||||||
        const transforms = [];
 | 
					        const transforms = [];
 | 
				
			||||||
        if (isFromETH) {
 | 
					        if (isFromETH) {
 | 
				
			||||||
@@ -380,91 +421,147 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
				
			|||||||
    ): Promise<string> {
 | 
					    ): Promise<string> {
 | 
				
			||||||
        throw new Error('Execution not supported for Exchange Proxy quotes');
 | 
					        throw new Error('Execution not supported for Exchange Proxy quotes');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function isDirectSwapCompatible(
 | 
					    private _encodeMultiplexBatchFillCalldata(quote: SwapQuote): string {
 | 
				
			||||||
    quote: SwapQuote,
 | 
					        const multiplex = new MultiplexFeatureContract(NULL_ADDRESS, this.provider);
 | 
				
			||||||
    opts: ExchangeProxyContractOpts,
 | 
					        const wrappedBatchCalls = [];
 | 
				
			||||||
    directSources: ERC20BridgeSource[],
 | 
					        for_loop: for (const [i, order] of quote.orders.entries()) {
 | 
				
			||||||
): boolean {
 | 
					            switch_statement: switch (order.source) {
 | 
				
			||||||
    // Must not be a mtx.
 | 
					                case ERC20BridgeSource.Native:
 | 
				
			||||||
    if (opts.isMetaTransaction) {
 | 
					                    if (order.type !== FillQuoteTransformerOrderType.Rfq) {
 | 
				
			||||||
        return false;
 | 
					                        // Should never happen because we check `isMultiplexBatchFillCompatible`
 | 
				
			||||||
    }
 | 
					                        // before calling this function.
 | 
				
			||||||
    // Must not have an affiliate fee.
 | 
					                        throw new Error('Multiplex batch fill only supported for RFQ native orders');
 | 
				
			||||||
    if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) {
 | 
					                    }
 | 
				
			||||||
        return false;
 | 
					                    wrappedBatchCalls.push({
 | 
				
			||||||
    }
 | 
					                        selector: multiplex.getSelector('_fillRfqOrder'),
 | 
				
			||||||
    // Must not have a positive slippage fee.
 | 
					                        sellAmount: order.takerAmount,
 | 
				
			||||||
    if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) {
 | 
					                        data: rfqDataEncoder.encode({
 | 
				
			||||||
        return false;
 | 
					                            order: order.fillData.order,
 | 
				
			||||||
    }
 | 
					                            signature: order.fillData.signature,
 | 
				
			||||||
    // Must be a single order.
 | 
					                        }),
 | 
				
			||||||
    if (quote.orders.length !== 1) {
 | 
					                    });
 | 
				
			||||||
        return false;
 | 
					                    break switch_statement;
 | 
				
			||||||
    }
 | 
					                case ERC20BridgeSource.UniswapV2:
 | 
				
			||||||
    const order = quote.orders[0];
 | 
					                case ERC20BridgeSource.SushiSwap:
 | 
				
			||||||
    if (!directSources.includes(order.source)) {
 | 
					                    wrappedBatchCalls.push({
 | 
				
			||||||
        return false;
 | 
					                        selector: multiplex.getSelector('_sellToUniswap'),
 | 
				
			||||||
    }
 | 
					                        sellAmount: order.takerAmount,
 | 
				
			||||||
    // VIP does not support selling the entire balance
 | 
					                        data: uniswapDataEncoder.encode({
 | 
				
			||||||
    if (opts.shouldSellEntireBalance) {
 | 
					                            tokens: (order.fillData as UniswapV2FillData).tokenAddressPath,
 | 
				
			||||||
        return false;
 | 
					                            isSushi: order.source === ERC20BridgeSource.SushiSwap,
 | 
				
			||||||
    }
 | 
					                        }),
 | 
				
			||||||
    return true;
 | 
					                    });
 | 
				
			||||||
}
 | 
					                    break switch_statement;
 | 
				
			||||||
 | 
					                case ERC20BridgeSource.LiquidityProvider:
 | 
				
			||||||
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
 | 
					                    wrappedBatchCalls.push({
 | 
				
			||||||
    return quote.type === MarketOperation.Buy;
 | 
					                        selector: multiplex.getSelector('_sellToLiquidityProvider'),
 | 
				
			||||||
}
 | 
					                        sellAmount: order.takerAmount,
 | 
				
			||||||
 | 
					                        data: plpDataEncoder.encode({
 | 
				
			||||||
function isOptimizedBridgeOrder(x: OptimizedMarketOrder): x is OptimizedMarketBridgeOrder {
 | 
					                            provider: (order.fillData as LiquidityProviderFillData).poolAddress,
 | 
				
			||||||
    return x.type === FillQuoteTransformerOrderType.Bridge;
 | 
					                            auxiliaryData: NULL_BYTES,
 | 
				
			||||||
}
 | 
					                        }),
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
function isOptimizedLimitOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeLimitOrderFillData> {
 | 
					                    break switch_statement;
 | 
				
			||||||
    return x.type === FillQuoteTransformerOrderType.Limit;
 | 
					                default:
 | 
				
			||||||
}
 | 
					                    const fqtData = encodeFillQuoteTransformerData({
 | 
				
			||||||
 | 
					                        side: FillQuoteTransformerSide.Sell,
 | 
				
			||||||
function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeRfqOrderFillData> {
 | 
					                        sellToken: quote.takerToken,
 | 
				
			||||||
    return x.type === FillQuoteTransformerOrderType.Rfq;
 | 
					                        buyToken: quote.makerToken,
 | 
				
			||||||
}
 | 
					                        ...getFQTTransformerDataFromOptimizedOrders(quote.orders.slice(i)),
 | 
				
			||||||
 | 
					                        refundReceiver: NULL_ADDRESS,
 | 
				
			||||||
function getFQTTransformerDataFromOptimizedOrders(
 | 
					                        fillAmount: MAX_UINT256,
 | 
				
			||||||
    orders: OptimizedMarketOrder[],
 | 
					                    });
 | 
				
			||||||
): Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> {
 | 
					                    const transformations = [
 | 
				
			||||||
    const fqtData: Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> = {
 | 
					                        { deploymentNonce: this.transformerNonces.fillQuoteTransformer, data: fqtData },
 | 
				
			||||||
        bridgeOrders: [],
 | 
					                        {
 | 
				
			||||||
        limitOrders: [],
 | 
					                            deploymentNonce: this.transformerNonces.payTakerTransformer,
 | 
				
			||||||
        rfqOrders: [],
 | 
					                            data: encodePayTakerTransformerData({
 | 
				
			||||||
        fillSequence: [],
 | 
					                                tokens: [quote.takerToken, quote.makerToken],
 | 
				
			||||||
    };
 | 
					                                amounts: [],
 | 
				
			||||||
 | 
					                            }),
 | 
				
			||||||
    for (const order of orders) {
 | 
					                        },
 | 
				
			||||||
        if (isOptimizedBridgeOrder(order)) {
 | 
					                    ];
 | 
				
			||||||
            fqtData.bridgeOrders.push({
 | 
					                    wrappedBatchCalls.push({
 | 
				
			||||||
                bridgeData: createBridgeDataForBridgeOrder(order),
 | 
					                        selector: this._exchangeProxy.getSelector('_transformERC20'),
 | 
				
			||||||
                makerTokenAmount: order.makerAmount,
 | 
					                        sellAmount: BigNumber.sum(...quote.orders.slice(i).map(o => o.takerAmount)),
 | 
				
			||||||
                takerTokenAmount: order.takerAmount,
 | 
					                        data: transformERC20Encoder.encode({
 | 
				
			||||||
                source: getERC20BridgeSourceToBridgeSource(order.source),
 | 
					                            transformations,
 | 
				
			||||||
            });
 | 
					                            ethValue: constants.ZERO_AMOUNT,
 | 
				
			||||||
        } else if (isOptimizedLimitOrder(order)) {
 | 
					                        }),
 | 
				
			||||||
            fqtData.limitOrders.push({
 | 
					                    });
 | 
				
			||||||
                order: order.fillData.order,
 | 
					                    break for_loop;
 | 
				
			||||||
                signature: order.fillData.signature,
 | 
					            }
 | 
				
			||||||
                maxTakerTokenFillAmount: order.takerAmount,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        } else if (isOptimizedRfqOrder(order)) {
 | 
					 | 
				
			||||||
            fqtData.rfqOrders.push({
 | 
					 | 
				
			||||||
                order: order.fillData.order,
 | 
					 | 
				
			||||||
                signature: order.fillData.signature,
 | 
					 | 
				
			||||||
                maxTakerTokenFillAmount: order.takerAmount,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            // Should never happen
 | 
					 | 
				
			||||||
            throw new Error('Unknown Order type');
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        fqtData.fillSequence.push(order.type);
 | 
					        return this._exchangeProxy
 | 
				
			||||||
 | 
					            .batchFill(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    inputToken: quote.takerToken,
 | 
				
			||||||
 | 
					                    outputToken: quote.makerToken,
 | 
				
			||||||
 | 
					                    sellAmount: quote.worstCaseQuoteInfo.totalTakerAmount,
 | 
				
			||||||
 | 
					                    calls: wrappedBatchCalls,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                quote.worstCaseQuoteInfo.makerAmount,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .getABIEncodedTransactionData();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private _encodeMultiplexMultiHopFillCalldata(quote: SwapQuote, opts: ExchangeProxyContractOpts): string {
 | 
				
			||||||
 | 
					        const multiplex = new MultiplexFeatureContract(NULL_ADDRESS, this.provider);
 | 
				
			||||||
 | 
					        const weth = new WETH9Contract(NULL_ADDRESS, this.provider);
 | 
				
			||||||
 | 
					        const wrappedMultiHopCalls = [];
 | 
				
			||||||
 | 
					        if (opts.isFromETH) {
 | 
				
			||||||
 | 
					            wrappedMultiHopCalls.push({
 | 
				
			||||||
 | 
					                selector: weth.getSelector('deposit'),
 | 
				
			||||||
 | 
					                data: NULL_BYTES,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const [firstHopOrder, secondHopOrder] = quote.orders;
 | 
				
			||||||
 | 
					        const intermediateToken = firstHopOrder.makerToken;
 | 
				
			||||||
 | 
					        for (const order of [firstHopOrder, secondHopOrder]) {
 | 
				
			||||||
 | 
					            switch (order.source) {
 | 
				
			||||||
 | 
					                case ERC20BridgeSource.UniswapV2:
 | 
				
			||||||
 | 
					                case ERC20BridgeSource.SushiSwap:
 | 
				
			||||||
 | 
					                    wrappedMultiHopCalls.push({
 | 
				
			||||||
 | 
					                        selector: multiplex.getSelector('_sellToUniswap'),
 | 
				
			||||||
 | 
					                        data: uniswapDataEncoder.encode({
 | 
				
			||||||
 | 
					                            tokens: (order.fillData as UniswapV2FillData).tokenAddressPath,
 | 
				
			||||||
 | 
					                            isSushi: order.source === ERC20BridgeSource.SushiSwap,
 | 
				
			||||||
 | 
					                        }),
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case ERC20BridgeSource.LiquidityProvider:
 | 
				
			||||||
 | 
					                    wrappedMultiHopCalls.push({
 | 
				
			||||||
 | 
					                        selector: multiplex.getSelector('_sellToLiquidityProvider'),
 | 
				
			||||||
 | 
					                        data: plpDataEncoder.encode({
 | 
				
			||||||
 | 
					                            tokens: (order.fillData as LiquidityProviderFillData).poolAddress,
 | 
				
			||||||
 | 
					                            auxiliaryData: NULL_BYTES,
 | 
				
			||||||
 | 
					                        }),
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    // Note: we'll need to redeploy TransformERC20Feature before we can
 | 
				
			||||||
 | 
					                    //       use other sources
 | 
				
			||||||
 | 
					                    // Should never happen because we check `isMultiplexMultiHopFillCompatible`
 | 
				
			||||||
 | 
					                    // before calling this function.
 | 
				
			||||||
 | 
					                    throw new Error(`Multiplex multi-hop unsupported source: ${order.source}`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (opts.isToETH) {
 | 
				
			||||||
 | 
					            wrappedMultiHopCalls.push({
 | 
				
			||||||
 | 
					                selector: weth.getSelector('withdraw'),
 | 
				
			||||||
 | 
					                data: NULL_BYTES,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this._exchangeProxy
 | 
				
			||||||
 | 
					            .multiHopFill(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    tokens: [quote.takerToken, intermediateToken, quote.makerToken],
 | 
				
			||||||
 | 
					                    sellAmount: quote.worstCaseQuoteInfo.totalTakerAmount,
 | 
				
			||||||
 | 
					                    calls: wrappedMultiHopCalls,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                quote.worstCaseQuoteInfo.makerAmount,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .getABIEncodedTransactionData();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return fqtData;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					import { FillQuoteTransformerData, FillQuoteTransformerOrderType } from '@0x/protocol-utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AffiliateFeeType, ExchangeProxyContractOpts, MarketBuySwapQuote, MarketOperation, SwapQuote } from '../types';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    createBridgeDataForBridgeOrder,
 | 
				
			||||||
 | 
					    getERC20BridgeSourceToBridgeSource,
 | 
				
			||||||
 | 
					} from '../utils/market_operation_utils/orders';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    ERC20BridgeSource,
 | 
				
			||||||
 | 
					    NativeLimitOrderFillData,
 | 
				
			||||||
 | 
					    NativeRfqOrderFillData,
 | 
				
			||||||
 | 
					    OptimizedMarketBridgeOrder,
 | 
				
			||||||
 | 
					    OptimizedMarketOrder,
 | 
				
			||||||
 | 
					    OptimizedMarketOrderBase,
 | 
				
			||||||
 | 
					} from '../utils/market_operation_utils/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MULTIPLEX_BATCH_FILL_SOURCES = [
 | 
				
			||||||
 | 
					    ERC20BridgeSource.UniswapV2,
 | 
				
			||||||
 | 
					    ERC20BridgeSource.SushiSwap,
 | 
				
			||||||
 | 
					    ERC20BridgeSource.LiquidityProvider,
 | 
				
			||||||
 | 
					    ERC20BridgeSource.Native,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Returns true iff a quote can be filled via `MultiplexFeature.batchFill`.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isMultiplexBatchFillCompatible(quote: SwapQuote, opts: ExchangeProxyContractOpts): boolean {
 | 
				
			||||||
 | 
					    if (requiresTransformERC20(opts)) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (quote.isTwoHop) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // batchFill does not support WETH wrapping/unwrapping at the moment
 | 
				
			||||||
 | 
					    if (opts.isFromETH || opts.isToETH) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (quote.orders.map(o => o.type).includes(FillQuoteTransformerOrderType.Limit)) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Use Multiplex if the non-fallback sources are a subset of
 | 
				
			||||||
 | 
					    // {Uniswap, Sushiswap, RFQ, PLP}
 | 
				
			||||||
 | 
					    const nonFallbackSources = Object.keys(quote.sourceBreakdown);
 | 
				
			||||||
 | 
					    return nonFallbackSources.every(source => MULTIPLEX_BATCH_FILL_SOURCES.includes(source as ERC20BridgeSource));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MULTIPLEX_MULTIHOP_FILL_SOURCES = [
 | 
				
			||||||
 | 
					    ERC20BridgeSource.UniswapV2,
 | 
				
			||||||
 | 
					    ERC20BridgeSource.SushiSwap,
 | 
				
			||||||
 | 
					    ERC20BridgeSource.LiquidityProvider,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Returns true iff a quote can be filled via `MultiplexFeature.multiHopFill`.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isMultiplexMultiHopFillCompatible(quote: SwapQuote, opts: ExchangeProxyContractOpts): boolean {
 | 
				
			||||||
 | 
					    if (requiresTransformERC20(opts)) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!quote.isTwoHop) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const [firstHopOrder, secondHopOrder] = quote.orders;
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        MULTIPLEX_MULTIHOP_FILL_SOURCES.includes(firstHopOrder.source) &&
 | 
				
			||||||
 | 
					        MULTIPLEX_MULTIHOP_FILL_SOURCES.includes(secondHopOrder.source)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Returns true iff a quote can be filled via a VIP feature.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isDirectSwapCompatible(
 | 
				
			||||||
 | 
					    quote: SwapQuote,
 | 
				
			||||||
 | 
					    opts: ExchangeProxyContractOpts,
 | 
				
			||||||
 | 
					    directSources: ERC20BridgeSource[],
 | 
				
			||||||
 | 
					): boolean {
 | 
				
			||||||
 | 
					    if (requiresTransformERC20(opts)) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Must be a single order.
 | 
				
			||||||
 | 
					    if (quote.orders.length !== 1) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const order = quote.orders[0];
 | 
				
			||||||
 | 
					    if (!directSources.includes(order.source)) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Whether a quote is a market buy or not.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
 | 
				
			||||||
 | 
					    return quote.type === MarketOperation.Buy;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isOptimizedBridgeOrder(x: OptimizedMarketOrder): x is OptimizedMarketBridgeOrder {
 | 
				
			||||||
 | 
					    return x.type === FillQuoteTransformerOrderType.Bridge;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isOptimizedLimitOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeLimitOrderFillData> {
 | 
				
			||||||
 | 
					    return x.type === FillQuoteTransformerOrderType.Limit;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeRfqOrderFillData> {
 | 
				
			||||||
 | 
					    return x.type === FillQuoteTransformerOrderType.Rfq;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Converts the given `OptimizedMarketOrder`s into bridge, limit, and RFQ orders for
 | 
				
			||||||
 | 
					 * FillQuoteTransformer.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function getFQTTransformerDataFromOptimizedOrders(
 | 
				
			||||||
 | 
					    orders: OptimizedMarketOrder[],
 | 
				
			||||||
 | 
					): Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> {
 | 
				
			||||||
 | 
					    const fqtData: Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> = {
 | 
				
			||||||
 | 
					        bridgeOrders: [],
 | 
				
			||||||
 | 
					        limitOrders: [],
 | 
				
			||||||
 | 
					        rfqOrders: [],
 | 
				
			||||||
 | 
					        fillSequence: [],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const order of orders) {
 | 
				
			||||||
 | 
					        if (isOptimizedBridgeOrder(order)) {
 | 
				
			||||||
 | 
					            fqtData.bridgeOrders.push({
 | 
				
			||||||
 | 
					                bridgeData: createBridgeDataForBridgeOrder(order),
 | 
				
			||||||
 | 
					                makerTokenAmount: order.makerAmount,
 | 
				
			||||||
 | 
					                takerTokenAmount: order.takerAmount,
 | 
				
			||||||
 | 
					                source: getERC20BridgeSourceToBridgeSource(order.source),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else if (isOptimizedLimitOrder(order)) {
 | 
				
			||||||
 | 
					            fqtData.limitOrders.push({
 | 
				
			||||||
 | 
					                order: order.fillData.order,
 | 
				
			||||||
 | 
					                signature: order.fillData.signature,
 | 
				
			||||||
 | 
					                maxTakerTokenFillAmount: order.takerAmount,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else if (isOptimizedRfqOrder(order)) {
 | 
				
			||||||
 | 
					            fqtData.rfqOrders.push({
 | 
				
			||||||
 | 
					                order: order.fillData.order,
 | 
				
			||||||
 | 
					                signature: order.fillData.signature,
 | 
				
			||||||
 | 
					                maxTakerTokenFillAmount: order.takerAmount,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Should never happen
 | 
				
			||||||
 | 
					            throw new Error('Unknown Order type');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        fqtData.fillSequence.push(order.type);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return fqtData;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Returns true if swap quote must go through `tranformERC20`.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function requiresTransformERC20(opts: ExchangeProxyContractOpts): boolean {
 | 
				
			||||||
 | 
					    // Is a mtx.
 | 
				
			||||||
 | 
					    if (opts.isMetaTransaction) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Has an affiliate fee.
 | 
				
			||||||
 | 
					    if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Has a positive slippage fee.
 | 
				
			||||||
 | 
					    if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // VIP does not support selling the entire balance
 | 
				
			||||||
 | 
					    if (opts.shouldSellEntireBalance) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -102,9 +102,16 @@ export const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000);
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
 | 
					export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } = Object.assign(
 | 
					// HACK(mzhu25): Limit and RFQ orders need to be treated as different sources
 | 
				
			||||||
 | 
					//               when computing the exchange proxy gas overhead.
 | 
				
			||||||
 | 
					export const SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } & {
 | 
				
			||||||
 | 
					    RfqOrder: number;
 | 
				
			||||||
 | 
					    LimitOrder: number;
 | 
				
			||||||
 | 
					} = Object.assign(
 | 
				
			||||||
    {},
 | 
					    {},
 | 
				
			||||||
    ...Object.values(ERC20BridgeSource).map((source: ERC20BridgeSource, index) => ({ [source]: 1 << index })),
 | 
					    ...['RfqOrder', 'LimitOrder', ...Object.values(ERC20BridgeSource)].map(
 | 
				
			||||||
 | 
					        (source: ERC20BridgeSource | 'RfqOrder' | 'LimitOrder', index) => ({ [source]: 1 << index }),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MIRROR_WRAPPED_TOKENS = {
 | 
					const MIRROR_WRAPPED_TOKENS = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -83,7 +83,7 @@ function nativeOrdersToFills(
 | 
				
			|||||||
    // Create a single path from all orders.
 | 
					    // Create a single path from all orders.
 | 
				
			||||||
    let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
 | 
					    let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
 | 
				
			||||||
    for (const o of orders) {
 | 
					    for (const o of orders) {
 | 
				
			||||||
        const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount } = o;
 | 
					        const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = o;
 | 
				
			||||||
        const makerAmount = fillableMakerAmount;
 | 
					        const makerAmount = fillableMakerAmount;
 | 
				
			||||||
        const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount);
 | 
					        const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount);
 | 
				
			||||||
        const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
 | 
					        const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
 | 
				
			||||||
@@ -114,11 +114,11 @@ function nativeOrdersToFills(
 | 
				
			|||||||
            adjustedOutput,
 | 
					            adjustedOutput,
 | 
				
			||||||
            input: clippedInput,
 | 
					            input: clippedInput,
 | 
				
			||||||
            output: clippedOutput,
 | 
					            output: clippedOutput,
 | 
				
			||||||
            flags: SOURCE_FLAGS[ERC20BridgeSource.Native],
 | 
					            flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'],
 | 
				
			||||||
            index: 0, // TBD
 | 
					            index: 0, // TBD
 | 
				
			||||||
            parent: undefined, // TBD
 | 
					            parent: undefined, // TBD
 | 
				
			||||||
            source: ERC20BridgeSource.Native,
 | 
					            source: ERC20BridgeSource.Native,
 | 
				
			||||||
            type: o.type,
 | 
					            type,
 | 
				
			||||||
            fillData: { ...o },
 | 
					            fillData: { ...o },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@ import { MarketOperation } from '../../types';
 | 
				
			|||||||
import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
 | 
					import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
 | 
				
			||||||
import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types';
 | 
					import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tslint:disable:no-bitwise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Returns the fee-adjusted rate of a two-hop quote. Returns zero if the
 | 
					 * Returns the fee-adjusted rate of a two-hop quote. Returns zero if the
 | 
				
			||||||
 * quote falls short of the target input.
 | 
					 * quote falls short of the target input.
 | 
				
			||||||
@@ -22,7 +24,11 @@ export function getTwoHopAdjustedRate(
 | 
				
			|||||||
        return ZERO_AMOUNT;
 | 
					        return ZERO_AMOUNT;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const penalty = outputAmountPerEth.times(
 | 
					    const penalty = outputAmountPerEth.times(
 | 
				
			||||||
        exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)),
 | 
					        exchangeProxyOverhead(
 | 
				
			||||||
 | 
					            SOURCE_FLAGS.MultiHop |
 | 
				
			||||||
 | 
					                SOURCE_FLAGS[twoHopQuote.fillData.firstHopSource.source] |
 | 
				
			||||||
 | 
					                SOURCE_FLAGS[twoHopQuote.fillData.secondHopSource.source],
 | 
				
			||||||
 | 
					        ).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
 | 
					    const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
 | 
				
			||||||
    return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
 | 
					    return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user