@0x/asset-swapper: Add ExchangeProxySwapQuoteConsumer.
				
					
				
			This commit is contained in:
		@@ -89,6 +89,10 @@
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Fix Uniswap V2 path ordering",
 | 
			
		||||
                "pr": 2601
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Add exchange proxy support",
 | 
			
		||||
                "pr": 2591
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,7 @@
 | 
			
		||||
        "@0x/assert": "^3.0.7",
 | 
			
		||||
        "@0x/contract-addresses": "^4.9.0",
 | 
			
		||||
        "@0x/contract-wrappers": "^13.6.3",
 | 
			
		||||
        "@0x/contracts-zero-ex": "^0.1.0",
 | 
			
		||||
        "@0x/json-schemas": "^5.0.7",
 | 
			
		||||
        "@0x/order-utils": "^10.2.4",
 | 
			
		||||
        "@0x/orderbook": "^2.2.5",
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,7 @@ export {
 | 
			
		||||
    SwapQuoteConsumerError,
 | 
			
		||||
    SignedOrderWithFillableAmounts,
 | 
			
		||||
    SwapQuoteOrdersBreakdown,
 | 
			
		||||
    ExchangeProxyContractOpts,
 | 
			
		||||
} from './types';
 | 
			
		||||
export {
 | 
			
		||||
    ERC20BridgeSource,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,156 @@
 | 
			
		||||
import { ContractAddresses } from '@0x/contract-addresses';
 | 
			
		||||
import { ITransformERC20Contract } from '@0x/contract-wrappers';
 | 
			
		||||
import {
 | 
			
		||||
    encodeFillQuoteTransformerData,
 | 
			
		||||
    encodePayTakerTransformerData,
 | 
			
		||||
    encodeWethTransformerData,
 | 
			
		||||
    ETH_TOKEN_ADDRESS,
 | 
			
		||||
    FillQuoteTransformerSide,
 | 
			
		||||
} from '@0x/contracts-zero-ex';
 | 
			
		||||
import { assetDataUtils, ERC20AssetData } from '@0x/order-utils';
 | 
			
		||||
import { AssetProxyId } from '@0x/types';
 | 
			
		||||
import { BigNumber, providerUtils } from '@0x/utils';
 | 
			
		||||
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
 | 
			
		||||
import * as _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { constants } from '../constants';
 | 
			
		||||
import {
 | 
			
		||||
    CalldataInfo,
 | 
			
		||||
    ExchangeProxyContractOpts,
 | 
			
		||||
    MarketBuySwapQuote,
 | 
			
		||||
    MarketOperation,
 | 
			
		||||
    MarketSellSwapQuote,
 | 
			
		||||
    SwapQuote,
 | 
			
		||||
    SwapQuoteConsumerBase,
 | 
			
		||||
    SwapQuoteConsumerOpts,
 | 
			
		||||
    SwapQuoteExecutionOpts,
 | 
			
		||||
    SwapQuoteGetOutputOpts,
 | 
			
		||||
} from '../types';
 | 
			
		||||
import { assert } from '../utils/assert';
 | 
			
		||||
 | 
			
		||||
// tslint:disable-next-line:custom-no-magic-numbers
 | 
			
		||||
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
 | 
			
		||||
 | 
			
		||||
export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
    public readonly provider: ZeroExProvider;
 | 
			
		||||
    public readonly chainId: number;
 | 
			
		||||
 | 
			
		||||
    private readonly _transformFeature: ITransformERC20Contract;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        supportedProvider: SupportedProvider,
 | 
			
		||||
        public readonly contractAddresses: ContractAddresses,
 | 
			
		||||
        options: Partial<SwapQuoteConsumerOpts> = {},
 | 
			
		||||
    ) {
 | 
			
		||||
        const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
 | 
			
		||||
        assert.isNumber('chainId', chainId);
 | 
			
		||||
        const provider = providerUtils.standardizeOrThrow(supportedProvider);
 | 
			
		||||
        this.provider = provider;
 | 
			
		||||
        this.chainId = chainId;
 | 
			
		||||
        this.contractAddresses = contractAddresses;
 | 
			
		||||
        this._transformFeature = new ITransformERC20Contract(contractAddresses.exchangeProxy, supportedProvider);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async getCalldataOrThrowAsync(
 | 
			
		||||
        quote: MarketBuySwapQuote | MarketSellSwapQuote,
 | 
			
		||||
        opts: Partial<SwapQuoteGetOutputOpts> = {},
 | 
			
		||||
    ): Promise<CalldataInfo> {
 | 
			
		||||
        assert.isValidSwapQuote('quote', quote);
 | 
			
		||||
        const exchangeProxyOpts = {
 | 
			
		||||
            ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
 | 
			
		||||
            ...{
 | 
			
		||||
                isFromETH: false,
 | 
			
		||||
                isToETH: false,
 | 
			
		||||
            },
 | 
			
		||||
            ...opts,
 | 
			
		||||
        }.extensionContractOpts as ExchangeProxyContractOpts;
 | 
			
		||||
 | 
			
		||||
        const sellToken = getTokenFromAssetData(quote.takerAssetData);
 | 
			
		||||
        const buyToken = getTokenFromAssetData(quote.makerAssetData);
 | 
			
		||||
 | 
			
		||||
        // Build up the transforms.
 | 
			
		||||
        const transforms = [];
 | 
			
		||||
        if (exchangeProxyOpts.isFromETH) {
 | 
			
		||||
            // Create a WETH wrapper if coming from ETH.
 | 
			
		||||
            transforms.push({
 | 
			
		||||
                transformer: this.contractAddresses.transformers.wethTransformer,
 | 
			
		||||
                data: encodeWethTransformerData({
 | 
			
		||||
                    token: ETH_TOKEN_ADDRESS,
 | 
			
		||||
                    amount: quote.worstCaseQuoteInfo.totalTakerAssetAmount,
 | 
			
		||||
                }),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // This transformer will fill the quote.
 | 
			
		||||
        transforms.push({
 | 
			
		||||
            transformer: this.contractAddresses.transformers.fillQuoteTransformer,
 | 
			
		||||
            data: encodeFillQuoteTransformerData({
 | 
			
		||||
                sellToken,
 | 
			
		||||
                buyToken,
 | 
			
		||||
                side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
 | 
			
		||||
                fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount,
 | 
			
		||||
                maxOrderFillAmounts: [],
 | 
			
		||||
                orders: quote.orders,
 | 
			
		||||
                signatures: quote.orders.map(o => o.signature),
 | 
			
		||||
            }),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (exchangeProxyOpts.isToETH) {
 | 
			
		||||
            // Create a WETH unwrapper if going to ETH.
 | 
			
		||||
            transforms.push({
 | 
			
		||||
                transformer: this.contractAddresses.transformers.wethTransformer,
 | 
			
		||||
                data: encodeWethTransformerData({
 | 
			
		||||
                    token: this.contractAddresses.etherToken,
 | 
			
		||||
                    amount: MAX_UINT256,
 | 
			
		||||
                }),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The final transformer will send all funds to the taker.
 | 
			
		||||
        transforms.push({
 | 
			
		||||
            transformer: this.contractAddresses.transformers.payTakerTransformer,
 | 
			
		||||
            data: encodePayTakerTransformerData({
 | 
			
		||||
                tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS],
 | 
			
		||||
                amounts: [],
 | 
			
		||||
            }),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const calldataHexString = this._transformFeature
 | 
			
		||||
            .transformERC20(
 | 
			
		||||
                sellToken,
 | 
			
		||||
                buyToken,
 | 
			
		||||
                quote.worstCaseQuoteInfo.totalTakerAssetAmount,
 | 
			
		||||
                quote.worstCaseQuoteInfo.makerAssetAmount,
 | 
			
		||||
                transforms,
 | 
			
		||||
            )
 | 
			
		||||
            .getABIEncodedTransactionData();
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            calldataHexString,
 | 
			
		||||
            ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
 | 
			
		||||
            toAddress: this._transformFeature.address,
 | 
			
		||||
            allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // tslint:disable-next-line:prefer-function-over-method
 | 
			
		||||
    public async executeSwapQuoteOrThrowAsync(
 | 
			
		||||
        _quote: SwapQuote,
 | 
			
		||||
        _opts: Partial<SwapQuoteExecutionOpts>,
 | 
			
		||||
    ): Promise<string> {
 | 
			
		||||
        throw new Error('Execution not supported for Exchange Proxy quotes');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
 | 
			
		||||
    return quote.type === MarketOperation.Buy;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTokenFromAssetData(assetData: string): string {
 | 
			
		||||
    const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
 | 
			
		||||
    if (data.assetProxyId !== AssetProxyId.ERC20) {
 | 
			
		||||
        throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
 | 
			
		||||
    }
 | 
			
		||||
    // tslint:disable-next-line:no-unnecessary-type-assertion
 | 
			
		||||
    return (data as ERC20AssetData).tokenAddress;
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        supportedProvider: SupportedProvider,
 | 
			
		||||
        contractAddresses: ContractAddresses,
 | 
			
		||||
        public readonly contractAddresses: ContractAddresses,
 | 
			
		||||
        options: Partial<SwapQuoteConsumerOpts> = {},
 | 
			
		||||
    ) {
 | 
			
		||||
        const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
 | 
			
		||||
@@ -59,6 +59,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
            calldataHexString,
 | 
			
		||||
            ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
 | 
			
		||||
            toAddress: this._exchangeContract.address,
 | 
			
		||||
            allowanceTarget: this.contractAddresses.erc20Proxy,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,16 +19,17 @@ import { affiliateFeeUtils } from '../utils/affiliate_fee_utils';
 | 
			
		||||
import { assert } from '../utils/assert';
 | 
			
		||||
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
 | 
			
		||||
 | 
			
		||||
const { NULL_ADDRESS } = constants;
 | 
			
		||||
 | 
			
		||||
export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
    public readonly provider: ZeroExProvider;
 | 
			
		||||
    public readonly chainId: number;
 | 
			
		||||
 | 
			
		||||
    private readonly _contractAddresses: ContractAddresses;
 | 
			
		||||
    private readonly _forwarder: ForwarderContract;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        supportedProvider: SupportedProvider,
 | 
			
		||||
        contractAddresses: ContractAddresses,
 | 
			
		||||
        public readonly contractAddresses: ContractAddresses,
 | 
			
		||||
        options: Partial<SwapQuoteConsumerOpts> = {},
 | 
			
		||||
    ) {
 | 
			
		||||
        const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
 | 
			
		||||
@@ -36,7 +37,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
        const provider = providerUtils.standardizeOrThrow(supportedProvider);
 | 
			
		||||
        this.provider = provider;
 | 
			
		||||
        this.chainId = chainId;
 | 
			
		||||
        this._contractAddresses = contractAddresses;
 | 
			
		||||
        this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -90,6 +90,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
            calldataHexString,
 | 
			
		||||
            toAddress: this._forwarder.address,
 | 
			
		||||
            ethAmount: ethAmountWithFees,
 | 
			
		||||
            allowanceTarget: NULL_ADDRESS,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -160,6 +161,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private _getEtherTokenAssetDataOrThrow(): string {
 | 
			
		||||
        return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken);
 | 
			
		||||
        return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import {
 | 
			
		||||
import { assert } from '../utils/assert';
 | 
			
		||||
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
 | 
			
		||||
 | 
			
		||||
import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer';
 | 
			
		||||
import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer';
 | 
			
		||||
import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer';
 | 
			
		||||
 | 
			
		||||
@@ -27,6 +28,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
    private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
 | 
			
		||||
    private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer;
 | 
			
		||||
    private readonly _contractAddresses: ContractAddresses;
 | 
			
		||||
    private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;
 | 
			
		||||
 | 
			
		||||
    public static getSwapQuoteConsumer(
 | 
			
		||||
        supportedProvider: SupportedProvider,
 | 
			
		||||
@@ -45,6 +47,11 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
        this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
 | 
			
		||||
        this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
 | 
			
		||||
        this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
 | 
			
		||||
        this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer(
 | 
			
		||||
            supportedProvider,
 | 
			
		||||
            this._contractAddresses,
 | 
			
		||||
            options,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -93,9 +100,13 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
 | 
			
		||||
        if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
 | 
			
		||||
            return this._forwarderConsumer;
 | 
			
		||||
        switch (opts.useExtensionContract) {
 | 
			
		||||
            case ExtensionContractType.Forwarder:
 | 
			
		||||
                return this._forwarderConsumer;
 | 
			
		||||
            case ExtensionContractType.ExchangeProxy:
 | 
			
		||||
                return this._exchangeProxyConsumer;
 | 
			
		||||
            default:
 | 
			
		||||
                return this._exchangeConsumer;
 | 
			
		||||
        }
 | 
			
		||||
        return this._exchangeConsumer;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,19 +50,22 @@ export interface SignedOrderWithFillableAmounts extends SignedOrder {
 | 
			
		||||
 * calldataHexString: The hexstring of the calldata.
 | 
			
		||||
 * toAddress: The contract address to call.
 | 
			
		||||
 * ethAmount: The eth amount in wei to send with the smart contract call.
 | 
			
		||||
 * allowanceTarget: The address the taker should grant an allowance to.
 | 
			
		||||
 */
 | 
			
		||||
export interface CalldataInfo {
 | 
			
		||||
    calldataHexString: string;
 | 
			
		||||
    toAddress: string;
 | 
			
		||||
    ethAmount: BigNumber;
 | 
			
		||||
    allowanceTarget: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents the varying smart contracts that can consume a valid swap quote
 | 
			
		||||
 */
 | 
			
		||||
export enum ExtensionContractType {
 | 
			
		||||
    Forwarder = 'FORWARDER',
 | 
			
		||||
    None = 'NONE',
 | 
			
		||||
    Forwarder = 'FORWARDER',
 | 
			
		||||
    ExchangeProxy = 'EXCHANGE_PROXY',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -97,7 +100,7 @@ export interface SwapQuoteConsumerOpts {
 | 
			
		||||
 */
 | 
			
		||||
export interface SwapQuoteGetOutputOpts {
 | 
			
		||||
    useExtensionContract: ExtensionContractType;
 | 
			
		||||
    extensionContractOpts?: ForwarderExtensionContractOpts | any;
 | 
			
		||||
    extensionContractOpts?: ForwarderExtensionContractOpts | ExchangeProxyContractOpts | any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -112,7 +115,6 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * ethAmount: The amount of eth (in Wei) sent to the forwarder contract.
 | 
			
		||||
 * feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient
 | 
			
		||||
 * feeRecipient: address of the receiver of the feePercentage of taker asset
 | 
			
		||||
 */
 | 
			
		||||
@@ -121,6 +123,15 @@ export interface ForwarderExtensionContractOpts {
 | 
			
		||||
    feeRecipient: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param isFromETH Whether the input token is ETH.
 | 
			
		||||
 * @param isToETH Whether the output token is ETH.
 | 
			
		||||
 */
 | 
			
		||||
export interface ExchangeProxyContractOpts {
 | 
			
		||||
    isFromETH: boolean;
 | 
			
		||||
    isToETH: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
 | 
			
		||||
 | 
			
		||||
export interface GetExtensionContractTypeOpts {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,247 @@
 | 
			
		||||
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
 | 
			
		||||
import { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils';
 | 
			
		||||
import {
 | 
			
		||||
    decodeFillQuoteTransformerData,
 | 
			
		||||
    decodePayTakerTransformerData,
 | 
			
		||||
    decodeWethTransformerData,
 | 
			
		||||
    ETH_TOKEN_ADDRESS,
 | 
			
		||||
    FillQuoteTransformerSide,
 | 
			
		||||
} from '@0x/contracts-zero-ex';
 | 
			
		||||
import { assetDataUtils } from '@0x/order-utils';
 | 
			
		||||
import { Order } from '@0x/types';
 | 
			
		||||
import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils';
 | 
			
		||||
import * as chai from 'chai';
 | 
			
		||||
import * as _ from 'lodash';
 | 
			
		||||
import 'mocha';
 | 
			
		||||
 | 
			
		||||
import { constants } from '../src/constants';
 | 
			
		||||
import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
 | 
			
		||||
import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types';
 | 
			
		||||
import { OptimizedMarketOrder } from '../src/utils/market_operation_utils/types';
 | 
			
		||||
 | 
			
		||||
import { chaiSetup } from './utils/chai_setup';
 | 
			
		||||
 | 
			
		||||
chaiSetup.configure();
 | 
			
		||||
const expect = chai.expect;
 | 
			
		||||
 | 
			
		||||
const { NULL_ADDRESS } = constants;
 | 
			
		||||
const { MAX_UINT256 } = contractConstants;
 | 
			
		||||
 | 
			
		||||
// tslint:disable: custom-no-magic-numbers
 | 
			
		||||
 | 
			
		||||
describe('ExchangeProxySwapQuoteConsumer', () => {
 | 
			
		||||
    const CHAIN_ID = 1;
 | 
			
		||||
    const TAKER_TOKEN = randomAddress();
 | 
			
		||||
    const MAKER_TOKEN = randomAddress();
 | 
			
		||||
    const contractAddresses = {
 | 
			
		||||
        ...getContractAddressesForChainOrThrow(CHAIN_ID),
 | 
			
		||||
        exchangeProxy: randomAddress(),
 | 
			
		||||
        exchangeProxyAllowanceTarget: randomAddress(),
 | 
			
		||||
        transformers: {
 | 
			
		||||
            wethTransformer: randomAddress(),
 | 
			
		||||
            payTakerTransformer: randomAddress(),
 | 
			
		||||
            fillQuoteTransformer: randomAddress(),
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
    let consumer: ExchangeProxySwapQuoteConsumer;
 | 
			
		||||
 | 
			
		||||
    before(async () => {
 | 
			
		||||
        const fakeProvider = {
 | 
			
		||||
            async sendAsync(): Promise<void> {
 | 
			
		||||
                /* noop */
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        consumer = new ExchangeProxySwapQuoteConsumer(fakeProvider, contractAddresses, { chainId: CHAIN_ID });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function getRandomAmount(maxAmount: Numberish = '1e18'): BigNumber {
 | 
			
		||||
        return getRandomInteger(1, maxAmount);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function createAssetData(token?: string): string {
 | 
			
		||||
        return assetDataUtils.encodeERC20AssetData(token || randomAddress());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getRandomOrder(): OptimizedMarketOrder {
 | 
			
		||||
        return {
 | 
			
		||||
            fillableMakerAssetAmount: getRandomAmount(),
 | 
			
		||||
            fillableTakerFeeAmount: getRandomAmount(),
 | 
			
		||||
            fillableTakerAssetAmount: getRandomAmount(),
 | 
			
		||||
            fills: [],
 | 
			
		||||
            chainId: CHAIN_ID,
 | 
			
		||||
            exchangeAddress: contractAddresses.exchange,
 | 
			
		||||
            expirationTimeSeconds: getRandomInteger(1, 2e9),
 | 
			
		||||
            feeRecipientAddress: randomAddress(),
 | 
			
		||||
            makerAddress: randomAddress(),
 | 
			
		||||
            makerAssetAmount: getRandomAmount(),
 | 
			
		||||
            takerAssetAmount: getRandomAmount(),
 | 
			
		||||
            makerFee: getRandomAmount(),
 | 
			
		||||
            takerFee: getRandomAmount(),
 | 
			
		||||
            salt: getRandomAmount(2e9),
 | 
			
		||||
            signature: hexUtils.random(66),
 | 
			
		||||
            senderAddress: NULL_ADDRESS,
 | 
			
		||||
            takerAddress: NULL_ADDRESS,
 | 
			
		||||
            makerAssetData: createAssetData(MAKER_TOKEN),
 | 
			
		||||
            takerAssetData: createAssetData(TAKER_TOKEN),
 | 
			
		||||
            makerFeeAssetData: createAssetData(),
 | 
			
		||||
            takerFeeAssetData: createAssetData(),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getRandomQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote {
 | 
			
		||||
        return {
 | 
			
		||||
            gasPrice: getRandomInteger(1, 1e9),
 | 
			
		||||
            type: side,
 | 
			
		||||
            makerAssetData: createAssetData(MAKER_TOKEN),
 | 
			
		||||
            takerAssetData: createAssetData(TAKER_TOKEN),
 | 
			
		||||
            orders: [getRandomOrder()],
 | 
			
		||||
            bestCaseQuoteInfo: {
 | 
			
		||||
                feeTakerAssetAmount: getRandomAmount(),
 | 
			
		||||
                makerAssetAmount: getRandomAmount(),
 | 
			
		||||
                gas: Math.floor(Math.random() * 8e6),
 | 
			
		||||
                protocolFeeInWeiAmount: getRandomAmount(),
 | 
			
		||||
                takerAssetAmount: getRandomAmount(),
 | 
			
		||||
                totalTakerAssetAmount: getRandomAmount(),
 | 
			
		||||
            },
 | 
			
		||||
            worstCaseQuoteInfo: {
 | 
			
		||||
                feeTakerAssetAmount: getRandomAmount(),
 | 
			
		||||
                makerAssetAmount: getRandomAmount(),
 | 
			
		||||
                gas: Math.floor(Math.random() * 8e6),
 | 
			
		||||
                protocolFeeInWeiAmount: getRandomAmount(),
 | 
			
		||||
                takerAssetAmount: getRandomAmount(),
 | 
			
		||||
                totalTakerAssetAmount: getRandomAmount(),
 | 
			
		||||
            },
 | 
			
		||||
            ...(side === MarketOperation.Buy
 | 
			
		||||
                ? { makerAssetFillAmount: getRandomAmount() }
 | 
			
		||||
                : { takerAssetFillAmount: getRandomAmount() }),
 | 
			
		||||
        } as any;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getRandomSellQuote(): MarketSellSwapQuote {
 | 
			
		||||
        return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getRandomBuyQuote(): MarketBuySwapQuote {
 | 
			
		||||
        return getRandomQuote(MarketOperation.Buy) as MarketBuySwapQuote;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    type PlainOrder = Exclude<Order, ['chainId', 'exchangeAddress']>;
 | 
			
		||||
 | 
			
		||||
    function cleanOrders(orders: OptimizedMarketOrder[]): PlainOrder[] {
 | 
			
		||||
        return orders.map(
 | 
			
		||||
            o =>
 | 
			
		||||
                _.omit(o, [
 | 
			
		||||
                    'chainId',
 | 
			
		||||
                    'exchangeAddress',
 | 
			
		||||
                    'fillableMakerAssetAmount',
 | 
			
		||||
                    'fillableTakerAssetAmount',
 | 
			
		||||
                    'fillableTakerFeeAmount',
 | 
			
		||||
                    'fills',
 | 
			
		||||
                    'signature',
 | 
			
		||||
                ]) as PlainOrder,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const callDataEncoder = AbiEncoder.createMethod('transformERC20', [
 | 
			
		||||
        { type: 'address', name: 'inputToken' },
 | 
			
		||||
        { type: 'address', name: 'outputToken' },
 | 
			
		||||
        { type: 'uint256', name: 'inputTokenAmount' },
 | 
			
		||||
        { type: 'uint256', name: 'minOutputTokenAmount' },
 | 
			
		||||
        {
 | 
			
		||||
            type: 'tuple[]',
 | 
			
		||||
            name: 'transformations',
 | 
			
		||||
            components: [{ type: 'address', name: 'transformer' }, { type: 'bytes', name: 'data' }],
 | 
			
		||||
        },
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    interface CallArgs {
 | 
			
		||||
        inputToken: string;
 | 
			
		||||
        outputToken: string;
 | 
			
		||||
        inputTokenAmount: BigNumber;
 | 
			
		||||
        minOutputTokenAmount: BigNumber;
 | 
			
		||||
        transformations: Array<{
 | 
			
		||||
            transformer: string;
 | 
			
		||||
            data: string;
 | 
			
		||||
        }>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    describe('getCalldataOrThrow()', () => {
 | 
			
		||||
        it('can produce a sell quote', async () => {
 | 
			
		||||
            const quote = getRandomSellQuote();
 | 
			
		||||
            const callInfo = await consumer.getCalldataOrThrowAsync(quote);
 | 
			
		||||
            const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
 | 
			
		||||
            expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
 | 
			
		||||
            expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
 | 
			
		||||
            expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
 | 
			
		||||
            expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
 | 
			
		||||
            expect(callArgs.transformations).to.be.length(2);
 | 
			
		||||
            expect(callArgs.transformations[0].transformer === contractAddresses.transformers.fillQuoteTransformer);
 | 
			
		||||
            expect(callArgs.transformations[1].transformer === contractAddresses.transformers.payTakerTransformer);
 | 
			
		||||
            const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
 | 
			
		||||
            expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
 | 
			
		||||
            expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerAssetFillAmount);
 | 
			
		||||
            expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
 | 
			
		||||
            expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
 | 
			
		||||
            expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
 | 
			
		||||
            expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
 | 
			
		||||
            const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
 | 
			
		||||
            expect(payTakerTransformerData.amounts).to.deep.eq([]);
 | 
			
		||||
            expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('can produce a buy quote', async () => {
 | 
			
		||||
            const quote = getRandomBuyQuote();
 | 
			
		||||
            const callInfo = await consumer.getCalldataOrThrowAsync(quote);
 | 
			
		||||
            const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
 | 
			
		||||
            expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
 | 
			
		||||
            expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
 | 
			
		||||
            expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
 | 
			
		||||
            expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
 | 
			
		||||
            expect(callArgs.transformations).to.be.length(2);
 | 
			
		||||
            expect(callArgs.transformations[0].transformer === contractAddresses.transformers.fillQuoteTransformer);
 | 
			
		||||
            expect(callArgs.transformations[1].transformer === contractAddresses.transformers.payTakerTransformer);
 | 
			
		||||
            const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
 | 
			
		||||
            expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Buy);
 | 
			
		||||
            expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerAssetFillAmount);
 | 
			
		||||
            expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
 | 
			
		||||
            expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
 | 
			
		||||
            expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
 | 
			
		||||
            expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
 | 
			
		||||
            const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
 | 
			
		||||
            expect(payTakerTransformerData.amounts).to.deep.eq([]);
 | 
			
		||||
            expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('ERC20 -> ERC20 does not have a WETH transformer', async () => {
 | 
			
		||||
            const quote = getRandomSellQuote();
 | 
			
		||||
            const callInfo = await consumer.getCalldataOrThrowAsync(quote);
 | 
			
		||||
            const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
 | 
			
		||||
            const transformers = callArgs.transformations.map(t => t.transformer);
 | 
			
		||||
            expect(transformers).to.not.include(contractAddresses.transformers.wethTransformer);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('ETH -> ERC20 has a WETH transformer before the fill', async () => {
 | 
			
		||||
            const quote = getRandomSellQuote();
 | 
			
		||||
            const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
 | 
			
		||||
                extensionContractOpts: { isFromETH: true },
 | 
			
		||||
            });
 | 
			
		||||
            const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
 | 
			
		||||
            expect(callArgs.transformations[0].transformer).to.eq(contractAddresses.transformers.wethTransformer);
 | 
			
		||||
            const wethTransformerData = decodeWethTransformerData(callArgs.transformations[0].data);
 | 
			
		||||
            expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
 | 
			
		||||
            expect(wethTransformerData.token).to.eq(ETH_TOKEN_ADDRESS);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('ERC20 -> ETH has a WETH transformer after the fill', async () => {
 | 
			
		||||
            const quote = getRandomSellQuote();
 | 
			
		||||
            const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
 | 
			
		||||
                extensionContractOpts: { isToETH: true },
 | 
			
		||||
            });
 | 
			
		||||
            const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
 | 
			
		||||
            expect(callArgs.transformations[1].transformer).to.eq(contractAddresses.transformers.wethTransformer);
 | 
			
		||||
            const wethTransformerData = decodeWethTransformerData(callArgs.transformations[1].data);
 | 
			
		||||
            expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256);
 | 
			
		||||
            expect(wethTransformerData.token).to.eq(contractAddresses.etherToken);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user