Move ExchangeTransferSimulator into contract-exchange tests since that's the only place it's still used and we no longer want to expose it to external developers
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
/**
|
||||
* An abstract class to be implemented in order to use OrderStateUtils. The class that
|
||||
* implements this interface must be capable of fetching the balance and proxyAllowance
|
||||
* for an Ethereum address and assetData
|
||||
*/
|
||||
export abstract class AbstractBalanceAndProxyAllowanceFetcher {
|
||||
/**
|
||||
* Get balance of assetData for userAddress
|
||||
* @param assetData AssetData for which to fetch the balance
|
||||
* @param userAddress Ethereum address for which to fetch the balance
|
||||
* @return Balance amount in base units
|
||||
*/
|
||||
public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>;
|
||||
/**
|
||||
* Get the 0x asset proxy allowance of assetData for userAddress
|
||||
* @param assetData AssetData for which to fetch the allowance
|
||||
* @param userAddress Ethereum address for which to fetch the allowance
|
||||
* @return Allowance amount in base units
|
||||
*/
|
||||
public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
export abstract class AbstractBalanceAndProxyAllowanceLazyStore {
|
||||
public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>;
|
||||
public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>;
|
||||
public abstract setBalance(assetData: string, userAddress: string, balance: BigNumber): void;
|
||||
public abstract deleteBalance(assetData: string, userAddress: string): void;
|
||||
public abstract setProxyAllowance(assetData: string, userAddress: string, proxyAllowance: BigNumber): void;
|
||||
public abstract deleteProxyAllowance(assetData: string, userAddress: string): void;
|
||||
public abstract deleteAll(): void;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
/**
|
||||
* An abstract class to be implemented in order to use OrderStateUtils. The class that
|
||||
* implements this interface must be capable of fetching the amount filled of an order
|
||||
* and whether it's been cancelled.
|
||||
*/
|
||||
export abstract class AbstractOrderFilledCancelledFetcher {
|
||||
/**
|
||||
* Get the amount of the order's takerToken amount already filled
|
||||
* @param orderHash OrderHash of order we are interested in
|
||||
* @return FilledTakerAmount
|
||||
*/
|
||||
public abstract async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber>;
|
||||
/**
|
||||
* Whether an order is cancelled
|
||||
* @param orderHash OrderHash of order we are interested in
|
||||
* @return Whether or not the order is cancelled
|
||||
*/
|
||||
public abstract async isOrderCancelledAsync(signedOrder: SignedOrder): Promise<boolean>;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
export abstract class AbstractOrderFilledCancelledLazyStore {
|
||||
public abstract async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber>;
|
||||
public abstract async getIsCancelledAsync(signedOrder: SignedOrder): Promise<boolean>;
|
||||
public abstract setFilledTakerAmount(orderHash: string, balance: BigNumber): void;
|
||||
public abstract deleteFilledTakerAmount(orderHash: string): void;
|
||||
public abstract setIsCancelled(orderHash: string, isCancelled: boolean): void;
|
||||
public abstract deleteIsCancelled(orderHash: string): void;
|
||||
public abstract deleteAll(): void;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { DevUtilsContract } from '@0x/abi-gen-wrappers';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
|
||||
|
||||
export class AssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
|
||||
private readonly _devUtilsContract: DevUtilsContract;
|
||||
constructor(devUtilsContract: DevUtilsContract) {
|
||||
this._devUtilsContract = devUtilsContract;
|
||||
}
|
||||
public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
|
||||
const balance = await this._devUtilsContract.getBalance.callAsync(userAddress, assetData);
|
||||
return balance;
|
||||
}
|
||||
public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
|
||||
const proxyAllowance = await this._devUtilsContract.getAssetProxyAllowance.callAsync(userAddress, assetData);
|
||||
return proxyAllowance;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { AssetProxyId, ExchangeContractErrs } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { AbstractBalanceAndProxyAllowanceLazyStore } from './abstract/abstract_balance_and_proxy_allowance_lazy_store';
|
||||
import { assetDataUtils } from './asset_data_utils';
|
||||
import { constants } from './constants';
|
||||
import { TradeSide, TransferType } from './types';
|
||||
|
||||
enum FailureReason {
|
||||
Balance = 'balance',
|
||||
ProxyAllowance = 'proxyAllowance',
|
||||
}
|
||||
|
||||
const ERR_MSG_MAPPING = {
|
||||
[FailureReason.Balance]: {
|
||||
[TradeSide.Maker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance,
|
||||
},
|
||||
[TradeSide.Taker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance,
|
||||
},
|
||||
},
|
||||
[FailureReason.ProxyAllowance]: {
|
||||
[TradeSide.Maker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance,
|
||||
},
|
||||
[TradeSide.Taker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* An exchange transfer simulator which simulates asset transfers exactly how the
|
||||
* 0x exchange contract would do them.
|
||||
*/
|
||||
export class ExchangeTransferSimulator {
|
||||
private readonly _store: AbstractBalanceAndProxyAllowanceLazyStore;
|
||||
private static _throwValidationError(
|
||||
failureReason: FailureReason,
|
||||
tradeSide: TradeSide,
|
||||
transferType: TransferType,
|
||||
): never {
|
||||
const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType];
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
/**
|
||||
* Instantiate a ExchangeTransferSimulator
|
||||
* @param store A class that implements AbstractBalanceAndProxyAllowanceLazyStore
|
||||
* @return an instance of ExchangeTransferSimulator
|
||||
*/
|
||||
constructor(store: AbstractBalanceAndProxyAllowanceLazyStore) {
|
||||
this._store = store;
|
||||
}
|
||||
/**
|
||||
* Simulates transferFrom call performed by a proxy
|
||||
* @param assetData Data of the asset being transferred. Includes
|
||||
* it's identifying information and assetType,
|
||||
* e.g address for ERC20, address & tokenId for ERC721
|
||||
* @param from Owner of the transferred tokens
|
||||
* @param to Recipient of the transferred tokens
|
||||
* @param amountInBaseUnits The amount of tokens being transferred
|
||||
* @param tradeSide Is Maker/Taker transferring
|
||||
* @param transferType Is it a fee payment or a value transfer
|
||||
*/
|
||||
public async transferFromAsync(
|
||||
assetData: string,
|
||||
from: string,
|
||||
to: string,
|
||||
amountInBaseUnits: BigNumber,
|
||||
tradeSide: TradeSide,
|
||||
transferType: TransferType,
|
||||
): Promise<void> {
|
||||
const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
|
||||
switch (assetProxyId) {
|
||||
case AssetProxyId.ERC1155:
|
||||
case AssetProxyId.ERC20:
|
||||
case AssetProxyId.ERC721: {
|
||||
// HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/
|
||||
// allowances for the taker. We do however, want to increase the balance of the maker since the maker
|
||||
// might be relying on those funds to fill subsequent orders or pay the order's fees.
|
||||
if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) {
|
||||
await this._increaseBalanceAsync(assetData, to, amountInBaseUnits);
|
||||
return;
|
||||
}
|
||||
const balance = await this._store.getBalanceAsync(assetData, from);
|
||||
const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, from);
|
||||
if (proxyAllowance.isLessThan(amountInBaseUnits)) {
|
||||
ExchangeTransferSimulator._throwValidationError(
|
||||
FailureReason.ProxyAllowance,
|
||||
tradeSide,
|
||||
transferType,
|
||||
);
|
||||
}
|
||||
if (balance.isLessThan(amountInBaseUnits)) {
|
||||
ExchangeTransferSimulator._throwValidationError(FailureReason.Balance, tradeSide, transferType);
|
||||
}
|
||||
if (assetProxyId !== AssetProxyId.ERC1155) {
|
||||
// No need to decrease allowance for ERC115 because it's all or nothing.
|
||||
await this._decreaseProxyAllowanceAsync(assetData, from, amountInBaseUnits);
|
||||
}
|
||||
await this._decreaseBalanceAsync(assetData, from, amountInBaseUnits);
|
||||
await this._increaseBalanceAsync(assetData, to, amountInBaseUnits);
|
||||
break;
|
||||
}
|
||||
case AssetProxyId.MultiAsset: {
|
||||
const decodedAssetData = assetDataUtils.decodeMultiAssetData(assetData);
|
||||
await this._decreaseBalanceAsync(assetData, from, amountInBaseUnits);
|
||||
await this._increaseBalanceAsync(assetData, to, amountInBaseUnits);
|
||||
for (const [index, nestedAssetDataElement] of decodedAssetData.nestedAssetData.entries()) {
|
||||
const amountsElement = decodedAssetData.amounts[index];
|
||||
const totalAmount = amountInBaseUnits.times(amountsElement);
|
||||
await this.transferFromAsync(
|
||||
nestedAssetDataElement,
|
||||
from,
|
||||
to,
|
||||
totalAmount,
|
||||
tradeSide,
|
||||
transferType,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unhandled asset proxy ID: ${assetProxyId}`);
|
||||
}
|
||||
}
|
||||
private async _decreaseProxyAllowanceAsync(
|
||||
assetData: string,
|
||||
userAddress: string,
|
||||
amountInBaseUnits: BigNumber,
|
||||
): Promise<void> {
|
||||
const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, userAddress);
|
||||
// HACK: This code assumes that all tokens with an UNLIMITED_ALLOWANCE_IN_BASE_UNITS set,
|
||||
// are UnlimitedAllowanceTokens. This is however not true, it just so happens that all
|
||||
// DummyERC20Tokens we use in tests are.
|
||||
if (!proxyAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
|
||||
this._store.setProxyAllowance(assetData, userAddress, proxyAllowance.minus(amountInBaseUnits));
|
||||
}
|
||||
}
|
||||
private async _increaseBalanceAsync(
|
||||
assetData: string,
|
||||
userAddress: string,
|
||||
amountInBaseUnits: BigNumber,
|
||||
): Promise<void> {
|
||||
const balance = await this._store.getBalanceAsync(assetData, userAddress);
|
||||
this._store.setBalance(assetData, userAddress, balance.plus(amountInBaseUnits));
|
||||
}
|
||||
private async _decreaseBalanceAsync(
|
||||
assetData: string,
|
||||
userAddress: string,
|
||||
amountInBaseUnits: BigNumber,
|
||||
): Promise<void> {
|
||||
const balance = await this._store.getBalanceAsync(assetData, userAddress);
|
||||
this._store.setBalance(assetData, userAddress, balance.minus(amountInBaseUnits));
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,6 @@ export { sortingUtils } from './sorting_utils';
|
||||
export { orderParsingUtils } from './parsing_utils';
|
||||
export { orderCalculationUtils } from './order_calculation_utils';
|
||||
|
||||
export { OrderStateUtils } from './order_state_utils';
|
||||
export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
|
||||
export { AbstractBalanceAndProxyAllowanceLazyStore } from './abstract/abstract_balance_and_proxy_allowance_lazy_store';
|
||||
export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
|
||||
export { AbstractOrderFilledCancelledLazyStore } from './abstract/abstract_order_filled_cancelled_lazy_store';
|
||||
|
||||
export { OrderValidationUtils } from './order_validation_utils';
|
||||
export { ExchangeTransferSimulator } from './exchange_transfer_simulator';
|
||||
export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
|
||||
export { OrderFilledCancelledLazyStore } from './store/order_filled_cancelled_lazy_store';
|
||||
|
||||
export { eip712Utils } from './eip712_utils';
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
import {
|
||||
AssetProxyId,
|
||||
ERC20AssetData,
|
||||
ERC721AssetData,
|
||||
ExchangeContractErrs,
|
||||
MultiAssetData,
|
||||
ObjectMap,
|
||||
OrderRelevantState,
|
||||
OrderState,
|
||||
OrderStateInvalid,
|
||||
OrderStateValid,
|
||||
SignedOrder,
|
||||
SingleAssetData,
|
||||
} 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 feeAssetData;
|
||||
let feeAmount;
|
||||
if (isMakerSide) {
|
||||
traderAddress = signedOrder.makerAddress;
|
||||
assetData = signedOrder.makerAssetData;
|
||||
assetAmount = signedOrder.makerAssetAmount;
|
||||
feeAssetData = signedOrder.makerFeeAssetData;
|
||||
feeAmount = signedOrder.makerFee;
|
||||
} else {
|
||||
traderAddress = takerAddress;
|
||||
assetData = signedOrder.takerAssetData;
|
||||
assetAmount = signedOrder.takerAssetAmount;
|
||||
feeAssetData = signedOrder.takerFeeAssetData;
|
||||
feeAmount = signedOrder.takerFee;
|
||||
}
|
||||
const isPercentageFee = assetData === feeAssetData;
|
||||
|
||||
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(
|
||||
feeAssetData,
|
||||
traderAddress,
|
||||
);
|
||||
const traderFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
|
||||
feeAssetData,
|
||||
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,
|
||||
isPercentageFee,
|
||||
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 (isERC20AssetData(decodedAssetData) || isERC721AssetData(decodedAssetData)) {
|
||||
const balance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, traderAddress);
|
||||
// tslint:disable-next-line:no-unnecessary-type-assertion
|
||||
const tokenAddress = (decodedAssetData as ERC20AssetData | ERC721AssetData).tokenAddress;
|
||||
balances[tokenAddress] =
|
||||
initialBalances[tokenAddress] === undefined ? balance : balances[tokenAddress].plus(balance);
|
||||
} else if (isMultiAssetData(decodedAssetData)) {
|
||||
for (const assetDataElement of (decodedAssetData as MultiAssetData).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 (isERC20AssetData(decodedAssetData) || isERC721AssetData(decodedAssetData)) {
|
||||
const allowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
|
||||
assetData,
|
||||
traderAddress,
|
||||
);
|
||||
// tslint:disable-next-line:no-unnecessary-type-assertion
|
||||
const tokenAddress = (decodedAssetData as ERC20AssetData | ERC721AssetData).tokenAddress;
|
||||
allowances[tokenAddress] =
|
||||
initialAllowances[tokenAddress] === undefined ? allowance : allowances[tokenAddress].plus(allowance);
|
||||
} else if (isMultiAssetData(decodedAssetData)) {
|
||||
for (const assetDataElement of (decodedAssetData as MultiAssetData).nestedAssetData) {
|
||||
allowances = await this._getAssetBalancesAsync(assetDataElement, traderAddress, allowances);
|
||||
}
|
||||
}
|
||||
return allowances;
|
||||
}
|
||||
}
|
||||
|
||||
function isERC20AssetData(decodedAssetData: SingleAssetData | MultiAssetData): boolean {
|
||||
return decodedAssetData.assetProxyId === AssetProxyId.ERC20;
|
||||
}
|
||||
function isERC721AssetData(decodedAssetData: SingleAssetData | MultiAssetData): boolean {
|
||||
return decodedAssetData.assetProxyId === AssetProxyId.ERC721;
|
||||
}
|
||||
function isMultiAssetData(decodedAssetData: SingleAssetData | MultiAssetData): boolean {
|
||||
return decodedAssetData.assetProxyId === AssetProxyId.MultiAsset;
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { ExchangeContractErrs, RevertReason, SignedOrder } from '@0x/types';
|
||||
import { BigNumber, providerUtils } from '@0x/utils';
|
||||
import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
|
||||
import { constants } from './constants';
|
||||
import { ExchangeTransferSimulator } from './exchange_transfer_simulator';
|
||||
import { orderHashUtils } from './order_hash';
|
||||
import { signatureUtils } from './signature_utils';
|
||||
import { TradeSide, TransferType, TypedDataError } from './types';
|
||||
import { utils } from './utils';
|
||||
|
||||
/**
|
||||
* A utility class for validating orders
|
||||
*/
|
||||
export class OrderValidationUtils {
|
||||
private readonly _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher;
|
||||
private readonly _provider: ZeroExProvider;
|
||||
/**
|
||||
* A TypeScript implementation mirroring the implementation of isRoundingError in the
|
||||
* Exchange smart contract
|
||||
* @param numerator Numerator value. When used to check an order, pass in `takerAssetFilledAmount`
|
||||
* @param denominator Denominator value. When used to check an order, pass in `order.takerAssetAmount`
|
||||
* @param target Target value. When used to check an order, pass in `order.makerAssetAmount`
|
||||
*/
|
||||
public static isRoundingErrorFloor(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean {
|
||||
// Solidity's mulmod() in JS
|
||||
// Source: https://solidity.readthedocs.io/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions
|
||||
if (denominator.eq(0)) {
|
||||
throw new Error('denominator cannot be 0');
|
||||
}
|
||||
const remainder = target.multipliedBy(numerator).mod(denominator);
|
||||
if (remainder.eq(0)) {
|
||||
return false; // no rounding error
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
const errPercentageTimes1000000 = remainder.multipliedBy(1000000).div(numerator.multipliedBy(target));
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
const isError = errPercentageTimes1000000.gt(1000);
|
||||
return isError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the maker & taker have sufficient balances/allowances
|
||||
* to fill the supplied order to the fillTakerAssetAmount amount
|
||||
* @param exchangeTradeEmulator ExchangeTradeEmulator to use
|
||||
* @param signedOrder SignedOrder to test
|
||||
* @param fillTakerAssetAmount Amount of takerAsset to fill the signedOrder
|
||||
* @param senderAddress Sender of the fillOrder tx
|
||||
*/
|
||||
public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator: ExchangeTransferSimulator,
|
||||
signedOrder: SignedOrder,
|
||||
fillTakerAssetAmount: BigNumber,
|
||||
senderAddress: string,
|
||||
): Promise<void> {
|
||||
const fillMakerTokenAmount = utils.getPartialAmountFloor(
|
||||
fillTakerAssetAmount,
|
||||
signedOrder.takerAssetAmount,
|
||||
signedOrder.makerAssetAmount,
|
||||
);
|
||||
const makerFeeAmount = utils.getPartialAmountFloor(
|
||||
fillTakerAssetAmount,
|
||||
signedOrder.takerAssetAmount,
|
||||
signedOrder.makerFee,
|
||||
);
|
||||
const takerFeeAmount = utils.getPartialAmountFloor(
|
||||
fillTakerAssetAmount,
|
||||
signedOrder.takerAssetAmount,
|
||||
signedOrder.takerFee,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
signedOrder.makerAssetData,
|
||||
signedOrder.makerAddress,
|
||||
senderAddress,
|
||||
fillMakerTokenAmount,
|
||||
TradeSide.Maker,
|
||||
TransferType.Trade,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
signedOrder.takerAssetData,
|
||||
senderAddress,
|
||||
signedOrder.makerAddress,
|
||||
fillTakerAssetAmount,
|
||||
TradeSide.Taker,
|
||||
TransferType.Trade,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
signedOrder.makerFeeAssetData,
|
||||
signedOrder.makerAddress,
|
||||
signedOrder.feeRecipientAddress,
|
||||
makerFeeAmount,
|
||||
TradeSide.Maker,
|
||||
TransferType.Fee,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
signedOrder.takerFeeAssetData,
|
||||
senderAddress,
|
||||
signedOrder.feeRecipientAddress,
|
||||
takerFeeAmount,
|
||||
TradeSide.Taker,
|
||||
TransferType.Fee,
|
||||
);
|
||||
}
|
||||
|
||||
private static _validateOrderNotExpiredOrThrow(expirationTimeSeconds: BigNumber): void {
|
||||
const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec();
|
||||
if (expirationTimeSeconds.isLessThan(currentUnixTimestampSec)) {
|
||||
throw new Error(RevertReason.OrderUnfillable);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Instantiate OrderValidationUtils
|
||||
* @param orderFilledCancelledFetcher A module that implements the AbstractOrderFilledCancelledFetcher
|
||||
* @param supportedProvider Web3 provider to use for JSON RPC calls
|
||||
* @return An instance of OrderValidationUtils
|
||||
*/
|
||||
constructor(
|
||||
orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher,
|
||||
supportedProvider: SupportedProvider,
|
||||
) {
|
||||
this._orderFilledCancelledFetcher = orderFilledCancelledFetcher;
|
||||
this._provider = providerUtils.standardizeOrThrow(supportedProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a call to FillOrder and throw if it wouldn't succeed
|
||||
* @param exchangeTradeEmulator ExchangeTradeEmulator to use
|
||||
* @param signedOrder SignedOrder of interest
|
||||
* @param fillTakerAssetAmount Amount we'd like to fill the order for
|
||||
* @param takerAddress The taker of the order
|
||||
*/
|
||||
public async validateFillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator: ExchangeTransferSimulator,
|
||||
signedOrder: SignedOrder,
|
||||
fillTakerAssetAmount: BigNumber,
|
||||
takerAddress: string,
|
||||
): Promise<BigNumber> {
|
||||
OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds);
|
||||
if (signedOrder.makerAssetAmount.eq(0) || signedOrder.takerAssetAmount.eq(0)) {
|
||||
throw new Error(RevertReason.OrderUnfillable);
|
||||
}
|
||||
if (fillTakerAssetAmount.eq(0)) {
|
||||
throw new Error(RevertReason.InvalidTakerAmount);
|
||||
}
|
||||
|
||||
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
|
||||
const isValid = await signatureUtils.isValidSignatureAsync(
|
||||
this._provider,
|
||||
orderHash,
|
||||
signedOrder.signature,
|
||||
signedOrder.makerAddress,
|
||||
);
|
||||
if (!isValid) {
|
||||
throw new Error(TypedDataError.InvalidSignature);
|
||||
}
|
||||
const filledTakerTokenAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash);
|
||||
if (signedOrder.takerAssetAmount.eq(filledTakerTokenAmount)) {
|
||||
throw new Error(RevertReason.OrderUnfillable);
|
||||
}
|
||||
if (signedOrder.takerAddress !== constants.NULL_ADDRESS && signedOrder.takerAddress !== takerAddress) {
|
||||
throw new Error(RevertReason.InvalidTaker);
|
||||
}
|
||||
const remainingTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount);
|
||||
const desiredFillTakerTokenAmount = remainingTakerTokenAmount.isLessThan(fillTakerAssetAmount)
|
||||
? remainingTakerTokenAmount
|
||||
: fillTakerAssetAmount;
|
||||
try {
|
||||
await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator,
|
||||
signedOrder,
|
||||
desiredFillTakerTokenAmount,
|
||||
takerAddress,
|
||||
);
|
||||
} catch (err) {
|
||||
const transferFailedErrorMessages = [
|
||||
ExchangeContractErrs.InsufficientMakerBalance,
|
||||
ExchangeContractErrs.InsufficientMakerFeeBalance,
|
||||
ExchangeContractErrs.InsufficientTakerBalance,
|
||||
ExchangeContractErrs.InsufficientTakerFeeBalance,
|
||||
ExchangeContractErrs.InsufficientMakerAllowance,
|
||||
ExchangeContractErrs.InsufficientMakerFeeAllowance,
|
||||
ExchangeContractErrs.InsufficientTakerAllowance,
|
||||
ExchangeContractErrs.InsufficientTakerFeeAllowance,
|
||||
];
|
||||
if (_.includes(transferFailedErrorMessages, err.message)) {
|
||||
throw new Error(RevertReason.TransferFailed);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const wouldRoundingErrorOccur = OrderValidationUtils.isRoundingErrorFloor(
|
||||
desiredFillTakerTokenAmount,
|
||||
signedOrder.takerAssetAmount,
|
||||
signedOrder.makerAssetAmount,
|
||||
);
|
||||
if (wouldRoundingErrorOccur) {
|
||||
throw new Error(RevertReason.RoundingError);
|
||||
}
|
||||
return filledTakerTokenAmount;
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AbstractBalanceAndProxyAllowanceFetcher } from '../abstract/abstract_balance_and_proxy_allowance_fetcher';
|
||||
import { AbstractBalanceAndProxyAllowanceLazyStore } from '../abstract/abstract_balance_and_proxy_allowance_lazy_store';
|
||||
|
||||
/**
|
||||
* Copy on read store for balances/proxyAllowances of tokens/accounts
|
||||
*/
|
||||
export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProxyAllowanceLazyStore {
|
||||
private readonly _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher;
|
||||
private _balance: {
|
||||
[assetData: string]: {
|
||||
[userAddress: string]: BigNumber;
|
||||
};
|
||||
};
|
||||
private _proxyAllowance: {
|
||||
[assetData: string]: {
|
||||
[userAddress: string]: BigNumber;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Instantiates a BalanceAndProxyAllowanceLazyStore
|
||||
* @param balanceAndProxyAllowanceFetcher Class the implements the AbstractBalanceAndProxyAllowanceFetcher
|
||||
* @return Instance of BalanceAndProxyAllowanceLazyStore
|
||||
*/
|
||||
constructor(balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher) {
|
||||
this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher;
|
||||
this._balance = {};
|
||||
this._proxyAllowance = {};
|
||||
}
|
||||
/**
|
||||
* Get a users balance of an asset
|
||||
* @param assetData AssetData of interest
|
||||
* @param userAddress Ethereum address of interest
|
||||
*/
|
||||
public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
|
||||
if (this._balance[assetData] === undefined || this._balance[assetData][userAddress] === undefined) {
|
||||
const balance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, userAddress);
|
||||
this.setBalance(assetData, userAddress, balance);
|
||||
}
|
||||
const cachedBalance = this._balance[assetData][userAddress];
|
||||
return cachedBalance;
|
||||
}
|
||||
/**
|
||||
* Set the balance of an asset for a user
|
||||
* @param assetData AssetData of interest
|
||||
* @param userAddress Ethereum address of interest
|
||||
*/
|
||||
public setBalance(assetData: string, userAddress: string, balance: BigNumber): void {
|
||||
if (this._balance[assetData] === undefined) {
|
||||
this._balance[assetData] = {};
|
||||
}
|
||||
this._balance[assetData][userAddress] = balance;
|
||||
}
|
||||
/**
|
||||
* Clear the balance of an asset for a user
|
||||
* @param assetData AssetData of interest
|
||||
* @param userAddress Ethereum address of interest
|
||||
*/
|
||||
public deleteBalance(assetData: string, userAddress: string): void {
|
||||
if (this._balance[assetData] !== undefined) {
|
||||
delete this._balance[assetData][userAddress];
|
||||
if (_.isEmpty(this._balance[assetData])) {
|
||||
delete this._balance[assetData];
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the 0x asset proxy allowance
|
||||
* @param assetData AssetData of interest
|
||||
* @param userAddress Ethereum address of interest
|
||||
*/
|
||||
public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
|
||||
if (
|
||||
this._proxyAllowance[assetData] === undefined ||
|
||||
this._proxyAllowance[assetData][userAddress] === undefined
|
||||
) {
|
||||
const proxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
|
||||
assetData,
|
||||
userAddress,
|
||||
);
|
||||
this.setProxyAllowance(assetData, userAddress, proxyAllowance);
|
||||
}
|
||||
const cachedProxyAllowance = this._proxyAllowance[assetData][userAddress];
|
||||
return cachedProxyAllowance;
|
||||
}
|
||||
/**
|
||||
* Set the 0x asset proxy allowance
|
||||
* @param assetData AssetData of interest
|
||||
* @param userAddress Ethereum address of interest
|
||||
*/
|
||||
public setProxyAllowance(assetData: string, userAddress: string, proxyAllowance: BigNumber): void {
|
||||
if (this._proxyAllowance[assetData] === undefined) {
|
||||
this._proxyAllowance[assetData] = {};
|
||||
}
|
||||
this._proxyAllowance[assetData][userAddress] = proxyAllowance;
|
||||
}
|
||||
/**
|
||||
* Clear the 0x asset proxy allowance
|
||||
* @param assetData AssetData of interest
|
||||
* @param userAddress Ethereum address of interest
|
||||
*/
|
||||
public deleteProxyAllowance(assetData: string, userAddress: string): void {
|
||||
if (this._proxyAllowance[assetData] !== undefined) {
|
||||
delete this._proxyAllowance[assetData][userAddress];
|
||||
if (_.isEmpty(this._proxyAllowance[assetData])) {
|
||||
delete this._proxyAllowance[assetData];
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Delete all balances & allowances
|
||||
*/
|
||||
public deleteAll(): void {
|
||||
this._balance = {};
|
||||
this._proxyAllowance = {};
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AbstractOrderFilledCancelledFetcher } from '../abstract/abstract_order_filled_cancelled_fetcher';
|
||||
import { AbstractOrderFilledCancelledLazyStore } from '../abstract/abstract_order_filled_cancelled_lazy_store';
|
||||
import { orderHashUtils } from '../order_hash';
|
||||
|
||||
/**
|
||||
* Copy on read store for balances/proxyAllowances of tokens/accounts
|
||||
*/
|
||||
export class OrderFilledCancelledLazyStore implements AbstractOrderFilledCancelledLazyStore {
|
||||
private readonly _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher;
|
||||
private _filledTakerAmount: {
|
||||
[orderHash: string]: BigNumber;
|
||||
};
|
||||
private _isCancelled: {
|
||||
[orderHash: string]: boolean;
|
||||
};
|
||||
/**
|
||||
* Instantiate a OrderFilledCancelledLazyStore
|
||||
* @param orderFilledCancelledFetcher Class instance that implements the AbstractOrderFilledCancelledFetcher
|
||||
* @returns An instance of OrderFilledCancelledLazyStore
|
||||
*/
|
||||
constructor(orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher) {
|
||||
this._orderFilledCancelledFetcher = orderFilledCancelledFetcher;
|
||||
this._filledTakerAmount = {};
|
||||
this._isCancelled = {};
|
||||
}
|
||||
/**
|
||||
* Get the filledTakerAssetAmount of an order
|
||||
* @param orderHash OrderHash from order of interest
|
||||
* @return filledTakerAssetAmount
|
||||
*/
|
||||
public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
|
||||
if (this._filledTakerAmount[orderHash] === undefined) {
|
||||
const filledTakerAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash);
|
||||
this.setFilledTakerAmount(orderHash, filledTakerAmount);
|
||||
}
|
||||
const cachedFilledTakerAmount = this._filledTakerAmount[orderHash];
|
||||
return cachedFilledTakerAmount;
|
||||
}
|
||||
/**
|
||||
* Set the filledTakerAssetAmount of an order
|
||||
* @param orderHash OrderHash from order of interest
|
||||
* @param filledTakerAmount Desired filledTakerAssetAmount
|
||||
*/
|
||||
public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void {
|
||||
this._filledTakerAmount[orderHash] = filledTakerAmount;
|
||||
}
|
||||
/**
|
||||
* Clear the filledTakerAssetAmount of an order
|
||||
* @param orderHash OrderHash from order of interest
|
||||
*/
|
||||
public deleteFilledTakerAmount(orderHash: string): void {
|
||||
delete this._filledTakerAmount[orderHash];
|
||||
}
|
||||
/**
|
||||
* Check if an order has been cancelled
|
||||
* @param orderHash OrderHash from order of interest
|
||||
* @return Whether the order has been cancelled
|
||||
*/
|
||||
public async getIsCancelledAsync(signedOrder: SignedOrder): Promise<boolean> {
|
||||
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
|
||||
if (this._isCancelled[orderHash] === undefined) {
|
||||
const isCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(signedOrder);
|
||||
this.setIsCancelled(orderHash, isCancelled);
|
||||
}
|
||||
const cachedIsCancelled = this._isCancelled[orderHash]; // tslint:disable-line:boolean-naming
|
||||
return cachedIsCancelled;
|
||||
}
|
||||
/**
|
||||
* Set whether an order has been cancelled or not
|
||||
* @param orderHash OrderHash from order of interest
|
||||
* @param isCancelled Whether this order should be cancelled or not
|
||||
*/
|
||||
public setIsCancelled(orderHash: string, isCancelled: boolean): void {
|
||||
this._isCancelled[orderHash] = isCancelled;
|
||||
}
|
||||
/**
|
||||
* Clear whether the order has been cancelled if already set
|
||||
* @param orderHash OrderHash from order of interest
|
||||
*/
|
||||
public deleteIsCancelled(orderHash: string): void {
|
||||
delete this._isCancelled[orderHash];
|
||||
}
|
||||
/**
|
||||
* Clear all filled/cancelled state
|
||||
*/
|
||||
public deleteAll(): void {
|
||||
this.deleteAllFilled();
|
||||
this.deleteAllIsCancelled();
|
||||
}
|
||||
/**
|
||||
* Clear all cancelled state
|
||||
*/
|
||||
public deleteAllIsCancelled(): void {
|
||||
this._isCancelled = {};
|
||||
}
|
||||
/**
|
||||
* Clear all filled state
|
||||
*/
|
||||
public deleteAllFilled(): void {
|
||||
this._filledTakerAmount = {};
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import {
|
||||
DevUtilsContract,
|
||||
DummyERC20TokenContract,
|
||||
ERC20ProxyContract,
|
||||
ERC20TokenContract,
|
||||
} from '@0x/abi-gen-wrappers';
|
||||
import * as artifacts from '@0x/contract-artifacts';
|
||||
import { BlockchainLifecycle, devConstants } from '@0x/dev-utils';
|
||||
import { ExchangeContractErrs } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
|
||||
import { constants } from '../src/constants';
|
||||
import { ExchangeTransferSimulator } from '../src/exchange_transfer_simulator';
|
||||
import { BalanceAndProxyAllowanceLazyStore } from '../src/store/balance_and_proxy_allowance_lazy_store';
|
||||
import { TradeSide, TransferType } from '../src/types';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { SimpleERC20BalanceAndProxyAllowanceFetcher } from './utils/simple_erc20_balance_and_proxy_allowance_fetcher';
|
||||
import { provider, web3Wrapper } from './utils/web3_wrapper';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
|
||||
|
||||
describe('ExchangeTransferSimulator', async () => {
|
||||
const transferAmount = new BigNumber(5);
|
||||
let userAddresses: string[];
|
||||
let dummyERC20Token: DummyERC20TokenContract;
|
||||
let coinbase: string;
|
||||
let sender: string;
|
||||
let recipient: string;
|
||||
let exampleAssetData: string;
|
||||
let exchangeTransferSimulator: ExchangeTransferSimulator;
|
||||
let txHash: string;
|
||||
let erc20ProxyAddress: string;
|
||||
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
|
||||
before(async function(): Promise<void> {
|
||||
const mochaTestTimeoutMs = 20000;
|
||||
this.timeout(mochaTestTimeoutMs); // tslint:disable-line:no-invalid-this
|
||||
|
||||
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
|
||||
[coinbase, sender, recipient] = userAddresses;
|
||||
|
||||
const txDefaults = {
|
||||
gas: devConstants.GAS_LIMIT,
|
||||
from: devConstants.TESTRPC_FIRST_ADDRESS,
|
||||
};
|
||||
|
||||
await blockchainLifecycle.startAsync();
|
||||
const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
|
||||
artifacts.ERC20Proxy,
|
||||
provider,
|
||||
txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
erc20ProxyAddress = erc20Proxy.address;
|
||||
|
||||
const totalSupply = new BigNumber(100000000000000000000);
|
||||
const name = 'Test';
|
||||
const symbol = 'TST';
|
||||
const decimals = new BigNumber(18);
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
dummyERC20Token = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
|
||||
artifacts.DummyERC20Token,
|
||||
provider,
|
||||
txDefaults,
|
||||
artifacts,
|
||||
name,
|
||||
symbol,
|
||||
decimals,
|
||||
totalSupply,
|
||||
);
|
||||
|
||||
exampleAssetData = await devUtils.encodeERC20AssetData.callAsync(dummyERC20Token.address);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
after(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('#transferFromAsync', function(): void {
|
||||
// HACK: For some reason these tests need a slightly longer timeout
|
||||
const mochaTestTimeoutMs = 3000;
|
||||
this.timeout(mochaTestTimeoutMs); // tslint:disable-line:no-invalid-this
|
||||
beforeEach(() => {
|
||||
const simpleERC20BalanceAndProxyAllowanceFetcher = new SimpleERC20BalanceAndProxyAllowanceFetcher(
|
||||
(dummyERC20Token as any) as ERC20TokenContract,
|
||||
erc20ProxyAddress,
|
||||
);
|
||||
const balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(
|
||||
simpleERC20BalanceAndProxyAllowanceFetcher,
|
||||
);
|
||||
exchangeTransferSimulator = new ExchangeTransferSimulator(balanceAndProxyAllowanceLazyStore);
|
||||
});
|
||||
it("throws if the user doesn't have enough allowance", async () => {
|
||||
return expect(
|
||||
exchangeTransferSimulator.transferFromAsync(
|
||||
exampleAssetData,
|
||||
sender,
|
||||
recipient,
|
||||
transferAmount,
|
||||
TradeSide.Taker,
|
||||
TransferType.Trade,
|
||||
),
|
||||
).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance);
|
||||
});
|
||||
it("throws if the user doesn't have enough balance", async () => {
|
||||
txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, {
|
||||
from: sender,
|
||||
});
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
|
||||
return expect(
|
||||
exchangeTransferSimulator.transferFromAsync(
|
||||
exampleAssetData,
|
||||
sender,
|
||||
recipient,
|
||||
transferAmount,
|
||||
TradeSide.Maker,
|
||||
TransferType.Trade,
|
||||
),
|
||||
).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
|
||||
});
|
||||
it('updates balances and proxyAllowance after transfer', async () => {
|
||||
txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, {
|
||||
from: coinbase,
|
||||
});
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
|
||||
|
||||
txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, {
|
||||
from: sender,
|
||||
});
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
|
||||
|
||||
await exchangeTransferSimulator.transferFromAsync(
|
||||
exampleAssetData,
|
||||
sender,
|
||||
recipient,
|
||||
transferAmount,
|
||||
TradeSide.Taker,
|
||||
TransferType.Trade,
|
||||
);
|
||||
const store = (exchangeTransferSimulator as any)._store;
|
||||
const senderBalance = await store.getBalanceAsync(exampleAssetData, sender);
|
||||
const recipientBalance = await store.getBalanceAsync(exampleAssetData, recipient);
|
||||
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleAssetData, sender);
|
||||
expect(senderBalance).to.be.bignumber.equal(0);
|
||||
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
|
||||
expect(senderProxyAllowance).to.be.bignumber.equal(0);
|
||||
});
|
||||
it("doesn't update proxyAllowance after transfer if unlimited", async () => {
|
||||
txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, {
|
||||
from: coinbase,
|
||||
});
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
|
||||
txHash = await dummyERC20Token.approve.sendTransactionAsync(
|
||||
erc20ProxyAddress,
|
||||
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
|
||||
{
|
||||
from: sender,
|
||||
},
|
||||
);
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
|
||||
await exchangeTransferSimulator.transferFromAsync(
|
||||
exampleAssetData,
|
||||
sender,
|
||||
recipient,
|
||||
transferAmount,
|
||||
TradeSide.Taker,
|
||||
TransferType.Trade,
|
||||
);
|
||||
const store = (exchangeTransferSimulator as any)._store;
|
||||
const senderBalance = await store.getBalanceAsync(exampleAssetData, sender);
|
||||
const recipientBalance = await store.getBalanceAsync(exampleAssetData, recipient);
|
||||
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleAssetData, sender);
|
||||
expect(senderBalance).to.be.bignumber.equal(0);
|
||||
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
|
||||
expect(senderProxyAllowance).to.be.bignumber.equal(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import { AbstractBalanceAndProxyAllowanceFetcher } from '../src/abstract/abstract_balance_and_proxy_allowance_fetcher';
|
||||
import { AbstractOrderFilledCancelledFetcher } from '../src/abstract/abstract_order_filled_cancelled_fetcher';
|
||||
import { OrderStateUtils } from '../src/order_state_utils';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { testOrderFactory } from './utils/test_order_factory';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('OrderStateUtils', () => {
|
||||
describe('#getOpenOrderStateAsync', () => {
|
||||
const buildMockBalanceFetcher = (takerBalance: BigNumber): AbstractBalanceAndProxyAllowanceFetcher => {
|
||||
const balanceFetcher = {
|
||||
async getBalanceAsync(_assetData: string, _userAddress: string): Promise<BigNumber> {
|
||||
return takerBalance;
|
||||
},
|
||||
async getProxyAllowanceAsync(_assetData: string, _userAddress: string): Promise<BigNumber> {
|
||||
return takerBalance;
|
||||
},
|
||||
};
|
||||
return balanceFetcher;
|
||||
};
|
||||
const buildMockOrderFilledFetcher = (
|
||||
filledAmount: BigNumber = new BigNumber(0),
|
||||
cancelled: boolean = false,
|
||||
): AbstractOrderFilledCancelledFetcher => {
|
||||
const orderFetcher = {
|
||||
async getFilledTakerAmountAsync(_orderHash: string): Promise<BigNumber> {
|
||||
return filledAmount;
|
||||
},
|
||||
async isOrderCancelledAsync(_signedOrder: SignedOrder): Promise<boolean> {
|
||||
return cancelled;
|
||||
},
|
||||
};
|
||||
return orderFetcher;
|
||||
};
|
||||
it('should have valid order state if order can be fully filled with small maker amount', async () => {
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const takerAssetAmount = new BigNumber(10000000000000000);
|
||||
const takerBalance = takerAssetAmount;
|
||||
const orderFilledAmount = new BigNumber(0);
|
||||
const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance);
|
||||
const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount);
|
||||
const [signedOrder] = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher);
|
||||
const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder);
|
||||
expect(orderState.isValid).to.eq(true);
|
||||
});
|
||||
it('should be invalid when an order is partially filled where only a rounding error remains', async () => {
|
||||
const makerAssetAmount = new BigNumber(1001);
|
||||
const takerAssetAmount = new BigNumber(3);
|
||||
const takerBalance = takerAssetAmount;
|
||||
const orderFilledAmount = new BigNumber(2);
|
||||
const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance);
|
||||
const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount);
|
||||
const [signedOrder] = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher);
|
||||
const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder);
|
||||
expect(orderState.isValid).to.eq(false);
|
||||
});
|
||||
it('should be invalid when an order is cancelled', async () => {
|
||||
const makerAssetAmount = new BigNumber(1000);
|
||||
const takerAssetAmount = new BigNumber(2);
|
||||
const takerBalance = takerAssetAmount;
|
||||
const orderFilledAmount = new BigNumber(0);
|
||||
const isCancelled = true;
|
||||
const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance);
|
||||
const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount, isCancelled);
|
||||
const [signedOrder] = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher);
|
||||
const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder);
|
||||
expect(orderState.isValid).to.eq(false);
|
||||
});
|
||||
it('should be invalid when an order is fully filled', async () => {
|
||||
const makerAssetAmount = new BigNumber(1000);
|
||||
const takerAssetAmount = new BigNumber(2);
|
||||
const takerBalance = takerAssetAmount;
|
||||
const orderFilledAmount = takerAssetAmount;
|
||||
const isCancelled = false;
|
||||
const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance);
|
||||
const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount, isCancelled);
|
||||
const [signedOrder] = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher);
|
||||
const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder);
|
||||
expect(orderState.isValid).to.eq(false);
|
||||
});
|
||||
it('should include the transactionHash in orderState if supplied in method invocation', async () => {
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const takerAssetAmount = new BigNumber(10000000000000000);
|
||||
const takerBalance = takerAssetAmount;
|
||||
const orderFilledAmount = new BigNumber(0);
|
||||
const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance);
|
||||
const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount);
|
||||
const [signedOrder] = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher);
|
||||
const transactionHash = '0xdeadbeef';
|
||||
const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder, transactionHash);
|
||||
expect(orderState.transactionHash).to.eq(transactionHash);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import { OrderValidationUtils } from '../src/order_validation_utils';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('OrderValidationUtils', () => {
|
||||
describe('#isRoundingError', () => {
|
||||
it('should return false if there is a rounding error of 0.1%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(999);
|
||||
const target = new BigNumber(50);
|
||||
// rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1%
|
||||
const isRoundingError = OrderValidationUtils.isRoundingErrorFloor(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
|
||||
it('should return false if there is a rounding of 0.09%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(9991);
|
||||
const target = new BigNumber(500);
|
||||
// rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09%
|
||||
const isRoundingError = OrderValidationUtils.isRoundingErrorFloor(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
|
||||
it('should return true if there is a rounding error of 0.11%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(9989);
|
||||
const target = new BigNumber(500);
|
||||
// rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011%
|
||||
const isRoundingError = OrderValidationUtils.isRoundingErrorFloor(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.true();
|
||||
});
|
||||
|
||||
it('should return true if there is a rounding error > 0.1%', async () => {
|
||||
const numerator = new BigNumber(3);
|
||||
const denominator = new BigNumber(7);
|
||||
const target = new BigNumber(10);
|
||||
// rounding error = ((3*10/7) - floor(3*10/7)) / (3*10/7) = 6.67%
|
||||
const isRoundingError = OrderValidationUtils.isRoundingErrorFloor(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.true();
|
||||
});
|
||||
|
||||
it('should return false when there is no rounding error', async () => {
|
||||
const numerator = new BigNumber(1);
|
||||
const denominator = new BigNumber(2);
|
||||
const target = new BigNumber(10);
|
||||
|
||||
const isRoundingError = OrderValidationUtils.isRoundingErrorFloor(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
|
||||
it('should return false when there is rounding error <= 0.1%', async () => {
|
||||
// randomly generated numbers
|
||||
const numerator = new BigNumber(76564);
|
||||
const denominator = new BigNumber(676373677);
|
||||
const target = new BigNumber(105762562);
|
||||
// rounding error = ((76564*105762562/676373677) - floor(76564*105762562/676373677)) /
|
||||
// (76564*105762562/676373677) = 0.0007%
|
||||
const isRoundingError = OrderValidationUtils.isRoundingErrorFloor(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ERC20TokenContract } from '@0x/abi-gen-wrappers';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { AbstractBalanceAndProxyAllowanceFetcher } from '../../src/abstract/abstract_balance_and_proxy_allowance_fetcher';
|
||||
|
||||
export class SimpleERC20BalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
|
||||
private readonly _erc20TokenContract: ERC20TokenContract;
|
||||
private readonly _erc20ProxyAddress: string;
|
||||
constructor(erc20TokenWrapper: ERC20TokenContract, erc20ProxyAddress: string) {
|
||||
this._erc20TokenContract = erc20TokenWrapper;
|
||||
this._erc20ProxyAddress = erc20ProxyAddress;
|
||||
}
|
||||
public async getBalanceAsync(_assetData: string, userAddress: string): Promise<BigNumber> {
|
||||
// HACK: We cheat and don't pass in the assetData since it's always the same token used
|
||||
// in our tests.
|
||||
const balance = await this._erc20TokenContract.balanceOf.callAsync(userAddress);
|
||||
return balance;
|
||||
}
|
||||
public async getProxyAllowanceAsync(_assetData: string, userAddress: string): Promise<BigNumber> {
|
||||
// HACK: We cheat and don't pass in the assetData since it's always the same token used
|
||||
// in our tests.
|
||||
const proxyAllowance = await this._erc20TokenContract.allowance.callAsync(userAddress, this._erc20ProxyAddress);
|
||||
return proxyAllowance;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user