import { DevUtilsContract } from '@0x/contracts-dev-utils'; import { constants, orderUtils } from '@0x/contracts-test-utils'; import { Order } from '@0x/order-utils'; import { FillResults } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { AbstractBalanceAndProxyAllowanceLazyStore as LazyStore } from './abstract/abstract_balance_and_proxy_allowance_lazy_store'; import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; import { TradeSide, TransferType } from './types'; export enum FillOrderError { OrderUnfillable = 'ORDER_UNFILLABLE', InvalidSender = 'INVALID_SENDER', InvalidTakerAmount = 'INVALID_TAKER_AMOUNT', InvalidMakerAmount = 'INVALID_MAKER_AMOUNT', InvalidTaker = 'INVALID_TAKER', InvalidFillPrice = 'INVALID_FILL_PRICE', TransferFailed = 'TRANSFER_FAILED', } /** * Simplified fill order simulator. */ export class FillOrderSimulator { public readonly lazyStore: LazyStore; private readonly _transferSimulator: ExchangeTransferSimulator; constructor(lazyStore: LazyStore, devUtilsContract: DevUtilsContract) { this.lazyStore = lazyStore; this._transferSimulator = new ExchangeTransferSimulator(lazyStore, devUtilsContract); } public async simulateFillOrderAsync( order: Order, takerAddress: string, takerAssetFillAmount: BigNumber, takerAssetFilledAmount: BigNumber = constants.ZERO_AMOUNT, ): Promise { const remainingTakerAssetAmount = order.takerAssetAmount.minus(takerAssetFilledAmount); const makerAssetFilledAmount = orderUtils.getPartialAmountFloor( takerAssetFilledAmount, order.takerAssetAmount, order.makerAssetAmount, ); validateFill( order, takerAddress, takerAssetFillAmount, makerAssetFilledAmount, takerAssetFilledAmount, remainingTakerAssetAmount, ); const finalTakerAssetFillAmount = BigNumber.min(takerAssetFillAmount, remainingTakerAssetAmount); const makerAssetFillAmount = orderUtils.getPartialAmountFloor( finalTakerAssetFillAmount, order.takerAssetAmount, order.makerAssetAmount, ); const makerFeePaid = orderUtils.getPartialAmountFloor( makerAssetFillAmount, order.makerAssetAmount, order.makerFee, ); const takerFeePaid = orderUtils.getPartialAmountFloor( finalTakerAssetFillAmount, order.takerAssetAmount, order.takerFee, ); try { // Taker -> Maker await this._transferSimulator.transferFromAsync( order.takerAssetData, takerAddress, order.makerAddress, finalTakerAssetFillAmount, TradeSide.Taker, TransferType.Trade, ); // Maker fee -> fee recipient if (order.makerAddress !== order.feeRecipientAddress) { await this._transferSimulator.transferFromAsync( order.makerFeeAssetData, order.makerAddress, order.feeRecipientAddress, makerFeePaid, TradeSide.Maker, TransferType.Fee, ); } // Maker -> Taker await this._transferSimulator.transferFromAsync( order.makerAssetData, order.makerAddress, takerAddress, makerAssetFillAmount, TradeSide.Maker, TransferType.Trade, ); // Taker fee -> fee recipient if (takerAddress !== order.feeRecipientAddress) { await this._transferSimulator.transferFromAsync( order.takerFeeAssetData, takerAddress, order.feeRecipientAddress, takerFeePaid, TradeSide.Taker, TransferType.Fee, ); } } catch (err) { throw new Error(FillOrderError.TransferFailed); } return { takerAssetFilledAmount: finalTakerAssetFillAmount, makerAssetFilledAmount: makerAssetFillAmount, makerFeePaid, takerFeePaid, protocolFeePaid: constants.ZERO_AMOUNT, }; } } function validateFill( order: Order, takerAddress: string, takerAssetFillAmount: BigNumber, makerAssetFilledAmount: BigNumber, takerAssetFilledAmount: BigNumber, remainingTakerAssetAmount: BigNumber, ): void { const now = Math.floor(_.now() / 1000); if (remainingTakerAssetAmount.isEqualTo(0) || order.expirationTimeSeconds.isLessThanOrEqualTo(now)) { throw new Error(FillOrderError.OrderUnfillable); } if (order.senderAddress !== constants.NULL_ADDRESS && order.senderAddress !== takerAddress) { throw new Error(FillOrderError.InvalidSender); } if (order.takerAddress !== constants.NULL_ADDRESS && order.takerAddress !== takerAddress) { throw new Error(FillOrderError.InvalidTaker); } if (order.makerAssetAmount.isEqualTo(0)) { throw new Error(FillOrderError.InvalidMakerAmount); } if (takerAssetFillAmount.isEqualTo(0)) { throw new Error(FillOrderError.InvalidTakerAmount); } if ( makerAssetFilledAmount .times(order.takerAssetAmount) .isGreaterThan(takerAssetFilledAmount.times(order.makerAssetAmount)) ) { throw new Error(FillOrderError.InvalidFillPrice); } }