import { schemas } from '@0xproject/json-schemas';
import {
    BlockParamLiteral,
    DecodedLogArgs,
    ECSignature,
    LogEntry,
    LogWithDecodedArgs,
    Order,
    SignedOrder,
} from '@0xproject/types';
import { AbiDecoder, BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';

import { artifacts } from '../artifacts';
import { SimpleBalanceAndProxyAllowanceFetcher } from '../fetchers/simple_balance_and_proxy_allowance_fetcher';
import { SimpleOrderFilledCancelledFetcher } from '../fetchers/simple_order_filled_cancelled_fetcher';
import {
    BlockRange,
    EventCallback,
    ExchangeContractErrCodes,
    ExchangeContractErrs,
    IndexedFilterValues,
    MethodOpts,
    OrderAddresses,
    OrderCancellationRequest,
    OrderFillRequest,
    OrderState,
    OrderTransactionOpts,
    OrderValues,
    ValidateOrderFillableOpts,
} from '../types';
import { assert } from '../utils/assert';
import { decorators } from '../utils/decorators';
import { ExchangeTransferSimulator } from '../utils/exchange_transfer_simulator';
import { OrderStateUtils } from '../utils/order_state_utils';
import { OrderValidationUtils } from '../utils/order_validation_utils';
import { utils } from '../utils/utils';

import { ContractWrapper } from './contract_wrapper';
import {
    ExchangeContract,
    ExchangeContractEventArgs,
    ExchangeEvents,
    LogErrorContractEventArgs,
} from './generated/exchange';
import { TokenWrapper } from './token_wrapper';
const SHOULD_VALIDATE_BY_DEFAULT = true;

interface ExchangeContractErrCodesToMsgs {
    [exchangeContractErrCodes: number]: string;
}

/**
 * This class includes all the functionality related to calling methods and subscribing to
 * events of the 0x Exchange smart contract.
 */
export class ExchangeWrapper extends ContractWrapper {
    private _exchangeContractIfExists?: ExchangeContract;
    private _orderValidationUtils: OrderValidationUtils;
    private _tokenWrapper: TokenWrapper;
    private _exchangeContractErrCodesToMsg: ExchangeContractErrCodesToMsgs = {
        [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.OrderFillExpired,
        [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.OrderFillExpired,
        [ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero,
        [ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero,
        [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.OrderFillRoundingError,
        [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FillBalanceAllowanceError,
    };
    private _contractAddressIfExists?: string;
    private _zrxContractAddressIfExists?: string;
    private static _getOrderAddressesAndValues(order: Order): [OrderAddresses, OrderValues] {
        const orderAddresses: OrderAddresses = [
            order.maker,
            order.taker,
            order.makerTokenAddress,
            order.takerTokenAddress,
            order.feeRecipient,
        ];
        const orderValues: OrderValues = [
            order.makerTokenAmount,
            order.takerTokenAmount,
            order.makerFee,
            order.takerFee,
            order.expirationUnixTimestampSec,
            order.salt,
        ];
        return [orderAddresses, orderValues];
    }
    constructor(
        web3Wrapper: Web3Wrapper,
        networkId: number,
        tokenWrapper: TokenWrapper,
        contractAddressIfExists?: string,
        zrxContractAddressIfExists?: string,
    ) {
        super(web3Wrapper, networkId);
        this._tokenWrapper = tokenWrapper;
        this._orderValidationUtils = new OrderValidationUtils(this);
        this._contractAddressIfExists = contractAddressIfExists;
        this._zrxContractAddressIfExists = zrxContractAddressIfExists;
    }
    /**
     * Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total
     * amount that has been filled or cancelled. The remaining takerAmount can be calculated by
     * subtracting the unavailable amount from the total order takerAmount.
     * @param   orderHash               The hex encoded orderHash for which you would like to retrieve the
     *                                  unavailable takerAmount.
     * @param   methodOpts              Optional arguments this method accepts.
     * @return  The amount of the order (in taker tokens) that has either been filled or cancelled.
     */
    public async getUnavailableTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
        assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);

        const exchangeContract = await this._getExchangeContractAsync();
        const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
        const txData = {};
        let unavailableTakerTokenAmount = await exchangeContract.getUnavailableTakerTokenAmount.callAsync(
            orderHash,
            txData,
            defaultBlock,
        );
        // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
        unavailableTakerTokenAmount = new BigNumber(unavailableTakerTokenAmount);
        return unavailableTakerTokenAmount;
    }
    /**
     * Retrieve the takerAmount of an order that has already been filled.
     * @param   orderHash    The hex encoded orderHash for which you would like to retrieve the filled takerAmount.
     * @param   methodOpts   Optional arguments this method accepts.
     * @return  The amount of the order (in taker tokens) that has already been filled.
     */
    public async getFilledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
        assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);

        const exchangeContract = await this._getExchangeContractAsync();
        const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
        const txData = {};
        let fillAmountInBaseUnits = await exchangeContract.filled.callAsync(orderHash, txData, defaultBlock);
        // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
        fillAmountInBaseUnits = new BigNumber(fillAmountInBaseUnits);
        return fillAmountInBaseUnits;
    }
    /**
     * Retrieve the takerAmount of an order that has been cancelled.
     * @param   orderHash    The hex encoded orderHash for which you would like to retrieve the
     *                       cancelled takerAmount.
     * @param   methodOpts   Optional arguments this method accepts.
     * @return  The amount of the order (in taker tokens) that has been cancelled.
     */
    public async getCancelledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
        assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);

        const exchangeContract = await this._getExchangeContractAsync();
        const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
        const txData = {};
        let cancelledAmountInBaseUnits = await exchangeContract.cancelled.callAsync(orderHash, txData, defaultBlock);
        // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
        cancelledAmountInBaseUnits = new BigNumber(cancelledAmountInBaseUnits);
        return cancelledAmountInBaseUnits;
    }
    /**
     * Fills a signed order with an amount denominated in baseUnits of the taker token.
     * Since the order in which transactions are included in the next block is indeterminate, race-conditions
     * could arise where a users balance or allowance changes before the fillOrder executes. Because of this,
     * we allow you to specify `shouldThrowOnInsufficientBalanceOrAllowance`.
     * If false, the smart contract will not throw if the parties
     * do not have sufficient balances/allowances, preserving gas costs. Setting it to true forgoes this check
     * and causes the smart contract to throw (using all the gas supplied) instead.
     * @param   signedOrder                                 An object that conforms to the SignedOrder interface.
     * @param   fillTakerTokenAmount                        The amount of the order (in taker tokens baseUnits) that
     *                                                      you wish to fill.
     * @param   shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw
     *                                                      if upon execution the tokens cannot be transferred.
     * @param   takerAddress                                The user Ethereum address who would like to fill this order.
     *                                                      Must be available via the supplied Provider
     *                                                      passed to 0x.js.
     * @param   orderTransactionOpts                        Optional arguments this method accepts.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async fillOrderAsync(
        signedOrder: SignedOrder,
        fillTakerTokenAmount: BigNumber,
        shouldThrowOnInsufficientBalanceOrAllowance: boolean,
        takerAddress: string,
        orderTransactionOpts: OrderTransactionOpts = {},
    ): Promise<string> {
        assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
        assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
        assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        const normalizedTakerAddress = takerAddress.toLowerCase();

        const exchangeInstance = await this._getExchangeContractAsync();
        const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
            ? SHOULD_VALIDATE_BY_DEFAULT
            : orderTransactionOpts.shouldValidate;
        if (shouldValidate) {
            const zrxTokenAddress = this.getZRXTokenAddress();
            const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
            await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
                exchangeTradeEmulator,
                signedOrder,
                fillTakerTokenAmount,
                normalizedTakerAddress,
                zrxTokenAddress,
            );
        }

        const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);

        const txHash: string = await exchangeInstance.fillOrder.sendTransactionAsync(
            orderAddresses,
            orderValues,
            fillTakerTokenAmount,
            shouldThrowOnInsufficientBalanceOrAllowance,
            signedOrder.ecSignature.v,
            signedOrder.ecSignature.r,
            signedOrder.ecSignature.s,
            {
                from: normalizedTakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
            },
        );
        return txHash;
    }
    /**
     * Sequentially and atomically fills signedOrders up to the specified takerTokenFillAmount.
     * If the fill amount is reached - it succeeds and does not fill the rest of the orders.
     * If fill amount is not reached - it fills as much of the fill amount as possible and succeeds.
     * @param   signedOrders                                The array of signedOrders that you would like to fill until
     *                                                      takerTokenFillAmount is reached.
     * @param   fillTakerTokenAmount                        The total amount of the takerTokens you would like to fill.
     * @param   shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw if
     *                                                      upon execution any of the tokens cannot be transferred.
     *                                                      If set to false, the call will continue to fill subsequent
     *                                                      signedOrders even when some cannot be filled.
     * @param   takerAddress                                The user Ethereum address who would like to fill these
     *                                                      orders. Must be available via the supplied Provider
     *                                                      passed to 0x.js.
     * @param   orderTransactionOpts                        Optional arguments this method accepts.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async fillOrdersUpToAsync(
        signedOrders: SignedOrder[],
        fillTakerTokenAmount: BigNumber,
        shouldThrowOnInsufficientBalanceOrAllowance: boolean,
        takerAddress: string,
        orderTransactionOpts: OrderTransactionOpts = {},
    ): Promise<string> {
        assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
        const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress);
        assert.hasAtMostOneUniqueValue(
            takerTokenAddresses,
            ExchangeContractErrs.MultipleTakerTokensInFillUpToDisallowed,
        );
        const exchangeContractAddresses = _.map(signedOrders, signedOrder => signedOrder.exchangeContractAddress);
        assert.hasAtMostOneUniqueValue(
            exchangeContractAddresses,
            ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress,
        );
        assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
        assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        const normalizedTakerAddress = takerAddress.toLowerCase();

        const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
            ? SHOULD_VALIDATE_BY_DEFAULT
            : orderTransactionOpts.shouldValidate;
        if (shouldValidate) {
            let filledTakerTokenAmount = new BigNumber(0);
            const zrxTokenAddress = this.getZRXTokenAddress();
            const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
            for (const signedOrder of signedOrders) {
                const singleFilledTakerTokenAmount = await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
                    exchangeTradeEmulator,
                    signedOrder,
                    fillTakerTokenAmount.minus(filledTakerTokenAmount),
                    normalizedTakerAddress,
                    zrxTokenAddress,
                );
                filledTakerTokenAmount = filledTakerTokenAmount.plus(singleFilledTakerTokenAmount);
                if (filledTakerTokenAmount.eq(fillTakerTokenAmount)) {
                    break;
                }
            }
        }

        if (_.isEmpty(signedOrders)) {
            throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
        }

        const orderAddressesValuesAndSignatureArray = _.map(signedOrders, signedOrder => {
            return [
                ...ExchangeWrapper._getOrderAddressesAndValues(signedOrder),
                signedOrder.ecSignature.v,
                signedOrder.ecSignature.r,
                signedOrder.ecSignature.s,
            ];
        });
        // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
        const [orderAddressesArray, orderValuesArray, vArray, rArray, sArray] = _.unzip<any>(
            orderAddressesValuesAndSignatureArray,
        );

        const exchangeInstance = await this._getExchangeContractAsync();
        const txHash = await exchangeInstance.fillOrdersUpTo.sendTransactionAsync(
            orderAddressesArray,
            orderValuesArray,
            fillTakerTokenAmount,
            shouldThrowOnInsufficientBalanceOrAllowance,
            vArray,
            rArray,
            sArray,
            {
                from: normalizedTakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
            },
        );
        return txHash;
    }
    /**
     * Batch version of fillOrderAsync.
     * Executes multiple fills atomically in a single transaction.
     * If shouldThrowOnInsufficientBalanceOrAllowance is set to false, it will continue filling subsequent orders even
     * when earlier ones fail.
     * When shouldThrowOnInsufficientBalanceOrAllowance is set to true, if any fill fails, the entire batch fails.
     * @param   orderFillRequests                               An array of objects that conform to the
     *                                                          OrderFillRequest interface.
     * @param   shouldThrowOnInsufficientBalanceOrAllowance     Whether or not you wish for the contract call to throw
     *                                                          if upon execution any of the tokens cannot be
     *                                                          transferred. If set to false, the call will continue to
     *                                                          fill subsequent signedOrders even when some
     *                                                          cannot be filled.
     * @param   takerAddress                                    The user Ethereum address who would like to fill
     *                                                          these orders. Must be available via the supplied
     *                                                          Provider passed to 0x.js.
     * @param   orderTransactionOpts                            Optional arguments this method accepts.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async batchFillOrdersAsync(
        orderFillRequests: OrderFillRequest[],
        shouldThrowOnInsufficientBalanceOrAllowance: boolean,
        takerAddress: string,
        orderTransactionOpts: OrderTransactionOpts = {},
    ): Promise<string> {
        assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema);
        const exchangeContractAddresses = _.map(
            orderFillRequests,
            orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress,
        );
        assert.hasAtMostOneUniqueValue(
            exchangeContractAddresses,
            ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress,
        );
        assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        const normalizedTakerAddress = takerAddress.toLowerCase();
        const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
            ? SHOULD_VALIDATE_BY_DEFAULT
            : orderTransactionOpts.shouldValidate;
        if (shouldValidate) {
            const zrxTokenAddress = this.getZRXTokenAddress();
            const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
            for (const orderFillRequest of orderFillRequests) {
                await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
                    exchangeTradeEmulator,
                    orderFillRequest.signedOrder,
                    orderFillRequest.takerTokenFillAmount,
                    normalizedTakerAddress,
                    zrxTokenAddress,
                );
            }
        }
        if (_.isEmpty(orderFillRequests)) {
            throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
        }

        const orderAddressesValuesAmountsAndSignatureArray = _.map(orderFillRequests, orderFillRequest => {
            return [
                ...ExchangeWrapper._getOrderAddressesAndValues(orderFillRequest.signedOrder),
                orderFillRequest.takerTokenFillAmount,
                orderFillRequest.signedOrder.ecSignature.v,
                orderFillRequest.signedOrder.ecSignature.r,
                orderFillRequest.signedOrder.ecSignature.s,
            ];
        });
        // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
        const [orderAddressesArray, orderValuesArray, fillTakerTokenAmounts, vArray, rArray, sArray] = _.unzip<any>(
            orderAddressesValuesAmountsAndSignatureArray,
        );

        const exchangeInstance = await this._getExchangeContractAsync();
        const txHash = await exchangeInstance.batchFillOrders.sendTransactionAsync(
            orderAddressesArray,
            orderValuesArray,
            fillTakerTokenAmounts,
            shouldThrowOnInsufficientBalanceOrAllowance,
            vArray,
            rArray,
            sArray,
            {
                from: normalizedTakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
            },
        );
        return txHash;
    }
    /**
     * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled,
     * the fill order is abandoned.
     * @param   signedOrder             An object that conforms to the SignedOrder interface. The
     *                                  signedOrder you wish to fill.
     * @param   fillTakerTokenAmount    The total amount of the takerTokens you would like to fill.
     * @param   takerAddress            The user Ethereum address who would like to fill this order.
     *                                  Must be available via the supplied Provider passed to 0x.js.
     * @param   orderTransactionOpts    Optional arguments this method accepts.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async fillOrKillOrderAsync(
        signedOrder: SignedOrder,
        fillTakerTokenAmount: BigNumber,
        takerAddress: string,
        orderTransactionOpts: OrderTransactionOpts = {},
    ): Promise<string> {
        assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
        assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        const normalizedTakerAddress = takerAddress.toLowerCase();

        const exchangeInstance = await this._getExchangeContractAsync();

        const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
            ? SHOULD_VALIDATE_BY_DEFAULT
            : orderTransactionOpts.shouldValidate;
        if (shouldValidate) {
            const zrxTokenAddress = this.getZRXTokenAddress();
            const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
            await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
                exchangeTradeEmulator,
                signedOrder,
                fillTakerTokenAmount,
                normalizedTakerAddress,
                zrxTokenAddress,
            );
        }

        const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);
        const txHash = await exchangeInstance.fillOrKillOrder.sendTransactionAsync(
            orderAddresses,
            orderValues,
            fillTakerTokenAmount,
            signedOrder.ecSignature.v,
            signedOrder.ecSignature.r,
            signedOrder.ecSignature.s,
            {
                from: normalizedTakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
            },
        );
        return txHash;
    }
    /**
     * Batch version of fillOrKill. Allows a taker to specify a batch of orders that will either be atomically
     * filled (each to the specified fillAmount) or aborted.
     * @param   orderFillRequests           An array of objects that conform to the OrderFillRequest interface.
     * @param   takerAddress                The user Ethereum address who would like to fill there orders.
     *                                      Must be available via the supplied Provider passed to 0x.js.
     * @param   orderTransactionOpts        Optional arguments this method accepts.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async batchFillOrKillAsync(
        orderFillRequests: OrderFillRequest[],
        takerAddress: string,
        orderTransactionOpts: OrderTransactionOpts = {},
    ): Promise<string> {
        assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema);
        const exchangeContractAddresses = _.map(
            orderFillRequests,
            orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress,
        );
        assert.hasAtMostOneUniqueValue(
            exchangeContractAddresses,
            ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress,
        );
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        const normalizedTakerAddress = takerAddress.toLowerCase();
        if (_.isEmpty(orderFillRequests)) {
            throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
        }
        const exchangeInstance = await this._getExchangeContractAsync();

        const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
            ? SHOULD_VALIDATE_BY_DEFAULT
            : orderTransactionOpts.shouldValidate;
        if (shouldValidate) {
            const zrxTokenAddress = this.getZRXTokenAddress();
            const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
            for (const orderFillRequest of orderFillRequests) {
                await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
                    exchangeTradeEmulator,
                    orderFillRequest.signedOrder,
                    orderFillRequest.takerTokenFillAmount,
                    normalizedTakerAddress,
                    zrxTokenAddress,
                );
            }
        }

        const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillRequests, request => {
            return [
                ...ExchangeWrapper._getOrderAddressesAndValues(request.signedOrder),
                request.takerTokenFillAmount,
                request.signedOrder.ecSignature.v,
                request.signedOrder.ecSignature.r,
                request.signedOrder.ecSignature.s,
            ];
        });

        // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
        const [orderAddresses, orderValues, fillTakerTokenAmounts, vParams, rParams, sParams] = _.unzip<any>(
            orderAddressesValuesAndTakerTokenFillAmounts,
        );
        const txHash = await exchangeInstance.batchFillOrKillOrders.sendTransactionAsync(
            orderAddresses,
            orderValues,
            fillTakerTokenAmounts,
            vParams,
            rParams,
            sParams,
            {
                from: normalizedTakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
            },
        );
        return txHash;
    }
    /**
     * Cancel a given fill amount of an order. Cancellations are cumulative.
     * @param   order                   An object that conforms to the Order or SignedOrder interface.
     *                                  The order you would like to cancel.
     * @param   cancelTakerTokenAmount  The amount (specified in taker tokens) that you would like to cancel.
     * @param   transactionOpts         Optional arguments this method accepts.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async cancelOrderAsync(
        order: Order | SignedOrder,
        cancelTakerTokenAmount: BigNumber,
        orderTransactionOpts: OrderTransactionOpts = {},
    ): Promise<string> {
        assert.doesConformToSchema('order', order, schemas.orderSchema);
        assert.isValidBaseUnitAmount('takerTokenCancelAmount', cancelTakerTokenAmount);
        await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper);
        const normalizedMakerAddress = order.maker.toLowerCase();

        const exchangeInstance = await this._getExchangeContractAsync();

        const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
            ? SHOULD_VALIDATE_BY_DEFAULT
            : orderTransactionOpts.shouldValidate;
        if (shouldValidate) {
            const orderHash = utils.getOrderHashHex(order);
            const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
            OrderValidationUtils.validateCancelOrderThrowIfInvalid(
                order,
                cancelTakerTokenAmount,
                unavailableTakerTokenAmount,
            );
        }

        const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order);
        const txHash = await exchangeInstance.cancelOrder.sendTransactionAsync(
            orderAddresses,
            orderValues,
            cancelTakerTokenAmount,
            {
                from: normalizedMakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
            },
        );
        return txHash;
    }
    /**
     * Batch version of cancelOrderAsync. Atomically cancels multiple orders in a single transaction.
     * All orders must be from the same maker.
     * @param   orderCancellationRequests   An array of objects that conform to the OrderCancellationRequest
     *                                      interface.
     * @param   transactionOpts             Optional arguments this method accepts.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async batchCancelOrdersAsync(
        orderCancellationRequests: OrderCancellationRequest[],
        orderTransactionOpts: OrderTransactionOpts = {},
    ): Promise<string> {
        assert.doesConformToSchema(
            'orderCancellationRequests',
            orderCancellationRequests,
            schemas.orderCancellationRequestsSchema,
        );
        const exchangeContractAddresses = _.map(
            orderCancellationRequests,
            orderCancellationRequest => orderCancellationRequest.order.exchangeContractAddress,
        );
        assert.hasAtMostOneUniqueValue(
            exchangeContractAddresses,
            ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress,
        );
        const makers = _.map(orderCancellationRequests, cancellationRequest => cancellationRequest.order.maker);
        assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed);
        const maker = makers[0];
        await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper);
        const normalizedMakerAddress = maker.toLowerCase();

        const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
            ? SHOULD_VALIDATE_BY_DEFAULT
            : orderTransactionOpts.shouldValidate;
        if (shouldValidate) {
            for (const orderCancellationRequest of orderCancellationRequests) {
                const orderHash = utils.getOrderHashHex(orderCancellationRequest.order);
                const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
                OrderValidationUtils.validateCancelOrderThrowIfInvalid(
                    orderCancellationRequest.order,
                    orderCancellationRequest.takerTokenCancelAmount,
                    unavailableTakerTokenAmount,
                );
            }
        }
        if (_.isEmpty(orderCancellationRequests)) {
            throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
        }
        const exchangeInstance = await this._getExchangeContractAsync();
        const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => {
            return [
                ...ExchangeWrapper._getOrderAddressesAndValues(cancellationRequest.order),
                cancellationRequest.takerTokenCancelAmount,
            ];
        });
        // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
        const [orderAddresses, orderValues, cancelTakerTokenAmounts] = _.unzip<any>(
            orderAddressesValuesAndTakerTokenCancelAmounts,
        );
        const txHash = await exchangeInstance.batchCancelOrders.sendTransactionAsync(
            orderAddresses,
            orderValues,
            cancelTakerTokenAmounts,
            {
                from: normalizedMakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
            },
        );
        return txHash;
    }
    /**
     * Subscribe to an event type emitted by the Exchange contract.
     * @param   eventName           The exchange contract event you would like to subscribe to.
     * @param   indexFilterValues   An object where the keys are indexed args returned by the event and
     *                              the value is the value you are interested in. E.g `{maker: aUserAddressHex}`
     * @param   callback            Callback that gets called when a log is added/removed
     * @return Subscription token used later to unsubscribe
     */
    public subscribe<ArgsType extends ExchangeContractEventArgs>(
        eventName: ExchangeEvents,
        indexFilterValues: IndexedFilterValues,
        callback: EventCallback<ArgsType>,
    ): string {
        assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
        assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
        assert.isFunction('callback', callback);
        const exchangeContractAddress = this.getContractAddress();
        const subscriptionToken = this._subscribe<ArgsType>(
            exchangeContractAddress,
            eventName,
            indexFilterValues,
            artifacts.ExchangeArtifact.abi,
            callback,
        );
        return subscriptionToken;
    }
    /**
     * Cancel a subscription
     * @param   subscriptionToken Subscription token returned by `subscribe()`
     */
    public unsubscribe(subscriptionToken: string): void {
        this._unsubscribe(subscriptionToken);
    }
    /**
     * Cancels all existing subscriptions
     */
    public unsubscribeAll(): void {
        super._unsubscribeAll();
    }
    /**
     * Gets historical logs without creating a subscription
     * @param   eventName           The exchange contract event you would like to subscribe to.
     * @param   blockRange          Block range to get logs from.
     * @param   indexFilterValues   An object where the keys are indexed args returned by the event and
     *                              the value is the value you are interested in. E.g `{_from: aUserAddressHex}`
     * @return  Array of logs that match the parameters
     */
    public async getLogsAsync<ArgsType extends ExchangeContractEventArgs>(
        eventName: ExchangeEvents,
        blockRange: BlockRange,
        indexFilterValues: IndexedFilterValues,
    ): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
        assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
        assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema);
        assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
        const exchangeContractAddress = this.getContractAddress();
        const logs = await this._getLogsAsync<ArgsType>(
            exchangeContractAddress,
            eventName,
            blockRange,
            indexFilterValues,
            artifacts.ExchangeArtifact.abi,
        );
        return logs;
    }
    /**
     * Retrieves the Ethereum address of the Exchange contract deployed on the network
     * that the user-passed web3 provider is connected to.
     * @returns The Ethereum address of the Exchange contract being used.
     */
    public getContractAddress(): string {
        const contractAddress = this._getContractAddress(artifacts.ExchangeArtifact, this._contractAddressIfExists);
        return contractAddress;
    }
    /**
     * Checks if order is still fillable and throws an error otherwise. Useful for orderbook
     * pruning where you want to remove stale orders without knowing who the taker will be.
     * @param   signedOrder     An object that conforms to the SignedOrder interface. The
     *                          signedOrder you wish to validate.
     * @param   opts            An object that conforms to the ValidateOrderFillableOpts
     *                          interface. Allows specifying a specific fillTakerTokenAmount
     *                          to validate for.
     */
    public async validateOrderFillableOrThrowAsync(
        signedOrder: SignedOrder,
        opts?: ValidateOrderFillableOpts,
    ): Promise<void> {
        assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
        const zrxTokenAddress = this.getZRXTokenAddress();
        const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined;
        const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
        await this._orderValidationUtils.validateOrderFillableOrThrowAsync(
            exchangeTradeEmulator,
            signedOrder,
            zrxTokenAddress,
            expectedFillTakerTokenAmount,
        );
    }
    /**
     * Checks if order fill will succeed and throws an error otherwise.
     * @param   signedOrder             An object that conforms to the SignedOrder interface. The
     *                                  signedOrder you wish to fill.
     * @param   fillTakerTokenAmount    The total amount of the takerTokens you would like to fill.
     * @param   takerAddress            The user Ethereum address who would like to fill this order.
     *                                  Must be available via the supplied Provider passed to 0x.js.
     */
    public async validateFillOrderThrowIfInvalidAsync(
        signedOrder: SignedOrder,
        fillTakerTokenAmount: BigNumber,
        takerAddress: string,
    ): Promise<void> {
        assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
        assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        const normalizedTakerAddress = takerAddress.toLowerCase();
        const zrxTokenAddress = this.getZRXTokenAddress();
        const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
        await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
            exchangeTradeEmulator,
            signedOrder,
            fillTakerTokenAmount,
            normalizedTakerAddress,
            zrxTokenAddress,
        );
    }
    /**
     * Checks if cancelling a given order will succeed and throws an informative error if it won't.
     * @param   order                   An object that conforms to the Order or SignedOrder interface.
     *                                  The order you would like to cancel.
     * @param   cancelTakerTokenAmount  The amount (specified in taker tokens) that you would like to cancel.
     */
    public async validateCancelOrderThrowIfInvalidAsync(
        order: Order,
        cancelTakerTokenAmount: BigNumber,
    ): Promise<void> {
        assert.doesConformToSchema('order', order, schemas.orderSchema);
        assert.isValidBaseUnitAmount('cancelTakerTokenAmount', cancelTakerTokenAmount);
        const orderHash = utils.getOrderHashHex(order);
        const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
        OrderValidationUtils.validateCancelOrderThrowIfInvalid(
            order,
            cancelTakerTokenAmount,
            unavailableTakerTokenAmount,
        );
    }
    /**
     * Checks if calling fillOrKill on a given order will succeed and throws an informative error if it won't.
     * @param   signedOrder             An object that conforms to the SignedOrder interface. The
     *                                  signedOrder you wish to fill.
     * @param   fillTakerTokenAmount    The total amount of the takerTokens you would like to fill.
     * @param   takerAddress            The user Ethereum address who would like to fill this order.
     *                                  Must be available via the supplied Provider passed to 0x.js.
     */
    public async validateFillOrKillOrderThrowIfInvalidAsync(
        signedOrder: SignedOrder,
        fillTakerTokenAmount: BigNumber,
        takerAddress: string,
    ): Promise<void> {
        assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
        assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        const normalizedTakerAddress = takerAddress.toLowerCase();
        const zrxTokenAddress = this.getZRXTokenAddress();
        const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
        await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
            exchangeTradeEmulator,
            signedOrder,
            fillTakerTokenAmount,
            normalizedTakerAddress,
            zrxTokenAddress,
        );
    }
    /**
     * Checks if rounding error will be > 0.1% when computing makerTokenAmount by doing:
     * `(fillTakerTokenAmount * makerTokenAmount) / takerTokenAmount`.
     * 0x Protocol does not accept any trades that result in large rounding errors. This means that tokens with few or
     * no decimals can only be filled in quantities and ratios that avoid large rounding errors.
     * @param   fillTakerTokenAmount   The amount of the order (in taker tokens baseUnits) that you wish to fill.
     * @param   takerTokenAmount       The order size on the taker side
     * @param   makerTokenAmount       The order size on the maker side
     */
    public async isRoundingErrorAsync(
        fillTakerTokenAmount: BigNumber,
        takerTokenAmount: BigNumber,
        makerTokenAmount: BigNumber,
    ): Promise<boolean> {
        assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
        assert.isValidBaseUnitAmount('takerTokenAmount', takerTokenAmount);
        assert.isValidBaseUnitAmount('makerTokenAmount', makerTokenAmount);
        const exchangeInstance = await this._getExchangeContractAsync();
        const isRoundingError = await exchangeInstance.isRoundingError.callAsync(
            fillTakerTokenAmount,
            takerTokenAmount,
            makerTokenAmount,
        );
        return isRoundingError;
    }
    /**
     * Checks if logs contain LogError, which is emitted by Exchange contract on transaction failure.
     * @param   logs   Transaction logs as returned by `zeroEx.awaitTransactionMinedAsync`
     */
    public throwLogErrorsAsErrors(logs: Array<LogWithDecodedArgs<DecodedLogArgs> | LogEntry>): void {
        const errLog = _.find(logs, {
            event: ExchangeEvents.LogError,
        });
        if (!_.isUndefined(errLog)) {
            const logArgs = (errLog as LogWithDecodedArgs<LogErrorContractEventArgs>).args;
            const errCode = logArgs.errorId;
            const errMessage = this._exchangeContractErrCodesToMsg[errCode];
            throw new Error(errMessage);
        }
    }
    /**
     * Gets the latest OrderState of a signedOrder
     * @param   signedOrder   The signedOrder
     * @param   stateLayer    Optional, desired blockchain state layer (defaults to latest).
     * @return  OrderState of the signedOrder
     */
    public async getOrderStateAsync(
        signedOrder: SignedOrder,
        stateLayer: BlockParamLiteral = BlockParamLiteral.Latest,
    ): Promise<OrderState> {
        const simpleBalanceAndProxyAllowanceFetcher = new SimpleBalanceAndProxyAllowanceFetcher(
            this._tokenWrapper,
            stateLayer,
        );
        const simpleOrderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher(this, stateLayer);
        const orderStateUtils = new OrderStateUtils(
            simpleBalanceAndProxyAllowanceFetcher,
            simpleOrderFilledCancelledFetcher,
        );
        const orderState = orderStateUtils.getOrderStateAsync(signedOrder);
        return orderState;
    }
    /**
     * Returns the ZRX token address used by the exchange contract.
     * @return Address of ZRX token
     */
    public getZRXTokenAddress(): string {
        const contractAddress = this._getContractAddress(artifacts.ZRXArtifact, this._zrxContractAddressIfExists);
        return contractAddress;
    }
    private _invalidateContractInstances(): void {
        this.unsubscribeAll();
        delete this._exchangeContractIfExists;
    }
    private async _isValidSignatureUsingContractCallAsync(
        dataHex: string,
        ecSignature: ECSignature,
        signerAddressHex: string,
    ): Promise<boolean> {
        assert.isHexString('dataHex', dataHex);
        assert.doesConformToSchema('ecSignature', ecSignature, schemas.ecSignatureSchema);
        assert.isETHAddressHex('signerAddressHex', signerAddressHex);
        const normalizedSignerAddress = signerAddressHex.toLowerCase();

        const exchangeInstance = await this._getExchangeContractAsync();

        const isValidSignature = await exchangeInstance.isValidSignature.callAsync(
            normalizedSignerAddress,
            dataHex,
            ecSignature.v,
            ecSignature.r,
            ecSignature.s,
        );
        return isValidSignature;
    }
    private async _getOrderHashHexUsingContractCallAsync(order: Order | SignedOrder): Promise<string> {
        const exchangeInstance = await this._getExchangeContractAsync();
        const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order);
        const orderHashHex = await exchangeInstance.getOrderHash.callAsync(orderAddresses, orderValues);
        return orderHashHex;
    }
    private async _getExchangeContractAsync(): Promise<ExchangeContract> {
        if (!_.isUndefined(this._exchangeContractIfExists)) {
            return this._exchangeContractIfExists;
        }
        const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
            artifacts.ExchangeArtifact,
            this._contractAddressIfExists,
        );
        const contractInstance = new ExchangeContract(
            abi,
            address,
            this._web3Wrapper.getProvider(),
            this._web3Wrapper.getContractDefaults(),
        );
        this._exchangeContractIfExists = contractInstance;
        return this._exchangeContractIfExists;
    }
    private async _getTokenTransferProxyAddressAsync(): Promise<string> {
        const exchangeInstance = await this._getExchangeContractAsync();
        const tokenTransferProxyAddress = await exchangeInstance.TOKEN_TRANSFER_PROXY_CONTRACT.callAsync();
        const tokenTransferProxyAddressLowerCase = tokenTransferProxyAddress.toLowerCase();
        return tokenTransferProxyAddressLowerCase;
    }
} // tslint:disable:max-file-line-count
