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:
fabioberger 2019-11-07 12:46:34 +00:00
parent 2915ee08ea
commit fba3870ef1
20 changed files with 56 additions and 2140 deletions

View File

@ -76,7 +76,7 @@
"dependencies": {
"@0x/base-contract": "^5.5.0-beta.1",
"@0x/contracts-asset-proxy": "^2.3.0-beta.1",
"@0x/contracts-dev-utils": "^0.1.0-beta.1",
"@0x/contracts-dev-utils": "^2.4.0-beta.1",
"@0x/contracts-erc1155": "^1.2.0-beta.1",
"@0x/contracts-erc20": "^2.3.0-beta.1",
"@0x/contracts-erc721": "^2.2.0-beta.1",

View File

@ -1,29 +1,21 @@
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 { artifacts as assetProxyArtifacts, ERC20ProxyContract } from '@0x/contracts-asset-proxy';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { artifacts as erc20Artifacts, DummyERC20TokenContract, ERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, chaiSetup, constants } from '@0x/contracts-test-utils';
import { TradeSide, TransferType } from '@0x/order-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 { ExchangeTransferSimulator } from './utils/exchange_transfer_simulator';
import { SimpleERC20BalanceAndProxyAllowanceFetcher } from './utils/simple_erc20_balance_and_proxy_allowance_fetcher';
import { provider, web3Wrapper } from './utils/web3_wrapper';
import { BalanceAndProxyAllowanceLazyStore } from './utils/store/balance_and_proxy_allowance_lazy_store';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('ExchangeTransferSimulator', async () => {
const GAS_LIMIT = 9e6;
blockchainTests('ExchangeTransferSimulator', env => {
const transferAmount = new BigNumber(5);
let userAddresses: string[];
let dummyERC20Token: DummyERC20TokenContract;
@ -34,25 +26,25 @@ describe('ExchangeTransferSimulator', async () => {
let exchangeTransferSimulator: ExchangeTransferSimulator;
let txHash: string;
let erc20ProxyAddress: string;
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, env.provider);
before(async function(): Promise<void> {
const mochaTestTimeoutMs = 20000;
this.timeout(mochaTestTimeoutMs); // tslint:disable-line:no-invalid-this
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
userAddresses = await env.web3Wrapper.getAvailableAddressesAsync();
[coinbase, sender, recipient] = userAddresses;
const txDefaults = {
gas: devConstants.GAS_LIMIT,
from: devConstants.TESTRPC_FIRST_ADDRESS,
gas: GAS_LIMIT,
from: userAddresses[0],
};
await blockchainLifecycle.startAsync();
await env.blockchainLifecycle.startAsync();
const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
artifacts.ERC20Proxy,
provider,
assetProxyArtifacts.ERC20Proxy,
env.provider,
txDefaults,
artifacts,
assetProxyArtifacts,
);
erc20ProxyAddress = erc20Proxy.address;
@ -62,10 +54,10 @@ describe('ExchangeTransferSimulator', async () => {
const decimals = new BigNumber(18);
// tslint:disable-next-line:no-unused-variable
dummyERC20Token = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.DummyERC20Token,
provider,
erc20Artifacts.DummyERC20Token,
env.provider,
txDefaults,
artifacts,
erc20Artifacts,
name,
symbol,
decimals,
@ -75,13 +67,13 @@ describe('ExchangeTransferSimulator', async () => {
exampleAssetData = await devUtils.encodeERC20AssetData.callAsync(dummyERC20Token.address);
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
await env.blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
await env.blockchainLifecycle.revertAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
await env.blockchainLifecycle.revertAsync();
});
describe('#transferFromAsync', function(): void {
// HACK: For some reason these tests need a slightly longer timeout
@ -95,7 +87,7 @@ describe('ExchangeTransferSimulator', async () => {
const balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(
simpleERC20BalanceAndProxyAllowanceFetcher,
);
exchangeTransferSimulator = new ExchangeTransferSimulator(balanceAndProxyAllowanceLazyStore);
exchangeTransferSimulator = new ExchangeTransferSimulator(balanceAndProxyAllowanceLazyStore, env.provider);
});
it("throws if the user doesn't have enough allowance", async () => {
return expect(
@ -113,7 +105,7 @@ describe('ExchangeTransferSimulator', async () => {
txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, {
from: sender,
});
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
await env.web3Wrapper.awaitTransactionSuccessAsync(txHash);
return expect(
exchangeTransferSimulator.transferFromAsync(
exampleAssetData,
@ -129,12 +121,12 @@ describe('ExchangeTransferSimulator', async () => {
txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, {
from: coinbase,
});
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
await env.web3Wrapper.awaitTransactionSuccessAsync(txHash);
txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, {
from: sender,
});
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
await env.web3Wrapper.awaitTransactionSuccessAsync(txHash);
await exchangeTransferSimulator.transferFromAsync(
exampleAssetData,
@ -156,7 +148,7 @@ describe('ExchangeTransferSimulator', async () => {
txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, {
from: coinbase,
});
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
await env.web3Wrapper.awaitTransactionSuccessAsync(txHash);
txHash = await dummyERC20Token.approve.sendTransactionAsync(
erc20ProxyAddress,
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
@ -164,7 +156,7 @@ describe('ExchangeTransferSimulator', async () => {
from: sender,
},
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
await env.web3Wrapper.awaitTransactionSuccessAsync(txHash);
await exchangeTransferSimulator.transferFromAsync(
exampleAssetData,
sender,

View File

@ -1,4 +1,3 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
/**
@ -13,10 +12,4 @@ export abstract class AbstractOrderFilledCancelledFetcher {
* @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>;
}

View File

@ -1,9 +1,7 @@
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;

View File

@ -1,4 +1,4 @@
import { DevUtilsContract } from '@0x/abi-gen-wrappers';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';

View File

@ -1,10 +1,10 @@
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { constants } from '@0x/contracts-test-utils';
import { SupportedProvider, TradeSide, TransferType } from '@0x/order-utils';
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',
@ -40,6 +40,7 @@ const ERR_MSG_MAPPING = {
*/
export class ExchangeTransferSimulator {
private readonly _store: AbstractBalanceAndProxyAllowanceLazyStore;
private readonly _devUtils: DevUtilsContract;
private static _throwValidationError(
failureReason: FailureReason,
tradeSide: TradeSide,
@ -53,8 +54,9 @@ export class ExchangeTransferSimulator {
* @param store A class that implements AbstractBalanceAndProxyAllowanceLazyStore
* @return an instance of ExchangeTransferSimulator
*/
constructor(store: AbstractBalanceAndProxyAllowanceLazyStore) {
constructor(store: AbstractBalanceAndProxyAllowanceLazyStore, provider: SupportedProvider) {
this._store = store;
this._devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
}
/**
* Simulates transferFrom call performed by a proxy
@ -75,7 +77,7 @@ export class ExchangeTransferSimulator {
tradeSide: TradeSide,
transferType: TransferType,
): Promise<void> {
const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
const assetProxyId = await this._devUtils.decodeAssetProxyId.callAsync(assetData);
switch (assetProxyId) {
case AssetProxyId.ERC1155:
case AssetProxyId.ERC20:
@ -108,11 +110,11 @@ export class ExchangeTransferSimulator {
break;
}
case AssetProxyId.MultiAsset: {
const decodedAssetData = assetDataUtils.decodeMultiAssetData(assetData);
const decodedAssetData = await this._devUtils.decodeMultiAssetData.callAsync(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];
for (const [index, nestedAssetDataElement] of decodedAssetData[2].entries()) {
const amountsElement = decodedAssetData[1][index];
const totalAmount = amountInBaseUnits.times(amountsElement);
await this.transferFromAsync(
nestedAssetDataElement,

View File

@ -7,7 +7,7 @@ import {
} from '@0x/contracts-asset-proxy';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { constants, expect, LogDecoder, orderUtils, signingUtils } from '@0x/contracts-test-utils';
import { BalanceAndProxyAllowanceLazyStore, ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils';
import { ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils';
import { FillResults, Order, SignatureType, SignedOrder } from '@0x/types';
import { BigNumber, errorUtils, providerUtils, RevertError, StringRevertError } from '@0x/utils';
import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper';
@ -35,6 +35,7 @@ import {
import { FillOrderError, FillOrderSimulator } from './fill_order_simulator';
import { OrderFactoryFromScenario } from './order_factory_from_scenario';
import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher';
import { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
const EMPTY_FILL_RESULTS = {
takerAssetFilledAmount: constants.ZERO_AMOUNT,
@ -503,7 +504,7 @@ export class FillOrderCombinatorialUtils {
takerAssetFillAmount: BigNumber,
lazyStore: BalanceAndProxyAllowanceLazyStore,
): Promise<FillResults> {
const simulator = new FillOrderSimulator(lazyStore);
const simulator = new FillOrderSimulator(lazyStore, this.provider);
return simulator.simulateFillOrderAsync(signedOrder, this.takerAddress, takerAssetFillAmount);
}

View File

@ -1,8 +1,7 @@
import { constants, orderUtils } from '@0x/contracts-test-utils';
import {
AbstractBalanceAndProxyAllowanceLazyStore as LazyStore,
ExchangeTransferSimulator,
Order,
SupportedProvider,
TradeSide,
TransferType,
} from '@0x/order-utils';
@ -10,6 +9,11 @@ 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';
export enum FillOrderError {
OrderUnfillable = 'ORDER_UNFILLABLE',
InvalidSender = 'INVALID_SENDER',
@ -27,9 +31,9 @@ export class FillOrderSimulator {
public readonly lazyStore: LazyStore;
private readonly _transferSimulator: ExchangeTransferSimulator;
constructor(lazyStore: LazyStore) {
constructor(lazyStore: LazyStore, provider: SupportedProvider) {
this.lazyStore = lazyStore;
this._transferSimulator = new ExchangeTransferSimulator(lazyStore);
this._transferSimulator = new ExchangeTransferSimulator(lazyStore, provider);
}
public async simulateFillOrderAsync(

View File

@ -1,6 +1,6 @@
import { AbstractBalanceAndProxyAllowanceFetcher } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
import { AssetWrapper } from './asset_wrapper';
export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {

View File

@ -1,7 +1,7 @@
import { ERC20TokenContract } from '@0x/abi-gen-wrappers';
import { ERC20TokenContract } from '@0x/contracts-erc20';
import { BigNumber } from '@0x/utils';
import { AbstractBalanceAndProxyAllowanceFetcher } from '../../src/abstract/abstract_balance_and_proxy_allowance_fetcher';
import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
export class SimpleERC20BalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
private readonly _erc20TokenContract: ERC20TokenContract;

View File

@ -1,10 +1,8 @@
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
@ -55,20 +53,6 @@ export class OrderFilledCancelledLazyStore implements AbstractOrderFilledCancell
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

File diff suppressed because it is too large Load Diff

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
});
});
});

View File

@ -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();
});
});
});