import * as _ from 'lodash'; import * as Web3 from 'web3'; import BigNumber from 'bignumber.js'; import { ExchangeContractErrs, SignedOrder, OrderRelevantState, MethodOpts, OrderState, OrderStateValid, OrderStateInvalid, } from '../types'; import {ZeroEx} from '../0x'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; import {utils} from '../utils/utils'; import {constants} from '../utils/constants'; import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; import { TokenTransferProxyWrapper } from '../contract_wrappers/token_transfer_proxy_wrapper'; const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001; export class OrderStateUtils { private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; constructor(balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore, orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore) { this.balanceAndProxyAllowanceLazyStore = balanceAndProxyAllowanceLazyStore; this.orderFilledCancelledLazyStore = orderFilledCancelledLazyStore; } public async getOrderStateAsync(signedOrder: SignedOrder): Promise { const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder); const orderHash = ZeroEx.getOrderHashHex(signedOrder); try { this.validateIfOrderIsValid(signedOrder, orderRelevantState); const orderState: OrderStateValid = { isValid: true, orderHash, orderRelevantState, }; return orderState; } catch (err) { const orderState: OrderStateInvalid = { isValid: false, orderHash, error: err.message, }; return orderState; } } public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise { // HACK: We access the private property here but otherwise the interface will be less nice. // If we pass it from the instantiator - there is no opportunity to get it there // because JS doesn't support async constructors. // Moreover - it's cached under the hood so it's equivalent to an async constructor. const exchange = (this.orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper; const zrxTokenAddress = await exchange.getZRXTokenAddressAsync(); const orderHash = ZeroEx.getOrderHashHex(signedOrder); const makerBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync( signedOrder.makerTokenAddress, signedOrder.maker, ); const makerProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync( signedOrder.makerTokenAddress, signedOrder.maker, ); const makerFeeBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync( zrxTokenAddress, signedOrder.maker, ); const makerFeeProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync( zrxTokenAddress, signedOrder.maker, ); const filledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getFilledTakerAmountAsync(orderHash); const cancelledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync( orderHash, ); const unavailableTakerTokenAmount = await exchange.getUnavailableTakerAmountAsync(orderHash); const totalMakerTokenAmount = signedOrder.makerTokenAmount; const totalTakerTokenAmount = signedOrder.takerTokenAmount; const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount) .dividedToIntegerBy(totalTakerTokenAmount); const remainingFeeTokenAmount = remainingTakerTokenAmount.times(signedOrder.makerFee) .dividedToIntegerBy(totalTakerTokenAmount); const transferrableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); const transferrableFeeTokenAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]); let remainingFillableMakerTokenAmount; if ((signedOrder.makerTokenAddress !== zrxTokenAddress || signedOrder.makerFee.isZero())) { remainingFillableMakerTokenAmount = this.calculateFillableMakerTokenAmount( transferrableMakerTokenAmount, transferrableFeeTokenAmount, remainingMakerTokenAmount, remainingFeeTokenAmount, totalMakerTokenAmount, signedOrder.makerFee, signedOrder.makerTokenAddress, zrxTokenAddress); } else { remainingFillableMakerTokenAmount = this.calculatePooledFillableMakerTokenAmount( transferrableMakerTokenAmount, transferrableFeeTokenAmount, remainingMakerTokenAmount, remainingFeeTokenAmount, totalMakerTokenAmount, signedOrder.makerFee, signedOrder.makerTokenAddress, zrxTokenAddress); } const remainingFillableTakerTokenAmount = remainingFillableMakerTokenAmount .times(totalTakerTokenAmount) .dividedToIntegerBy(totalMakerTokenAmount); const orderRelevantState = { makerBalance, makerProxyAllowance, makerFeeBalance, makerFeeProxyAllowance, filledTakerTokenAmount, cancelledTakerTokenAmount, remainingFillableMakerTokenAmount, remainingFillableTakerTokenAmount, }; return orderRelevantState; } private calculateFillableMakerTokenAmount(makerTransferrableAmount: BigNumber, makerFeeTransferrableAmount: BigNumber, remainingMakerAmount: BigNumber, remainingMakerFeeAmount: BigNumber, totalMakerAmount: BigNumber, makerFeeAmount: BigNumber, makerTokenAddress: string, zrxTokenAddress: string): BigNumber { if (makerFeeAmount.isZero()) { return BigNumber.min(remainingMakerAmount, makerTransferrableAmount); } else if (makerTransferrableAmount.gte(remainingMakerAmount) && makerFeeTransferrableAmount.gte(remainingMakerFeeAmount)) { return makerTransferrableAmount; } else { return this.calculatePartiallyFillableMakerTokenAmount( makerTransferrableAmount, makerFeeTransferrableAmount, remainingMakerAmount, remainingMakerFeeAmount, totalMakerAmount, makerFeeAmount, makerTokenAddress, zrxTokenAddress); } } private calculatePooledFillableMakerTokenAmount(makerTransferrableAmount: BigNumber, makerFeeTransferrableAmount: BigNumber, remainingMakerAmount: BigNumber, remainingMakerFeeAmount: BigNumber, totalMakerAmount: BigNumber, makerFeeAmount: BigNumber, makerTokenAddress: string, zrxTokenAddress: string): BigNumber { if (makerTransferrableAmount.plus(makerFeeTransferrableAmount).gte( remainingMakerAmount.plus(remainingMakerFeeAmount))) { return remainingMakerAmount; } else { return this.calculatePartiallyFillableMakerTokenAmount( makerTransferrableAmount, makerFeeTransferrableAmount, remainingMakerAmount, remainingMakerFeeAmount, totalMakerAmount, makerFeeAmount, makerTokenAddress, zrxTokenAddress); } } private calculatePartiallyFillableMakerTokenAmount(makerTransferrableAmount: BigNumber, makerFeeTransferrableAmount: BigNumber, remainingMakerAmount: BigNumber, remainingMakerFeeAmount: BigNumber, totalMakerAmount: BigNumber, makerFeeAmount: BigNumber, makerTokenAddress: string, zrxTokenAddress: string): BigNumber { const orderToFeeRatio = totalMakerAmount.dividedToIntegerBy(makerFeeAmount); const fillableTimesInFeeToken = BigNumber.min(makerFeeTransferrableAmount, remainingMakerFeeAmount); let fillableTimesInMakerToken = makerTransferrableAmount.dividedToIntegerBy(orderToFeeRatio); if (makerTokenAddress === zrxTokenAddress) { const totalFeeTokenPool = makerTransferrableAmount.plus(makerFeeTransferrableAmount); fillableTimesInMakerToken = totalFeeTokenPool.dividedToIntegerBy( orderToFeeRatio.plus( ZeroEx.toBaseUnitAmount(new BigNumber(1), 18))); } return BigNumber.min(fillableTimesInMakerToken.times(orderToFeeRatio), fillableTimesInFeeToken.times(orderToFeeRatio)); } private validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void { const unavailableTakerTokenAmount = orderRelevantState.cancelledTakerTokenAmount.add( orderRelevantState.filledTakerTokenAmount, ); const availableTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); if (availableTakerTokenAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); } if (orderRelevantState.makerBalance.eq(0)) { throw new Error(ExchangeContractErrs.InsufficientMakerBalance); } if (orderRelevantState.makerProxyAllowance.eq(0)) { throw new Error(ExchangeContractErrs.InsufficientMakerAllowance); } if (!signedOrder.makerFee.eq(0)) { if (orderRelevantState.makerFeeBalance.eq(0)) { throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance); } if (orderRelevantState.makerFeeProxyAllowance.eq(0)) { throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance); } } const minFillableTakerTokenAmountWithinNoRoundingErrorRange = signedOrder.takerTokenAmount .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) .dividedBy(signedOrder.makerTokenAmount); if (orderRelevantState.remainingFillableTakerTokenAmount .lessThan(minFillableTakerTokenAmountWithinNoRoundingErrorRange)) { throw new Error(ExchangeContractErrs.OrderFillRoundingError); } // TODO Add linear function solver when maker token is ZRX #badass // Return the max amount that's fillable } }