import { ExchangeContractErrs, OrderRelevantState, OrderState, OrderStateInvalid, OrderStateValid, SignedOrder, } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; import { orderHashUtils } from './order_hash'; import { RemainingFillableCalculator } from './remaining_fillable_calculator'; import { utils } from './utils'; interface SidedOrderRelevantState { isMakerSide: boolean; traderBalance: BigNumber; traderProxyAllowance: BigNumber; traderFeeBalance: BigNumber; traderFeeProxyAllowance: BigNumber; filledTakerAssetAmount: BigNumber; remainingFillableAssetAmount: BigNumber; } const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001; export class OrderStateUtils { private readonly _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher; private readonly _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher; private static _validateIfOrderIsValid( signedOrder: SignedOrder, sidedOrderRelevantState: SidedOrderRelevantState, ): void { const isMakerSide = sidedOrderRelevantState.isMakerSide; const availableTakerAssetAmount = signedOrder.takerAssetAmount.minus( sidedOrderRelevantState.filledTakerAssetAmount, ); if (availableTakerAssetAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); } if (sidedOrderRelevantState.traderBalance.eq(0)) { throw new Error( isMakerSide ? ExchangeContractErrs.InsufficientMakerBalance : ExchangeContractErrs.InsufficientTakerBalance, ); } if (sidedOrderRelevantState.traderProxyAllowance.eq(0)) { throw new Error( isMakerSide ? ExchangeContractErrs.InsufficientMakerAllowance : ExchangeContractErrs.InsufficientTakerAllowance, ); } if (!signedOrder.makerFee.eq(0)) { if (sidedOrderRelevantState.traderFeeBalance.eq(0)) { throw new Error( isMakerSide ? ExchangeContractErrs.InsufficientMakerFeeBalance : ExchangeContractErrs.InsufficientTakerFeeBalance, ); } if (sidedOrderRelevantState.traderFeeProxyAllowance.eq(0)) { throw new Error( isMakerSide ? ExchangeContractErrs.InsufficientMakerFeeAllowance : ExchangeContractErrs.InsufficientTakerFeeAllowance, ); } } let minFillableTakerAssetAmountWithinNoRoundingErrorRange; if (isMakerSide) { minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) .dividedBy(signedOrder.makerAssetAmount); } else { minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.makerAssetAmount .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) .dividedBy(signedOrder.takerAssetAmount); } if ( sidedOrderRelevantState.remainingFillableAssetAmount.lessThan( minFillableTakerAssetAmountWithinNoRoundingErrorRange, ) ) { throw new Error(ExchangeContractErrs.OrderFillRoundingError); } } constructor( balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher, ) { this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher; this._orderFilledCancelledFetcher = orderFilledCancelledFetcher; } public async getOpenOrderStateAsync(signedOrder: SignedOrder): Promise { const orderRelevantState = await this.getOpenOrderRelevantStateAsync(signedOrder); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const sidedOrderRelevantState = { isMakerSide: true, traderBalance: orderRelevantState.makerBalance, traderProxyAllowance: orderRelevantState.makerProxyAllowance, traderFeeBalance: orderRelevantState.makerFeeBalance, traderFeeProxyAllowance: orderRelevantState.makerFeeProxyAllowance, filledTakerAssetAmount: orderRelevantState.filledTakerAssetAmount, remainingFillableAssetAmount: orderRelevantState.remainingFillableMakerAssetAmount, }; try { OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState); const orderState: OrderStateValid = { isValid: true, orderHash, orderRelevantState, }; return orderState; } catch (err) { const orderState: OrderStateInvalid = { isValid: false, orderHash, error: err.message, }; return orderState; } } public async getOpenOrderRelevantStateAsync(signedOrder: SignedOrder): Promise { const isMaker = true; const sidedOrderRelevantState = await this._getSidedOrderRelevantStateAsync( isMaker, signedOrder, signedOrder.takerAddress, ); const remainingFillableTakerAssetAmount = sidedOrderRelevantState.remainingFillableAssetAmount .times(signedOrder.takerAssetAmount) .dividedToIntegerBy(signedOrder.makerAssetAmount); const orderRelevantState = { makerBalance: sidedOrderRelevantState.traderBalance, makerProxyAllowance: sidedOrderRelevantState.traderProxyAllowance, makerFeeBalance: sidedOrderRelevantState.traderFeeBalance, makerFeeProxyAllowance: sidedOrderRelevantState.traderFeeProxyAllowance, filledTakerAssetAmount: sidedOrderRelevantState.filledTakerAssetAmount, remainingFillableMakerAssetAmount: sidedOrderRelevantState.remainingFillableAssetAmount, remainingFillableTakerAssetAmount, }; return orderRelevantState; } public async getMaxFillableTakerAssetAmountAsync( signedOrder: SignedOrder, takerAddress: string, ): Promise { // Get max fillable amount for an order, considering the makers ability to fill let isMaker = true; const orderRelevantMakerState = await this._getSidedOrderRelevantStateAsync( isMaker, signedOrder, signedOrder.takerAddress, ); const remainingFillableTakerAssetAmountGivenMakersStatus = signedOrder.makerAssetAmount.eq(0) ? new BigNumber(0) : utils.getPartialAmount( orderRelevantMakerState.remainingFillableAssetAmount, signedOrder.makerAssetAmount, signedOrder.takerAssetAmount, ); // Get max fillable amount for an order, considering the takers ability to fill isMaker = false; const orderRelevantTakerState = await this._getSidedOrderRelevantStateAsync(isMaker, signedOrder, takerAddress); const remainingFillableTakerAssetAmountGivenTakersStatus = orderRelevantTakerState.remainingFillableAssetAmount; // The min of these two in the actualy max fillable by either party const fillableTakerAssetAmount = BigNumber.min([ remainingFillableTakerAssetAmountGivenMakersStatus, remainingFillableTakerAssetAmountGivenTakersStatus, ]); return fillableTakerAssetAmount; } public async getMaxFillableTakerAssetAmountForFailingOrderAsync( signedOrder: SignedOrder, takerAddress: string, ): Promise { // Get min of taker balance & allowance const takerAssetBalanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( signedOrder.takerAssetData, takerAddress, ); const takerAssetAllowanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( signedOrder.takerAssetData, takerAddress, ); const minTakerAssetAmount = BigNumber.min([takerAssetBalanceOfTaker, takerAssetAllowanceOfTaker]); // get remainingFillAmount const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); const remainingFillTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerAssetAmount); if (minTakerAssetAmount.gte(remainingFillTakerAssetAmount)) { return remainingFillTakerAssetAmount; } else { return minTakerAssetAmount; } } private async _getSidedOrderRelevantStateAsync( isMakerSide: boolean, signedOrder: SignedOrder, takerAddress: string, ): Promise { let traderAddress; let assetData; let assetAmount; let feeAmount; if (isMakerSide) { traderAddress = signedOrder.makerAddress; assetData = signedOrder.makerAssetData; assetAmount = signedOrder.makerAssetAmount; feeAmount = signedOrder.makerFee; } else { traderAddress = takerAddress; assetData = signedOrder.takerAssetData; assetAmount = signedOrder.takerAssetAmount; feeAmount = signedOrder.takerFee; } const zrxAssetData = this._orderFilledCancelledFetcher.getZRXAssetData(); const isAssetZRX = assetData === zrxAssetData; const traderBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, traderAddress); const traderProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( assetData, traderAddress, ); const traderFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( zrxAssetData, traderAddress, ); const traderFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( zrxAssetData, traderAddress, ); const transferrableTraderAssetAmount = BigNumber.min([traderProxyAllowance, traderBalance]); const transferrableFeeAssetAmount = BigNumber.min([traderFeeProxyAllowance, traderFeeBalance]); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); const totalMakerAssetAmount = signedOrder.makerAssetAmount; const totalTakerAssetAmount = signedOrder.takerAssetAmount; const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash); const remainingTakerAssetAmount = isOrderCancelled ? new BigNumber(0) : totalTakerAssetAmount.minus(filledTakerAssetAmount); const remainingMakerAssetAmount = remainingTakerAssetAmount.eq(0) ? new BigNumber(0) : remainingTakerAssetAmount.times(totalMakerAssetAmount).dividedToIntegerBy(totalTakerAssetAmount); const remainingAssetAmount = isMakerSide ? remainingMakerAssetAmount : remainingTakerAssetAmount; const remainingFillableCalculator = new RemainingFillableCalculator( feeAmount, assetAmount, isAssetZRX, transferrableTraderAssetAmount, transferrableFeeAssetAmount, remainingAssetAmount, ); const remainingFillableAssetAmount = remainingFillableCalculator.computeRemainingFillable(); const sidedOrderRelevantState = { isMakerSide, traderBalance, traderProxyAllowance, traderFeeBalance, traderFeeProxyAllowance, filledTakerAssetAmount, remainingFillableAssetAmount, }; return sidedOrderRelevantState; } }