protocol/contracts/exchange/test/utils/fill_order_simulator.ts

164 lines
5.5 KiB
TypeScript

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) {
this.lazyStore = lazyStore;
this._transferSimulator = new ExchangeTransferSimulator(lazyStore);
}
public async simulateFillOrderAsync(
order: Order,
takerAddress: string,
takerAssetFillAmount: BigNumber,
takerAssetFilledAmount: BigNumber = constants.ZERO_AMOUNT,
): Promise<FillResults> {
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);
}
}