347 lines
16 KiB
TypeScript
347 lines
16 KiB
TypeScript
import {
|
|
ExchangeContractErrs,
|
|
ObjectMap,
|
|
OrderRelevantState,
|
|
OrderState,
|
|
OrderStateInvalid,
|
|
OrderStateValid,
|
|
SignedOrder,
|
|
} from '@0x/types';
|
|
import { BigNumber } from '@0x/utils';
|
|
import * as _ from 'lodash';
|
|
|
|
import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
|
|
import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
|
|
import { assetDataUtils } from './asset_data_utils';
|
|
import { orderHashUtils } from './order_hash';
|
|
import { OrderValidationUtils } from './order_validation_utils';
|
|
import { RemainingFillableCalculator } from './remaining_fillable_calculator';
|
|
import { utils } from './utils';
|
|
|
|
interface SidedOrderRelevantState {
|
|
isMakerSide: boolean;
|
|
traderBalance: BigNumber;
|
|
traderIndividualBalances: ObjectMap<BigNumber>;
|
|
traderProxyAllowance: BigNumber;
|
|
traderIndividualProxyAllowances: ObjectMap<BigNumber>;
|
|
traderFeeBalance: BigNumber;
|
|
traderFeeProxyAllowance: BigNumber;
|
|
filledTakerAssetAmount: BigNumber;
|
|
remainingFillableAssetAmount: BigNumber;
|
|
isOrderCancelled: boolean;
|
|
}
|
|
interface OrderValidResult {
|
|
isValid: true;
|
|
}
|
|
interface OrderInvalidResult {
|
|
isValid: false;
|
|
error: ExchangeContractErrs;
|
|
}
|
|
type OrderValidationResult = OrderValidResult | OrderInvalidResult;
|
|
|
|
export class OrderStateUtils {
|
|
private readonly _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher;
|
|
private readonly _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher;
|
|
private static _validateIfOrderIsValid(
|
|
signedOrder: SignedOrder,
|
|
sidedOrderRelevantState: SidedOrderRelevantState,
|
|
): OrderValidationResult {
|
|
const isMakerSide = sidedOrderRelevantState.isMakerSide;
|
|
if (sidedOrderRelevantState.isOrderCancelled) {
|
|
return { isValid: false, error: ExchangeContractErrs.OrderCancelled };
|
|
}
|
|
const availableTakerAssetAmount = signedOrder.takerAssetAmount.minus(
|
|
sidedOrderRelevantState.filledTakerAssetAmount,
|
|
);
|
|
if (availableTakerAssetAmount.eq(0)) {
|
|
return { isValid: false, error: ExchangeContractErrs.OrderRemainingFillAmountZero };
|
|
}
|
|
|
|
if (sidedOrderRelevantState.traderBalance.eq(0)) {
|
|
const error = isMakerSide
|
|
? ExchangeContractErrs.InsufficientMakerBalance
|
|
: ExchangeContractErrs.InsufficientTakerBalance;
|
|
return { isValid: false, error };
|
|
}
|
|
if (sidedOrderRelevantState.traderProxyAllowance.eq(0)) {
|
|
const error = isMakerSide
|
|
? ExchangeContractErrs.InsufficientMakerAllowance
|
|
: ExchangeContractErrs.InsufficientTakerAllowance;
|
|
return { isValid: false, error };
|
|
}
|
|
if (!signedOrder.makerFee.eq(0)) {
|
|
if (sidedOrderRelevantState.traderFeeBalance.eq(0)) {
|
|
const error = isMakerSide
|
|
? ExchangeContractErrs.InsufficientMakerFeeBalance
|
|
: ExchangeContractErrs.InsufficientTakerFeeBalance;
|
|
return { isValid: false, error };
|
|
}
|
|
if (sidedOrderRelevantState.traderFeeProxyAllowance.eq(0)) {
|
|
const error = isMakerSide
|
|
? ExchangeContractErrs.InsufficientMakerFeeAllowance
|
|
: ExchangeContractErrs.InsufficientTakerFeeAllowance;
|
|
return { isValid: false, error };
|
|
}
|
|
}
|
|
const remainingTakerAssetAmount = signedOrder.takerAssetAmount.minus(
|
|
sidedOrderRelevantState.filledTakerAssetAmount,
|
|
);
|
|
const isRoundingError = OrderValidationUtils.isRoundingErrorFloor(
|
|
remainingTakerAssetAmount,
|
|
signedOrder.takerAssetAmount,
|
|
signedOrder.makerAssetAmount,
|
|
);
|
|
if (isRoundingError) {
|
|
return { isValid: false, error: ExchangeContractErrs.OrderFillRoundingError };
|
|
}
|
|
return { isValid: true };
|
|
}
|
|
/**
|
|
* Instantiate OrderStateUtils
|
|
* @param balanceAndProxyAllowanceFetcher A class that is capable of fetching balances
|
|
* and proxyAllowances for Ethereum addresses. It must implement AbstractBalanceAndProxyAllowanceFetcher
|
|
* @param orderFilledCancelledFetcher A class that is capable of fetching whether an order
|
|
* is cancelled and how much of it has been filled. It must implement AbstractOrderFilledCancelledFetcher
|
|
* @return Instance of OrderStateUtils
|
|
*/
|
|
constructor(
|
|
balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher,
|
|
orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher,
|
|
) {
|
|
this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher;
|
|
this._orderFilledCancelledFetcher = orderFilledCancelledFetcher;
|
|
}
|
|
/**
|
|
* Get the orderState for an "open" order (i.e where takerAddress=NULL_ADDRESS)
|
|
* This method will only check the maker's balance/allowance to calculate the
|
|
* OrderState.
|
|
* @param signedOrder The order of interest
|
|
* @return State relevant to the signedOrder, as well as whether the signedOrder is "valid".
|
|
* Validity is defined as a non-zero amount of the order can still be filled.
|
|
*/
|
|
public async getOpenOrderStateAsync(signedOrder: SignedOrder, transactionHash?: string): Promise<OrderState> {
|
|
const orderRelevantState = await this.getOpenOrderRelevantStateAsync(signedOrder);
|
|
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
|
|
const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(signedOrder);
|
|
const sidedOrderRelevantState = {
|
|
isMakerSide: true,
|
|
traderBalance: orderRelevantState.makerBalance,
|
|
traderIndividualBalances: orderRelevantState.makerIndividualBalances,
|
|
traderProxyAllowance: orderRelevantState.makerProxyAllowance,
|
|
traderIndividualProxyAllowances: orderRelevantState.makerIndividualProxyAllowances,
|
|
traderFeeBalance: orderRelevantState.makerFeeBalance,
|
|
traderFeeProxyAllowance: orderRelevantState.makerFeeProxyAllowance,
|
|
filledTakerAssetAmount: orderRelevantState.filledTakerAssetAmount,
|
|
remainingFillableAssetAmount: orderRelevantState.remainingFillableMakerAssetAmount,
|
|
isOrderCancelled,
|
|
};
|
|
const orderValidationResult = OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState);
|
|
if (orderValidationResult.isValid) {
|
|
const orderState: OrderStateValid = {
|
|
isValid: true,
|
|
orderHash,
|
|
orderRelevantState,
|
|
transactionHash,
|
|
};
|
|
return orderState;
|
|
} else {
|
|
const orderState: OrderStateInvalid = {
|
|
isValid: false,
|
|
orderHash,
|
|
error: orderValidationResult.error,
|
|
transactionHash,
|
|
};
|
|
return orderState;
|
|
}
|
|
}
|
|
/**
|
|
* Get state relevant to an order (i.e makerBalance, makerAllowance, filledTakerAssetAmount, etc...
|
|
* @param signedOrder Order of interest
|
|
* @return An instance of OrderRelevantState
|
|
*/
|
|
public async getOpenOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> {
|
|
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,
|
|
makerIndividualBalances: sidedOrderRelevantState.traderIndividualBalances,
|
|
makerProxyAllowance: sidedOrderRelevantState.traderProxyAllowance,
|
|
makerIndividualProxyAllowances: sidedOrderRelevantState.traderIndividualProxyAllowances,
|
|
makerFeeBalance: sidedOrderRelevantState.traderFeeBalance,
|
|
makerFeeProxyAllowance: sidedOrderRelevantState.traderFeeProxyAllowance,
|
|
filledTakerAssetAmount: sidedOrderRelevantState.filledTakerAssetAmount,
|
|
remainingFillableMakerAssetAmount: sidedOrderRelevantState.remainingFillableAssetAmount,
|
|
remainingFillableTakerAssetAmount,
|
|
};
|
|
return orderRelevantState;
|
|
}
|
|
/**
|
|
* Get the max amount of the supplied order's takerAmount that could still be filled
|
|
* @param signedOrder Order of interest
|
|
* @param takerAddress Hypothetical taker of the order
|
|
* @return fillableTakerAssetAmount
|
|
*/
|
|
public async getMaxFillableTakerAssetAmountAsync(
|
|
signedOrder: SignedOrder,
|
|
takerAddress: string,
|
|
): Promise<BigNumber> {
|
|
// 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.getPartialAmountFloor(
|
|
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;
|
|
}
|
|
private async _getSidedOrderRelevantStateAsync(
|
|
isMakerSide: boolean,
|
|
signedOrder: SignedOrder,
|
|
takerAddress: string,
|
|
): Promise<SidedOrderRelevantState> {
|
|
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 traderIndividualBalances = await this._getAssetBalancesAsync(assetData, traderAddress);
|
|
const traderProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
|
|
assetData,
|
|
traderAddress,
|
|
);
|
|
const traderIndividualProxyAllowances = await this._getAssetProxyAllowancesAsync(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(signedOrder);
|
|
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,
|
|
traderIndividualBalances,
|
|
traderProxyAllowance,
|
|
traderIndividualProxyAllowances,
|
|
traderFeeBalance,
|
|
traderFeeProxyAllowance,
|
|
filledTakerAssetAmount,
|
|
remainingFillableAssetAmount,
|
|
isOrderCancelled,
|
|
};
|
|
return sidedOrderRelevantState;
|
|
}
|
|
private async _getAssetBalancesAsync(
|
|
assetData: string,
|
|
traderAddress: string,
|
|
initialBalances: ObjectMap<BigNumber> = {},
|
|
): Promise<ObjectMap<BigNumber>> {
|
|
const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
|
|
let balances: ObjectMap<BigNumber> = { ...initialBalances };
|
|
if (assetDataUtils.isERC20AssetData(decodedAssetData) || assetDataUtils.isERC721AssetData(decodedAssetData)) {
|
|
const balance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, traderAddress);
|
|
const tokenAddress = decodedAssetData.tokenAddress;
|
|
balances[tokenAddress] = _.isUndefined(initialBalances[tokenAddress])
|
|
? balance
|
|
: balances[tokenAddress].plus(balance);
|
|
} else if (assetDataUtils.isMultiAssetData(decodedAssetData)) {
|
|
for (const assetDataElement of decodedAssetData.nestedAssetData) {
|
|
balances = await this._getAssetBalancesAsync(assetDataElement, traderAddress, balances);
|
|
}
|
|
}
|
|
return balances;
|
|
}
|
|
private async _getAssetProxyAllowancesAsync(
|
|
assetData: string,
|
|
traderAddress: string,
|
|
initialAllowances: ObjectMap<BigNumber> = {},
|
|
): Promise<ObjectMap<BigNumber>> {
|
|
const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
|
|
let allowances: ObjectMap<BigNumber> = { ...initialAllowances };
|
|
if (assetDataUtils.isERC20AssetData(decodedAssetData) || assetDataUtils.isERC721AssetData(decodedAssetData)) {
|
|
const allowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
|
|
assetData,
|
|
traderAddress,
|
|
);
|
|
const tokenAddress = decodedAssetData.tokenAddress;
|
|
allowances[tokenAddress] = _.isUndefined(initialAllowances[tokenAddress])
|
|
? allowance
|
|
: allowances[tokenAddress].plus(allowance);
|
|
} else if (assetDataUtils.isMultiAssetData(decodedAssetData)) {
|
|
for (const assetDataElement of decodedAssetData.nestedAssetData) {
|
|
allowances = await this._getAssetBalancesAsync(assetDataElement, traderAddress, allowances);
|
|
}
|
|
}
|
|
return allowances;
|
|
}
|
|
}
|