Asset-swapper aggregator utils (#2353)

* `@0x/asset-swapper`: Add ERC20Bridge aggregator library.

* `@0x/asset-swapper`: Finish off `aggregate.ts`.

* `@0x/types`: Add `OrderWithoutDomain` type.

* `@0x/asset-swapper`: Add testing infra for sampler/aggregator.

* `@0x/types`: Add `SignedOrderWithoutDomain` type.

* `@0x/asset-swapper`: Update aggregator to take and return orders with signatures.

* `@0x/asset-swapper`: Fix broken aggregator tests.

* `@0x/asset-swapper`: Pass the sampler contract into aggregator entry points.

* `@0x/contract-artifacts`: Add `IERC20BridgeSampler` artifact.

* `@0x/contract-wrappers`: Add `IERC20BridgeSampler` wrapper.

* `@0x/asset-swapper`: Address review comments.

* fixed testing

* refactored aggregate.ts and embeded into asset-swapper

* added adjusted rates for taker and maker fees

* remove PrunedSignedOrders

* updated contract-addresses and addressed some other todos

* streamlined logic

* patched in lawrences changes

* renamed aggregator utils and removed market_utils.ts

* added ack heartbeats

* fixed bug

* patches

* added dummy order things

* Dummy with valid sig

* Tweak gas price calculation to wei

* added test coverage and fixed bugs

* fixed migrations

* Fix CHANGELOGs and types export

* Deploy latest ERC20BridgeSampler on Mainnet

* `@0x/types` Revert CHANGELOG.

* `@0x/asset-swapper`: Address review comments.
`@0x/contract-addresses`: Make kyber lowercase.

* made protocol fee multiplier async

* `@0x/asset-swapper: Fix build errors and do some code cleanup.

* use assetDataUtils where possible
This commit is contained in:
Lawrence Forman
2019-12-16 12:35:58 -08:00
committed by Xianny
parent 6808e0d531
commit 994908549d
55 changed files with 4568 additions and 1396 deletions

View File

@@ -5,6 +5,10 @@
{
"note": "Fix gasPrice from `ethgasstation` to be in WEI instead of GWEI",
"pr": 2393
},
{
"note": "Add aggregator utils",
"pr": 2353
}
]
},

View File

@@ -48,9 +48,11 @@
"@0x/orderbook": "^1.0.1",
"@0x/utils": "^5.1.0",
"@0x/web3-wrapper": "^7.0.1",
"heartbeats": "^5.0.1",
"lodash": "^4.17.11"
},
"devDependencies": {
"@0x/base-contract": "^6.0.1",
"@0x/contracts-test-utils": "^5.0.0",
"@0x/dev-utils": "^3.0.1",
"@0x/mesh-rpc-client": "^7.0.4-beta-0xv3",

View File

@@ -11,6 +11,8 @@ import {
SwapQuoterOpts,
} from './types';
import { constants as marketOperationUtilConstants } from './utils/market_operation_utils/constants';
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
const NULL_BYTES = '0x';
const NULL_ERC20_ASSET_DATA = '0xf47261b00000000000000000000000000000000000000000000000000000000000000000';
@@ -27,6 +29,13 @@ const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
]), // Default asset-swapper for CFL oriented fee types
};
// 15 seconds polling interval
const PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS = 15000;
const PROTOCOL_FEE_MULTIPLIER = new BigNumber(150000);
// default 50% buffer for selecting native orders to be aggregated with other sources
const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5;
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
...{
chainId: MAINNET_CHAIN_ID,
@@ -48,11 +57,15 @@ const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
slippagePercentage: 0.2, // 20% slippage protection,
...{
slippagePercentage: 0.2, // 20% slippage protection,
},
...marketOperationUtilConstants.DEFAULT_GET_MARKET_ORDERS_OPTS,
};
export const constants = {
ETH_GAS_STATION_API_BASE_URL,
PROTOCOL_FEE_MULTIPLIER,
NULL_BYTES,
ZERO_AMOUNT: new BigNumber(0),
NULL_ADDRESS,
@@ -68,4 +81,6 @@ export const constants = {
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
DEFAULT_PER_PAGE,
NULL_ERC20_ASSET_DATA,
PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE,
};

View File

@@ -4,3 +4,5 @@ declare module '*.json' {
export default json;
/* tslint:enable */
}
declare module 'heartbeats';

View File

@@ -45,7 +45,6 @@ export {
LiquidityForTakerMakerAssetDataPair,
MarketBuySwapQuote,
MarketSellSwapQuote,
PrunedSignedOrder,
SmartContractParamsInfo,
SwapQuote,
SwapQuoteConsumerBase,
@@ -57,6 +56,8 @@ export {
SwapQuoterError,
SwapQuoterOpts,
SwapQuoteConsumerError,
SignedOrderWithFillableAmounts,
} from './types';
export { ERC20BridgeSource } from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils';

View File

@@ -1,5 +1,6 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, ForwarderContract } from '@0x/contract-wrappers';
import { ForwarderContract } from '@0x/contract-wrappers';
import { assetDataUtils } from '@0x/order-utils';
import { AbiEncoder, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import { MethodAbi } from 'ethereum-types';
@@ -28,7 +29,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
private readonly _contractAddresses: ContractAddresses;
private readonly _forwarder: ForwarderContract;
private readonly _devUtils: DevUtilsContract;
constructor(
supportedProvider: SupportedProvider,
@@ -42,7 +42,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
this.chainId = chainId;
this._contractAddresses = contractAddresses;
this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
this._devUtils = new DevUtilsContract(contractAddresses.devUtils, supportedProvider);
}
/**
@@ -54,7 +53,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
const { toAddress, methodAbi, ethAmount, params } = await this.getSmartContractParamsOrThrowAsync(quote, opts);
@@ -86,7 +85,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<SmartContractParamsInfo<ForwarderSmartContractParams>> {
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
const { extensionContractOpts } = _.merge({}, constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, opts);
@@ -152,7 +151,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string> {
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
const { ethAmount: providedEthAmount, takerAddress, gasLimit, extensionContractOpts } = _.merge(
{},
@@ -217,7 +216,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
return txHash;
}
private async _getEtherTokenAssetDataOrThrowAsync(): Promise<string> {
return this._devUtils.encodeERC20AssetData(this._contractAddresses.etherToken).callAsync();
private _getEtherTokenAssetDataOrThrow(): string {
return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken);
}
}

View File

@@ -1,7 +1,7 @@
import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contract-wrappers';
import { DevUtilsContract, IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { schemas } from '@0x/json-schemas';
import { SignedOrder } from '@0x/order-utils';
import { assetDataUtils, SignedOrder } from '@0x/order-utils';
import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
@@ -14,18 +14,20 @@ import {
MarketOperation,
MarketSellSwapQuote,
OrderPrunerPermittedFeeTypes,
PrunedSignedOrder,
SignedOrderWithFillableAmounts,
SwapQuote,
SwapQuoteRequestOpts,
SwapQuoterError,
SwapQuoterOpts,
} from './types';
import { assert } from './utils/assert';
import { calculateLiquidity } from './utils/calculate_liquidity';
import { OrderPruner } from './utils/order_prune_utils';
import { MarketOperationUtils } from './utils/market_operation_utils';
import { dummyOrderUtils } from './utils/market_operation_utils/dummy_order_utils';
import { orderPrunerUtils } from './utils/order_prune_utils';
import { OrderStateUtils } from './utils/order_state_utils';
import { ProtocolFeeUtils } from './utils/protocol_fee_utils';
import { sortingUtils } from './utils/sorting_utils';
import { swapQuoteCalculator } from './utils/swap_quote_calculator';
import { SwapQuoteCalculator } from './utils/swap_quote_calculator';
export class SwapQuoter {
public readonly provider: ZeroExProvider;
@@ -35,8 +37,11 @@ export class SwapQuoter {
public readonly permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>;
private readonly _contractAddresses: ContractAddresses;
private readonly _protocolFeeUtils: ProtocolFeeUtils;
private readonly _orderPruner: OrderPruner;
private readonly _swapQuoteCalculator: SwapQuoteCalculator;
private readonly _devUtilsContract: DevUtilsContract;
private readonly _marketOperationUtils: MarketOperationUtils;
private readonly _orderStateUtils: OrderStateUtils;
/**
* Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders.
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
@@ -155,11 +160,17 @@ export class SwapQuoter {
this.permittedOrderFeeTypes = permittedOrderFeeTypes;
this._contractAddresses = getContractAddressesForChainOrThrow(chainId);
this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider);
this._protocolFeeUtils = new ProtocolFeeUtils();
this._orderPruner = new OrderPruner(this._devUtilsContract, {
expiryBufferMs: this.expiryBufferMs,
permittedOrderFeeTypes: this.permittedOrderFeeTypes,
this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
this._orderStateUtils = new OrderStateUtils(this._devUtilsContract);
const samplerContract = new IERC20BridgeSamplerContract(
this._contractAddresses.erc20BridgeSampler,
this.provider,
);
this._marketOperationUtils = new MarketOperationUtils(samplerContract, this._contractAddresses, {
chainId,
exchangeAddress: this._contractAddresses.exchange,
});
this._swapQuoteCalculator = new SwapQuoteCalculator(this._protocolFeeUtils, this._marketOperationUtils);
}
/**
@@ -232,8 +243,8 @@ export class SwapQuoter {
assert.isETHAddressHex('makerTokenAddress', makerTokenAddress);
assert.isETHAddressHex('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount);
const makerAssetData = await this._devUtilsContract.encodeERC20AssetData(makerTokenAddress).callAsync();
const takerAssetData = await this._devUtilsContract.encodeERC20AssetData(takerTokenAddress).callAsync();
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
const swapQuote = this.getMarketBuySwapQuoteForAssetDataAsync(
makerAssetData,
takerAssetData,
@@ -262,8 +273,8 @@ export class SwapQuoter {
assert.isETHAddressHex('makerTokenAddress', makerTokenAddress);
assert.isETHAddressHex('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('takerAssetSellAmount', takerAssetSellAmount);
const makerAssetData = await this._devUtilsContract.encodeERC20AssetData(makerTokenAddress).callAsync();
const takerAssetData = await this._devUtilsContract.encodeERC20AssetData(takerTokenAddress).callAsync();
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
const swapQuote = this.getMarketSellSwapQuoteForAssetDataAsync(
makerAssetData,
takerAssetData,
@@ -287,8 +298,8 @@ export class SwapQuoter {
): Promise<LiquidityForTakerMakerAssetDataPair> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
const assetPairs = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
if (!assetPairs.includes(makerAssetData)) {
return {
@@ -297,8 +308,11 @@ export class SwapQuoter {
};
}
const prunedOrders = await this.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData);
return calculateLiquidity(prunedOrders);
const ordersWithFillableAmounts = await this.getSignedOrdersWithFillableAmountsAsync(
makerAssetData,
takerAssetData,
);
return calculateLiquidity(ordersWithFillableAmounts);
}
/**
@@ -308,7 +322,7 @@ export class SwapQuoter {
*/
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
assert.isString('makerAssetData', makerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs
.filter(pair => pair.assetDataA.assetData === makerAssetData)
@@ -323,7 +337,7 @@ export class SwapQuoter {
*/
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
assert.isString('takerAssetData', takerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs
.filter(pair => pair.assetDataB.assetData === takerAssetData)
@@ -344,8 +358,8 @@ export class SwapQuoter {
): Promise<boolean> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
const availableMakerAssetDatas = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
return _.includes(availableMakerAssetDatas, makerAssetData);
}
@@ -355,20 +369,27 @@ export class SwapQuoter {
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
*/
public async getPrunedSignedOrdersAsync(
public async getSignedOrdersWithFillableAmountsAsync(
makerAssetData: string,
takerAssetData: string,
): Promise<PrunedSignedOrder[]> {
): Promise<SignedOrderWithFillableAmounts[]> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
// get orders
const apiOrders = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData);
const orders = _.map(apiOrders, o => o.order);
const prunedOrders = await this._orderPruner.pruneSignedOrdersAsync(orders);
const prunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
this.permittedOrderFeeTypes,
this.expiryBufferMs,
);
const sortedPrunedOrders = sortingUtils.sortOrders(prunedOrders);
return sortedPrunedOrders;
const ordersWithFillableAmounts = await this._orderStateUtils.getSignedOrdersWithFillableAmountsAsync(
sortedPrunedOrders,
);
return ordersWithFillableAmounts;
}
/**
@@ -400,7 +421,29 @@ export class SwapQuoter {
* Utility function to get assetData for Ether token.
*/
public async getEtherTokenAssetDataOrThrowAsync(): Promise<string> {
return this._devUtilsContract.encodeERC20AssetData(this._contractAddresses.etherToken).callAsync();
return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken);
}
/**
* Grab orders from the order provider, prunes for valid orders with provided OrderPruner options
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
*/
private async _getSignedOrdersAsync(makerAssetData: string, takerAssetData: string): Promise<SignedOrder[]> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
// get orders
const apiOrders = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData);
const orders = _.map(apiOrders, o => o.order);
const prunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
this.permittedOrderFeeTypes,
this.expiryBufferMs,
);
const sortedPrunedOrders = sortingUtils.sortOrders(prunedOrders);
return sortedPrunedOrders;
}
/**
@@ -413,7 +456,11 @@ export class SwapQuoter {
marketOperation: MarketOperation,
options: Partial<SwapQuoteRequestOpts>,
): Promise<SwapQuote> {
const { slippagePercentage } = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assert.isNumber('slippagePercentage', slippagePercentage);
@@ -425,35 +472,39 @@ export class SwapQuoter {
gasPrice = await this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync();
}
// get the relevant orders for the makerAsset
const prunedOrders = await this.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData);
let prunedOrders = await this._getSignedOrdersAsync(makerAssetData, takerAssetData);
// if no native orders, pass in a dummy order for the sampler to have required metadata for sampling
if (prunedOrders.length === 0) {
throw new Error(
`${
SwapQuoterError.AssetUnavailable
}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`,
);
prunedOrders = [
dummyOrderUtils.createDummyOrderForSampler(
makerAssetData,
takerAssetData,
this._contractAddresses.uniswapBridge,
),
];
}
let swapQuote: SwapQuote;
if (marketOperation === MarketOperation.Buy) {
swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
prunedOrders,
assetFillAmount,
slippagePercentage,
gasPrice,
this._protocolFeeUtils,
calculateSwapQuoteOpts,
);
} else {
swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
prunedOrders,
assetFillAmount,
slippagePercentage,
gasPrice,
this._protocolFeeUtils,
calculateSwapQuoteOpts,
);
}
return swapQuote;
}
}
// tslint:disable-next-line: max-file-line-count

View File

@@ -2,6 +2,8 @@ import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { MethodAbi } from 'ethereum-types';
import { GetMarketOrdersOpts } from './utils/market_operation_utils/types';
/**
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
* permittedOrderFeeTypes: A set of all the takerFee types that OrderPruner will filter for
@@ -36,7 +38,7 @@ export interface OrderProviderRequest {
* fillableTakerAssetAmount: Amount of takerAsset that is fillable
* fillableTakerFeeAmount: Amount of takerFee paid to fill fillableTakerAssetAmount
*/
export interface PrunedSignedOrder extends SignedOrder {
export interface SignedOrderWithFillableAmounts extends SignedOrder {
fillableMakerAssetAmount: BigNumber;
fillableTakerAssetAmount: BigNumber;
fillableTakerFeeAmount: BigNumber;
@@ -254,11 +256,16 @@ export interface SwapQuoteInfo {
* slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%).
* gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount
*/
export interface SwapQuoteRequestOpts {
export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts {
slippagePercentage: number;
gasPrice?: BigNumber;
}
/**
* Opts required to generate a SwapQuote with SwapQuoteCalculator
*/
export interface CalculateSwapQuoteOpts extends GetMarketOrdersOpts {}
/**
* chainId: The ethereum chain id. Defaults to 1 (mainnet).
* orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s).

View File

@@ -1,10 +1,12 @@
import { BigNumber } from '@0x/utils';
import { LiquidityForTakerMakerAssetDataPair, PrunedSignedOrder } from '../types';
import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../types';
import { utils } from './utils';
export const calculateLiquidity = (prunedOrders: PrunedSignedOrder[]): LiquidityForTakerMakerAssetDataPair => {
export const calculateLiquidity = (
prunedOrders: SignedOrderWithFillableAmounts[],
): LiquidityForTakerMakerAssetDataPair => {
const liquidityInBigNumbers = prunedOrders.reduce(
(acc, order) => {
const fillableMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)

View File

@@ -0,0 +1,23 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { SignedOrderWithFillableAmounts } from '../types';
import { utils } from './utils';
export const fillableAmountsUtils = {
getTakerAssetAmountSwappedAfterFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount);
} else {
return order.fillableTakerAssetAmount;
}
},
getMakerAssetAmountSwappedAfterFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount);
} else {
return order.fillableMakerAssetAmount;
}
},
};

View File

@@ -0,0 +1,43 @@
import { BigNumber } from '@0x/utils';
import { ERC20BridgeSource, GetMarketOrdersOpts } from './types';
const INFINITE_TIMESTAMP_SEC = new BigNumber(2524604400);
/**
* Convert a source to a canonical address used by the sampler contract.
*/
const SOURCE_TO_ADDRESS: { [key: string]: string } = {
[ERC20BridgeSource.Eth2Dai]: '0x39755357759ce0d7f32dc8dc45414cca409ae24e',
[ERC20BridgeSource.Uniswap]: '0xc0a47dfe034b400b47bdad5fecda2621de6c4d95',
[ERC20BridgeSource.Kyber]: '0x818e6fecd516ecc3849daf6845e3ec868087b755',
};
/**
* Valid sources for market sell.
*/
export const SELL_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
/**
* Valid sources for market buy.
*/
export const BUY_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
runLimit: 1024,
excludedSources: [],
bridgeSlippage: 0.0005,
dustFractionThreshold: 0.01,
numSamples: 8,
noConflicts: true,
};
export const constants = {
INFINITE_TIMESTAMP_SEC,
SOURCE_TO_ADDRESS,
SELL_SOURCES,
BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS,
ERC20_PROXY_ID: '0xf47261b0',
WALLET_SIGNATURE: '0x04',
};

View File

@@ -0,0 +1,160 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { constants } from '../../constants';
import { SignedOrderWithFillableAmounts } from '../../types';
import { constants as marketOperationUtilConstants } from './constants';
import { AggregationError, ERC20BridgeSource, Fill, FillData, NativeFillData, OrderDomain } from './types';
const { NULL_BYTES, NULL_ADDRESS, ZERO_AMOUNT } = constants;
const { INFINITE_TIMESTAMP_SEC, WALLET_SIGNATURE } = marketOperationUtilConstants;
export class CreateOrderUtils {
private readonly _contractAddress: ContractAddresses;
constructor(contractAddress: ContractAddresses) {
this._contractAddress = contractAddress;
}
// Convert sell fills into orders.
public createSellOrdersFromPath(
orderDomain: OrderDomain,
inputToken: string,
outputToken: string,
path: Fill[],
bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] {
const orders: SignedOrderWithFillableAmounts[] = [];
for (const fill of path) {
const source = (fill.fillData as FillData).source;
if (source === ERC20BridgeSource.Native) {
orders.push((fill.fillData as NativeFillData).order);
} else {
orders.push(
createBridgeOrder(
orderDomain,
this._getBridgeAddressFromSource(source),
outputToken,
inputToken,
fill.output,
fill.input,
bridgeSlippage,
),
);
}
}
return orders;
}
// Convert buy fills into orders.
public createBuyOrdersFromPath(
orderDomain: OrderDomain,
inputToken: string,
outputToken: string,
path: Fill[],
bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] {
const orders: SignedOrderWithFillableAmounts[] = [];
for (const fill of path) {
const source = (fill.fillData as FillData).source;
if (source === ERC20BridgeSource.Native) {
orders.push((fill.fillData as NativeFillData).order);
} else {
orders.push(
createBridgeOrder(
orderDomain,
this._getBridgeAddressFromSource(source),
inputToken,
outputToken,
fill.input,
fill.output,
bridgeSlippage,
true,
),
);
}
}
return orders;
}
private _getBridgeAddressFromSource(source: ERC20BridgeSource): string {
switch (source) {
case ERC20BridgeSource.Eth2Dai:
return this._contractAddress.eth2DaiBridge;
case ERC20BridgeSource.Kyber:
return this._contractAddress.kyberBridge;
case ERC20BridgeSource.Uniswap:
return this._contractAddress.uniswapBridge;
default:
break;
}
throw new Error(AggregationError.NoBridgeForSource);
}
}
function createBridgeOrder(
orderDomain: OrderDomain,
bridgeAddress: string,
makerToken: string,
takerToken: string,
makerAssetAmount: BigNumber,
takerAssetAmount: BigNumber,
slippage: number,
isBuy: boolean = false,
): SignedOrderWithFillableAmounts {
return {
makerAddress: bridgeAddress,
makerAssetData: assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBridgeData(takerToken),
),
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
...createCommonOrderFields(orderDomain, makerAssetAmount, takerAssetAmount, slippage, isBuy),
};
}
function createBridgeData(tokenAddress: string): string {
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
return encoder.encode({ tokenAddress });
}
type CommonOrderFields = Pick<
SignedOrderWithFillableAmounts,
Exclude<keyof SignedOrderWithFillableAmounts, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
>;
function createCommonOrderFields(
orderDomain: OrderDomain,
makerAssetAmount: BigNumber,
takerAssetAmount: BigNumber,
slippage: number,
isBuy: boolean = false,
): CommonOrderFields {
const makerAssetAmountAdjustedWithSlippage = isBuy
? makerAssetAmount
: makerAssetAmount.times(1 - slippage).integerValue(BigNumber.ROUND_UP);
const takerAssetAmountAdjustedWithSlippage = isBuy
? takerAssetAmount.times(slippage + 1).integerValue(BigNumber.ROUND_UP)
: takerAssetAmount;
return {
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: generatePseudoRandomSalt(),
expirationTimeSeconds: INFINITE_TIMESTAMP_SEC,
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetAmount: makerAssetAmountAdjustedWithSlippage,
fillableMakerAssetAmount: makerAssetAmountAdjustedWithSlippage,
takerAssetAmount: takerAssetAmountAdjustedWithSlippage,
fillableTakerAssetAmount: takerAssetAmountAdjustedWithSlippage,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
...orderDomain,
};
}

View File

@@ -0,0 +1,32 @@
import { SignedOrder } from '@0x/types';
import { constants } from '../../constants';
import { constants as marketOperationUtilConstants } from './constants';
const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants;
const { INFINITE_TIMESTAMP_SEC } = marketOperationUtilConstants;
export const dummyOrderUtils = {
createDummyOrderForSampler(makerAssetData: string, takerAssetData: string, makerAddress: string): SignedOrder {
return {
makerAddress,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: ZERO_AMOUNT,
expirationTimeSeconds: INFINITE_TIMESTAMP_SEC,
makerAssetData,
takerAssetData,
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetAmount: ZERO_AMOUNT,
takerAssetAmount: ZERO_AMOUNT,
signature: NULL_BYTES,
chainId: 1,
exchangeAddress: NULL_ADDRESS,
};
},
};

View File

@@ -0,0 +1,137 @@
import { BigNumber } from '@0x/utils';
import { constants } from '../../constants';
import { Fill } from './types';
const { ZERO_AMOUNT } = constants;
// Used internally by `FillsOptimizer`.
interface FillsOptimizerContext {
currentPath: Fill[];
currentPathInput: BigNumber;
currentPathOutput: BigNumber;
currentPathFlags: number;
}
/**
* Class for finding optimized fill paths.
*/
export class FillsOptimizer {
private readonly _runLimit: number;
private readonly _shouldMinimize: boolean;
private _currentRunCount: number = 0;
private _optimalPath?: Fill[] = undefined;
private _optimalPathOutput: BigNumber = ZERO_AMOUNT;
constructor(runLimit: number, shouldMinimize?: boolean) {
this._runLimit = runLimit;
this._shouldMinimize = !!shouldMinimize;
}
public optimize(fills: Fill[], input: BigNumber, upperBoundPath?: Fill[]): Fill[] | undefined {
this._currentRunCount = 0;
this._optimalPath = upperBoundPath;
this._optimalPathOutput = upperBoundPath ? getPathOutput(upperBoundPath, input) : ZERO_AMOUNT;
const ctx = {
currentPath: [],
currentPathInput: ZERO_AMOUNT,
currentPathOutput: ZERO_AMOUNT,
currentPathFlags: 0,
};
// Visit all valid combinations of fills to find the optimal path.
this._walk(fills, input, ctx);
return this._optimalPath;
}
private _walk(fills: Fill[], input: BigNumber, ctx: FillsOptimizerContext): void {
const { currentPath, currentPathInput, currentPathOutput, currentPathFlags } = ctx;
// Stop if the current path is already complete.
if (currentPathInput.gte(input)) {
this._updateOptimalPath(currentPath, currentPathOutput);
return;
}
const lastNode = currentPath.length !== 0 ? currentPath[currentPath.length - 1] : undefined;
// Visit next fill candidates.
for (const nextFill of fills) {
// Subsequent fills must be a root node or be preceded by its parent,
// enforcing contiguous fills.
if (nextFill.parent && nextFill.parent !== lastNode) {
continue;
}
// Stop if we've hit our run limit.
if (this._currentRunCount++ >= this._runLimit) {
break;
}
const nextPath = [...currentPath, nextFill];
const nextPathInput = BigNumber.min(input, currentPathInput.plus(nextFill.input));
const nextPathOutput = currentPathOutput.plus(
getPartialFillOutput(nextFill, nextPathInput.minus(currentPathInput)),
);
// tslint:disable-next-line: no-bitwise
const nextPathFlags = currentPathFlags | nextFill.flags;
this._walk(
// Filter out incompatible fills.
// tslint:disable-next-line no-bitwise
fills.filter(f => f !== nextFill && (nextPathFlags & f.exclusionMask) === 0),
input,
{
currentPath: nextPath,
currentPathInput: nextPathInput,
currentPathOutput: nextPathOutput,
// tslint:disable-next-line: no-bitwise
currentPathFlags: nextPathFlags,
},
);
}
}
private _updateOptimalPath(path: Fill[], output: BigNumber): void {
if (!this._optimalPath || this._compareOutputs(output, this._optimalPathOutput) === 1) {
this._optimalPath = path;
this._optimalPathOutput = output;
}
}
private _compareOutputs(a: BigNumber, b: BigNumber): number {
return comparePathOutputs(a, b, this._shouldMinimize);
}
}
/**
* Compute the total output for a fill path, optionally clipping the input
* to `maxInput`.
*/
export function getPathOutput(path: Fill[], maxInput?: BigNumber): BigNumber {
let currentInput = ZERO_AMOUNT;
let currentOutput = ZERO_AMOUNT;
for (const fill of path) {
if (maxInput && currentInput.plus(fill.input).gte(maxInput)) {
const partialInput = maxInput.minus(currentInput);
currentOutput = currentOutput.plus(getPartialFillOutput(fill, partialInput));
currentInput = partialInput;
break;
} else {
currentInput = currentInput.plus(fill.input);
currentOutput = currentOutput.plus(fill.output);
}
}
return currentOutput;
}
/**
* Compares two rewards, returning -1, 0, or 1
* if `a` is less than, equal to, or greater than `b`.
*/
export function comparePathOutputs(a: BigNumber, b: BigNumber, shouldMinimize: boolean): number {
return shouldMinimize ? b.comparedTo(a) : a.comparedTo(b);
}
// Get the partial output earned by a fill at input `partialInput`.
function getPartialFillOutput(fill: Fill, partialInput: BigNumber): BigNumber {
return BigNumber.min(fill.output, fill.output.div(fill.input).times(partialInput)).integerValue(
BigNumber.ROUND_DOWN,
);
}

View File

@@ -0,0 +1,407 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { assetDataUtils, ERC20AssetData, orderCalculationUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { constants } from '../../constants';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../fillable_amounts_utils';
import { constants as marketOperationUtilConstants } from './constants';
import { CreateOrderUtils } from './create_order';
import { comparePathOutputs, FillsOptimizer, getPathOutput } from './fill_optimizer';
import { DexOrderSampler } from './sampler';
import {
AggregationError,
DexSample,
ERC20BridgeSource,
Fill,
FillData,
FillFlags,
GetMarketOrdersOpts,
OrderDomain,
} from './types';
const { ZERO_AMOUNT } = constants;
const { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, ERC20_PROXY_ID, SELL_SOURCES } = marketOperationUtilConstants;
export class MarketOperationUtils {
private readonly _dexSampler: DexOrderSampler;
private readonly _createOrderUtils: CreateOrderUtils;
private readonly _orderDomain: OrderDomain;
constructor(
samplerContract: IERC20BridgeSamplerContract,
contractAddresses: ContractAddresses,
orderDomain: OrderDomain,
) {
this._dexSampler = new DexOrderSampler(samplerContract);
this._createOrderUtils = new CreateOrderUtils(contractAddresses);
this._orderDomain = orderDomain;
}
/**
* gets the orders required for a market sell operation by (potentially) merging native orders with
* generated bridge orders.
* @param nativeOrders Native orders.
* @param takerAmount Amount of taker asset to sell.
* @param opts Options object.
* @return orders.
*/
public async getMarketSellOrdersAsync(
nativeOrders: SignedOrder[],
takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<SignedOrderWithFillableAmounts[]> {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts,
};
const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketSellAsync(
nativeOrders,
DexOrderSampler.getSampleAmounts(takerAmount, _opts.numSamples),
difference(SELL_SOURCES, _opts.excludedSources),
);
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
nativeOrders,
fillableAmounts,
MarketOperation.Sell,
);
const prunedNativePath = pruneDustFillsFromNativePath(
createSellPathFromNativeOrders(nativeOrdersWithFillableAmounts),
takerAmount,
_opts.dustFractionThreshold,
);
const clippedNativePath = clipPathToInput(prunedNativePath, takerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts);
const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths);
// If native orders are allowed, splice them in.
if (!_opts.excludedSources.includes(ERC20BridgeSource.Native)) {
allPaths.splice(0, 0, clippedNativePath);
allFills.splice(0, 0, ...clippedNativePath);
}
const optimizer = new FillsOptimizer(_opts.runLimit);
const upperBoundPath = pickBestUpperBoundPath(allPaths, takerAmount);
const optimalPath = optimizer.optimize(
// Sorting the orders by price effectively causes the optimizer to walk
// the greediest solution first, which is the optimal solution in most
// cases.
sortFillsByPrice(allFills),
takerAmount,
upperBoundPath,
);
if (!optimalPath) {
throw new Error(AggregationError.NoOptimalPath);
}
const [outputToken, inputToken] = getOrderTokens(nativeOrders[0]);
return this._createOrderUtils.createSellOrdersFromPath(
this._orderDomain,
inputToken,
outputToken,
simplifyPath(optimalPath),
_opts.bridgeSlippage,
);
}
/**
* gets the orders required for a market buy operation by (potentially) merging native orders with
* generated bridge orders.
* @param nativeOrders Native orders.
* @param makerAmount Amount of maker asset to buy.
* @param opts Options object.
* @return orders.
*/
public async getMarketBuyOrdersAsync(
nativeOrders: SignedOrder[],
makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<SignedOrderWithFillableAmounts[]> {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts,
};
const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketBuyAsync(
nativeOrders,
DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples),
difference(BUY_SOURCES, _opts.excludedSources),
);
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
nativeOrders,
fillableAmounts,
MarketOperation.Buy,
);
const prunedNativePath = pruneDustFillsFromNativePath(
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts),
makerAmount,
_opts.dustFractionThreshold,
);
const clippedNativePath = clipPathToInput(prunedNativePath, makerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts);
const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths);
// If native orders are allowed, splice them in.
if (!_opts.excludedSources.includes(ERC20BridgeSource.Native)) {
allPaths.splice(0, 0, clippedNativePath);
allFills.splice(0, 0, ...clippedNativePath);
}
const optimizer = new FillsOptimizer(_opts.runLimit, true);
const upperBoundPath = pickBestUpperBoundPath(allPaths, makerAmount, true);
const optimalPath = optimizer.optimize(
// Sorting the orders by price effectively causes the optimizer to walk
// the greediest solution first, which is the optimal solution in most
// cases.
sortFillsByPrice(allFills),
makerAmount,
upperBoundPath,
);
if (!optimalPath) {
throw new Error(AggregationError.NoOptimalPath);
}
const [inputToken, outputToken] = getOrderTokens(nativeOrders[0]);
return this._createOrderUtils.createBuyOrdersFromPath(
this._orderDomain,
inputToken,
outputToken,
simplifyPath(optimalPath),
_opts.bridgeSlippage,
);
}
}
function createSignedOrdersWithFillableAmounts(
signedOrders: SignedOrder[],
fillableAmounts: BigNumber[],
operation: MarketOperation,
): SignedOrderWithFillableAmounts[] {
return signedOrders
.map(
(order: SignedOrder, i: number): SignedOrderWithFillableAmounts => {
const fillableAmount = fillableAmounts[i];
const fillableMakerAssetAmount =
operation === MarketOperation.Buy
? fillableAmount
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
const fillableTakerAssetAmount =
operation === MarketOperation.Sell
? fillableAmount
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
return {
fillableMakerAssetAmount,
fillableTakerAssetAmount,
fillableTakerFeeAmount,
...order,
};
},
)
.filter(order => {
return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero();
});
}
// Gets the difference between two sets.
function difference<T>(a: T[], b: T[]): T[] {
return a.filter(x => b.indexOf(x) === -1);
}
function createSellPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[]): Fill[] {
const path: Fill[] = [];
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(order);
// Native orders can be filled in any order, so they're all root nodes.
path.push({
flags: FillFlags.SourceNative,
exclusionMask: 0,
input: takerAmount,
output: makerAmount,
fillData: {
source: ERC20BridgeSource.Native,
order,
},
});
}
return path;
}
function createBuyPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[]): Fill[] {
const path: Fill[] = [];
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(order);
// Native orders can be filled in any order, so they're all root nodes.
path.push({
flags: FillFlags.SourceNative,
exclusionMask: 0,
input: makerAmount,
output: takerAmount,
fillData: {
source: ERC20BridgeSource.Native,
order,
},
});
}
return path;
}
function createPathsFromDexQuotes(dexQuotes: DexSample[][], noConflicts: boolean): Fill[][] {
const paths: Fill[][] = [];
for (const quote of dexQuotes) {
// Native orders can be filled in any order, so they're all root nodes.
const path: Fill[] = [];
paths.push(path);
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < quote.length; i++) {
const sample = quote[i];
const prev = i !== 0 ? quote[i - 1] : undefined;
const parent = i !== 0 ? path[path.length - 1] : undefined;
path.push({
parent,
flags: sourceToFillFlags(sample.source),
exclusionMask: noConflicts ? sourceToExclusionMask(sample.source) : 0,
input: sample.input.minus(prev ? prev.input : 0),
output: sample.output.minus(prev ? prev.output : 0),
fillData: { source: sample.source },
});
}
}
return paths;
}
function sourceToFillFlags(source: ERC20BridgeSource): number {
if (source === ERC20BridgeSource.Kyber) {
return FillFlags.SourceKyber;
}
if (source === ERC20BridgeSource.Eth2Dai) {
return FillFlags.SourceEth2Dai;
}
if (source === ERC20BridgeSource.Uniswap) {
return FillFlags.SourceUniswap;
}
return FillFlags.SourceNative;
}
function sourceToExclusionMask(source: ERC20BridgeSource): number {
if (source === ERC20BridgeSource.Kyber) {
// tslint:disable-next-line: no-bitwise
return FillFlags.SourceEth2Dai | FillFlags.SourceUniswap;
}
if (source === ERC20BridgeSource.Eth2Dai) {
return FillFlags.SourceKyber;
}
if (source === ERC20BridgeSource.Uniswap) {
return FillFlags.SourceKyber;
}
return 0;
}
function pruneDustFillsFromNativePath(path: Fill[], fillAmount: BigNumber, dustFractionThreshold: number): Fill[] {
const dustAmount = fillAmount.times(dustFractionThreshold);
return path.filter(f => f.input.gt(dustAmount));
}
// Convert a list of DEX paths to a flattened list of `Fills`.
function flattenDexPaths(dexFills: Fill[][]): Fill[] {
const fills: Fill[] = [];
for (const quote of dexFills) {
for (const fill of quote) {
fills.push(fill);
}
}
return fills;
}
// Picks the path with the highest (or lowest if `shouldMinimize` is true) output.
function pickBestUpperBoundPath(paths: Fill[][], maxInput: BigNumber, shouldMinimize?: boolean): Fill[] | undefined {
let optimalPath: Fill[] | undefined;
let optimalPathOutput: BigNumber = ZERO_AMOUNT;
for (const path of paths) {
if (getPathInput(path).gte(maxInput)) {
const output = getPathOutput(path, maxInput);
if (!optimalPath || comparePathOutputs(output, optimalPathOutput, !!shouldMinimize) === 1) {
optimalPath = path;
optimalPathOutput = output;
}
}
}
return optimalPath;
}
// Gets the total input of a path.
function getPathInput(path: Fill[]): BigNumber {
return BigNumber.sum(...path.map(p => p.input));
}
// Merges contiguous fills from the same DEX.
function simplifyPath(path: Fill[]): Fill[] {
const simplified: Fill[] = [];
for (const fill of path) {
const source = (fill.fillData as FillData).source;
if (simplified.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = simplified[simplified.length - 1];
const prevSource = (prevFill.fillData as FillData).source;
// If the last fill is from the same source, merge them.
if (prevSource === source) {
prevFill.input = prevFill.input.plus(fill.input);
prevFill.output = prevFill.output.plus(fill.output);
continue;
}
}
simplified.push(fill);
}
return simplified;
}
// Sort fills by descending price.
function sortFillsByPrice(fills: Fill[]): Fill[] {
return fills.sort((a, b) => {
const d = b.output.div(b.input).minus(a.output.div(a.input));
if (d.gt(0)) {
return 1;
} else if (d.lt(0)) {
return -1;
}
return 0;
});
}
function getOrderTokens(order: SignedOrder): [string, string] {
const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [
ERC20AssetData,
ERC20AssetData
];
if (assets.some(a => a.assetProxyId !== ERC20_PROXY_ID)) {
throw new Error(AggregationError.NotERC20AssetData);
}
return assets.map(a => a.tokenAddress) as [string, string];
}
function clipPathToInput(path: Fill[], assetAmount: BigNumber): Fill[] {
const clipped = [];
let totalInput = ZERO_AMOUNT;
for (const fill of path) {
if (totalInput.gte(assetAmount)) {
break;
}
clipped.push(fill);
totalInput = totalInput.plus(fill.input);
}
return clipped;
}

View File

@@ -0,0 +1,72 @@
import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { constants as marketOperationUtilConstants } from './constants';
import { DexSample, ERC20BridgeSource } from './types';
const { SOURCE_TO_ADDRESS } = marketOperationUtilConstants;
export class DexOrderSampler {
private readonly _samplerContract: IERC20BridgeSamplerContract;
/**
* Generate sample amounts up to `maxFillAmount`.
*/
public static getSampleAmounts(maxFillAmount: BigNumber, numSamples: number): BigNumber[] {
const amounts = [];
for (let i = 0; i < numSamples; i++) {
amounts.push(
maxFillAmount
.times(i + 1)
.div(numSamples)
.integerValue(BigNumber.ROUND_UP),
);
}
return amounts;
}
constructor(samplerContract: IERC20BridgeSamplerContract) {
this._samplerContract = samplerContract;
}
public async getFillableAmountsAndSampleMarketBuyAsync(
nativeOrders: SignedOrder[],
sampleAmounts: BigNumber[],
sources: ERC20BridgeSource[],
): Promise<[BigNumber[], DexSample[][]]> {
const signatures = nativeOrders.map(o => o.signature);
const [fillableAmount, rawSamples] = await this._samplerContract
.queryOrdersAndSampleBuys(nativeOrders, signatures, sources.map(s => SOURCE_TO_ADDRESS[s]), sampleAmounts)
.callAsync();
const quotes = rawSamples.map((rawDexSamples, sourceIdx) => {
const source = sources[sourceIdx];
return rawDexSamples.map((sample, sampleIdx) => ({
source,
input: sampleAmounts[sampleIdx],
output: sample,
}));
});
return [fillableAmount, quotes];
}
public async getFillableAmountsAndSampleMarketSellAsync(
nativeOrders: SignedOrder[],
sampleAmounts: BigNumber[],
sources: ERC20BridgeSource[],
): Promise<[BigNumber[], DexSample[][]]> {
const signatures = nativeOrders.map(o => o.signature);
const [fillableAmount, rawSamples] = await this._samplerContract
.queryOrdersAndSampleSells(nativeOrders, signatures, sources.map(s => SOURCE_TO_ADDRESS[s]), sampleAmounts)
.callAsync();
const quotes = rawSamples.map((rawDexSamples, sourceIdx) => {
const source = sources[sourceIdx];
return rawDexSamples.map((sample, sampleIdx) => ({
source,
input: sampleAmounts[sampleIdx],
output: sample,
}));
});
return [fillableAmount, quotes];
}
}

View File

@@ -0,0 +1,117 @@
import { BigNumber } from '@0x/utils';
import { SignedOrderWithFillableAmounts } from '../../types';
/**
* Order domain keys: chainId and exchange
*/
export interface OrderDomain {
chainId: number;
exchangeAddress: string;
}
/**
* Common exception messages thrown by aggregation logic.
*/
export enum AggregationError {
NoOptimalPath = 'NO_OPTIMAL_PATH',
EmptyOrders = 'EMPTY_ORDERS',
NotERC20AssetData = 'NOT_ERC20ASSET_DATA',
NoBridgeForSource = 'NO_BRIDGE_FOR_SOURCE',
}
/**
* DEX sources to aggregate.
*/
export enum ERC20BridgeSource {
Native = 'Native',
Uniswap = 'Uniswap',
Eth2Dai = 'Eth2Dai',
Kyber = 'Kyber',
}
// Internal `fillData` field for `Fill` objects.
export interface FillData {
source: ERC20BridgeSource;
}
// `FillData` for native fills.
export interface NativeFillData extends FillData {
order: SignedOrderWithFillableAmounts;
}
/**
* Represents an individual DEX sample from the sampler contract.
*/
export interface DexSample {
source: ERC20BridgeSource;
input: BigNumber;
output: BigNumber;
}
/**
* Flags for `Fill` objects.
*/
export enum FillFlags {
SourceNative = 0x1,
SourceUniswap = 0x2,
SourceEth2Dai = 0x4,
SourceKyber = 0x8,
}
/**
* Represents a node on a fill path.
*/
export interface Fill {
// See `FillFlags`.
flags: FillFlags;
// `FillFlags` that are incompatible with this fill, e.g., to prevent
// Kyber from mixing with Uniswap and Eth2Dai and vice versa.
exclusionMask: number;
// Input fill amount (taker asset amount in a sell, maker asset amount in a buy).
input: BigNumber;
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
output: BigNumber;
// Fill that must precede this one. This enforces certain fills to be contiguous.
parent?: Fill;
// Data associated with this this Fill object. Used to reconstruct orders
// from paths.
fillData: FillData | NativeFillData;
}
/**
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
*/
export interface GetMarketOrdersOpts {
/**
* Liquidity sources to exclude. Default is none.
*/
excludedSources: ERC20BridgeSource[];
/**
* Whether to prevent mixing Kyber orders with Uniswap and Eth2Dai orders.
*/
noConflicts: boolean;
/**
* Complexity limit on the search algorithm, i.e., maximum number of
* nodes to visit. Default is 1024.
*/
runLimit: number;
/**
* When generating bridge orders, we use
* sampled rate * (1 - bridgeSlippage)
* as the rate for calculating maker/taker asset amounts.
* This should be a small positive number (e.g., 0.0005) to make up for
* small discrepancies between samples and truth.
* Default is 0.0005 (5 basis points).
*/
bridgeSlippage: number;
/**
* Number of samples to take for each DEX quote.
*/
numSamples: number;
/**
* Dust amount, as a fraction of the fill amount.
* Default is 0.01 (100 basis points).
*/
dustFractionThreshold: number;
}

View File

@@ -1,92 +0,0 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import { MarketOperation, PrunedSignedOrder } from '../types';
import { assert } from './assert';
import { utils } from './utils';
export const marketUtils = {
findOrdersThatCoverTakerAssetFillAmount(
sortedOrders: PrunedSignedOrder[],
takerAssetFillAmount: BigNumber,
slippageBufferAmount: BigNumber = new BigNumber(0),
): { resultOrders: PrunedSignedOrder[]; remainingFillAmount: BigNumber } {
return findOrdersThatCoverAssetFillAmount(
sortedOrders,
takerAssetFillAmount,
MarketOperation.Sell,
slippageBufferAmount,
);
},
findOrdersThatCoverMakerAssetFillAmount(
sortedOrders: PrunedSignedOrder[],
makerAssetFillAmount: BigNumber,
slippageBufferAmount: BigNumber = new BigNumber(0),
): { resultOrders: PrunedSignedOrder[]; remainingFillAmount: BigNumber } {
return findOrdersThatCoverAssetFillAmount(
sortedOrders,
makerAssetFillAmount,
MarketOperation.Buy,
slippageBufferAmount,
);
},
};
function findOrdersThatCoverAssetFillAmount(
sortedOrders: PrunedSignedOrder[],
assetFillAmount: BigNumber,
operation: MarketOperation,
slippageBufferAmount: BigNumber,
): { resultOrders: PrunedSignedOrder[]; remainingFillAmount: BigNumber } {
assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
// calculate total amount of asset needed to be filled
const totalFillAmount = assetFillAmount.plus(slippageBufferAmount);
// iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount
const result = _.reduce(
sortedOrders,
({ resultOrders, remainingFillAmount }, order) => {
if (remainingFillAmount.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) {
return {
resultOrders,
remainingFillAmount: constants.ZERO_AMOUNT,
};
} else {
const assetAmountAvailable = getAssetAmountAvailable(order, operation);
const shouldIncludeOrder = assetAmountAvailable.gt(constants.ZERO_AMOUNT);
// if there is no assetAmountAvailable do not append order to resultOrders
// if we have exceeded the total amount we want to fill set remainingFillAmount to 0
return {
resultOrders: shouldIncludeOrder ? _.concat(resultOrders, order) : resultOrders,
remainingFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingFillAmount.minus(assetAmountAvailable),
),
};
}
},
{
resultOrders: [] as PrunedSignedOrder[],
remainingFillAmount: totalFillAmount,
},
);
return result;
}
function getAssetAmountAvailable(order: PrunedSignedOrder, operation: MarketOperation): BigNumber {
if (operation === MarketOperation.Buy) {
if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount);
} else {
return order.fillableMakerAssetAmount;
}
} else {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount);
} else {
return order.fillableTakerAssetAmount;
}
}
}

View File

@@ -1,96 +1,29 @@
import { DevUtilsContract } from '@0x/contract-wrappers';
import { orderCalculationUtils } from '@0x/order-utils';
import { OrderStatus, SignedOrder } from '@0x/types';
import { SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { constants } from '../constants';
import { OrderPrunerOnChainMetadata, OrderPrunerOpts, OrderPrunerPermittedFeeTypes, PrunedSignedOrder } from '../types';
import { OrderPrunerPermittedFeeTypes } from '../types';
import { utils } from '../utils/utils';
export class OrderPruner {
public readonly expiryBufferMs: number;
public readonly permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>;
private readonly _devUtils: DevUtilsContract;
// TODO(dave4506): OrderPruneCalculator can be more powerful if it takes in a specified takerAddress
constructor(devUtils: DevUtilsContract, opts: Partial<OrderPrunerOpts> = {}) {
const { expiryBufferMs, permittedOrderFeeTypes } = _.assign({}, constants.DEFAULT_ORDER_PRUNER_OPTS, opts);
this.expiryBufferMs = expiryBufferMs;
this.permittedOrderFeeTypes = permittedOrderFeeTypes;
this._devUtils = devUtils;
}
public async pruneSignedOrdersAsync(signedOrders: SignedOrder[]): Promise<PrunedSignedOrder[]> {
const unsortedOrders = this._filterForUsableOrders(signedOrders, this.expiryBufferMs);
const signatures = _.map(unsortedOrders, o => o.signature);
const [ordersInfo, fillableTakerAssetAmounts, isValidSignatures] = await this._devUtils
.getOrderRelevantStates(unsortedOrders, signatures)
.callAsync();
const ordersOnChainMetadata: OrderPrunerOnChainMetadata[] = ordersInfo.map((orderInfo, index) => {
return {
...orderInfo,
fillableTakerAssetAmount: fillableTakerAssetAmounts[index],
isValidSignature: isValidSignatures[index],
};
});
// take orders + on chain information and find the valid orders and fillable makerAsset or takerAsset amounts
const prunedOrders = this._filterForFillableAndPermittedFeeTypeOrders(unsortedOrders, ordersOnChainMetadata);
return prunedOrders;
}
// tslint:disable-next-line: prefer-function-over-method
private _filterForFillableAndPermittedFeeTypeOrders(
orders: SignedOrder[],
ordersOnChainMetadata: OrderPrunerOnChainMetadata[],
): PrunedSignedOrder[] {
const result = _.chain(orders)
.filter(
(order: SignedOrder, index: number): boolean => {
const { isValidSignature, orderStatus } = ordersOnChainMetadata[index];
return (
isValidSignature &&
orderStatus === OrderStatus.Fillable &&
((this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
order.takerFee.eq(constants.ZERO_AMOUNT)) ||
(this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithTakerAsset(order)) ||
(this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithMakerAsset(order)))
);
},
)
.map(
(order: SignedOrder, index: number): PrunedSignedOrder => {
const { fillableTakerAssetAmount } = ordersOnChainMetadata[index];
return {
...order,
fillableTakerAssetAmount,
fillableMakerAssetAmount: orderCalculationUtils.getMakerFillAmount(
order,
fillableTakerAssetAmount,
),
fillableTakerFeeAmount: orderCalculationUtils.getTakerFeeAmount(
order,
fillableTakerAssetAmount,
),
};
},
)
.value();
return result;
}
// tslint:disable-next-line: prefer-function-over-method
private _filterForUsableOrders(orders: SignedOrder[], expiryBufferMs: number): SignedOrder[] {
const result = _.filter(orders, order => {
export const orderPrunerUtils = {
pruneForUsableSignedOrders(
signedOrders: SignedOrder[],
permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>,
expiryBufferMs: number,
): SignedOrder[] {
const result = _.filter(signedOrders, order => {
return (
orderCalculationUtils.isOpenOrder(order) &&
!orderCalculationUtils.willOrderExpire(order, expiryBufferMs / constants.ONE_SECOND_MS)
!orderCalculationUtils.willOrderExpire(order, expiryBufferMs / constants.ONE_SECOND_MS) &&
((permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
order.takerFee.eq(constants.ZERO_AMOUNT)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithTakerAsset(order)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithMakerAsset(order)))
);
});
return result;
}
}
},
};

View File

@@ -0,0 +1,49 @@
import { DevUtilsContract } from '@0x/contract-wrappers';
import { orderCalculationUtils } from '@0x/order-utils';
import { OrderStatus, SignedOrder } from '@0x/types';
import { constants } from '../constants';
import { OrderPrunerOnChainMetadata, SignedOrderWithFillableAmounts } from '../types';
/**
* Utility class to retrieve order state if needed outside of using the ERC20BridgeSampler
*/
export class OrderStateUtils {
private readonly _devUtils: DevUtilsContract;
constructor(devUtils: DevUtilsContract) {
this._devUtils = devUtils;
}
public async getSignedOrdersWithFillableAmountsAsync(
signedOrders: SignedOrder[],
): Promise<SignedOrderWithFillableAmounts[]> {
const signatures = signedOrders.map(o => o.signature);
const [ordersInfo, fillableTakerAssetAmounts, isValidSignatures] = await this._devUtils
.getOrderRelevantStates(signedOrders, signatures)
.callAsync();
const ordersOnChainMetadata: OrderPrunerOnChainMetadata[] = ordersInfo.map((orderInfo, index) => {
return {
...orderInfo,
fillableTakerAssetAmount: fillableTakerAssetAmounts[index],
isValidSignature: isValidSignatures[index],
};
});
// take orders + on chain information and find the valid orders and fillable makerAsset or takerAsset amounts
return signedOrders.map(
(order: SignedOrder, index: number): SignedOrderWithFillableAmounts => {
const orderMetadata = ordersOnChainMetadata[index];
const fillableTakerAssetAmount =
orderMetadata.isValidSignature && orderMetadata.orderStatus === OrderStatus.Fillable
? orderMetadata.fillableTakerAssetAmount
: constants.ZERO_AMOUNT;
return {
...order,
fillableTakerAssetAmount,
fillableMakerAssetAmount: orderCalculationUtils.getMakerFillAmount(order, fillableTakerAssetAmount),
fillableTakerFeeAmount: orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount),
};
},
);
}
}

View File

@@ -1,32 +1,42 @@
import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import * as heartbeats from 'heartbeats';
import { constants } from '../constants';
import { SwapQuoterError } from '../types';
export class ProtocolFeeUtils {
public gasPriceEstimation: BigNumber;
private readonly _gasPriceHeart: any;
constructor(gasPricePollingIntervalInMs: number) {
this._gasPriceHeart = heartbeats.createHeart(gasPricePollingIntervalInMs);
this.gasPriceEstimation = constants.ZERO_AMOUNT;
this._initializeHeartBeat();
}
// TODO(dave4506) at some point, we should add a heart beat to the multiplier, or some RPC call to fetch latest multiplier.
// tslint:disable-next-line:prefer-function-over-method
public async getProtocolFeeMultiplierAsync(): Promise<BigNumber> {
return new BigNumber(150000);
return constants.PROTOCOL_FEE_MULTIPLIER;
}
// tslint:disable-next-line: prefer-function-over-method
public async getGasPriceEstimationOrThrowAsync(): Promise<BigNumber> {
try {
const res = await fetch(`${constants.ETH_GAS_STATION_API_BASE_URL}/json/ethgasAPI.json`);
const gasInfo = await res.json();
// Eth Gas Station result is gwei * 10
// tslint:disable-next-line:custom-no-magic-numbers
const BASE_TEN = 10;
const gasPriceGwei = new BigNumber(gasInfo.fast / BASE_TEN);
const unit = new BigNumber(BASE_TEN).pow(BASE_TEN);
const gasPriceWei = unit.times(gasPriceGwei);
return gasPriceWei;
} catch (e) {
throw new Error(SwapQuoterError.NoGasPriceProvidedOrEstimated);
public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise<BigNumber> {
if (shouldHardRefresh) {
return this._getGasPriceFromGasStationOrThrowAsync();
} else {
return this.gasPriceEstimation;
}
}
/**
* Destroys any subscriptions or connections.
*/
public async destroyAsync(): Promise<void> {
this._gasPriceHeart.kill();
}
/**
* Calculates protocol fee with protofol fee multiplier for each fill.
*/
@@ -38,4 +48,28 @@ export class ProtocolFeeUtils {
const protocolFee = new BigNumber(orders.length).times(protocolFeeMultiplier).times(gasPrice);
return protocolFee;
}
// tslint:disable-next-line: prefer-function-over-method
private async _getGasPriceFromGasStationOrThrowAsync(): Promise<BigNumber> {
try {
const res = await fetch(`${constants.ETH_GAS_STATION_API_BASE_URL}/json/ethgasAPI.json`);
const gasInfo = await res.json();
// Eth Gas Station result is gwei * 10
// tslint:disable-next-line:custom-no-magic-numbers
const BASE_TEN = 10;
const gasPriceGwei = new BigNumber(gasInfo.fast / BASE_TEN);
// tslint:disable-next-line:custom-no-magic-numbers
const unit = new BigNumber(BASE_TEN).pow(9);
const gasPriceWei = unit.times(gasPriceGwei);
return gasPriceWei;
} catch (e) {
throw new Error(SwapQuoterError.NoGasPriceProvidedOrEstimated);
}
}
private _initializeHeartBeat(): void {
this._gasPriceHeart.createEvent(1, async () => {
this.gasPriceEstimation = await this._getGasPriceFromGasStationOrThrowAsync();
});
}
}

View File

@@ -1,277 +1,274 @@
import { orderCalculationUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import { InsufficientAssetLiquidityError } from '../errors';
import {
CalculateSwapQuoteOpts,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
PrunedSignedOrder,
SignedOrderWithFillableAmounts,
SwapQuote,
SwapQuoteInfo,
} from '../types';
import { marketUtils } from './market_utils';
import { fillableAmountsUtils } from './fillable_amounts_utils';
import { MarketOperationUtils } from './market_operation_utils';
import { ProtocolFeeUtils } from './protocol_fee_utils';
import { utils } from './utils';
// Calculates a swap quote for orders
export const swapQuoteCalculator = {
async calculateMarketSellSwapQuoteAsync(
prunedOrders: PrunedSignedOrder[],
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
export class SwapQuoteCalculator {
private readonly _protocolFeeUtils: ProtocolFeeUtils;
private readonly _marketOperationUtils: MarketOperationUtils;
constructor(protocolFeeUtils: ProtocolFeeUtils, marketOperationUtils: MarketOperationUtils) {
this._protocolFeeUtils = protocolFeeUtils;
this._marketOperationUtils = marketOperationUtils;
}
public async calculateMarketSellSwapQuoteAsync(
prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber,
slippagePercentage: number,
gasPrice: BigNumber,
protocolFeeUtils: ProtocolFeeUtils,
opts: CalculateSwapQuoteOpts,
): Promise<MarketSellSwapQuote> {
return (await calculateSwapQuoteAsync(
return (await this._calculateSwapQuoteAsync(
prunedOrders,
takerAssetFillAmount,
slippagePercentage,
gasPrice,
protocolFeeUtils,
MarketOperation.Sell,
opts,
)) as MarketSellSwapQuote;
},
async calculateMarketBuySwapQuoteAsync(
prunedOrders: PrunedSignedOrder[],
}
public async calculateMarketBuySwapQuoteAsync(
prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber,
slippagePercentage: number,
gasPrice: BigNumber,
protocolFeeUtils: ProtocolFeeUtils,
opts: CalculateSwapQuoteOpts,
): Promise<MarketBuySwapQuote> {
return (await calculateSwapQuoteAsync(
return (await this._calculateSwapQuoteAsync(
prunedOrders,
takerAssetFillAmount,
slippagePercentage,
gasPrice,
protocolFeeUtils,
MarketOperation.Buy,
opts,
)) as MarketBuySwapQuote;
},
};
async function calculateSwapQuoteAsync(
prunedOrders: PrunedSignedOrder[],
assetFillAmount: BigNumber,
slippagePercentage: number,
gasPrice: BigNumber,
protocolFeeUtils: ProtocolFeeUtils,
marketOperation: MarketOperation,
): Promise<SwapQuote> {
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
let resultOrders: PrunedSignedOrder[];
let remainingFillAmount: BigNumber;
if (marketOperation === MarketOperation.Buy) {
// find the orders that cover the desired assetBuyAmount (with slippage)
({ resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
prunedOrders,
assetFillAmount,
slippageBufferAmount,
));
} else {
// find the orders that cover the desired assetBuyAmount (with slippage)
({ resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
prunedOrders,
assetFillAmount,
slippageBufferAmount,
));
}
// if we do not have enough orders to cover the desired assetBuyAmount, throw
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
// We needed the amount they requested to buy, plus the amount for slippage
const totalAmountRequested = assetFillAmount.plus(slippageBufferAmount);
const amountAbleToFill = totalAmountRequested.minus(remainingFillAmount);
// multiplierNeededWithSlippage represents what we need to multiply the assetBuyAmount by
// in order to get the total amount needed considering slippage
// i.e. if slippagePercent was 0.2 (20%), multiplierNeededWithSlippage would be 1.2
const multiplierNeededWithSlippage = new BigNumber(1).plus(slippagePercentage);
// Given amountAvailableToFillConsideringSlippage * multiplierNeededWithSlippage = amountAbleToFill
// We divide amountUnableToFill by multiplierNeededWithSlippage to determine amountAvailableToFillConsideringSlippage
const amountAvailableToFillConsideringSlippage = amountAbleToFill
.div(multiplierNeededWithSlippage)
.integerValue(BigNumber.ROUND_FLOOR);
private async _calculateSwapQuoteAsync(
prunedOrders: SignedOrder[],
assetFillAmount: BigNumber,
slippagePercentage: number,
gasPrice: BigNumber,
operation: MarketOperation,
opts: CalculateSwapQuoteOpts,
): Promise<SwapQuote> {
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
throw new InsufficientAssetLiquidityError(amountAvailableToFillConsideringSlippage);
}
// assetData information for the result
const takerAssetData = resultOrders[0].takerAssetData;
const makerAssetData = resultOrders[0].makerAssetData;
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
let resultOrders: SignedOrderWithFillableAmounts[] = [];
const bestCaseQuoteInfo = await calculateQuoteInfoAsync(
resultOrders,
assetFillAmount,
gasPrice,
protocolFeeUtils,
marketOperation,
);
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
const worstCaseQuoteInfo = await calculateQuoteInfoAsync(
_.reverse(_.clone(resultOrders)),
assetFillAmount,
gasPrice,
protocolFeeUtils,
marketOperation,
);
if (operation === MarketOperation.Buy) {
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync(
prunedOrders,
assetFillAmount.plus(slippageBufferAmount),
opts,
);
} else {
resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders,
assetFillAmount.plus(slippageBufferAmount),
opts,
);
}
const quoteBase = {
takerAssetData,
makerAssetData,
orders: resultOrders,
bestCaseQuoteInfo,
worstCaseQuoteInfo,
gasPrice,
};
// assetData information for the result
const takerAssetData = resultOrders[0].takerAssetData;
const makerAssetData = resultOrders[0].makerAssetData;
if (marketOperation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
resultOrders,
assetFillAmount,
gasPrice,
operation,
);
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
_.reverse(resultOrders.slice()),
assetFillAmount,
gasPrice,
operation,
);
const quoteBase = {
takerAssetData,
makerAssetData,
orders: resultOrders,
bestCaseQuoteInfo,
worstCaseQuoteInfo,
gasPrice,
};
} else {
if (operation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
};
}
}
// tslint:disable-next-line: prefer-function-over-method
private async _calculateQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[],
assetFillAmount: BigNumber,
gasPrice: BigNumber,
operation: MarketOperation,
): Promise<SwapQuoteInfo> {
if (operation === MarketOperation.Buy) {
return this._calculateMarketBuyQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice);
} else {
return this._calculateMarketSellQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice);
}
}
private async _calculateMarketSellQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[],
takerAssetSellAmount: BigNumber,
gasPrice: BigNumber,
): Promise<SwapQuoteInfo> {
const result = prunedOrders.reduce(
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
remainingTakerAssetFillAmount,
} = acc;
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
order,
);
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
order,
);
const takerAssetAmountWithFees = BigNumber.min(
remainingTakerAssetFillAmount,
adjustedFillableTakerAssetAmount,
);
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
const makerAssetAmount = takerAssetAmountWithFees
.div(adjustedFillableTakerAssetAmount)
.multipliedBy(adjustedFillableMakerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
return {
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerAssetAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingTakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount.minus(takerAssetAmountWithFees),
),
};
},
{
totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount: takerAssetSellAmount,
},
);
const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
prunedOrders,
gasPrice,
);
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
protocolFeeInWeiAmount,
};
}
}
async function calculateQuoteInfoAsync(
prunedOrders: PrunedSignedOrder[],
assetFillAmount: BigNumber,
gasPrice: BigNumber,
protocolFeeUtils: ProtocolFeeUtils,
operation: MarketOperation,
): Promise<SwapQuoteInfo> {
if (operation === MarketOperation.Buy) {
return calculateMarketBuyQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice, protocolFeeUtils);
} else {
return calculateMarketSellQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice, protocolFeeUtils);
private async _calculateMarketBuyQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[],
makerAssetBuyAmount: BigNumber,
gasPrice: BigNumber,
): Promise<SwapQuoteInfo> {
const result = prunedOrders.reduce(
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
remainingMakerAssetFillAmount,
} = acc;
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
order,
);
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
order,
);
const makerFillAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);
const takerAssetAmountWithFees = makerFillAmount
.div(adjustedFillableMakerAssetAmount)
.multipliedBy(adjustedFillableTakerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
return {
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerFillAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingMakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingMakerAssetFillAmount.minus(makerFillAmount),
),
};
},
{
totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingMakerAssetFillAmount: makerAssetBuyAmount,
},
);
const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
prunedOrders,
gasPrice,
);
return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
protocolFeeInWeiAmount,
};
}
}
async function calculateMarketSellQuoteInfoAsync(
prunedOrders: PrunedSignedOrder[],
takerAssetSellAmount: BigNumber,
gasPrice: BigNumber,
protocolFeeUtils: ProtocolFeeUtils,
): Promise<SwapQuoteInfo> {
const result = _.reduce(
prunedOrders,
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
remainingTakerAssetFillAmount,
} = acc;
const [
adjustedFillableMakerAssetAmount,
adjustedFillableTakerAssetAmount,
] = utils.getAdjustedFillableMakerAndTakerAmountsFromTakerFees(order);
const takerAssetAmountWithFees = BigNumber.min(
remainingTakerAssetFillAmount,
adjustedFillableTakerAssetAmount,
);
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
const makerAssetAmount = takerAssetAmountWithFees
.div(adjustedFillableTakerAssetAmount)
.multipliedBy(adjustedFillableMakerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
return {
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerAssetAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingTakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount.minus(takerAssetAmountWithFees),
),
};
},
{
totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount: takerAssetSellAmount,
},
);
const protocolFeeInWeiAmount = await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(prunedOrders, gasPrice);
return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
protocolFeeInWeiAmount,
};
}
async function calculateMarketBuyQuoteInfoAsync(
prunedOrders: PrunedSignedOrder[],
makerAssetBuyAmount: BigNumber,
gasPrice: BigNumber,
protocolFeeUtils: ProtocolFeeUtils,
): Promise<SwapQuoteInfo> {
const result = _.reduce(
prunedOrders,
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
remainingMakerAssetFillAmount,
} = acc;
const [
adjustedFillableMakerAssetAmount,
adjustedFillableTakerAssetAmount,
] = utils.getAdjustedFillableMakerAndTakerAmountsFromTakerFees(order);
const makerFillAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);
const takerAssetAmountWithFees = makerFillAmount
.div(adjustedFillableMakerAssetAmount)
.multipliedBy(adjustedFillableTakerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
return {
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerFillAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingMakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingMakerAssetFillAmount.minus(makerFillAmount),
),
};
},
{
totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingMakerAssetFillAmount: makerAssetBuyAmount,
},
);
const protocolFeeInWeiAmount = await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(prunedOrders, gasPrice);
return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
protocolFeeInWeiAmount,
};
}
function getTakerAssetAmountBreakDown(
order: PrunedSignedOrder,
order: SignedOrderWithFillableAmounts,
takerAssetAmountWithFees: BigNumber,
): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {

View File

@@ -1,5 +1,6 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, WETH9Contract } from '@0x/contract-wrappers';
import { WETH9Contract } from '@0x/contract-wrappers';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper';
@@ -72,8 +73,7 @@ export const swapQuoteConsumerUtils = {
provider: Provider,
opts: Partial<GetExtensionContractTypeOpts>,
): Promise<ExtensionContractType> {
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
const wethAssetData = await devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync();
const wethAssetData = assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken);
if (swapQuoteConsumerUtils.isValidForwarderSwapQuote(quote, wethAssetData)) {
if (opts.takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', opts.takerAddress);

View File

@@ -5,7 +5,6 @@ import { AbiDefinition, ContractAbi, MethodAbi } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from '../constants';
import { PrunedSignedOrder } from '../types';
// tslint:disable:no-unnecessary-type-assertion
export const utils = {
@@ -42,15 +41,4 @@ export const utils = {
: order.takerAssetAmount;
return [adjustedMakerAssetAmount, adjustedTakerAssetAmount];
},
getAdjustedFillableMakerAndTakerAmountsFromTakerFees<T extends PrunedSignedOrder>(
order: T,
): [BigNumber, BigNumber] {
const adjustedFillableMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)
? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount)
: order.fillableMakerAssetAmount;
const adjustedFillableTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order)
? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount)
: order.fillableTakerAssetAmount;
return [adjustedFillableMakerAssetAmount, adjustedFillableTakerAssetAmount];
},
};

View File

@@ -11,15 +11,15 @@ import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
const {
PRUNED_SIGNED_ORDERS_FEELESS,
PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
} = testOrders;
// tslint:disable:custom-no-magic-numbers
describe('#calculateLiquidity', () => {
it('should provide correct liquidity result with feeless orders', () => {
const prunedSignedOrders = PRUNED_SIGNED_ORDERS_FEELESS;
const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
@@ -27,7 +27,7 @@ describe('#calculateLiquidity', () => {
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9));
});
it('should provide correct liquidity result with orders with takerFees in takerAsset', () => {
const prunedSignedOrders = PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET;
const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
@@ -35,7 +35,7 @@ describe('#calculateLiquidity', () => {
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(15));
});
it('should provide correct liquidity result with orders with takerFees in makerAsset', () => {
const prunedSignedOrders = PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET;
const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
@@ -44,9 +44,9 @@ describe('#calculateLiquidity', () => {
});
it('should provide correct liquidity result with mixed orders with fees and no fees', () => {
const prunedSignedOrders = _.concat(
PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEELESS,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
);
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,

View File

@@ -1,8 +1,9 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
@@ -16,7 +17,7 @@ import {
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
PrunedSignedOrder,
SignedOrderWithFillableAmounts,
} from '../src/types';
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
@@ -33,7 +34,7 @@ const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const UNLIMITED_ALLOWANCE = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<PrunedSignedOrder>> = [
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
@@ -85,7 +86,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
const chainId = TESTRPC_CHAIN_ID;
let orders: PrunedSignedOrder[];
let orders: SignedOrderWithFillableAmounts[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ExchangeSwapQuoteConsumer;
@@ -104,12 +105,11 @@ describe('ExchangeSwapQuoteConsumer', () => {
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
[makerAssetData, takerAssetData, wethAssetData] = await Promise.all([
devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
]);
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
@@ -130,7 +130,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
protocolFeeUtils = new ProtocolFeeUtils();
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TakerTokenContract,
makerAddress,
@@ -154,7 +154,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
...order,
...partialOrder,
};
orders.push(prunedOrder as PrunedSignedOrder);
orders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(

View File

@@ -0,0 +1,71 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { fillableAmountsUtils } from '../src/utils/fillable_amounts_utils';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
// tslint:disable:custom-no-magic-numbers
const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b22222222222222222222222222222222222222222222222222222222222222222';
const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b11111111111111111111111111111111111111111111111111111111111111111';
const TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSignedOrderWithFillableAmounts({
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
fillableMakerAssetAmount: baseUnitAmount(5),
fillableTakerAssetAmount: baseUnitAmount(10),
fillableTakerFeeAmount: baseUnitAmount(2),
});
const MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSignedOrderWithFillableAmounts({
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
fillableMakerAssetAmount: baseUnitAmount(10),
fillableTakerAssetAmount: baseUnitAmount(5),
fillableTakerFeeAmount: baseUnitAmount(2),
});
describe('fillableAmountsUtils', () => {
describe('getTakerAssetAmountSwappedAfterFees', () => {
it('should return fillableTakerAssetAmount if takerFee is not denominated in taker', () => {
const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER.fillableTakerAssetAmount,
);
});
it('should return fillableTakerAssetAmount + fillableTakerFeeAmount if takerFee is not denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(12));
});
});
describe('getMakerAssetAmountSwappedAfterFees', () => {
it('should return fillableMakerAssetAmount if takerFee is not denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER.fillableMakerAssetAmount,
);
});
it('should return fillableMakerAssetAmount - fillableTakerFeeif takerFee is denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(8));
});
});
});

View File

@@ -1,8 +1,9 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, ERC20TokenContract, ForwarderContract } from '@0x/contract-wrappers';
import { ERC20TokenContract, ForwarderContract } from '@0x/contract-wrappers';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
@@ -15,7 +16,7 @@ import {
ForwarderMarketSellSmartContractParams,
MarketBuySwapQuote,
MarketOperation,
PrunedSignedOrder,
SignedOrderWithFillableAmounts,
} from '../src/types';
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
@@ -33,7 +34,7 @@ const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const FEE_PERCENTAGE = 0.05;
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<PrunedSignedOrder>> = [
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
@@ -83,8 +84,8 @@ describe('ForwarderSwapQuoteConsumer', () => {
let erc20TokenContract: ERC20TokenContract;
let forwarderContract: ForwarderContract;
let orders: PrunedSignedOrder[];
let invalidOrders: PrunedSignedOrder[];
let orders: SignedOrderWithFillableAmounts[];
let invalidOrders: SignedOrderWithFillableAmounts[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let invalidMarketBuySwapQuote: SwapQuote;
@@ -103,12 +104,11 @@ describe('ForwarderSwapQuoteConsumer', () => {
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider);
forwarderContract = new ForwarderContract(contractAddresses.forwarder, provider);
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
[makerAssetData, takerAssetData, wethAssetData] = await Promise.all([
devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
]);
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
@@ -132,7 +132,7 @@ describe('ForwarderSwapQuoteConsumer', () => {
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
protocolFeeUtils = new ProtocolFeeUtils();
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TokenContract,
makerAddress,
@@ -166,7 +166,7 @@ describe('ForwarderSwapQuoteConsumer', () => {
...order,
...partialOrder,
};
orders.push(prunedOrder as PrunedSignedOrder);
orders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
invalidOrders = [];
@@ -176,7 +176,7 @@ describe('ForwarderSwapQuoteConsumer', () => {
...order,
...partialOrder,
};
invalidOrders.push(prunedOrder as PrunedSignedOrder);
invalidOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(

View File

@@ -0,0 +1,744 @@
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import {
assertRoughlyEquals,
constants,
expect,
getRandomFloat,
getRandomInteger,
Numberish,
randomAddress,
} from '@0x/contracts-test-utils';
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { Order, SignedOrder } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
import { MockSamplerContract, QueryAndSampleResult } from './utils/mock_sampler_contract';
const { SOURCE_TO_ADDRESS, BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants;
// Because the bridges and the DEX sources are only deployed on mainnet, tests will resort to using mainnet addresses
const CHAIN_ID = 1;
// tslint:disable: custom-no-magic-numbers
describe('MarketOperationUtils tests', () => {
const contractAddresses = getContractAddressesForChainOrThrow(CHAIN_ID);
const ETH2DAI_BRIDGE_ADDRESS = contractAddresses.eth2DaiBridge;
const KYBER_BRIDGE_ADDRESS = contractAddresses.kyberBridge;
const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge;
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN);
const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
interface RatesBySource {
[source: string]: Numberish[];
}
function createOrder(overrides?: Partial<SignedOrder>): SignedOrder {
return {
chainId: CHAIN_ID,
exchangeAddress: contractAddresses.exchange,
makerAddress: constants.NULL_ADDRESS,
takerAddress: constants.NULL_ADDRESS,
senderAddress: constants.NULL_ADDRESS,
feeRecipientAddress: randomAddress(),
salt: generatePseudoRandomSalt(),
expirationTimeSeconds: getRandomInteger(0, 2 ** 64),
makerAssetData: MAKER_ASSET_DATA,
takerAssetData: TAKER_ASSET_DATA,
makerFeeAssetData: assetDataUtils.encodeERC20AssetData(randomAddress()),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(randomAddress()),
makerAssetAmount: getRandomInteger(1, 1e18),
takerAssetAmount: getRandomInteger(1, 1e18),
makerFee: getRandomInteger(1, 1e17),
takerFee: getRandomInteger(1, 1e17),
signature: hexUtils.random(),
...overrides,
};
}
function getSourceFromAssetData(assetData: string): ERC20BridgeSource {
if (assetData.length === 74) {
return ERC20BridgeSource.Native;
}
const bridgeAddress = hexUtils.slice(assetData, 48, 68).toLowerCase();
switch (bridgeAddress) {
case KYBER_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.Kyber;
case ETH2DAI_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.Eth2Dai;
case UNISWAP_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.Uniswap;
default:
break;
}
throw new Error(`Unknown bridge address: ${bridgeAddress}`);
}
function getSourceFromAddress(sourceAddress: string): ERC20BridgeSource {
for (const k of Object.keys(SOURCE_TO_ADDRESS)) {
if (SOURCE_TO_ADDRESS[k].toLowerCase() === sourceAddress.toLowerCase()) {
return k as ERC20BridgeSource;
}
}
throw new Error(`Unknown source address: ${sourceAddress}`);
}
function assertSamePrefix(actual: string, expected: string): void {
expect(actual.substr(0, expected.length)).to.eq(expected);
}
function createOrdersFromSellRates(takerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] {
const singleTakerAssetAmount = takerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP);
return rates.map(r =>
createOrder({
makerAssetAmount: singleTakerAssetAmount.times(r),
takerAssetAmount: singleTakerAssetAmount,
}),
);
}
function createOrdersFromBuyRates(makerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] {
const singleMakerAssetAmount = makerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP);
return (rates as any).map((r: Numberish) =>
createOrder({
makerAssetAmount: singleMakerAssetAmount,
takerAssetAmount: singleMakerAssetAmount.div(r),
}),
);
}
function createSamplerFromSellRates(rates: RatesBySource): MockSamplerContract {
return new MockSamplerContract({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
const fillableTakerAssetAmounts = orders.map(o => o.takerAssetAmount);
const samplesBySourceIndex = sources.map(s =>
fillAmounts.map((fillAmount, idx) =>
fillAmount.times(rates[getSourceFromAddress(s)][idx]).integerValue(BigNumber.ROUND_UP),
),
);
return [fillableTakerAssetAmounts, samplesBySourceIndex];
},
});
}
function createSamplerFromBuyRates(rates: RatesBySource): MockSamplerContract {
return new MockSamplerContract({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
const fillableMakerAssetAmounts = orders.map(o => o.makerAssetAmount);
const samplesBySourceIndex = sources.map(s =>
fillAmounts.map((fillAmount, idx) =>
fillAmount.div(rates[getSourceFromAddress(s)][idx]).integerValue(BigNumber.ROUND_UP),
),
);
return [fillableMakerAssetAmounts, samplesBySourceIndex];
},
});
}
const DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL = (
orders: Order[],
signatures: string[],
sources: string[],
fillAmounts: BigNumber[],
): QueryAndSampleResult => [
orders.map((order: Order) => order.takerAssetAmount),
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
];
const DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY = (
orders: Order[],
signatures: string[],
sources: string[],
fillAmounts: BigNumber[],
): QueryAndSampleResult => [
orders.map((order: Order) => order.makerAssetAmount),
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
];
const ORDER_DOMAIN = {
exchangeAddress: contractAddresses.exchange,
chainId: CHAIN_ID,
};
describe('DexOrderSampler', () => {
describe('getSampleAmounts()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const NUM_SAMPLES = 16;
it('generates the correct number of amounts', () => {
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
expect(amounts).to.be.length(NUM_SAMPLES);
});
it('first amount is nonzero', () => {
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
expect(amounts[0]).to.not.bignumber.eq(0);
});
it('last amount is the fill amount', () => {
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
expect(amounts[NUM_SAMPLES - 1]).to.bignumber.eq(FILL_AMOUNT);
});
it('can generate a single amount', () => {
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, 1);
expect(amounts).to.be.length(1);
expect(amounts[0]).to.bignumber.eq(FILL_AMOUNT);
});
it('generates ascending amounts', () => {
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
for (const i of _.times(NUM_SAMPLES).slice(1)) {
const prev = amounts[i - 1];
const amount = amounts[i];
expect(prev).to.bignumber.lt(amount);
}
});
});
describe('getFillableAmountsAndSampleMarketOperationAsync()', () => {
const SAMPLE_AMOUNTS = [100, 500, 1000].map(v => new BigNumber(v));
const ORDERS = _.times(4, () => createOrder());
it('makes an eth_call with the correct arguments for a sell', async () => {
const sampler = new MockSamplerContract({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
expect(orders).to.deep.eq(ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
expect(sources).to.deep.eq(SELL_SOURCES.map(s => SOURCE_TO_ADDRESS[s]));
expect(fillAmounts).to.deep.eq(SAMPLE_AMOUNTS);
return [
orders.map(() => getRandomInteger(1, 1e18)),
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
];
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync(ORDERS, SAMPLE_AMOUNTS, SELL_SOURCES);
});
it('makes an eth_call with the correct arguments for a buy', async () => {
const sampler = new MockSamplerContract({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
expect(orders).to.deep.eq(ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
expect(sources).to.deep.eq(BUY_SOURCES.map(s => SOURCE_TO_ADDRESS[s]));
expect(fillAmounts).to.deep.eq(SAMPLE_AMOUNTS);
return [
orders.map(() => getRandomInteger(1, 1e18)),
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
];
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
await dexOrderSampler.getFillableAmountsAndSampleMarketBuyAsync(ORDERS, SAMPLE_AMOUNTS, BUY_SOURCES);
});
it('returns correct fillable amounts', async () => {
const fillableAmounts = _.times(SAMPLE_AMOUNTS.length, () => getRandomInteger(1, 1e18));
const sampler = new MockSamplerContract({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => [
fillableAmounts,
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
],
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [actualFillableAmounts] = await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync(
ORDERS,
SAMPLE_AMOUNTS,
SELL_SOURCES,
);
expect(actualFillableAmounts).to.deep.eq(fillableAmounts);
});
it('converts samples to DEX quotes', async () => {
const quotes = SELL_SOURCES.map(source =>
SAMPLE_AMOUNTS.map(s => ({
source,
input: s,
output: getRandomInteger(1, 1e18),
})),
);
const sampler = new MockSamplerContract({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => [
orders.map(() => getRandomInteger(1, 1e18)),
quotes.map(q => q.map(s => s.output)),
],
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [, actualQuotes] = await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync(
ORDERS,
SAMPLE_AMOUNTS,
SELL_SOURCES,
);
expect(actualQuotes).to.deep.eq(quotes);
});
});
});
function createRandomRates(numSamples: number = 32): RatesBySource {
const ALL_SOURCES = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Uniswap,
];
return _.zipObject(
ALL_SOURCES,
_.times(ALL_SOURCES.length, () => _.fill(new Array(numSamples), getRandomFloat(1e-3, 2))),
);
}
describe('MarketOperationUtils', () => {
describe('getMarketSellOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const SOURCE_RATES = createRandomRates();
const ORDERS = createOrdersFromSellRates(
FILL_AMOUNT,
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]),
);
const DEFAULT_SAMPLER = createSamplerFromSellRates(SOURCE_RATES);
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0 };
const defaultMarketOperationUtils = new MarketOperationUtils(
DEFAULT_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => {
let wasCalled = false;
const sampler = new MockSamplerContract({
queryOrdersAndSampleSells: (...args) => {
wasCalled = true;
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(...args);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, DEFAULT_OPTS);
expect(wasCalled).to.be.true();
});
it('queries `numSamples` samples', async () => {
const numSamples = _.random(1, 16);
let fillAmountsLength = 0;
const sampler = new MockSamplerContract({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
fillAmountsLength = fillAmounts.length;
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
numSamples,
});
expect(fillAmountsLength).eq(numSamples);
});
it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a));
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources: [],
});
expect(sourcesPolled).to.deep.eq(SELL_SOURCES);
});
it('does not poll DEXes in `excludedSources`', async () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a));
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources,
});
expect(sourcesPolled).to.deep.eq(_.without(SELL_SOURCES, ...excludedSources));
});
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
const bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES)));
const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate));
expect(bestSource).to.exist('');
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
runLimit: 0,
});
const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData));
expect(uniqueAssetDatas).to.be.length(1);
expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource);
});
it('generates bridge orders with correct asset data', async () => {
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
// Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT,
DEFAULT_OPTS,
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
expect(getSourceFromAssetData(order.makerAssetData)).to.exist('');
const makerAssetDataPrefix = hexUtils.slice(
assetDataUtils.encodeERC20BridgeAssetData(
MAKER_TOKEN,
constants.NULL_ADDRESS,
constants.NULL_BYTES,
),
0,
36,
);
assertSamePrefix(order.makerAssetData, makerAssetDataPrefix);
expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA);
}
});
it('generates bridge orders with correct taker amount', async () => {
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
// Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT,
DEFAULT_OPTS,
);
const totalTakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAssetAmount));
expect(totalTakerAssetAmount).to.bignumber.gte(FILL_AMOUNT);
});
it('generates bridge orders with max slippage of `bridgeSlippage`', async () => {
const bridgeSlippage = _.random(0.1, true);
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
// Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, bridgeSlippage },
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData);
const expectedMakerAmount = FILL_AMOUNT.times(SOURCE_RATES[source][0]);
const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber();
assertRoughlyEquals(slippage, bridgeSlippage, 8);
}
});
it('ignores native orders below `dustFractionThreshold`', async () => {
const dustFractionThreshold = 0.01;
const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN);
const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount)));
// Pass in an order with the globally best rate but with a dust input amount.
const dustOrder = createOrder({
makerAssetAmount: dustAmount.times(maxRate.plus(0.01)),
takerAssetAmount: dustAmount,
});
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
_.shuffle([dustOrder, ...ORDERS]),
FILL_AMOUNT,
// Ignore all DEX sources so only native orders are returned.
{ ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: SELL_SOURCES },
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
expect(order.takerAssetAmount).to.bignumber.gt(dustAmount);
}
});
it('can mix convex sources', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils(
createSamplerFromSellRates(rates),
contractAddresses,
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)).sort();
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
].sort();
expect(orderSources).to.deep.eq(expectedSources);
});
it('excludes Kyber when `noConflicts` enabled and Uniswap or Eth2Dai are used first', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils(
createSamplerFromSellRates(rates),
contractAddresses,
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)).sort();
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
].sort();
expect(orderSources).to.deep.eq(expectedSources);
});
it('excludes Uniswap and Eth2Dai when `noConflicts` enabled and Kyber is used first', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.Uniswap] = [0.15, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.15, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils(
createSamplerFromSellRates(rates),
contractAddresses,
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)).sort();
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Kyber,
].sort();
expect(orderSources).to.deep.eq(expectedSources);
});
});
describe('getMarketBuyOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const SOURCE_RATES = _.omit(createRandomRates(), [ERC20BridgeSource.Kyber]);
const ORDERS = createOrdersFromBuyRates(
FILL_AMOUNT,
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]),
);
const DEFAULT_SAMPLER = createSamplerFromBuyRates(SOURCE_RATES);
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0 };
const defaultMarketOperationUtils = new MarketOperationUtils(
DEFAULT_SAMPLER,
contractAddresses,
ORDER_DOMAIN,
);
it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => {
let wasCalled = false;
const sampler = new MockSamplerContract({
queryOrdersAndSampleBuys: (...args) => {
wasCalled = true;
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(...args);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, DEFAULT_OPTS);
expect(wasCalled).to.be.true();
});
it('queries `numSamples` samples', async () => {
const numSamples = _.random(1, 16);
let fillAmountsLength = 0;
const sampler = new MockSamplerContract({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
fillAmountsLength = fillAmounts.length;
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
numSamples,
});
expect(fillAmountsLength).eq(numSamples);
});
it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a));
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources: [],
});
expect(sourcesPolled).to.deep.eq(BUY_SOURCES);
});
it('does not poll DEXes in `excludedSources`', async () => {
const excludedSources = _.sampleSize(BUY_SOURCES, _.random(1, BUY_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = [];
const sampler = new MockSamplerContract({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
sourcesPolled = sources.map(a => getSourceFromAddress(a));
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts);
},
});
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources,
});
expect(sourcesPolled).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources));
});
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
const bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES)));
const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate));
expect(bestSource).to.exist('');
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
runLimit: 0,
});
const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData));
expect(uniqueAssetDatas).to.be.length(1);
expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource);
});
it('generates bridge orders with correct asset data', async () => {
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
// Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT,
DEFAULT_OPTS,
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
expect(getSourceFromAssetData(order.makerAssetData)).to.exist('');
const makerAssetDataPrefix = hexUtils.slice(
assetDataUtils.encodeERC20BridgeAssetData(
MAKER_TOKEN,
constants.NULL_ADDRESS,
constants.NULL_BYTES,
),
0,
36,
);
assertSamePrefix(order.makerAssetData, makerAssetDataPrefix);
expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA);
}
});
it('generates bridge orders with correct taker amount', async () => {
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
// Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT,
DEFAULT_OPTS,
);
const totalMakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAssetAmount));
expect(totalMakerAssetAmount).to.bignumber.gte(FILL_AMOUNT);
});
it('generates bridge orders with max slippage of `bridgeSlippage`', async () => {
const bridgeSlippage = _.random(0.1, true);
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
// Pass in empty orders to prevent native orders from being used.
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, bridgeSlippage },
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData);
const expectedTakerAmount = FILL_AMOUNT.div(SOURCE_RATES[source][0]).integerValue(
BigNumber.ROUND_UP,
);
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1;
assertRoughlyEquals(slippage, bridgeSlippage, 8);
}
});
it('Ignores native orders below `dustFractionThreshold`', async () => {
const dustFractionThreshold = 0.01;
const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN);
const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount)));
// Pass in an order with the globally best rate but with a dust input amount.
const dustOrder = createOrder({
makerAssetAmount: dustAmount,
takerAssetAmount: dustAmount.div(maxRate.plus(0.01)).integerValue(BigNumber.ROUND_DOWN),
});
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
_.shuffle([dustOrder, ...ORDERS]),
FILL_AMOUNT,
// Ignore all DEX sources so only native orders are returned.
{ ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: BUY_SOURCES },
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
expect(order.makerAssetAmount).to.bignumber.gt(dustAmount);
}
});
it('can mix convex sources', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
const marketOperationUtils = new MarketOperationUtils(
createSamplerFromBuyRates(rates),
contractAddresses,
ORDER_DOMAIN,
);
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData)).sort();
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
].sort();
expect(orderSources).to.deep.eq(expectedSources);
});
});
});
});
// tslint:disable-next-line: max-file-line-count

View File

@@ -1,374 +0,0 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { constants } from '../src/constants';
import { marketUtils } from '../src/utils/market_utils';
import { chaiSetup } from './utils/chai_setup';
import { testOrders } from './utils/test_orders';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
// tslint:disable:custom-no-magic-numbers
// tslint:disable: no-unused-expression
describe('marketUtils', () => {
describe('#findOrdersThatCoverTakerAssetFillAmount', () => {
describe('no orders', () => {
it('returns empty and unchanged remainingFillAmount', async () => {
const fillAmount = baseUnitAmount(9);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
[],
fillAmount,
);
expect(resultOrders).to.be.empty;
expect(remainingFillAmount).to.be.bignumber.equal(fillAmount);
});
});
describe('orders do not have fees', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEELESS;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(5);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(3));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(1);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders have fees in takerAsset', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(5);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(1));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(9);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders are feeless or have fees in takerAsset', () => {
const inputOrders = _.concat(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
);
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(20);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(12);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(20);
const slippageBufferAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(2));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first four orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(16);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1], inputOrders[2], inputOrders[3]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
});
describe('#findOrdersThatCoverMakerAssetFillAmount', () => {
describe('no orders', () => {
it('returns empty and unchanged remainingFillAmount', async () => {
const fillAmount = baseUnitAmount(9);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
[],
fillAmount,
);
expect(resultOrders).to.be.empty;
expect(remainingFillAmount).to.be.bignumber.equal(fillAmount);
});
});
describe('orders do not have fees', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEELESS;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(2));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(8);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders have fees in makerAsset', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(2);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(4);
const slippageBufferAmount = baseUnitAmount(0.5);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(3);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(1));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(1);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders are feeless or have fees in makerAsset', () => {
const inputOrders = _.concat(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
);
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(12);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(12);
const slippageBufferAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(14);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(3));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(1);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first four orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(11);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1], inputOrders[2], inputOrders[3]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
});
});

View File

@@ -1,8 +1,9 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { constants as devConstants, getLatestBlockTimestampAsync, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
@@ -10,7 +11,7 @@ import 'mocha';
import { constants } from '../src/constants';
import { OrderPrunerPermittedFeeTypes } from '../src/types';
import { OrderPruner } from '../src/utils/order_prune_utils';
import { orderPrunerUtils } from '../src/utils/order_prune_utils';
import { chaiSetup } from './utils/chai_setup';
import { provider, web3Wrapper } from './utils/web3_wrapper';
@@ -25,14 +26,14 @@ const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PROTOCOL_FEE_MULTIPLIER = 150000;
const PROTOCOL_FEE_PER_FILL = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER);
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const EXPIRY_BUFFER_MS = 120000;
// tslint:disable: no-unused-expression
// tslint:disable: custom-no-magic-numbers
describe('OrderPruner', () => {
describe('orderPrunerUtils', () => {
let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let exchangeContract: ExchangeContract;
let devUtilsContract: DevUtilsContract;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
@@ -45,16 +46,12 @@ describe('OrderPruner', () => {
let orderFactory: OrderFactory;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let orderPruner: OrderPruner;
let nonOpenSignedOrder: SignedOrder;
let expiredOpenSignedOrder: SignedOrder;
let invalidSignatureOpenSignedOrder: SignedOrder;
let fullyFillableOpenSignedOrder: SignedOrder;
let partiallyFilledOpenSignedOrderFeeless: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInTakerAsset: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInMakerAsset: SignedOrder;
let filledOpenSignedOrder: SignedOrder;
const chainId = TESTRPC_CHAIN_ID;
const fillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI);
@@ -70,12 +67,11 @@ describe('OrderPruner', () => {
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
devUtilsContract = new DevUtilsContract(contractAddresses.devUtils, provider);
[makerAssetData, takerAssetData, wethAssetData] = [
await devUtilsContract.encodeERC20AssetData(makerTokenAddress).callAsync(),
await devUtilsContract.encodeERC20AssetData(takerTokenAddress).callAsync(),
await devUtilsContract.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
// Configure order defaults
@@ -107,17 +103,7 @@ describe('OrderPruner', () => {
});
expiredOpenSignedOrder = await orderFactory.newSignedOrderAsync({
expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).minus(10),
});
invalidSignatureOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAddress,
});
invalidSignatureOpenSignedOrder.signature = constants.NULL_BYTES;
fullyFillableOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).plus(60000),
});
// give double fillableAmount to maker and taker as buffer
@@ -194,59 +180,41 @@ describe('OrderPruner', () => {
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
filledOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(filledOpenSignedOrder, fillableAmount, filledOpenSignedOrder.signature)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]),
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('constructor options', () => {
describe('prunedForUsableSignedOrders', () => {
it('should filter for only feeless orders if options permit only feeless orders', async () => {
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([OrderPrunerPermittedFeeTypes.NoFees]),
});
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([OrderPrunerPermittedFeeTypes.NoFees]);
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(partiallyFilledOpenSignedOrderFeeless.signature);
});
it('should filter for only takerFee in takerAsset orders if options permit only takerFee in takerAsset orders', async () => {
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]),
});
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]);
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(
@@ -254,83 +222,52 @@ describe('OrderPruner', () => {
);
});
it('should filter for only makerFee in takerAsset orders if options permit only makerFee orders', async () => {
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
]),
});
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
]);
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(
partiallyFilledOpenSignedOrderFeeInMakerAsset.signature,
);
});
});
describe('#pruneSignedOrdersAsync', () => {
it('should filter out non open orders', async () => {
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]);
const orders = [nonOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
expect(resultPrunedOrders).to.be.empty;
});
it('should filter out expired orders', async () => {
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]);
const orders = [expiredOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
expect(resultPrunedOrders).to.be.empty;
});
it('should filter out invalid signature orders', async () => {
const orders = [invalidSignatureOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders).to.be.empty;
});
it('should filter out fully filled orders', async () => {
const orders = [filledOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders).to.be.empty;
});
it('should provide correct pruned signed orders for fully fillable orders', async () => {
const orders = [fullyFillableOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
const prunedOrder = resultPrunedOrders[0];
expect(prunedOrder.fillableMakerAssetAmount).to.bignumber.equal(fillableAmount);
expect(prunedOrder.fillableTakerAssetAmount).to.bignumber.equal(fillableAmount);
});
it('should provide correct pruned signed orders for partially fillable orders', async () => {
const orders = [
partiallyFilledOpenSignedOrderFeeless,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeInMakerAsset,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders[0].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[0].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[1].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[1].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[1].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
expect(resultPrunedOrders[2].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[2].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[2].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
});
});
});

View File

@@ -0,0 +1,275 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { constants as devConstants, getLatestBlockTimestampAsync, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { SignedOrderWithFillableAmounts } from '../src/types';
import { OrderStateUtils } from '../src/utils/order_state_utils';
import { chaiSetup } from './utils/chai_setup';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PROTOCOL_FEE_MULTIPLIER = 150000;
const PROTOCOL_FEE_PER_FILL = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER);
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const isSignedOrdersWithFillableAmountsNotFillable = (signedOrders: SignedOrderWithFillableAmounts[]) => {
signedOrders.forEach(order => {
expect(order.fillableMakerAssetAmount).to.bignumber.eq(constants.ZERO_AMOUNT);
expect(order.fillableTakerAssetAmount).to.bignumber.eq(constants.ZERO_AMOUNT);
expect(order.fillableTakerFeeAmount).to.bignumber.eq(constants.ZERO_AMOUNT);
});
};
// tslint:disable: no-unused-expression
// tslint:disable: custom-no-magic-numbers
describe('OrderStateUtils', () => {
let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let exchangeContract: ExchangeContract;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let orderFactory: OrderFactory;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let orderStateUtils: OrderStateUtils;
let expiredOpenSignedOrder: SignedOrder;
let invalidSignatureOpenSignedOrder: SignedOrder;
let fullyFillableOpenSignedOrder: SignedOrder;
let partiallyFilledOpenSignedOrderFeeless: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInTakerAsset: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInMakerAsset: SignedOrder;
let filledOpenSignedOrder: SignedOrder;
const chainId = TESTRPC_CHAIN_ID;
const fillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI);
const partialFillAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
const takerFeeAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
before(async () => {
contractAddresses = await migrateOnceAsync(provider);
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: feeRecipient,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
expiredOpenSignedOrder = await orderFactory.newSignedOrderAsync({
expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).minus(10),
});
invalidSignatureOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAddress,
});
invalidSignatureOpenSignedOrder.signature = expiredOpenSignedOrder.signature;
fullyFillableOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
// give double fillableAmount to maker and taker as buffer
await erc20MakerTokenContract
.transfer(makerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20TakerTokenContract
.transfer(takerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: makerAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
await erc20TakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
partiallyFilledOpenSignedOrderFeeless = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeless,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeless.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInTakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: takerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInTakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInMakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: makerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInMakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
filledOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(filledOpenSignedOrder, fillableAmount, filledOpenSignedOrder.signature)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
orderStateUtils = new OrderStateUtils(new DevUtilsContract(contractAddresses.devUtils, provider));
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('#getSignedOrdersWithFillableAmountsAsync', () => {
it('should 0 fillableTakerAssetAmount for expired orders', async () => {
const orders = [expiredOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
isSignedOrdersWithFillableAmountsNotFillable(resultOrders);
});
it('should filter out invalid signature orders', async () => {
const orders = [invalidSignatureOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
isSignedOrdersWithFillableAmountsNotFillable(resultOrders);
});
it('should return 0 fillableTakerAssetAmount for fully filled orders', async () => {
const orders = [filledOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
isSignedOrdersWithFillableAmountsNotFillable(resultOrders);
});
it('should provide correct pruned signed orders for fully fillable orders', async () => {
const orders = [fullyFillableOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
const order = resultOrders[0];
expect(order.fillableMakerAssetAmount).to.bignumber.equal(fillableAmount);
expect(order.fillableTakerAssetAmount).to.bignumber.equal(fillableAmount);
});
it('should provide correct pruned signed orders for partially fillable orders', async () => {
const orders = [
partiallyFilledOpenSignedOrderFeeless,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeInMakerAsset,
];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
expect(resultOrders[0].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[0].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[1].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[1].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[1].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
expect(resultOrders[2].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[2].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[2].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
});
});
});

View File

@@ -1,150 +1,259 @@
import { constants as devConstants } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { ContractAddresses, migrateOnceAsync } from '@0x/migrations';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import * as TypeMoq from 'typemoq';
import { constants } from '../src/constants';
import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
import { swapQuoteCalculator } from '../src/utils/swap_quote_calculator';
import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator';
import { chaiSetup } from './utils/chai_setup';
import { MockSamplerContract } from './utils/mock_sampler_contract';
import { protocolFeeUtilsMock } from './utils/mocks';
import { testHelpers } from './utils/test_helpers';
import { testOrders } from './utils/test_orders';
import { baseUnitAmount } from './utils/utils';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const MIXED_TEST_ORDERS = _.concat(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
);
// const MIXED_TEST_ORDERS = _.concat(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
// );
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const { DEFAULT_GET_MARKET_ORDERS_OPTS, SELL_SOURCES } = marketOperationUtilConstants;
// Excludes all non native sources
const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...{
excludedSources: SELL_SOURCES,
},
};
const createSamplerFromSignedOrdersWithFillableAmounts = (
signedOrders: SignedOrderWithFillableAmounts[],
): MockSamplerContract => {
const sampler = new MockSamplerContract({
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
const fillableAmounts = signatures.map((s: string) => {
const order = (signedOrders.find(o => o.signature === s) as any) as SignedOrderWithFillableAmounts;
return order.fillableMakerAssetAmount;
});
return [fillableAmounts, sources.map(() => fillAmounts.map(() => constants.ZERO_AMOUNT))];
},
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
const fillableAmounts = signatures.map((s: string) => {
const order = (signedOrders.find(o => o.signature === s) as any) as SignedOrderWithFillableAmounts;
return order.fillableTakerAssetAmount;
});
return [fillableAmounts, sources.map(() => fillAmounts.map(() => constants.ZERO_AMOUNT))];
},
});
return sampler;
};
// tslint:disable:max-file-line-count
// tslint:disable:custom-no-magic-numbers
describe('swapQuoteCalculator', () => {
let mockProtocolFeeUtils: TypeMoq.IMock<ProtocolFeeUtils>;
let protocolFeeUtils: ProtocolFeeUtils;
let contractAddresses: ContractAddresses;
before(async () => {
mockProtocolFeeUtils = protocolFeeUtilsMock();
contractAddresses = await migrateOnceAsync(provider);
protocolFeeUtils = protocolFeeUtilsMock().object;
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('#calculateMarketSellSwapQuote', () => {
describe('InsufficientLiquidityError', () => {
it('should throw if not enough taker asset liquidity (multiple feeless orders)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
baseUnitAmount(10),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9));
});
it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
baseUnitAmount(10),
0.2,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5));
});
it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
baseUnitAmount(20),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15));
});
it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
baseUnitAmount(20),
0.2,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(12.5));
});
it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
baseUnitAmount(10),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9));
});
it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
baseUnitAmount(10),
0.2,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5));
});
it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
MIXED_TEST_ORDERS,
baseUnitAmount(40),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(33));
});
it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
MIXED_TEST_ORDERS,
baseUnitAmount(40),
0.2,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(27.5));
});
});
// TODO(dave4506) InsufficientLiquidityError is not thrown anymore, consider how to test for insufficient liquidity
// describe('InsufficientLiquidityError', () => {
// it('should throw if not enough taker asset liquidity (multiple feeless orders)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
// baseUnitAmount(10),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9));
// });
// it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => {
// const errorFunction = async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
// baseUnitAmount(10),
// 0.2,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5));
// });
// it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
// baseUnitAmount(20),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15));
// });
// it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
// baseUnitAmount(20),
// 0.2,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(12.5));
// });
// it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
// baseUnitAmount(10),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9));
// });
// it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
// baseUnitAmount(10),
// 0.2,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5));
// });
// it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// MIXED_TEST_ORDERS,
// baseUnitAmount(40),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(33));
// });
// it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
// MIXED_TEST_ORDERS,
// baseUnitAmount(40),
// 0.2,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(27.5));
// });
// });
it('calculates a correct swapQuote with no slippage (feeless orders)', async () => {
const assetSellAmount = baseUnitAmount(0.5);
const slippagePercentage = 0;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
assetSellAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([testOrders.PRUNED_SIGNED_ORDERS_FEELESS[0]]);
expect(swapQuote.orders).to.deep.equal([testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0]]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
@@ -165,18 +274,26 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with slippage (feeless orders)', async () => {
const assetSellAmount = baseUnitAmount(1);
const slippagePercentage = 0.2;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
assetSellAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.PRUNED_SIGNED_ORDERS_FEELESS[0],
testOrders.PRUNED_SIGNED_ORDERS_FEELESS[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
@@ -198,15 +315,25 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => {
const assetSellAmount = baseUnitAmount(4);
const slippagePercentage = 0;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
assetSellAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET[0]]);
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
@@ -227,17 +354,25 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with slippage (takerAsset denominated fee orders)', async () => {
const assetSellAmount = baseUnitAmount(3);
const slippagePercentage = 0.5;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
assetSellAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET[0],
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[1],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
@@ -259,15 +394,25 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => {
const assetSellAmount = baseUnitAmount(4);
const slippagePercentage = 0;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
assetSellAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET[0]]);
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
@@ -288,17 +433,25 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => {
const assetSellAmount = baseUnitAmount(4);
const slippagePercentage = 0.5;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
assetSellAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET[0],
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
@@ -320,116 +473,173 @@ describe('swapQuoteCalculator', () => {
});
});
describe('#calculateMarketBuySwapQuoteAsync', () => {
describe('InsufficientLiquidityError', () => {
it('should throw if not enough maker asset liquidity (multiple feeless orders)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
baseUnitAmount(12),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10));
});
it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
baseUnitAmount(10),
0.6,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25));
});
it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
baseUnitAmount(12),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10));
});
it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
baseUnitAmount(12),
0.6,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25));
});
it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
baseUnitAmount(6),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(5));
});
it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
baseUnitAmount(6),
0.6,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(3.125));
});
it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
MIXED_TEST_ORDERS,
baseUnitAmount(40),
0,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(25));
});
it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => {
const errorFunction = async () => {
await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
MIXED_TEST_ORDERS,
baseUnitAmount(40),
0.6,
GAS_PRICE,
mockProtocolFeeUtils.object,
);
};
await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15.625));
});
});
// TODO(dave4506) InsufficientLiquidityError is not thrown anymore, consider how to test for insufficient liquidity
// describe('InsufficientLiquidityError', () => {
// it('should throw if not enough maker asset liquidity (multiple feeless orders)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
// baseUnitAmount(12),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10));
// });
// it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
// baseUnitAmount(10),
// 0.6,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25));
// });
// it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
// baseUnitAmount(12),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10));
// });
// it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
// baseUnitAmount(12),
// 0.6,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25));
// });
// it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
// baseUnitAmount(6),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(5));
// });
// it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
// baseUnitAmount(6),
// 0.6,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(3.125));
// });
// it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// MIXED_TEST_ORDERS,
// baseUnitAmount(40),
// 0,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(25));
// });
// it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => {
// const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS);
// const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
// exchangeAddress: contractAddresses.exchange,
// chainId: TESTRPC_CHAIN_ID,
// });
// const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
// const errorFunction = async () => {
// await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
// MIXED_TEST_ORDERS,
// baseUnitAmount(40),
// 0.6,
// GAS_PRICE,
// CALCULATE_SWAP_QUOTE_OPTS,
// );
// };
// await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15.625));
// });
// });
it('calculates a correct swapQuote with no slippage (feeless orders)', async () => {
const assetBuyAmount = baseUnitAmount(3);
const slippagePercentage = 0;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
assetBuyAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([testOrders.PRUNED_SIGNED_ORDERS_FEELESS[0]]);
expect(swapQuote.orders).to.deep.equal([testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0]]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
@@ -450,17 +660,25 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with slippage (feeless orders)', async () => {
const assetBuyAmount = baseUnitAmount(5);
const slippagePercentage = 0.5;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
assetBuyAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.PRUNED_SIGNED_ORDERS_FEELESS[0],
testOrders.PRUNED_SIGNED_ORDERS_FEELESS[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
@@ -487,15 +705,25 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => {
const assetBuyAmount = baseUnitAmount(3);
const slippagePercentage = 0;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
assetBuyAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET[0]]);
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
@@ -516,12 +744,20 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with slippage (takerAsset denominated fee orders)', async () => {
const assetBuyAmount = baseUnitAmount(5);
const slippagePercentage = 0.5;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
assetBuyAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
const fiveSixthEthInWei = new BigNumber(5)
.div(new BigNumber(6))
@@ -529,8 +765,8 @@ describe('swapQuoteCalculator', () => {
.integerValue(BigNumber.ROUND_CEIL);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET[0],
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[1],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
@@ -552,15 +788,25 @@ describe('swapQuoteCalculator', () => {
it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => {
const assetBuyAmount = baseUnitAmount(1);
const slippagePercentage = 0;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
assetBuyAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET[0]]);
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
@@ -580,37 +826,58 @@ describe('swapQuoteCalculator', () => {
});
it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => {
const assetBuyAmount = baseUnitAmount(2.5);
const slippagePercentage = 0.5;
const slippagePercentage = 0.48;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
);
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, {
exchangeAddress: contractAddresses.exchange,
chainId: TESTRPC_CHAIN_ID,
});
const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils);
const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
assetBuyAmount,
slippagePercentage,
GAS_PRICE,
mockProtocolFeeUtils.object,
CALCULATE_SWAP_QUOTE_OPTS,
);
const totalTakerAssetAmount = new BigNumber(5)
.div(new BigNumber(6))
.multipliedBy(ONE_ETH_IN_WEI)
.integerValue(BigNumber.ROUND_CEIL);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET[0],
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
// 50 takerAsset units to fill the first order + 100 takerAsset units for fees
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.75),
takerAssetAmount: baseUnitAmount(2.75),
totalTakerAssetAmount: baseUnitAmount(5.5),
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: totalTakerAssetAmount.div(2),
takerAssetAmount: totalTakerAssetAmount.div(2),
totalTakerAssetAmount,
const oneThirdEthInWei = new BigNumber(1)
.div(new BigNumber(3))
.multipliedBy(ONE_ETH_IN_WEI)
.integerValue(BigNumber.ROUND_CEIL);
const oneSixthEthInWei = new BigNumber(1)
.div(new BigNumber(6))
.multipliedBy(ONE_ETH_IN_WEI)
.integerValue(BigNumber.ROUND_CEIL);
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(4)
.plus(oneSixthEthInWei)
.multipliedBy(0.1)
.integerValue(BigNumber.ROUND_CEIL),
takerAssetAmount: baseUnitAmount(4)
.plus(oneSixthEthInWei)
.multipliedBy(0.1)
.integerValue(BigNumber.ROUND_CEIL),
totalTakerAssetAmount: baseUnitAmount(8)
.plus(oneThirdEthInWei)
.multipliedBy(0.1)
.integerValue(BigNumber.ROUND_CEIL),
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});

View File

@@ -1,15 +1,16 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, WETH9Contract } from '@0x/contract-wrappers';
import { WETH9Contract } from '@0x/contract-wrappers';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote, SwapQuoteConsumer } from '../src';
import { constants } from '../src/constants';
import { ExtensionContractType, MarketOperation, PrunedSignedOrder } from '../src/types';
import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
import { chaiSetup } from './utils/chai_setup';
@@ -24,7 +25,7 @@ const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = 1337;
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PARTIAL_PRUNED_SIGNED_ORDERS: Array<Partial<PrunedSignedOrder>> = [
const PARTIAL_PRUNED_SIGNED_ORDERS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
@@ -45,7 +46,7 @@ const PARTIAL_PRUNED_SIGNED_ORDERS: Array<Partial<PrunedSignedOrder>> = [
},
];
const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array<Partial<PrunedSignedOrder>> = [
const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
@@ -87,14 +88,13 @@ describe('swapQuoteConsumerUtils', () => {
contractAddresses = await migrateOnceAsync(provider);
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
wethContract = new WETH9Contract(contractAddresses.etherToken, provider);
[takerAddress, makerAddress] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [
await devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
await devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
await devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
const defaultOrderParams = {
@@ -119,7 +119,7 @@ describe('swapQuoteConsumerUtils', () => {
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
protocolFeeUtils = new ProtocolFeeUtils();
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams);
swapQuoteConsumer = new SwapQuoteConsumer(provider, {
@@ -128,6 +128,7 @@ describe('swapQuoteConsumerUtils', () => {
});
after(async () => {
await blockchainLifecycle.revertAsync();
await protocolFeeUtils.destroyAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
@@ -137,9 +138,9 @@ describe('swapQuoteConsumerUtils', () => {
});
describe('getConsumerTypeForSwapQuoteAsync', () => {
let forwarderOrders: PrunedSignedOrder[];
let exchangeOrders: PrunedSignedOrder[];
let largeForwarderOrders: PrunedSignedOrder[];
let forwarderOrders: SignedOrderWithFillableAmounts[];
let exchangeOrders: SignedOrderWithFillableAmounts[];
let largeForwarderOrders: SignedOrderWithFillableAmounts[];
let forwarderSwapQuote: SwapQuote;
let exchangeSwapQuote: SwapQuote;
let largeForwarderSwapQuote: SwapQuote;
@@ -152,7 +153,7 @@ describe('swapQuoteConsumerUtils', () => {
...order,
...partialOrder,
};
exchangeOrders.push(prunedOrder as PrunedSignedOrder);
exchangeOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
forwarderOrders = [];
@@ -162,7 +163,7 @@ describe('swapQuoteConsumerUtils', () => {
...order,
...partialOrder,
};
forwarderOrders.push(prunedOrder as PrunedSignedOrder);
forwarderOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
largeForwarderOrders = [];
@@ -172,7 +173,7 @@ describe('swapQuoteConsumerUtils', () => {
...order,
...partialOrder,
};
largeForwarderOrders.push(prunedOrder as PrunedSignedOrder);
largeForwarderOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
forwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(

View File

@@ -8,10 +8,10 @@ import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../src';
import { constants } from '../src/constants';
import { LiquidityForTakerMakerAssetDataPair, PrunedSignedOrder } from '../src/types';
import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import { mockAvailableAssetDatas, mockedSwapQuoterWithPrunedSignedOrders, orderbookMock } from './utils/mocks';
import { mockAvailableAssetDatas, mockedSwapQuoterWithFillableAmounts, orderbookMock } from './utils/mocks';
import { testOrderFactory } from './utils/test_order_factory';
import { baseUnitAmount } from './utils/utils';
@@ -60,15 +60,15 @@ const assetsToAssetPairItems = (makerAssetData: string, takerAssetData: string):
const expectLiquidityResult = async (
web3Provider: Web3ProviderEngine,
orderbook: Orderbook,
prunedOrders: PrunedSignedOrder[],
orders: SignedOrderWithFillableAmounts[],
expectedLiquidityResult: LiquidityForTakerMakerAssetDataPair,
) => {
const mockedSwapQuoter = mockedSwapQuoterWithPrunedSignedOrders(
const mockedSwapQuoter = mockedSwapQuoterWithFillableAmounts(
web3Provider,
orderbook,
FAKE_MAKER_ASSET_DATA,
WETH_ASSET_DATA,
prunedOrders,
orders,
);
const liquidityResult = await mockedSwapQuoter.object.getLiquidityForMakerTakerAssetDataPairAsync(
FAKE_MAKER_ASSET_DATA,
@@ -159,21 +159,16 @@ describe('SwapQuoter', () => {
});
it('should return 0s when no orders available', async () => {
const prunedOrders: PrunedSignedOrder[] = [];
const orders: SignedOrderWithFillableAmounts[] = [];
const expectedResult = {
makerAssetAvailableInBaseUnits: new BigNumber(0),
takerAssetAvailableInBaseUnits: new BigNumber(0),
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderbook.object,
prunedOrders,
expectedResult,
);
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return correct computed value when orders provided with full fillableAmounts', async () => {
const prunedOrders: PrunedSignedOrder[] = [
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
@@ -191,28 +186,19 @@ describe('SwapQuoter', () => {
},
},
];
const expectedMakerAssetAvailable = prunedOrders[0].makerAssetAmount.plus(
prunedOrders[1].makerAssetAmount,
);
const expectedTakerAssetAvailable = prunedOrders[0].takerAssetAmount.plus(
prunedOrders[1].takerAssetAmount,
);
const expectedMakerAssetAvailable = orders[0].makerAssetAmount.plus(orders[1].makerAssetAmount);
const expectedTakerAssetAvailable = orders[0].takerAssetAmount.plus(orders[1].takerAssetAmount);
const expectedResult = {
makerAssetAvailableInBaseUnits: expectedMakerAssetAvailable,
takerAssetAvailableInBaseUnits: expectedTakerAssetAvailable,
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderbook.object,
prunedOrders,
expectedResult,
);
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return correct computed value with one partial fillableAmounts', async () => {
const prunedOrders: PrunedSignedOrder[] = [
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
@@ -228,16 +214,11 @@ describe('SwapQuoter', () => {
takerAssetAvailableInBaseUnits: baseUnitAmount(0.5, WETH_DECIMALS),
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderbook.object,
prunedOrders,
expectedResult,
);
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return correct computed value with multiple orders and fillable amounts', async () => {
const prunedOrders: PrunedSignedOrder[] = [
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
@@ -261,16 +242,11 @@ describe('SwapQuoter', () => {
takerAssetAvailableInBaseUnits: baseUnitAmount(3.5, WETH_DECIMALS),
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderbook.object,
prunedOrders,
expectedResult,
);
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return 0s when no amounts fillable', async () => {
const prunedOrders: PrunedSignedOrder[] = [
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
@@ -294,12 +270,7 @@ describe('SwapQuoter', () => {
takerAssetAvailableInBaseUnits: constants.ZERO_AMOUNT,
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderbook.object,
prunedOrders,
expectedResult,
);
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
});
});

View File

@@ -0,0 +1,74 @@
import { ContractFunctionObj } from '@0x/base-contract';
import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { constants } from '@0x/contracts-test-utils';
import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
export type QueryAndSampleResult = [BigNumber[], BigNumber[][]];
export type QueryAndSampleHandler = (
orders: Order[],
signatures: string[],
sources: string[],
fillAmounts: BigNumber[],
) => QueryAndSampleResult;
const DUMMY_PROVIDER = {
sendAsync: (...args: any[]): any => {
/* no-op */
},
};
export class MockSamplerContract extends IERC20BridgeSamplerContract {
public readonly queryOrdersAndSampleSellsHandler?: QueryAndSampleHandler;
public readonly queryOrdersAndSampleBuysHandler?: QueryAndSampleHandler;
public constructor(
handlers?: Partial<{
queryOrdersAndSampleSells: QueryAndSampleHandler;
queryOrdersAndSampleBuys: QueryAndSampleHandler;
}>,
) {
super(constants.NULL_ADDRESS, DUMMY_PROVIDER);
const _handlers = {
queryOrdersAndSampleSells: undefined,
queryOrdersAndSampleBuys: undefined,
...handlers,
};
this.queryOrdersAndSampleSellsHandler = _handlers.queryOrdersAndSampleSells;
this.queryOrdersAndSampleBuysHandler = _handlers.queryOrdersAndSampleBuys;
}
public queryOrdersAndSampleSells(
orders: Order[],
signatures: string[],
sources: string[],
fillAmounts: BigNumber[],
): ContractFunctionObj<QueryAndSampleResult> {
return {
...super.queryOrdersAndSampleSells(orders, signatures, sources, fillAmounts),
callAsync: async (...args: any[]): Promise<QueryAndSampleResult> => {
if (!this.queryOrdersAndSampleSellsHandler) {
throw new Error('queryOrdersAndSampleSells handler undefined');
}
return this.queryOrdersAndSampleSellsHandler(orders, signatures, sources, fillAmounts);
},
};
}
public queryOrdersAndSampleBuys(
orders: Order[],
signatures: string[],
sources: string[],
fillAmounts: BigNumber[],
): ContractFunctionObj<QueryAndSampleResult> {
return {
...super.queryOrdersAndSampleBuys(orders, signatures, sources, fillAmounts),
callAsync: async (...args: any[]): Promise<QueryAndSampleResult> => {
if (!this.queryOrdersAndSampleBuysHandler) {
throw new Error('queryOrdersAndSampleBuys handler undefined');
}
return this.queryOrdersAndSampleBuysHandler(orders, signatures, sources, fillAmounts);
},
};
}
}

View File

@@ -6,7 +6,7 @@ import { BigNumber } from '@0x/utils';
import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../../src/swap_quoter';
import { PrunedSignedOrder } from '../../src/types';
import { SignedOrderWithFillableAmounts } from '../../src/types';
import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils';
const PROTOCOL_FEE_MULTIPLIER = 150000;
@@ -16,15 +16,15 @@ const PROTOCOL_FEE_MULTIPLIER = 150000;
class OrderbookClass extends Orderbook {
// tslint:disable-next-line:prefer-function-over-method
public async getOrdersAsync(_makerAssetData: string, _takerAssetData: string): Promise<APIOrder[]> {
return Promise.resolve([]);
return [];
}
// tslint:disable-next-line:prefer-function-over-method
public async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]> {
return Promise.resolve([]);
return [];
}
// tslint:disable-next-line:prefer-function-over-method
public async addOrdersAsync(_orders: SignedOrder[]): Promise<AcceptedRejectedOrders> {
return Promise.resolve({ accepted: [], rejected: [] });
return { accepted: [], rejected: [] };
}
}
export const orderbookMock = () => {
@@ -37,7 +37,7 @@ export const mockAvailableAssetDatas = (
) => {
mockOrderbook
.setup(async op => op.getAvailableAssetDatasAsync())
.returns(async () => Promise.resolve(availableAssetDatas))
.returns(async () => availableAssetDatas)
.verifiable(TypeMoq.Times.once());
mockOrderbook
.setup(o => (o as any)._orderProvider)
@@ -59,11 +59,11 @@ const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orde
class ProtocolFeeUtilsClass extends ProtocolFeeUtils {
// tslint:disable-next-line:prefer-function-over-method
public async getProtocolFeeMultiplierAsync(): Promise<BigNumber> {
return Promise.resolve(new BigNumber(PROTOCOL_FEE_MULTIPLIER));
return new BigNumber(PROTOCOL_FEE_MULTIPLIER);
}
// tslint:disable-next-line:prefer-function-over-method
public async getGasPriceEstimationOrThrowAsync(): Promise<BigNumber> {
return Promise.resolve(new BigNumber(devConstants.DEFAULT_GAS_PRICE));
public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise<BigNumber> {
return new BigNumber(devConstants.DEFAULT_GAS_PRICE);
}
}
@@ -73,26 +73,26 @@ export const protocolFeeUtilsMock = (): TypeMoq.IMock<ProtocolFeeUtils> => {
return mockProtocolFeeUtils;
};
const mockGetPrunedSignedOrdersAsync = (
const mockGetSignedOrdersWithFillableAmountsAsyncAsync = (
mockedSwapQuoter: TypeMoq.IMock<SwapQuoter>,
makerAssetData: string,
takerAssetData: string,
prunedOrders: PrunedSignedOrder[],
signedOrders: SignedOrderWithFillableAmounts[],
): void => {
mockedSwapQuoter
.setup(async a => a.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData))
.returns(async () => Promise.resolve(prunedOrders))
.setup(async a => a.getSignedOrdersWithFillableAmountsAsync(makerAssetData, takerAssetData))
.returns(async () => signedOrders)
.verifiable(TypeMoq.Times.once());
};
export const mockedSwapQuoterWithPrunedSignedOrders = (
export const mockedSwapQuoterWithFillableAmounts = (
provider: Web3ProviderEngine,
orderbook: Orderbook,
makerAssetData: string,
takerAssetData: string,
prunedOrders: PrunedSignedOrder[],
signedOrders: SignedOrderWithFillableAmounts[],
): TypeMoq.IMock<SwapQuoter> => {
const mockedAssetQuoter = partiallyMockedSwapQuoter(provider, orderbook);
mockGetPrunedSignedOrdersAsync(mockedAssetQuoter, makerAssetData, takerAssetData, prunedOrders);
mockGetSignedOrdersWithFillableAmountsAsyncAsync(mockedAssetQuoter, makerAssetData, takerAssetData, signedOrders);
return mockedAssetQuoter;
};

View File

@@ -1,29 +1,23 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../../src/constants';
import { MarketOperation, PrunedSignedOrder, SwapQuote } from '../../src/types';
import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types';
import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils';
export const getFullyFillableSwapQuoteWithNoFeesAsync = async (
/**
* Creates a swap quote given orders.
*/
export async function getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData: string,
takerAssetData: string,
orders: PrunedSignedOrder[],
orders: SignedOrderWithFillableAmounts[],
operation: MarketOperation,
gasPrice: BigNumber,
protocolFeeUtils: ProtocolFeeUtils,
): Promise<SwapQuote> => {
const makerAssetFillAmount = _.reduce(
orders,
(a: BigNumber, c: SignedOrder) => a.plus(c.makerAssetAmount),
constants.ZERO_AMOUNT,
);
const totalTakerAssetAmount = _.reduce(
orders,
(a: BigNumber, c: SignedOrder) => a.plus(c.takerAssetAmount),
constants.ZERO_AMOUNT,
);
): Promise<SwapQuote> {
const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]);
const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]);
const quoteInfo = {
makerAssetAmount: makerAssetFillAmount,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
@@ -54,4 +48,4 @@ export const getFullyFillableSwapQuoteWithNoFeesAsync = async (
takerAssetFillAmount: totalTakerAssetAmount,
};
}
};
}

View File

@@ -3,7 +3,7 @@ import { Order, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { constants } from '../../src/constants';
import { PrunedSignedOrder } from '../../src/types';
import { SignedOrderWithFillableAmounts } from '../../src/types';
const CHAIN_ID = 1337;
const BASE_TEST_ORDER: Order = orderFactory.createOrder(
@@ -21,7 +21,7 @@ const BASE_TEST_SIGNED_ORDER: SignedOrder = {
signature: constants.NULL_BYTES,
};
const BASE_TEST_PRUNED_SIGNED_ORDER: PrunedSignedOrder = {
const BASE_TEST_PRUNED_SIGNED_ORDER: SignedOrderWithFillableAmounts = {
...BASE_TEST_SIGNED_ORDER,
fillableMakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerAssetAmount: constants.ZERO_AMOUNT,
@@ -39,20 +39,28 @@ export const testOrderFactory = {
generateTestSignedOrders(partialOrders: Array<Partial<SignedOrder>>): SignedOrder[] {
return _.map(partialOrders, partialOrder => transformObject(BASE_TEST_SIGNED_ORDER, partialOrder));
},
generateTestPrunedSignedOrder(partialOrder: Partial<PrunedSignedOrder>): PrunedSignedOrder {
generateTestSignedOrderWithFillableAmounts(
partialOrder: Partial<SignedOrderWithFillableAmounts>,
): SignedOrderWithFillableAmounts {
return transformObject(BASE_TEST_PRUNED_SIGNED_ORDER, partialOrder);
},
generateIdenticalTestPrunedSignedOrders(
partialOrder: Partial<PrunedSignedOrder>,
generateIdenticalTestSignedOrdersWithFillableAmounts(
partialOrder: Partial<SignedOrderWithFillableAmounts>,
numOrders: number,
): PrunedSignedOrder[] {
): SignedOrderWithFillableAmounts[] {
const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_PRUNED_SIGNED_ORDER);
return _.map(baseTestOrders, (baseOrder): PrunedSignedOrder => transformObject(baseOrder, partialOrder));
return _.map(
baseTestOrders,
(baseOrder): SignedOrderWithFillableAmounts => transformObject(baseOrder, partialOrder),
);
},
generateTestPrunedSignedOrders(partialOrders: Array<Partial<PrunedSignedOrder>>): PrunedSignedOrder[] {
generateTestSignedOrdersWithFillableAmounts(
partialOrders: Array<Partial<SignedOrderWithFillableAmounts>>,
): SignedOrderWithFillableAmounts[] {
return _.map(
partialOrders,
(partialOrder): PrunedSignedOrder => transformObject(BASE_TEST_PRUNED_SIGNED_ORDER, partialOrder),
(partialOrder): SignedOrderWithFillableAmounts =>
transformObject(BASE_TEST_PRUNED_SIGNED_ORDER, partialOrder),
);
},
};

View File

@@ -1,33 +1,33 @@
import { PrunedSignedOrder } from '../../src/types';
import { SignedOrderWithFillableAmounts } from '../../src/types';
import { testOrderFactory } from './test_order_factory';
import { baseUnitAmount } from './utils';
// tslint:disable:custom-no-magic-numbers
const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b22222222222222222222222222222222222222222222222222222222222222222';
const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b11111111111111111111111111111111111111111111111111111111111111111';
const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b02222222222222222222222222222222222222222222222222222222222222222';
const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b01111111111111111111111111111111111111111111111111111111111111111';
const PARTIAL_ORDER: Partial<PrunedSignedOrder> = {
const PARTIAL_ORDER: Partial<SignedOrderWithFillableAmounts> = {
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
};
const PARTIAL_ORDER_FEE_IN_TAKER_ASSET: Partial<PrunedSignedOrder> = {
const PARTIAL_ORDER_FEE_IN_TAKER_ASSET: Partial<SignedOrderWithFillableAmounts> = {
...{
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
},
...PARTIAL_ORDER,
};
const PARTIAL_ORDER_FEE_IN_MAKER_ASSET: Partial<PrunedSignedOrder> = {
const PARTIAL_ORDER_FEE_IN_MAKER_ASSET: Partial<SignedOrderWithFillableAmounts> = {
...{
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
},
...PARTIAL_ORDER,
};
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<PrunedSignedOrder>> = [
const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
...{
takerAssetAmount: baseUnitAmount(1),
@@ -57,7 +57,7 @@ const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<PrunedSignedOrder>> =
},
];
const PARTIAL_PRUNED_SIGNED_FEE_IN_TAKER_ASSET: Array<Partial<PrunedSignedOrder>> = [
const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
...{
takerAssetAmount: baseUnitAmount(1),
@@ -93,7 +93,7 @@ const PARTIAL_PRUNED_SIGNED_FEE_IN_TAKER_ASSET: Array<Partial<PrunedSignedOrder>
},
];
const PARTIAL_PRUNED_SIGNED_FEE_IN_MAKER_ASSET: Array<Partial<PrunedSignedOrder>> = [
const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
...{
takerAssetAmount: baseUnitAmount(5),
@@ -129,18 +129,18 @@ const PARTIAL_PRUNED_SIGNED_FEE_IN_MAKER_ASSET: Array<Partial<PrunedSignedOrder>
},
];
const PRUNED_SIGNED_ORDERS_FEELESS = testOrderFactory.generateTestPrunedSignedOrders(
PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS,
const SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS = testOrderFactory.generateTestSignedOrdersWithFillableAmounts(
PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
);
const PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET = testOrderFactory.generateTestPrunedSignedOrders(
PARTIAL_PRUNED_SIGNED_FEE_IN_TAKER_ASSET,
const SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET = testOrderFactory.generateTestSignedOrdersWithFillableAmounts(
PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
);
const PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET = testOrderFactory.generateTestPrunedSignedOrders(
PARTIAL_PRUNED_SIGNED_FEE_IN_MAKER_ASSET,
const SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET = testOrderFactory.generateTestSignedOrdersWithFillableAmounts(
PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
);
export const testOrders = {
PRUNED_SIGNED_ORDERS_FEELESS,
PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
};