Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/connect/sra-api-v2

This commit is contained in:
fragosti 2018-08-16 16:08:02 -07:00
commit 89174cd63f
47 changed files with 633 additions and 214 deletions

View File

@ -81,7 +81,7 @@
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^2.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"json-loader": "^0.5.4",
"make-promises-safe": "^1.1.0",

View File

@ -56,7 +56,7 @@
"@types/tmp": "^0.0.33",
"@types/yargs": "^10.0.0",
"chai": "^4.1.2",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^5.2.0",

View File

@ -36,7 +36,7 @@
"@types/mocha": "^2.2.42",
"@types/valid-url": "^1.0.2",
"chai": "^4.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -34,7 +34,7 @@
"@0xproject/tslint-config": "^1.0.5",
"@types/lodash": "4.14.104",
"chai": "^4.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",
"npm-run-all": "^4.1.2",

View File

@ -64,7 +64,7 @@
"devDependencies": {
"@0xproject/monorepo-scripts": "^1.0.5",
"@0xproject/tslint-config": "^1.0.5",
"@types/fetch-mock": "^5.12.2",
"@types/fetch-mock": "^6.0.3",
"@types/lodash": "4.14.104",
"@types/mocha": "^2.2.42",
"@types/query-string": "^5.0.1",
@ -73,7 +73,7 @@
"async-child-process": "^1.1.1",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"fetch-mock": "^5.13.1",
"make-promises-safe": "^1.1.0",

View File

@ -57,7 +57,7 @@
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^2.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -59,7 +59,7 @@
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^2.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -104,6 +104,17 @@ describe('FillOrder Tests', () => {
};
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount with zero decimals', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetAmountScenario: OrderAssetAmountScenario.Small,
makerAssetDataScenario: AssetDataScenario.ERC20ZeroDecimals,
},
};
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => {
const fillScenario = {
...defaultFillScenario,

View File

@ -81,6 +81,12 @@ export async function fillOrderCombinatorialUtilsFactoryAsync(
erc20FiveDecimalTokenCount,
fiveDecimals,
);
const zeroDecimals = new BigNumber(0);
const erc20ZeroDecimalTokenCount = 2;
const [erc20ZeroDecimalTokenA, erc20ZeroDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
erc20ZeroDecimalTokenCount,
zeroDecimals,
);
const erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();
@ -119,6 +125,7 @@ export async function fillOrderCombinatorialUtilsFactoryAsync(
zrxToken.address,
[erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address],
[erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address],
[erc20ZeroDecimalTokenA.address, erc20ZeroDecimalTokenB.address],
erc721Token,
erc721Balances,
exchangeContract.address,

View File

@ -21,6 +21,8 @@ const POINT_ONE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(100_000_000_000_000_000)
const POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(50_000_000_000_000_000);
const TEN_UNITS_FIVE_DECIMALS = new BigNumber(1_000_000);
const FIVE_UNITS_FIVE_DECIMALS = new BigNumber(500_000);
const TEN_UNITS_ZERO_DECIMALS = new BigNumber(10);
const ONE_THOUSAND_UNITS_ZERO_DECIMALS = new BigNumber(1000);
const ONE_NFT_UNIT = new BigNumber(1);
export class OrderFactoryFromScenario {
@ -28,6 +30,7 @@ export class OrderFactoryFromScenario {
private readonly _zrxAddress: string;
private readonly _nonZrxERC20EighteenDecimalTokenAddresses: string[];
private readonly _erc20FiveDecimalTokenAddresses: string[];
private readonly _erc20ZeroDecimalTokenAddresses: string[];
private readonly _erc721Token: DummyERC721TokenContract;
private readonly _erc721Balances: ERC721TokenIdsByOwner;
private readonly _exchangeAddress: string;
@ -36,6 +39,7 @@ export class OrderFactoryFromScenario {
zrxAddress: string,
nonZrxERC20EighteenDecimalTokenAddresses: string[],
erc20FiveDecimalTokenAddresses: string[],
erc20ZeroDecimalTokenAddresses: string[],
erc721Token: DummyERC721TokenContract,
erc721Balances: ERC721TokenIdsByOwner,
exchangeAddress: string,
@ -44,6 +48,7 @@ export class OrderFactoryFromScenario {
this._zrxAddress = zrxAddress;
this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses;
this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses;
this._erc20ZeroDecimalTokenAddresses = erc20ZeroDecimalTokenAddresses;
this._erc721Token = erc721Token;
this._erc721Balances = erc721Balances;
this._exchangeAddress = exchangeAddress;
@ -89,6 +94,9 @@ export class OrderFactoryFromScenario {
erc721MakerAssetIds[0],
);
break;
case AssetDataScenario.ERC20ZeroDecimals:
makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[0]);
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
}
@ -109,6 +117,9 @@ export class OrderFactoryFromScenario {
erc721TakerAssetIds[0],
);
break;
case AssetDataScenario.ERC20ZeroDecimals:
takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[1]);
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
}
@ -126,6 +137,9 @@ export class OrderFactoryFromScenario {
case AssetDataScenario.ERC721:
makerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
makerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
}
@ -142,6 +156,9 @@ export class OrderFactoryFromScenario {
case AssetDataScenario.ERC721:
makerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
makerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
}
@ -166,6 +183,9 @@ export class OrderFactoryFromScenario {
case AssetDataScenario.ERC721:
takerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
takerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
}
@ -182,6 +202,9 @@ export class OrderFactoryFromScenario {
case AssetDataScenario.ERC721:
takerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
takerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
}

View File

@ -177,10 +177,11 @@ export enum ExpirationTimeSecondsScenario {
}
export enum AssetDataScenario {
ERC721 = 'ERC721',
ERC20ZeroDecimals = 'ERC20_ZERO_DECIMALS',
ZRXFeeToken = 'ZRX_FEE_TOKEN',
ERC20FiveDecimals = 'ERC20_FIVE_DECIMALS',
ERC20NonZRXEighteenDecimals = 'ERC20_NON_ZRX_EIGHTEEN_DECIMALS',
ERC721 = 'ERC721',
}
export enum TakerAssetFillAmountScenario {

View File

@ -35,7 +35,7 @@
"@types/lodash": "4.14.104",
"@types/mocha": "^2.2.42",
"chai": "^4.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",
"npm-run-all": "^4.1.2",

View File

@ -37,7 +37,7 @@
"devDependencies": {
"@0xproject/monorepo-scripts": "^1.0.5",
"@0xproject/tslint-config": "^1.0.5",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"shx": "^0.2.2",
"tslint": "5.11.0",

View File

@ -33,7 +33,7 @@
"@0xproject/sol-compiler": "^1.0.5",
"@0xproject/tslint-config": "^1.0.5",
"@types/lodash": "4.14.104",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",

View File

@ -60,7 +60,7 @@
"@types/lodash.values": "^4.3.3",
"@types/mocha": "^2.2.42",
"chai": "^4.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"lodash.foreach": "^4.5.0",
"make-promises-safe": "^1.1.0",

View File

@ -44,7 +44,7 @@
"@0xproject/tslint-config": "^1.0.5",
"@0xproject/types": "^1.0.1-rc.4",
"@types/yargs": "^10.0.0",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",

View File

@ -1,4 +1,22 @@
[
{
"version": "1.0.1-rc.4",
"changes": [
{
"note": "Remove rounding error being thrown when maker amount is very small",
"pr": 959
},
{
"note": "Added rateUtils and sortingUtils",
"pr": 953
},
{
"note":
"Update marketUtils api such that all optional parameters are bundled into one optional param and more defaults are provided",
"pr": 954
}
]
},
{
"version": "1.0.1-rc.3",
"changes": [

View File

@ -61,7 +61,7 @@
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^2.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -25,3 +25,5 @@ export { EIP712Utils } from './eip712_utils';
export { OrderValidationUtils } from './order_validation_utils';
export { ExchangeTransferSimulator } from './exchange_transfer_simulator';
export { marketUtils } from './market_utils';
export { rateUtils } from './rate_utils';
export { sortingUtils } from './sorting_utils';

View File

@ -1,46 +1,51 @@
import { schemas } from '@0xproject/json-schemas';
import { SignedOrder } from '@0xproject/types';
import { Order } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import { assert } from './assert';
import { constants } from './constants';
import { FindFeeOrdersThatCoverFeesForTargetOrdersOpts, FindOrdersThatCoverMakerAssetFillAmountOpts } from './types';
export const marketUtils = {
/**
* Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances,
* allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last.
* Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount
* in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last order.
* Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH.
* @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset.
* All orders should specify WETH as the takerAsset.
* @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter.
* You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
* for these values.
* @param makerAssetFillAmount The amount of makerAsset desired to be filled.
* @param slippageBufferAmount An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills.
* @param orders An array of objects that extend the Order interface. All orders should specify the same makerAsset.
* All orders should specify WETH as the takerAsset.
* @param makerAssetFillAmount The amount of makerAsset desired to be filled.
* @param opts Optional arguments this function accepts.
* @return Resulting orders and remaining fill amount that could not be covered by the input.
*/
findOrdersThatCoverMakerAssetFillAmount(
signedOrders: SignedOrder[],
remainingFillableMakerAssetAmounts: BigNumber[],
findOrdersThatCoverMakerAssetFillAmount<T extends Order>(
orders: T[],
makerAssetFillAmount: BigNumber,
slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
opts?: FindOrdersThatCoverMakerAssetFillAmountOpts,
): { resultOrders: T[]; remainingFillAmount: BigNumber } {
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount);
// try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders
const remainingFillableMakerAssetAmounts = _.get(
opts,
'remainingFillableMakerAssetAmounts',
_.map(orders, order => order.makerAssetAmount),
) as BigNumber[];
_.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
);
assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount);
assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
assert.assert(
signedOrders.length === remainingFillableMakerAssetAmounts.length,
'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
orders.length === remainingFillableMakerAssetAmounts.length,
'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length',
);
// try to get slippageBufferAmount from opts, if it's not there, default to 0
const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber;
assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount);
// calculate total amount of makerAsset needed to be filled
const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount);
// iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount
// iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount
const result = _.reduce(
signedOrders,
orders,
({ resultOrders, remainingFillAmount }, order, index) => {
if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) {
return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT };
@ -59,55 +64,61 @@ export const marketUtils = {
};
}
},
{ resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount },
{ resultOrders: [] as T[], remainingFillAmount: totalFillAmount },
);
return result;
},
/**
* Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account
* on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a
* slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of
* Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX
* in order to fill the takerFees required by orders plus a slippageBufferAmount.
* Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of
* feeOrders that will cost the least ETH.
* @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter.
* You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
* for these values.
* @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter.
* You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
* for these values.
* @param slippageBufferAmount An additional amount of fee to be covered by the result in case of trade collisions or partial fills.
* @param orders An array of objects that extend the Order interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @param feeOrders An array of objects that extend the Order interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @param opts Optional arguments this function accepts.
* @return Resulting orders and remaining fee amount that could not be covered by the input.
*/
findFeeOrdersThatCoverFeesForTargetOrders(
signedOrders: SignedOrder[],
remainingFillableMakerAssetAmounts: BigNumber[],
signedFeeOrders: SignedOrder[],
remainingFillableFeeAmounts: BigNumber[],
slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
findFeeOrdersThatCoverFeesForTargetOrders<T extends Order>(
orders: T[],
feeOrders: T[],
opts?: FindFeeOrdersThatCoverFeesForTargetOrdersOpts,
): { resultOrders: T[]; remainingFeeAmount: BigNumber } {
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
assert.doesConformToSchema('feeOrders', feeOrders, schemas.ordersSchema);
// try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders
const remainingFillableMakerAssetAmounts = _.get(
opts,
'remainingFillableMakerAssetAmounts',
_.map(orders, order => order.makerAssetAmount),
) as BigNumber[];
_.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
);
assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
assert.assert(
orders.length === remainingFillableMakerAssetAmounts.length,
'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length',
);
// try to get remainingFillableFeeAmounts from opts, if it's not there, use makerAssetAmount values from feeOrders
const remainingFillableFeeAmounts = _.get(
opts,
'remainingFillableFeeAmounts',
_.map(feeOrders, order => order.makerAssetAmount),
) as BigNumber[];
_.forEach(remainingFillableFeeAmounts, (amount, index) =>
assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount),
);
assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
assert.assert(
signedOrders.length === remainingFillableMakerAssetAmounts.length,
'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
feeOrders.length === remainingFillableFeeAmounts.length,
'Expected feeOrders.length to equal opts.remainingFillableFeeAmounts.length',
);
assert.assert(
signedOrders.length === remainingFillableMakerAssetAmounts.length,
'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length',
);
// calculate total amount of ZRX needed to fill signedOrders
// try to get slippageBufferAmount from opts, if it's not there, default to 0
const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber;
assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount);
// calculate total amount of ZRX needed to fill orders
const totalFeeAmount = _.reduce(
signedOrders,
orders,
(accFees, order, index) => {
const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable
@ -118,10 +129,12 @@ export const marketUtils = {
constants.ZERO_AMOUNT,
);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
signedFeeOrders,
remainingFillableFeeAmounts,
feeOrders,
totalFeeAmount,
slippageBufferAmount,
{
remainingFillableMakerAssetAmounts: remainingFillableFeeAmounts,
slippageBufferAmount,
},
);
return {
resultOrders,

View File

@ -11,6 +11,7 @@ import { BigNumber } from '@0xproject/utils';
import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
import { orderHashUtils } from './order_hash';
import { OrderValidationUtils } from './order_validation_utils';
import { RemainingFillableCalculator } from './remaining_fillable_calculator';
import { utils } from './utils';
@ -22,9 +23,16 @@ interface SidedOrderRelevantState {
traderFeeProxyAllowance: BigNumber;
filledTakerAssetAmount: BigNumber;
remainingFillableAssetAmount: BigNumber;
isOrderCancelled: boolean;
}
const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001;
interface OrderValidResult {
isValid: true;
}
interface OrderInvalidResult {
isValid: false;
error: ExchangeContractErrs;
}
type OrderValidationResult = OrderValidResult | OrderInvalidResult;
export class OrderStateUtils {
private readonly _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher;
@ -32,64 +40,56 @@ export class OrderStateUtils {
private static _validateIfOrderIsValid(
signedOrder: SignedOrder,
sidedOrderRelevantState: SidedOrderRelevantState,
): void {
): 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)) {
throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
return { isValid: false, error: ExchangeContractErrs.OrderRemainingFillAmountZero };
}
if (sidedOrderRelevantState.traderBalance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerBalance
: ExchangeContractErrs.InsufficientTakerBalance,
);
const error = isMakerSide
? ExchangeContractErrs.InsufficientMakerBalance
: ExchangeContractErrs.InsufficientTakerBalance;
return { isValid: false, error };
}
if (sidedOrderRelevantState.traderProxyAllowance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerAllowance
: ExchangeContractErrs.InsufficientTakerAllowance,
);
const error = isMakerSide
? ExchangeContractErrs.InsufficientMakerAllowance
: ExchangeContractErrs.InsufficientTakerAllowance;
return { isValid: false, error };
}
if (!signedOrder.makerFee.eq(0)) {
if (sidedOrderRelevantState.traderFeeBalance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerFeeBalance
: ExchangeContractErrs.InsufficientTakerFeeBalance,
);
const error = isMakerSide
? ExchangeContractErrs.InsufficientMakerFeeBalance
: ExchangeContractErrs.InsufficientTakerFeeBalance;
return { isValid: false, error };
}
if (sidedOrderRelevantState.traderFeeProxyAllowance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerFeeAllowance
: ExchangeContractErrs.InsufficientTakerFeeAllowance,
);
const error = isMakerSide
? ExchangeContractErrs.InsufficientMakerFeeAllowance
: ExchangeContractErrs.InsufficientTakerFeeAllowance;
return { isValid: false, error };
}
}
let minFillableTakerAssetAmountWithinNoRoundingErrorRange;
if (isMakerSide) {
minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount
.dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR)
.dividedBy(signedOrder.makerAssetAmount);
} else {
minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.makerAssetAmount
.dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR)
.dividedBy(signedOrder.takerAssetAmount);
}
if (
sidedOrderRelevantState.remainingFillableAssetAmount.lessThan(
minFillableTakerAssetAmountWithinNoRoundingErrorRange,
)
) {
throw new Error(ExchangeContractErrs.OrderFillRoundingError);
const remainingTakerAssetAmount = signedOrder.takerAssetAmount.minus(
sidedOrderRelevantState.filledTakerAssetAmount,
);
const isRoundingError = OrderValidationUtils.isRoundingError(
remainingTakerAssetAmount,
signedOrder.takerAssetAmount,
signedOrder.makerAssetAmount,
);
if (isRoundingError) {
return { isValid: false, error: ExchangeContractErrs.OrderFillRoundingError };
}
return { isValid: true };
}
constructor(
balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher,
@ -101,6 +101,7 @@ export class OrderStateUtils {
public async getOpenOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> {
const orderRelevantState = await this.getOpenOrderRelevantStateAsync(signedOrder);
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash);
const sidedOrderRelevantState = {
isMakerSide: true,
traderBalance: orderRelevantState.makerBalance,
@ -109,20 +110,21 @@ export class OrderStateUtils {
traderFeeProxyAllowance: orderRelevantState.makerFeeProxyAllowance,
filledTakerAssetAmount: orderRelevantState.filledTakerAssetAmount,
remainingFillableAssetAmount: orderRelevantState.remainingFillableMakerAssetAmount,
isOrderCancelled,
};
try {
OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState);
const orderValidationResult = OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState);
if (orderValidationResult.isValid) {
const orderState: OrderStateValid = {
isValid: true,
orderHash,
orderRelevantState,
};
return orderState;
} catch (err) {
} else {
const orderState: OrderStateInvalid = {
isValid: false,
orderHash,
error: err.message,
error: orderValidationResult.error,
};
return orderState;
}
@ -278,6 +280,7 @@ export class OrderStateUtils {
traderFeeProxyAllowance,
filledTakerAssetAmount,
remainingFillableAssetAmount,
isOrderCancelled,
};
return sidedOrderRelevantState;
}

View File

@ -0,0 +1,48 @@
import { schemas } from '@0xproject/json-schemas';
import { Order } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import { assert } from './assert';
import { constants } from './constants';
export const rateUtils = {
/**
* Takes an order and calculates the fee adjusted rate (takerAsset/makerAsset) by calculating how much takerAsset
* is required to cover the fees (feeRate * takerFee), adding the takerAssetAmount and dividing by makerAssetAmount
* @param order An object that conforms to the order interface
* @param feeRate The market rate of ZRX denominated in takerAssetAmount
* (ex. feeRate is 0.1 takerAsset/ZRX if it takes 1 unit of takerAsset to buy 10 ZRX)
* Defaults to 0
* @return The rate (takerAsset/makerAsset) of the order adjusted for fees
*/
getFeeAdjustedRateOfOrder(order: Order, feeRate: BigNumber = constants.ZERO_AMOUNT): BigNumber {
assert.doesConformToSchema('order', order, schemas.orderSchema);
assert.isBigNumber('feeRate', feeRate);
assert.assert(
feeRate.gte(constants.ZERO_AMOUNT),
`Expected feeRate: ${feeRate} to be greater than or equal to 0`,
);
const takerAssetAmountNeededToPayForFees = order.takerFee.mul(feeRate);
const totalTakerAssetAmount = takerAssetAmountNeededToPayForFees.plus(order.takerAssetAmount);
const rate = totalTakerAssetAmount.div(order.makerAssetAmount);
return rate;
},
/**
* Takes a fee order (makerAssetData corresponds to ZRX and takerAssetData corresponds to WETH) and calculates
* the fee adjusted rate (WETH/ZRX) by dividing the takerAssetAmount by the makerAmount minus the takerFee
* @param feeOrder An object that conforms to the order interface
* @return The rate (WETH/ZRX) of the fee order adjusted for fees
*/
getFeeAdjustedRateOfFeeOrder(feeOrder: Order): BigNumber {
assert.doesConformToSchema('feeOrder', feeOrder, schemas.orderSchema);
const zrxAmountAfterFees = feeOrder.makerAssetAmount.sub(feeOrder.takerFee);
assert.assert(
zrxAmountAfterFees.greaterThan(constants.ZERO_AMOUNT),
`Expected takerFee: ${JSON.stringify(feeOrder.takerFee)} to be less than makerAssetAmount: ${JSON.stringify(
feeOrder.makerAssetAmount,
)}`,
);
const rate = feeOrder.takerAssetAmount.div(zrxAmountAfterFees);
return rate;
},
};

View File

@ -0,0 +1,54 @@
import { schemas } from '@0xproject/json-schemas';
import { Order } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import { assert } from './assert';
import { constants } from './constants';
import { rateUtils } from './rate_utils';
export const sortingUtils = {
/**
* Takes an array of orders and sorts them by takerAsset/makerAsset rate in ascending order (best rate first).
* Adjusts the rate of each order according to the feeRate and takerFee for that order.
* @param orders An array of objects that extend the Order interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @param feeRate The market rate of ZRX denominated in takerAssetAmount
* (ex. feeRate is 0.1 takerAsset/ZRX if it takes 1 unit of takerAsset to buy 10 ZRX)
* Defaults to 0
* @return The input orders sorted by rate in ascending order
*/
sortOrdersByFeeAdjustedRate<T extends Order>(orders: T[], feeRate: BigNumber = constants.ZERO_AMOUNT): T[] {
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
assert.isBigNumber('feeRate', feeRate);
const rateCalculator = (order: Order) => rateUtils.getFeeAdjustedRateOfOrder(order, feeRate);
const sortedOrders = sortOrders(orders, rateCalculator);
return sortedOrders;
},
/**
* Takes an array of fee orders (makerAssetData corresponds to ZRX and takerAssetData corresponds to WETH)
* and sorts them by rate in ascending order (best rate first). Adjusts the rate according to the takerFee.
* @param feeOrders An array of objects that extend the Order interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @return The input orders sorted by rate in ascending order
*/
sortFeeOrdersByFeeAdjustedRate(feeOrders: Order[]): Order[] {
assert.doesConformToSchema('feeOrders', feeOrders, schemas.ordersSchema);
const rateCalculator = rateUtils.getFeeAdjustedRateOfFeeOrder.bind(rateUtils);
const sortedOrders = sortOrders(feeOrders, rateCalculator);
return sortedOrders;
},
};
type RateCalculator = (order: Order) => BigNumber;
// takes an array of orders, copies them, and sorts the copy based on the rate definition provided by rateCalculator
function sortOrders<T extends Order>(orders: T[], rateCalculator: RateCalculator): T[] {
const copiedOrders = _.cloneDeep(orders);
copiedOrders.sort((firstOrder, secondOrder) => {
const firstOrderRate = rateCalculator(firstOrder);
const secondOrderRate = rateCalculator(secondOrder);
return firstOrderRate.comparedTo(secondOrderRate);
});
return copiedOrders;
}

View File

@ -41,3 +41,31 @@ export interface CreateOrderOpts {
salt?: BigNumber;
expirationTimeSeconds?: BigNumber;
}
/**
* remainingFillableMakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter.
* You can use `OrderStateUtils` `@0xproject/order-utils` to perform blockchain lookups for these values.
* Defaults to `makerAssetAmount` values from the orders param.
* slippageBufferAmount: An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills.
* Defaults to 0
*/
export interface FindOrdersThatCoverMakerAssetFillAmountOpts {
remainingFillableMakerAssetAmounts?: BigNumber[];
slippageBufferAmount?: BigNumber;
}
/**
* remainingFillableMakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter.
* You can use `OrderStateUtils` `@0xproject/order-utils` to perform blockchain lookups for these values.
* Defaults to `makerAssetAmount` values from the orders param.
* remainingFillableFeeAmounts: An array of BigNumbers corresponding to the feeOrders parameter.
* You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups for these values.
* Defaults to `makerAssetAmount` values from the feeOrders param.
* slippageBufferAmount: An additional amount of fee to be covered by the result in case of trade collisions or partial fills.
* Defaults to 0
*/
export interface FindFeeOrdersThatCoverFeesForTargetOrdersOpts {
remainingFillableMakerAssetAmounts?: BigNumber[];
remainingFillableFeeAmounts?: BigNumber[];
slippageBufferAmount?: BigNumber;
}

View File

@ -17,7 +17,6 @@ describe('marketUtils', () => {
it('returns empty and unchanged remainingFillAmount', async () => {
const fillAmount = new BigNumber(10);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
[],
[],
fillAmount,
);
@ -34,8 +33,6 @@ describe('marketUtils', () => {
},
3,
);
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
// try to fill 20 units of makerAsset
// include 10 units of slippageBufferAmount
@ -43,9 +40,10 @@ describe('marketUtils', () => {
const slippageBufferAmount = new BigNumber(10);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
remainingFillableMakerAssetAmounts,
fillAmount,
slippageBufferAmount,
{
slippageBufferAmount,
},
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
@ -57,9 +55,10 @@ describe('marketUtils', () => {
const slippageBufferAmount = new BigNumber(10);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
remainingFillableMakerAssetAmounts,
fillAmount,
slippageBufferAmount,
{
slippageBufferAmount,
},
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
@ -71,9 +70,10 @@ describe('marketUtils', () => {
const slippageBufferAmount = new BigNumber(5);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
remainingFillableMakerAssetAmounts,
fillAmount,
slippageBufferAmount,
{
slippageBufferAmount,
},
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5));
@ -83,7 +83,6 @@ describe('marketUtils', () => {
const fillAmount = new BigNumber(10);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
remainingFillableMakerAssetAmounts,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
@ -94,7 +93,6 @@ describe('marketUtils', () => {
const fillAmount = new BigNumber(15);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
remainingFillableMakerAssetAmounts,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
@ -120,8 +118,10 @@ describe('marketUtils', () => {
const fillAmount = new BigNumber(30);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
remainingFillableMakerAssetAmounts,
fillAmount,
{
remainingFillableMakerAssetAmounts,
},
);
expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]);
expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15));
@ -137,15 +137,11 @@ describe('marketUtils', () => {
},
3,
);
// generate remainingFillableFeeAmounts that equal the zrxAmount
const remainingFillableFeeAmounts = [zrxAmount, zrxAmount, zrxAmount];
describe('no target orders', () => {
it('returns empty and zero remainingFeeAmount', async () => {
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
[],
[],
inputFeeOrders,
remainingFillableFeeAmounts,
);
expect(resultOrders).to.be.empty;
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
@ -168,9 +164,10 @@ describe('marketUtils', () => {
it('returns empty and non-zero remainingFeeAmount', async () => {
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
inputOrders,
remainingFillableMakerAssetAmounts,
[],
[],
{
remainingFillableMakerAssetAmounts,
},
);
expect(resultOrders).to.be.empty;
expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30));
@ -185,14 +182,10 @@ describe('marketUtils', () => {
},
3,
);
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
it('returns empty and zero remainingFeeAmount', async () => {
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
inputOrders,
remainingFillableMakerAssetAmounts,
inputFeeOrders,
remainingFillableFeeAmounts,
);
expect(resultOrders).to.be.empty;
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
@ -210,14 +203,10 @@ describe('marketUtils', () => {
},
3,
);
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
it('returns input fee orders and zero remainingFeeAmount', async () => {
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
inputOrders,
remainingFillableMakerAssetAmounts,
inputFeeOrders,
remainingFillableFeeAmounts,
);
expect(resultOrders).to.be.deep.equal(inputFeeOrders);
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
@ -243,9 +232,10 @@ describe('marketUtils', () => {
it('returns first two input fee orders and zero remainingFeeAmount', async () => {
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
inputOrders,
remainingFillableMakerAssetAmounts,
inputFeeOrders,
remainingFillableFeeAmounts,
{
remainingFillableMakerAssetAmounts,
},
);
expect(resultOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]);
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
@ -263,14 +253,10 @@ describe('marketUtils', () => {
},
3,
);
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
it('returns input fee orders and non-zero remainingFeeAmount', async () => {
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
inputOrders,
remainingFillableMakerAssetAmounts,
inputFeeOrders,
remainingFillableFeeAmounts,
);
expect(resultOrders).to.be.deep.equal(inputFeeOrders);
expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30));

View File

@ -0,0 +1,124 @@
import { BigNumber } from '@0xproject/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(_orderHash: string): Promise<boolean> {
return cancelled;
},
getZRXAssetData(): string {
return '';
},
};
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);
});
});
});

View File

@ -0,0 +1,55 @@
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import 'mocha';
import { rateUtils } from '../src';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
chaiSetup.configure();
const expect = chai.expect;
describe('rateUtils', () => {
const testOrder = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(20),
});
describe('#getFeeAdjustedRateOfOrder', () => {
it('throws when feeRate is less than zero', async () => {
const feeRate = new BigNumber(-1);
expect(() => rateUtils.getFeeAdjustedRateOfOrder(testOrder, feeRate)).to.throw(
'Expected feeRate: -1 to be greater than or equal to 0',
);
});
it('correctly calculates fee adjusted rate when feeRate is provided', async () => {
const feeRate = new BigNumber(2); // ZRX costs 2 units of takerAsset per 1 unit of ZRX
const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfOrder(testOrder, feeRate);
// the order actually takes 100 + (2 * 20) takerAsset units to fill 100 units of makerAsset
expect(feeAdjustedRate).to.bignumber.equal(new BigNumber(1.4));
});
it('correctly calculates fee adjusted rate when no feeRate is provided', async () => {
const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfOrder(testOrder);
// because no feeRate was provided we just assume 0 fees
// the order actually takes 100 takerAsset units to fill 100 units of makerAsset
expect(feeAdjustedRate).to.bignumber.equal(new BigNumber(1));
});
});
describe('#getFeeAdjustedRateOfFeeOrder', () => {
it('throws when takerFee exceeds makerAssetAmount', async () => {
const badOrder = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(101),
});
expect(() => rateUtils.getFeeAdjustedRateOfFeeOrder(badOrder)).to.throw(
'Expected takerFee: "101" to be less than makerAssetAmount: "100"',
);
});
it('correctly calculates fee adjusted rate', async () => {
const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfFeeOrder(testOrder);
// the order actually takes 100 takerAsset units to fill (100 - 20) units of makerAsset
expect(feeAdjustedRate).to.bignumber.equal(new BigNumber(1.25));
});
});
});

View File

@ -0,0 +1,67 @@
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import 'mocha';
import { sortingUtils } from '../src';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
chaiSetup.configure();
const expect = chai.expect;
describe('sortingUtils', () => {
describe('#sortOrdersByFeeAdjustedRate', () => {
const feeRate = new BigNumber(1); // ZRX costs 1 unit of takerAsset per 1 unit of ZRX
// rate: 2 takerAsset / makerAsset
const testOrder1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
});
// rate: 1 takerAsset / makerAsset
const testOrder2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100),
});
// rate: 2.5 takerAsset / makerAsset
const testOrder3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
takerFee: new BigNumber(50),
});
it('correctly sorts by fee adjusted rate when feeRate is Provided', async () => {
const orders = [testOrder1, testOrder2, testOrder3];
const sortedOrders = sortingUtils.sortOrdersByFeeAdjustedRate(orders, feeRate);
expect(sortedOrders).to.deep.equal([testOrder2, testOrder1, testOrder3]);
});
it('correctly sorts by fee adjusted rate when no feeRate is Provided', async () => {
const orders = [testOrder1, testOrder2, testOrder3];
const sortedOrders = sortingUtils.sortOrdersByFeeAdjustedRate(orders);
expect(sortedOrders).to.deep.equal([testOrder2, testOrder1, testOrder3]);
});
});
describe('#sortFeeOrdersByFeeAdjustedRate', () => {
// rate: 200 takerAsset / makerAsset
const testOrder1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
takerFee: new BigNumber(99),
});
// rate: 1 takerAsset / makerAsset
const testOrder2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100),
});
// rate: 4 takerAsset / makerAsset
const testOrder3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
takerFee: new BigNumber(50),
});
it('correctly sorts by fee adjusted rate', async () => {
const orders = [testOrder1, testOrder2, testOrder3];
const sortedOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(orders);
expect(sortedOrders).to.deep.equal([testOrder2, testOrder3, testOrder1]);
});
});
});

View File

@ -55,7 +55,7 @@
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^2.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"json-loader": "^0.5.4",
"make-promises-safe": "^1.1.0",

View File

@ -501,25 +501,27 @@ describe('OrderWatcher', () => {
expect(orderState.isValid).to.be.false();
const invalidOrderState = orderState as OrderStateInvalid;
expect(invalidOrderState.orderHash).to.be.equal(orderHash);
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderFillRoundingError);
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderCancelled);
});
orderWatcher.subscribe(callback);
await contractWrappers.exchange.cancelOrderAsync(signedOrder);
})().catch(done);
});
it('should emit orderStateInvalid when within rounding error range', (done: DoneCallback) => {
it('should emit orderStateInvalid when within rounding error range after a partial fill', (done: DoneCallback) => {
(async () => {
const remainingFillableAmountInBaseUnits = new BigNumber(100);
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
const fillAmountInBaseUnits = new BigNumber(2);
const makerAssetAmount = new BigNumber(1001);
const takerAssetAmount = new BigNumber(3);
signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
fillableAmount,
makerAssetAmount,
takerAssetAmount,
);
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
await orderWatcher.addOrderAsync(signedOrder);
const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
expect(orderState.isValid).to.be.false();
const invalidOrderState = orderState as OrderStateInvalid;
@ -527,11 +529,7 @@ describe('OrderWatcher', () => {
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderFillRoundingError);
});
orderWatcher.subscribe(callback);
await contractWrappers.exchange.fillOrderAsync(
signedOrder,
fillableAmount.minus(remainingFillableAmountInBaseUnits),
takerAddress,
);
await contractWrappers.exchange.fillOrderAsync(signedOrder, fillAmountInBaseUnits, takerAddress);
})().catch(done);
});
describe('erc721', () => {

View File

@ -34,7 +34,7 @@
"@types/react-dom": "^16.0.3",
"@types/react-tap-event-plugin": "0.0.30",
"awesome-typescript-loader": "^3.1.3",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"css-loader": "^0.28.9",
"json-loader": "^0.5.4",
"less": "^2.7.2",

View File

@ -29,7 +29,7 @@
"@0xproject/monorepo-scripts": "^1.0.5",
"@0xproject/tslint-config": "^1.0.5",
"@types/compare-versions": "^3.0.0",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"shx": "^0.2.2",
"tslint": "^5.9.1",

View File

@ -28,7 +28,7 @@
"@0xproject/dev-utils": "^1.0.4",
"@0xproject/monorepo-scripts": "^1.0.5",
"@0xproject/tslint-config": "^1.0.5",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"shx": "^0.2.2",
"tslint": "^5.9.1",

View File

@ -61,7 +61,7 @@
"@types/semver": "^5.5.0",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -79,7 +79,7 @@
"@types/rimraf": "^2.0.2",
"@types/solidity-parser-antlr": "^0.2.1",
"chai": "^4.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -26,7 +26,7 @@
"devDependencies": {
"@0xproject/monorepo-scripts": "^1.0.5",
"@0xproject/tslint-config": "^1.0.5",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"shx": "^0.2.2",
"tslint": "5.11.0",

View File

@ -57,7 +57,7 @@
"@types/yargs": "^10.0.0",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -74,7 +74,7 @@
"@types/sinon": "^2.2.2",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",

View File

@ -36,7 +36,7 @@
"devDependencies": {
"@0xproject/monorepo-scripts": "^1.0.5",
"@types/lodash": "4.14.104",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"shx": "^0.2.2",
"typescript": "3.0.1"

View File

@ -26,7 +26,7 @@
"devDependencies": {
"@0xproject/monorepo-scripts": "^1.0.5",
"@0xproject/tslint-config": "^1.0.5",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"shx": "^0.2.2",
"tslint": "5.11.0",

View File

@ -62,8 +62,7 @@ export interface ValidatorSignature {
export enum ExchangeContractErrs {
OrderFillExpired = 'ORDER_FILL_EXPIRED',
OrderCancelExpired = 'ORDER_CANCEL_EXPIRED',
OrderCancelAmountZero = 'ORDER_CANCEL_AMOUNT_ZERO',
OrderAlreadyCancelledOrFilled = 'ORDER_ALREADY_CANCELLED_OR_FILLED',
OrderCancelled = 'ORDER_CANCELLED',
OrderFillAmountZero = 'ORDER_FILL_AMOUNT_ZERO',
OrderRemainingFillAmountZero = 'ORDER_REMAINING_FILL_AMOUNT_ZERO',
OrderFillRoundingError = 'ORDER_FILL_ROUNDING_ERROR',

View File

@ -33,7 +33,7 @@
},
"devDependencies": {
"@0xproject/monorepo-scripts": "^1.0.5",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"shx": "^0.2.2"
},
"publishConfig": {

View File

@ -34,7 +34,7 @@
"@types/lodash": "4.14.104",
"@types/mocha": "^2.2.42",
"chai": "^4.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"make-promises-safe": "^1.1.0",
"mocha": "^4.1.0",
"npm-run-all": "^4.1.2",

View File

@ -51,7 +51,7 @@
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^2.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"dirty-chai": "^2.0.1",
"ganache-core": "0xProject/ganache-core#monorepo-dep",
"make-promises-safe": "^1.1.0",

View File

@ -84,7 +84,7 @@
"@types/redux": "^3.6.0",
"awesome-typescript-loader": "^3.1.3",
"copy-webpack-plugin": "^4.0.1",
"copyfiles": "^1.2.0",
"copyfiles": "^2.0.0",
"css-loader": "0.23.x",
"exports-loader": "0.6.x",
"imports-loader": "0.6.x",

View File

@ -247,12 +247,9 @@ export const utils = {
} = {
[ExchangeContractErrs.OrderFillExpired]: 'This order has expired',
[ExchangeContractErrs.OrderCancelExpired]: 'This order has expired',
[ExchangeContractErrs.OrderCancelAmountZero]: "Order cancel amount can't be 0",
[ExchangeContractErrs.OrderAlreadyCancelledOrFilled]:
'This order has already been completely filled or cancelled',
[ExchangeContractErrs.OrderCancelled]: 'This order has been cancelled',
[ExchangeContractErrs.OrderFillAmountZero]: "Order fill amount can't be 0",
[ExchangeContractErrs.OrderRemainingFillAmountZero]:
'This order has already been completely filled or cancelled',
[ExchangeContractErrs.OrderRemainingFillAmountZero]: 'This order has already been completely filled',
[ExchangeContractErrs.OrderFillRoundingError]:
'Rounding error will occur when filling this order. Please try filling a different amount.',
[ExchangeContractErrs.InsufficientTakerBalance]:

View File

@ -944,9 +944,9 @@
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
"@types/fetch-mock@^5.12.2":
version "5.12.2"
resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-5.12.2.tgz#8c96517ff74303031c65c5da2d99858e34c844d2"
"@types/fetch-mock@^6.0.3":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-6.0.3.tgz#54ba90776293e728089bf53ba53e3c69f1c80655"
"@types/find-versions@^2.0.0":
version "2.0.0"
@ -1371,7 +1371,7 @@ aes-js@3.0.0:
aes-js@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.1.1.tgz#89fd1f94ae51b4c72d62466adc1a7323ff52f072"
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.1.tgz#89fd1f94ae51b4c72d62466adc1a7323ff52f072"
ajv-keywords@^2.1.0:
version "2.1.1"
@ -2844,7 +2844,7 @@ bs58@^2.0.1:
bs58check@^2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
dependencies:
bs58 "^4.0.0"
create-hash "^1.1.0"
@ -3745,8 +3745,8 @@ copy-to-clipboard@^3:
toggle-selection "^1.0.3"
copy-webpack-plugin@^4.0.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c"
version "4.5.2"
resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz#d53444a8fea2912d806e78937390ddd7e632ee5c"
dependencies:
cacache "^10.0.4"
find-cache-dir "^1.0.0"
@ -3757,17 +3757,6 @@ copy-webpack-plugin@^4.0.1:
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
copyfiles@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-1.2.0.tgz#a8da3ac41aa2220ae29bd3c58b6984294f2c593c"
dependencies:
glob "^7.0.5"
ltcdr "^2.2.1"
minimatch "^3.0.3"
mkdirp "^0.5.1"
noms "0.0.0"
through2 "^2.0.1"
copyfiles@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.0.0.tgz#bbd78bb78e8fd6db5c67adf54249317b24560f2a"
@ -5256,7 +5245,7 @@ ethereumjs-vm@^2.0.2, ethereumjs-vm@^2.1.0, ethereumjs-vm@^2.3.4:
ethereumjs-wallet@~0.6.0:
version "0.6.2"
resolved "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda"
resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda"
dependencies:
aes-js "^3.1.1"
bs58check "^2.1.2"
@ -5267,9 +5256,9 @@ ethereumjs-wallet@~0.6.0:
utf8 "^3.0.0"
uuid "^3.3.2"
ethers@3.0.22:
version "3.0.22"
resolved "https://registry.npmjs.org/ethers/-/ethers-3.0.22.tgz#7fab1ea16521705837aa43c15831877b2716b436"
ethers@0xproject/ethers.js#eip-838-reasons, ethers@3.0.22:
version "3.0.18"
resolved "https://codeload.github.com/0xproject/ethers.js/tar.gz/b91342bd200d142af0165d6befddf783c8ae8447"
dependencies:
aes-js "3.0.0"
bn.js "^4.4.0"
@ -6693,8 +6682,8 @@ hdkey@^0.7.1:
secp256k1 "^3.0.1"
hdkey@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29"
version "1.0.0"
resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-1.0.0.tgz#1d658dfe966aaa542c1d499586200b325e5c0cf4"
dependencies:
coinstring "^2.0.0"
safe-buffer "^5.1.1"
@ -8559,10 +8548,6 @@ lru-cache@~2.2.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
ltcdr@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ltcdr/-/ltcdr-2.2.1.tgz#5ab87ad1d4c1dab8e8c08bbf037ee0c1902287cf"
ltgt@^2.1.2, ltgt@~2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
@ -14038,7 +14023,7 @@ utf8@^2.1.1:
utf8@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
util-deprecate@~1.0.1:
version "1.0.2"
@ -14087,7 +14072,7 @@ uuid@^2.0.1:
uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
uvm@1.7.0:
version "1.7.0"