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:
@@ -5,6 +5,10 @@
|
||||
{
|
||||
"note": "Fix gasPrice from `ethgasstation` to be in WEI instead of GWEI",
|
||||
"pr": 2393
|
||||
},
|
||||
{
|
||||
"note": "Add aggregator utils",
|
||||
"pr": 2353
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@@ -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",
|
||||
|
@@ -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,
|
||||
};
|
||||
|
2
packages/asset-swapper/src/globals.d.ts
vendored
2
packages/asset-swapper/src/globals.d.ts
vendored
@@ -4,3 +4,5 @@ declare module '*.json' {
|
||||
export default json;
|
||||
/* tslint:enable */
|
||||
}
|
||||
|
||||
declare module 'heartbeats';
|
||||
|
@@ -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';
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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).
|
||||
|
@@ -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)
|
||||
|
23
packages/asset-swapper/src/utils/fillable_amounts_utils.ts
Normal file
23
packages/asset-swapper/src/utils/fillable_amounts_utils.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
@@ -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',
|
||||
};
|
@@ -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,
|
||||
};
|
||||
}
|
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
@@ -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,
|
||||
);
|
||||
}
|
407
packages/asset-swapper/src/utils/market_operation_utils/index.ts
Normal file
407
packages/asset-swapper/src/utils/market_operation_utils/index.ts
Normal 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;
|
||||
}
|
@@ -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];
|
||||
}
|
||||
}
|
117
packages/asset-swapper/src/utils/market_operation_utils/types.ts
Normal file
117
packages/asset-swapper/src/utils/market_operation_utils/types.ts
Normal 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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
49
packages/asset-swapper/src/utils/order_state_utils.ts
Normal file
49
packages/asset-swapper/src/utils/order_state_utils.ts
Normal 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),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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)) {
|
||||
|
@@ -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);
|
||||
|
@@ -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];
|
||||
},
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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(
|
||||
|
71
packages/asset-swapper/test/fillable_amounts_utils_test.ts
Normal file
71
packages/asset-swapper/test/fillable_amounts_utils_test.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
@@ -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(
|
||||
|
744
packages/asset-swapper/test/market_operation_utils_test.ts
Normal file
744
packages/asset-swapper/test/market_operation_utils_test.ts
Normal 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
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
275
packages/asset-swapper/test/order_state_utils_test.ts
Normal file
275
packages/asset-swapper/test/order_state_utils_test.ts
Normal 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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),
|
||||
});
|
||||
|
@@ -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(
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
74
packages/asset-swapper/test/utils/mock_sampler_contract.ts
Normal file
74
packages/asset-swapper/test/utils/mock_sampler_contract.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@@ -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;
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -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),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user