Merge pull request #2513 from 0xProject/feat/asset-swapper/death-to-reverts
Asset-swapper: Fallback orders + refactors
This commit is contained in:
commit
baf6372179
@ -5,6 +5,26 @@
|
||||
{
|
||||
"note": "Add support for private liquidity providers",
|
||||
"pr": 2505
|
||||
},
|
||||
{
|
||||
"note": "Big refactor of market operation utils",
|
||||
"pr": 2513
|
||||
},
|
||||
{
|
||||
"note": "Remove `dustFractionThreshold`, `noConflicts` options.",
|
||||
"pr": 2513
|
||||
},
|
||||
{
|
||||
"note": "Revamp fill optimization algorithm",
|
||||
"pr": 2513
|
||||
},
|
||||
{
|
||||
"note": "Add fallback orders to quotes via `allowFallback` option.",
|
||||
"pr": 2513
|
||||
},
|
||||
{
|
||||
"note": "Add `maxFallbackSlippage` option.",
|
||||
"pr": 2513
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -11,8 +11,7 @@ import {
|
||||
SwapQuoterOpts,
|
||||
} from './types';
|
||||
|
||||
import { constants as marketOperationUtilConstants } from './utils/market_operation_utils/constants';
|
||||
import { ERC20BridgeSource } from './utils/market_operation_utils/types';
|
||||
import { DEFAULT_GET_MARKET_ORDERS_OPTS } from './utils/market_operation_utils/constants';
|
||||
|
||||
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
|
||||
const NULL_BYTES = '0x';
|
||||
@ -43,7 +42,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
|
||||
orderRefreshIntervalMs: 10000, // 10 seconds
|
||||
},
|
||||
...DEFAULT_ORDER_PRUNER_OPTS,
|
||||
samplerGasLimit: 59e6,
|
||||
samplerGasLimit: 250e6,
|
||||
};
|
||||
|
||||
const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = {
|
||||
@ -59,48 +58,7 @@ 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,
|
||||
},
|
||||
...marketOperationUtilConstants.DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
};
|
||||
|
||||
// Mainnet Curve configuration
|
||||
const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = {
|
||||
[ERC20BridgeSource.CurveUsdcDai]: {
|
||||
version: 1,
|
||||
curveAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56',
|
||||
tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
|
||||
},
|
||||
[ERC20BridgeSource.CurveUsdcDaiUsdt]: {
|
||||
version: 1,
|
||||
curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c',
|
||||
tokens: [
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
],
|
||||
},
|
||||
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: {
|
||||
version: 1,
|
||||
curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51',
|
||||
tokens: [
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
'0x0000000000085d4780b73119b644ae5ecd22b376',
|
||||
],
|
||||
},
|
||||
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: {
|
||||
version: 1,
|
||||
curveAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27',
|
||||
tokens: [
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
'0x4fabb145d64652a948d72533023f6e7a623c7c53',
|
||||
],
|
||||
},
|
||||
...DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
};
|
||||
|
||||
export const constants = {
|
||||
@ -123,5 +81,4 @@ export const constants = {
|
||||
PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
|
||||
MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE,
|
||||
BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3',
|
||||
DEFAULT_CURVE_OPTS,
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
import { assert } from './utils/assert';
|
||||
import { calculateLiquidity } from './utils/calculate_liquidity';
|
||||
import { MarketOperationUtils } from './utils/market_operation_utils';
|
||||
import { dummyOrderUtils } from './utils/market_operation_utils/dummy_order_utils';
|
||||
import { createDummyOrderForSampler } from './utils/market_operation_utils/orders';
|
||||
import { DexOrderSampler } from './utils/market_operation_utils/sampler';
|
||||
import { orderPrunerUtils } from './utils/order_prune_utils';
|
||||
import { OrderStateUtils } from './utils/order_state_utils';
|
||||
@ -242,11 +242,7 @@ export class SwapQuoter {
|
||||
): Promise<Array<MarketBuySwapQuote | undefined>> {
|
||||
makerAssetBuyAmount.map((a, i) => assert.isBigNumber(`makerAssetBuyAmount[${i}]`, a));
|
||||
let gasPrice: BigNumber;
|
||||
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge(
|
||||
{},
|
||||
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
|
||||
options,
|
||||
);
|
||||
const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
|
||||
if (!!options.gasPrice) {
|
||||
gasPrice = options.gasPrice;
|
||||
assert.isBigNumber('gasPrice', gasPrice);
|
||||
@ -264,7 +260,7 @@ export class SwapQuoter {
|
||||
);
|
||||
if (prunedOrders.length === 0) {
|
||||
return [
|
||||
dummyOrderUtils.createDummyOrderForSampler(
|
||||
createDummyOrderForSampler(
|
||||
makerAssetDatas[i],
|
||||
takerAssetData,
|
||||
this._contractAddresses.uniswapBridge,
|
||||
@ -278,7 +274,6 @@ export class SwapQuoter {
|
||||
const swapQuotes = await this._swapQuoteCalculator.calculateBatchMarketBuySwapQuoteAsync(
|
||||
allPrunedOrders,
|
||||
makerAssetBuyAmount,
|
||||
slippagePercentage,
|
||||
gasPrice,
|
||||
calculateSwapQuoteOpts,
|
||||
);
|
||||
@ -517,14 +512,9 @@ export class SwapQuoter {
|
||||
marketOperation: MarketOperation,
|
||||
options: Partial<SwapQuoteRequestOpts>,
|
||||
): Promise<SwapQuote> {
|
||||
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge(
|
||||
{},
|
||||
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
|
||||
options,
|
||||
);
|
||||
const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
|
||||
assert.isString('makerAssetData', makerAssetData);
|
||||
assert.isString('takerAssetData', takerAssetData);
|
||||
assert.isNumber('slippagePercentage', slippagePercentage);
|
||||
let gasPrice: BigNumber;
|
||||
if (!!options.gasPrice) {
|
||||
gasPrice = options.gasPrice;
|
||||
@ -537,11 +527,7 @@ export class SwapQuoter {
|
||||
// if no native orders, pass in a dummy order for the sampler to have required metadata for sampling
|
||||
if (prunedOrders.length === 0) {
|
||||
prunedOrders = [
|
||||
dummyOrderUtils.createDummyOrderForSampler(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
this._contractAddresses.uniswapBridge,
|
||||
),
|
||||
createDummyOrderForSampler(makerAssetData, takerAssetData, this._contractAddresses.uniswapBridge),
|
||||
];
|
||||
}
|
||||
|
||||
@ -551,7 +537,6 @@ export class SwapQuoter {
|
||||
swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
|
||||
prunedOrders,
|
||||
assetFillAmount,
|
||||
slippagePercentage,
|
||||
gasPrice,
|
||||
calculateSwapQuoteOpts,
|
||||
);
|
||||
@ -559,7 +544,6 @@ export class SwapQuoter {
|
||||
swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
|
||||
prunedOrders,
|
||||
assetFillAmount,
|
||||
slippagePercentage,
|
||||
gasPrice,
|
||||
calculateSwapQuoteOpts,
|
||||
);
|
||||
|
@ -169,6 +169,7 @@ export interface MarketBuySwapQuote extends SwapQuoteBase {
|
||||
* totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees).
|
||||
* makerAssetAmount: The amount of makerAsset that will be acquired through the swap.
|
||||
* protocolFeeInWeiAmount: The amount of ETH to pay (in WEI) as protocol fee to perform the swap for desired asset.
|
||||
* gas: Amount of estimated gas needed to fill the quote.
|
||||
*/
|
||||
export interface SwapQuoteInfo {
|
||||
feeTakerAssetAmount: BigNumber;
|
||||
@ -176,6 +177,7 @@ export interface SwapQuoteInfo {
|
||||
totalTakerAssetAmount: BigNumber;
|
||||
makerAssetAmount: BigNumber;
|
||||
protocolFeeInWeiAmount: BigNumber;
|
||||
gas: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,11 +188,9 @@ export interface SwapQuoteOrdersBreakdown {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 extends CalculateSwapQuoteOpts {
|
||||
slippagePercentage: number;
|
||||
gasPrice?: BigNumber;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,12 @@ import * as _ from 'lodash';
|
||||
|
||||
import { MarketOperation, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
|
||||
|
||||
import { utils } from './utils';
|
||||
import {
|
||||
isAssetDataEquivalent,
|
||||
isExactAssetData,
|
||||
isOrderTakerFeePayableWithMakerAsset,
|
||||
isOrderTakerFeePayableWithTakerAsset,
|
||||
} from './utils';
|
||||
|
||||
export const assert = {
|
||||
...sharedAssert,
|
||||
@ -36,13 +41,13 @@ export const assert = {
|
||||
): void {
|
||||
_.every(orders, (order: SignedOrder, index: number) => {
|
||||
assert.assert(
|
||||
utils.isAssetDataEquivalent(takerAssetData, order.takerAssetData),
|
||||
isAssetDataEquivalent(takerAssetData, order.takerAssetData),
|
||||
`Expected ${variableName}[${index}].takerAssetData to be ${takerAssetData} but found ${
|
||||
order.takerAssetData
|
||||
}`,
|
||||
);
|
||||
assert.assert(
|
||||
utils.isAssetDataEquivalent(makerAssetData, order.makerAssetData),
|
||||
isAssetDataEquivalent(makerAssetData, order.makerAssetData),
|
||||
`Expected ${variableName}[${index}].makerAssetData to be ${makerAssetData} but found ${
|
||||
order.makerAssetData
|
||||
}`,
|
||||
@ -53,8 +58,8 @@ export const assert = {
|
||||
_.every(orders, (order: T, index: number) => {
|
||||
assert.assert(
|
||||
order.takerFee.isZero() ||
|
||||
utils.isOrderTakerFeePayableWithTakerAsset(order) ||
|
||||
utils.isOrderTakerFeePayableWithMakerAsset(order),
|
||||
isOrderTakerFeePayableWithTakerAsset(order) ||
|
||||
isOrderTakerFeePayableWithMakerAsset(order),
|
||||
`Expected ${variableName}[${index}].takerFeeAssetData to be ${order.makerAssetData} or ${
|
||||
order.takerAssetData
|
||||
} but found ${order.takerFeeAssetData}`,
|
||||
@ -72,11 +77,12 @@ export const assert = {
|
||||
},
|
||||
isValidForwarderSignedOrder(variableName: string, order: SignedOrder, wethAssetData: string): void {
|
||||
assert.assert(
|
||||
utils.isExactAssetData(order.takerAssetData, wethAssetData),
|
||||
isExactAssetData(order.takerAssetData, wethAssetData),
|
||||
`Expected ${variableName} to have takerAssetData set as ${wethAssetData}, but is ${order.takerAssetData}`,
|
||||
);
|
||||
},
|
||||
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
|
||||
sharedAssert.isNumber(`${variableName}.gas`, swapQuoteInfo.gas);
|
||||
sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount);
|
||||
sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount);
|
||||
sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount);
|
||||
|
@ -2,17 +2,17 @@ import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../types';
|
||||
|
||||
import { utils } from './utils';
|
||||
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
|
||||
|
||||
export const calculateLiquidity = (
|
||||
prunedOrders: SignedOrderWithFillableAmounts[],
|
||||
): LiquidityForTakerMakerAssetDataPair => {
|
||||
const liquidityInBigNumbers = prunedOrders.reduce(
|
||||
(acc, order) => {
|
||||
const fillableMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)
|
||||
const fillableMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order)
|
||||
? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount)
|
||||
: order.fillableMakerAssetAmount;
|
||||
const fillableTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order)
|
||||
const fillableTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order)
|
||||
? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount)
|
||||
: order.fillableTakerAssetAmount;
|
||||
return {
|
||||
|
@ -3,18 +3,18 @@ import * as _ from 'lodash';
|
||||
|
||||
import { SignedOrderWithFillableAmounts } from '../types';
|
||||
|
||||
import { utils } from './utils';
|
||||
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
|
||||
|
||||
export const fillableAmountsUtils = {
|
||||
getTakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
|
||||
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
|
||||
if (isOrderTakerFeePayableWithTakerAsset(order)) {
|
||||
return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount);
|
||||
} else {
|
||||
return order.fillableTakerAssetAmount;
|
||||
}
|
||||
},
|
||||
getMakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
|
||||
if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
|
||||
if (isOrderTakerFeePayableWithMakerAsset(order)) {
|
||||
return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount);
|
||||
} else {
|
||||
return order.fillableMakerAssetAmount;
|
||||
|
@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { ERC20BridgeSource, GetMarketOrdersOpts } from './types';
|
||||
|
||||
const INFINITE_TIMESTAMP_SEC = new BigNumber(2524604400);
|
||||
// tslint:disable: custom-no-magic-numbers
|
||||
|
||||
/**
|
||||
* Valid sources for market sell.
|
||||
@ -27,12 +27,13 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
|
||||
// tslint:disable-next-line: custom-no-magic-numbers
|
||||
runLimit: 2 ** 15,
|
||||
excludedSources: [],
|
||||
bridgeSlippage: 0.0005,
|
||||
dustFractionThreshold: 0.0025,
|
||||
bridgeSlippage: 0.005,
|
||||
maxFallbackSlippage: 0.05,
|
||||
numSamples: 13,
|
||||
noConflicts: true,
|
||||
sampleDistributionBase: 1.05,
|
||||
fees: {},
|
||||
feeSchedule: {},
|
||||
gasSchedule: {},
|
||||
allowFallback: true,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -40,13 +41,53 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
|
||||
*/
|
||||
export const FEE_QUOTE_SOURCES = SELL_SOURCES;
|
||||
|
||||
export const constants = {
|
||||
INFINITE_TIMESTAMP_SEC,
|
||||
SELL_SOURCES,
|
||||
BUY_SOURCES,
|
||||
DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
ERC20_PROXY_ID: '0xf47261b0',
|
||||
FEE_QUOTE_SOURCES,
|
||||
WALLET_SIGNATURE: '0x04',
|
||||
ONE_ETHER: new BigNumber(1e18),
|
||||
/**
|
||||
* Mainnet Curve configuration
|
||||
*/
|
||||
export const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = {
|
||||
[ERC20BridgeSource.CurveUsdcDai]: {
|
||||
version: 1,
|
||||
curveAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56',
|
||||
tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
|
||||
},
|
||||
[ERC20BridgeSource.CurveUsdcDaiUsdt]: {
|
||||
version: 1,
|
||||
curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c',
|
||||
tokens: [
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
],
|
||||
},
|
||||
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: {
|
||||
version: 1,
|
||||
curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51',
|
||||
tokens: [
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
'0x0000000000085d4780b73119b644ae5ecd22b376',
|
||||
],
|
||||
},
|
||||
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: {
|
||||
version: 1,
|
||||
curveAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27',
|
||||
tokens: [
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
'0x4fabb145d64652a948d72533023f6e7a623c7c53',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ERC20_PROXY_ID = '0xf47261b0';
|
||||
export const WALLET_SIGNATURE = '0x04';
|
||||
export const ONE_ETHER = new BigNumber(1e18);
|
||||
export const NEGATIVE_INF = new BigNumber('-Infinity');
|
||||
export const POSITIVE_INF = new BigNumber('Infinity');
|
||||
export const ZERO_AMOUNT = new BigNumber(0);
|
||||
export const ONE_HOUR_IN_SECONDS = 60 * 60;
|
||||
export const ONE_SECOND_MS = 1000;
|
||||
export const NULL_BYTES = '0x';
|
||||
export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
|
@ -1,234 +0,0 @@
|
||||
import { assert } from '@0x/assert';
|
||||
import { ContractAddresses } from '@0x/contract-addresses';
|
||||
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { AbiEncoder, BigNumber } from '@0x/utils';
|
||||
|
||||
import { constants } from '../../constants';
|
||||
|
||||
import { constants as marketOperationUtilConstants } from './constants';
|
||||
import {
|
||||
AggregationError,
|
||||
CollapsedFill,
|
||||
ERC20BridgeSource,
|
||||
NativeCollapsedFill,
|
||||
OptimizedMarketOrder,
|
||||
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;
|
||||
|
||||
// utility function for asset-swapper to ignore market operation utils for specific asset types
|
||||
public static convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOrder): OptimizedMarketOrder {
|
||||
return {
|
||||
...order,
|
||||
fillableMakerAssetAmount: order.makerAssetAmount,
|
||||
fillableTakerAssetAmount: order.takerAssetAmount,
|
||||
fillableTakerFeeAmount: order.takerFee,
|
||||
fill: {
|
||||
source: ERC20BridgeSource.Native,
|
||||
totalMakerAssetAmount: order.makerAssetAmount,
|
||||
totalTakerAssetAmount: order.takerAssetAmount,
|
||||
subFills: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor(contractAddress: ContractAddresses) {
|
||||
this._contractAddress = contractAddress;
|
||||
}
|
||||
|
||||
// Convert sell fills into orders.
|
||||
public createSellOrdersFromPath(
|
||||
orderDomain: OrderDomain,
|
||||
inputToken: string,
|
||||
outputToken: string,
|
||||
path: CollapsedFill[],
|
||||
bridgeSlippage: number,
|
||||
liquidityProviderAddress?: string,
|
||||
): OptimizedMarketOrder[] {
|
||||
const orders: OptimizedMarketOrder[] = [];
|
||||
for (const fill of path) {
|
||||
if (fill.source === ERC20BridgeSource.Native) {
|
||||
orders.push(createNativeOrder(fill));
|
||||
} else {
|
||||
orders.push(
|
||||
createBridgeOrder(
|
||||
orderDomain,
|
||||
fill,
|
||||
this._getBridgeAddressFromSource(fill.source, liquidityProviderAddress),
|
||||
outputToken,
|
||||
inputToken,
|
||||
bridgeSlippage,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
// Convert buy fills into orders.
|
||||
public createBuyOrdersFromPath(
|
||||
orderDomain: OrderDomain,
|
||||
inputToken: string,
|
||||
outputToken: string,
|
||||
path: CollapsedFill[],
|
||||
bridgeSlippage: number,
|
||||
liquidityProviderAddress?: string,
|
||||
): OptimizedMarketOrder[] {
|
||||
const orders: OptimizedMarketOrder[] = [];
|
||||
for (const fill of path) {
|
||||
if (fill.source === ERC20BridgeSource.Native) {
|
||||
orders.push(createNativeOrder(fill));
|
||||
} else {
|
||||
orders.push(
|
||||
createBridgeOrder(
|
||||
orderDomain,
|
||||
fill,
|
||||
this._getBridgeAddressFromSource(fill.source, liquidityProviderAddress),
|
||||
inputToken,
|
||||
outputToken,
|
||||
bridgeSlippage,
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
private _getBridgeAddressFromSource(source: ERC20BridgeSource, liquidityProviderAddress?: string): string {
|
||||
switch (source) {
|
||||
case ERC20BridgeSource.Eth2Dai:
|
||||
return this._contractAddress.eth2DaiBridge;
|
||||
case ERC20BridgeSource.Kyber:
|
||||
return this._contractAddress.kyberBridge;
|
||||
case ERC20BridgeSource.Uniswap:
|
||||
return this._contractAddress.uniswapBridge;
|
||||
case ERC20BridgeSource.CurveUsdcDai:
|
||||
case ERC20BridgeSource.CurveUsdcDaiUsdt:
|
||||
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
|
||||
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
|
||||
return this._contractAddress.curveBridge;
|
||||
case ERC20BridgeSource.LiquidityProvider:
|
||||
if (liquidityProviderAddress === undefined) {
|
||||
throw new Error(
|
||||
'Cannot create a LiquidityProvider order without a LiquidityProvider pool address.',
|
||||
);
|
||||
}
|
||||
assert.isETHAddressHex('liquidityProviderAddress', liquidityProviderAddress);
|
||||
return liquidityProviderAddress;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
throw new Error(AggregationError.NoBridgeForSource);
|
||||
}
|
||||
}
|
||||
|
||||
function createBridgeOrder(
|
||||
orderDomain: OrderDomain,
|
||||
fill: CollapsedFill,
|
||||
bridgeAddress: string,
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
slippage: number,
|
||||
isBuy: boolean = false,
|
||||
): OptimizedMarketOrder {
|
||||
let makerAssetData;
|
||||
if (Object.keys(constants.DEFAULT_CURVE_OPTS).includes(fill.source)) {
|
||||
const { curveAddress, tokens, version } = constants.DEFAULT_CURVE_OPTS[fill.source];
|
||||
const fromTokenIdx = tokens.indexOf(takerToken);
|
||||
const toTokenIdx = tokens.indexOf(makerToken);
|
||||
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
|
||||
makerToken,
|
||||
bridgeAddress,
|
||||
createCurveBridgeData(curveAddress, fromTokenIdx, toTokenIdx, version),
|
||||
);
|
||||
} else {
|
||||
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
|
||||
makerToken,
|
||||
bridgeAddress,
|
||||
createBridgeData(takerToken),
|
||||
);
|
||||
}
|
||||
return {
|
||||
makerAddress: bridgeAddress,
|
||||
makerAssetData,
|
||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
|
||||
...createCommonOrderFields(orderDomain, fill, slippage, isBuy),
|
||||
};
|
||||
}
|
||||
|
||||
function createBridgeData(tokenAddress: string): string {
|
||||
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
|
||||
return encoder.encode({ tokenAddress });
|
||||
}
|
||||
|
||||
function createCurveBridgeData(
|
||||
curveAddress: string,
|
||||
fromTokenIdx: number,
|
||||
toTokenIdx: number,
|
||||
version: number,
|
||||
): string {
|
||||
const curveBridgeDataEncoder = AbiEncoder.create([
|
||||
{ name: 'curveAddress', type: 'address' },
|
||||
{ name: 'fromTokenIdx', type: 'int128' },
|
||||
{ name: 'toTokenIdx', type: 'int128' },
|
||||
{ name: 'version', type: 'int128' },
|
||||
]);
|
||||
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]);
|
||||
}
|
||||
|
||||
type CommonOrderFields = Pick<
|
||||
OptimizedMarketOrder,
|
||||
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
|
||||
>;
|
||||
|
||||
function createCommonOrderFields(
|
||||
orderDomain: OrderDomain,
|
||||
fill: CollapsedFill,
|
||||
slippage: number,
|
||||
isBuy: boolean = false,
|
||||
): CommonOrderFields {
|
||||
const makerAssetAmountAdjustedWithSlippage = isBuy
|
||||
? fill.totalMakerAssetAmount
|
||||
: fill.totalMakerAssetAmount.times(1 - slippage).integerValue(BigNumber.ROUND_DOWN);
|
||||
const takerAssetAmountAdjustedWithSlippage = isBuy
|
||||
? fill.totalTakerAssetAmount.times(slippage + 1).integerValue(BigNumber.ROUND_UP)
|
||||
: fill.totalTakerAssetAmount;
|
||||
return {
|
||||
fill,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
|
||||
return {
|
||||
fill: {
|
||||
source: fill.source,
|
||||
totalMakerAssetAmount: fill.totalMakerAssetAmount,
|
||||
totalTakerAssetAmount: fill.totalTakerAssetAmount,
|
||||
subFills: fill.subFills,
|
||||
},
|
||||
...(fill as NativeCollapsedFill).nativeOrder,
|
||||
};
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
@ -1,181 +0,0 @@
|
||||
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;
|
||||
currentPathAdjustedOutput: 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 _optimalPathAdjustedOutput: 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._optimalPathAdjustedOutput = upperBoundPath ? getPathAdjustedOutput(upperBoundPath, input) : ZERO_AMOUNT;
|
||||
const ctx = {
|
||||
currentPath: [],
|
||||
currentPathInput: ZERO_AMOUNT,
|
||||
currentPathAdjustedOutput: ZERO_AMOUNT,
|
||||
currentPathFlags: 0,
|
||||
};
|
||||
// Visit all valid combinations of fills to find the optimal path.
|
||||
this._walk(fills, input, ctx);
|
||||
if (this._optimalPath) {
|
||||
return sortFillsByAdjustedRate(this._optimalPath, this._shouldMinimize);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _walk(fills: Fill[], input: BigNumber, ctx: FillsOptimizerContext): void {
|
||||
const { currentPath, currentPathInput, currentPathAdjustedOutput, currentPathFlags } = ctx;
|
||||
|
||||
// Stop if the current path is already complete.
|
||||
if (currentPathInput.gte(input)) {
|
||||
this._updateOptimalPath(currentPath, currentPathAdjustedOutput);
|
||||
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 nextPathAdjustedOutput = currentPathAdjustedOutput.plus(
|
||||
getPartialFillOutput(nextFill, nextPathInput.minus(currentPathInput)).minus(nextFill.fillPenalty),
|
||||
);
|
||||
// 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,
|
||||
currentPathAdjustedOutput: nextPathAdjustedOutput,
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
currentPathFlags: nextPathFlags,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _updateOptimalPath(path: Fill[], adjustedOutput: BigNumber): void {
|
||||
if (!this._optimalPath || this._compareOutputs(adjustedOutput, this._optimalPathAdjustedOutput) === 1) {
|
||||
this._optimalPath = path;
|
||||
this._optimalPathAdjustedOutput = adjustedOutput;
|
||||
}
|
||||
}
|
||||
|
||||
private _compareOutputs(a: BigNumber, b: BigNumber): number {
|
||||
return comparePathOutputs(a, b, this._shouldMinimize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the total output minus penalty for a fill path, optionally clipping the input
|
||||
* to `maxInput`.
|
||||
*/
|
||||
export function getPathAdjustedOutput(path: Fill[], maxInput?: BigNumber): BigNumber {
|
||||
let currentInput = ZERO_AMOUNT;
|
||||
let currentOutput = ZERO_AMOUNT;
|
||||
let currentPenalty = ZERO_AMOUNT;
|
||||
for (const fill of path) {
|
||||
currentPenalty = currentPenalty.plus(fill.fillPenalty);
|
||||
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.minus(currentPenalty);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a path by adjusted input -> output rate while keeping sub-fills contiguous.
|
||||
*/
|
||||
export function sortFillsByAdjustedRate(path: Fill[], shouldMinimize: boolean = false): Fill[] {
|
||||
return path.slice(0).sort((a, b) => {
|
||||
const rootA = getFillRoot(a);
|
||||
const rootB = getFillRoot(b);
|
||||
const adjustedRateA = rootA.output.minus(rootA.fillPenalty).div(rootA.input);
|
||||
const adjustedRateB = rootB.output.minus(rootB.fillPenalty).div(rootB.input);
|
||||
if ((!a.parent && !b.parent) || a.fillData.source !== b.fillData.source) {
|
||||
return shouldMinimize ? adjustedRateA.comparedTo(adjustedRateB) : adjustedRateB.comparedTo(adjustedRateA);
|
||||
}
|
||||
if (isFillAncestorOf(a, b)) {
|
||||
return -1;
|
||||
}
|
||||
if (isFillAncestorOf(b, a)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getFillRoot(fill: Fill): Fill {
|
||||
let root = fill;
|
||||
while (root.parent) {
|
||||
root = root.parent;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function isFillAncestorOf(ancestor: Fill, fill: Fill): boolean {
|
||||
let currFill = fill.parent;
|
||||
while (currFill) {
|
||||
if (currFill === ancestor) {
|
||||
return true;
|
||||
}
|
||||
currFill = currFill.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
304
packages/asset-swapper/src/utils/market_operation_utils/fills.ts
Normal file
304
packages/asset-swapper/src/utils/market_operation_utils/fills.ts
Normal file
@ -0,0 +1,304 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
|
||||
|
||||
import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
|
||||
import {
|
||||
CollapsedFill,
|
||||
DexSample,
|
||||
ERC20BridgeSource,
|
||||
Fill,
|
||||
FillFlags,
|
||||
NativeCollapsedFill,
|
||||
NativeFillData,
|
||||
} from './types';
|
||||
|
||||
// tslint:disable: prefer-for-of no-bitwise completed-docs
|
||||
|
||||
/**
|
||||
* Create fill paths from orders and dex quotes.
|
||||
*/
|
||||
export function createFillPaths(opts: {
|
||||
side: MarketOperation;
|
||||
orders?: SignedOrderWithFillableAmounts[];
|
||||
dexQuotes?: DexSample[][];
|
||||
targetInput?: BigNumber;
|
||||
ethToOutputRate?: BigNumber;
|
||||
excludedSources?: ERC20BridgeSource[];
|
||||
feeSchedule?: { [source: string]: BigNumber };
|
||||
}): Fill[][] {
|
||||
const { side } = opts;
|
||||
const excludedSources = opts.excludedSources || [];
|
||||
const feeSchedule = opts.feeSchedule || {};
|
||||
const orders = opts.orders || [];
|
||||
const dexQuotes = opts.dexQuotes || [];
|
||||
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT;
|
||||
// Create native fill paths.
|
||||
const nativePath = nativeOrdersToPath(side, orders, ethToOutputRate, feeSchedule);
|
||||
// Create DEX fill paths.
|
||||
const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule);
|
||||
return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources);
|
||||
}
|
||||
|
||||
function filterPaths(paths: Fill[][], excludedSources: ERC20BridgeSource[]): Fill[][] {
|
||||
return paths.filter(path => {
|
||||
if (path.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const [input, output] = getPathSize(path);
|
||||
if (input.eq(0) || output.eq(0)) {
|
||||
return false;
|
||||
}
|
||||
if (excludedSources.includes(path[0].source)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function nativeOrdersToPath(
|
||||
side: MarketOperation,
|
||||
orders: SignedOrderWithFillableAmounts[],
|
||||
ethToOutputRate: BigNumber,
|
||||
fees: { [source: string]: BigNumber },
|
||||
): Fill[] {
|
||||
// Create a single path from all orders.
|
||||
let path: Fill[] = [];
|
||||
for (const order of orders) {
|
||||
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
|
||||
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
|
||||
const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
|
||||
const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
|
||||
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.Native] || 0);
|
||||
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
|
||||
const rate = makerAmount.div(takerAmount);
|
||||
const adjustedRate =
|
||||
side === MarketOperation.Sell
|
||||
? makerAmount.minus(penalty).div(takerAmount)
|
||||
: makerAmount.div(takerAmount.plus(penalty));
|
||||
// Skip orders with rates that are <= 0.
|
||||
if (adjustedRate.lte(0)) {
|
||||
continue;
|
||||
}
|
||||
path.push({
|
||||
input,
|
||||
output,
|
||||
rate,
|
||||
adjustedRate,
|
||||
adjustedOutput,
|
||||
flags: 0,
|
||||
index: 0, // TBD
|
||||
parent: undefined, // TBD
|
||||
source: ERC20BridgeSource.Native,
|
||||
fillData: { order },
|
||||
});
|
||||
}
|
||||
// Sort by descending adjusted rate.
|
||||
path = path.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate));
|
||||
// Re-index fills.
|
||||
for (let i = 0; i < path.length; ++i) {
|
||||
path[i].parent = i === 0 ? undefined : path[i - 1];
|
||||
path[i].index = i;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function dexQuotesToPaths(
|
||||
side: MarketOperation,
|
||||
dexQuotes: DexSample[][],
|
||||
ethToOutputRate: BigNumber,
|
||||
fees: { [source: string]: BigNumber },
|
||||
): Fill[][] {
|
||||
const paths: Fill[][] = [];
|
||||
for (const quote of dexQuotes) {
|
||||
const path: Fill[] = [];
|
||||
for (let i = 0; i < quote.length; i++) {
|
||||
const sample = quote[i];
|
||||
const prevSample = i === 0 ? undefined : quote[i - 1];
|
||||
const source = sample.source;
|
||||
// Stop of the sample has zero output, which can occur if the source
|
||||
// cannot fill the full amount.
|
||||
// TODO(dorothy-zbornak): Sometimes Kyber will dip to zero then pick back up.
|
||||
if (sample.output.eq(0)) {
|
||||
break;
|
||||
}
|
||||
const input = sample.input.minus(prevSample ? prevSample.input : 0);
|
||||
const output = sample.output.minus(prevSample ? prevSample.output : 0);
|
||||
const penalty =
|
||||
i === 0 // Only the first fill in a DEX path incurs a penalty.
|
||||
? ethToOutputRate.times(fees[source] || 0)
|
||||
: ZERO_AMOUNT;
|
||||
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
|
||||
const rate = side === MarketOperation.Sell ? output.div(input) : input.div(output);
|
||||
const adjustedRate = side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
|
||||
|
||||
path.push({
|
||||
input,
|
||||
output,
|
||||
rate,
|
||||
adjustedRate,
|
||||
adjustedOutput,
|
||||
source,
|
||||
index: i,
|
||||
parent: i !== 0 ? path[path.length - 1] : undefined,
|
||||
flags: sourceToFillFlags(source),
|
||||
});
|
||||
}
|
||||
paths.push(path);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
function sourceToFillFlags(source: ERC20BridgeSource): number {
|
||||
if (source === ERC20BridgeSource.Kyber) {
|
||||
return FillFlags.Kyber;
|
||||
}
|
||||
if (source === ERC20BridgeSource.Eth2Dai) {
|
||||
return FillFlags.ConflictsWithKyber;
|
||||
}
|
||||
if (source === ERC20BridgeSource.Uniswap) {
|
||||
return FillFlags.ConflictsWithKyber;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getPathSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] {
|
||||
let input = ZERO_AMOUNT;
|
||||
let output = ZERO_AMOUNT;
|
||||
for (const fill of path) {
|
||||
if (input.plus(fill.input).gte(targetInput)) {
|
||||
const di = targetInput.minus(input);
|
||||
input = input.plus(di);
|
||||
output = output.plus(fill.output.times(di.div(fill.input)));
|
||||
break;
|
||||
} else {
|
||||
input = input.plus(fill.input);
|
||||
output = output.plus(fill.output);
|
||||
}
|
||||
}
|
||||
return [input.integerValue(), output.integerValue()];
|
||||
}
|
||||
|
||||
export function getPathAdjustedSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] {
|
||||
let input = ZERO_AMOUNT;
|
||||
let output = ZERO_AMOUNT;
|
||||
for (const fill of path) {
|
||||
if (input.plus(fill.input).gte(targetInput)) {
|
||||
const di = targetInput.minus(input);
|
||||
input = input.plus(di);
|
||||
output = output.plus(fill.adjustedOutput.times(di.div(fill.input)));
|
||||
break;
|
||||
} else {
|
||||
input = input.plus(fill.input);
|
||||
output = output.plus(fill.adjustedOutput);
|
||||
}
|
||||
}
|
||||
return [input.integerValue(), output.integerValue()];
|
||||
}
|
||||
|
||||
export function isValidPath(path: Fill[]): boolean {
|
||||
let flags = 0;
|
||||
for (let i = 0; i < path.length; ++i) {
|
||||
// Fill must immediately follow its parent.
|
||||
if (path[i].parent) {
|
||||
if (i === 0 || path[i - 1] !== path[i].parent) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Fill must not be duplicated.
|
||||
for (let j = 0; j < i; ++j) {
|
||||
if (path[i] === path[j]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
flags |= path[i].flags;
|
||||
}
|
||||
const conflictFlags = FillFlags.Kyber | FillFlags.ConflictsWithKyber;
|
||||
return (flags & conflictFlags) !== conflictFlags;
|
||||
}
|
||||
|
||||
export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] {
|
||||
const clipped: Fill[] = [];
|
||||
let input = ZERO_AMOUNT;
|
||||
for (const fill of path) {
|
||||
if (input.gte(targetInput)) {
|
||||
break;
|
||||
}
|
||||
input = input.plus(fill.input);
|
||||
clipped.push(fill);
|
||||
}
|
||||
return clipped;
|
||||
}
|
||||
|
||||
export function collapsePath(side: MarketOperation, path: Fill[]): CollapsedFill[] {
|
||||
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
|
||||
for (const fill of path) {
|
||||
const makerAssetAmount = side === MarketOperation.Sell ? fill.output : fill.input;
|
||||
const takerAssetAmount = side === MarketOperation.Sell ? fill.input : fill.output;
|
||||
const source = fill.source;
|
||||
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
|
||||
const prevFill = collapsed[collapsed.length - 1];
|
||||
// If the last fill is from the same source, merge them.
|
||||
if (prevFill.source === source) {
|
||||
prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
|
||||
prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
|
||||
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
collapsed.push({
|
||||
source: fill.source,
|
||||
totalMakerAssetAmount: makerAssetAmount,
|
||||
totalTakerAssetAmount: takerAssetAmount,
|
||||
subFills: [{ makerAssetAmount, takerAssetAmount }],
|
||||
nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined,
|
||||
});
|
||||
}
|
||||
return collapsed;
|
||||
}
|
||||
|
||||
export function getFallbackSourcePaths(optimalPath: Fill[], allPaths: Fill[][]): Fill[][] {
|
||||
const optimalSources: ERC20BridgeSource[] = [];
|
||||
for (const fill of optimalPath) {
|
||||
if (!optimalSources.includes(fill.source)) {
|
||||
optimalSources.push(fill.source);
|
||||
}
|
||||
}
|
||||
const fallbackPaths: Fill[][] = [];
|
||||
for (const path of allPaths) {
|
||||
if (optimalSources.includes(path[0].source)) {
|
||||
continue;
|
||||
}
|
||||
// HACK(dorothy-zbornak): We *should* be filtering out paths that
|
||||
// conflict with the optimal path (i.e., Kyber conflicts), but in
|
||||
// practice we often end up not being able to find a fallback path
|
||||
// because we've lost 2 major liquiduty sources. The end result is
|
||||
// we end up with many more reverts than what would be actually caused
|
||||
// by conflicts.
|
||||
fallbackPaths.push(path);
|
||||
}
|
||||
return fallbackPaths;
|
||||
}
|
||||
|
||||
export function getPathAdjustedRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber {
|
||||
const [input, output] = getPathAdjustedSize(path, targetInput);
|
||||
if (input.eq(0) || output.eq(0)) {
|
||||
return ZERO_AMOUNT;
|
||||
}
|
||||
return side === MarketOperation.Sell ? output.div(input) : input.div(output);
|
||||
}
|
||||
|
||||
export function getPathAdjustedSlippage(
|
||||
side: MarketOperation,
|
||||
path: Fill[],
|
||||
inputAmount: BigNumber,
|
||||
maxRate: BigNumber,
|
||||
): number {
|
||||
if (maxRate.eq(0)) {
|
||||
return 0;
|
||||
}
|
||||
const totalRate = getPathAdjustedRate(side, path, inputAmount);
|
||||
const rateChange = maxRate.minus(totalRate);
|
||||
return rateChange.div(maxRate).toNumber();
|
||||
}
|
@ -1,55 +1,41 @@
|
||||
import { ContractAddresses } from '@0x/contract-addresses';
|
||||
import { assetDataUtils, ERC20AssetData, orderCalculationUtils } from '@0x/order-utils';
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
|
||||
|
||||
import { constants } from '../../constants';
|
||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||
import { fillableAmountsUtils } from '../fillable_amounts_utils';
|
||||
import { MarketOperation } from '../../types';
|
||||
import { difference } from '../utils';
|
||||
|
||||
import { constants as marketOperationUtilConstants } from './constants';
|
||||
import { CreateOrderUtils } from './create_order';
|
||||
import { comparePathOutputs, FillsOptimizer, getPathAdjustedOutput, sortFillsByAdjustedRate } from './fill_optimizer';
|
||||
import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants';
|
||||
import {
|
||||
createFillPaths,
|
||||
getFallbackSourcePaths,
|
||||
getPathAdjustedRate,
|
||||
getPathAdjustedSlippage,
|
||||
getPathSize,
|
||||
} from './fills';
|
||||
import { createOrdersFromPath, createSignedOrdersWithFillableAmounts, getNativeOrderTokens } from './orders';
|
||||
import { findOptimalPath } from './path_optimizer';
|
||||
import { DexOrderSampler, getSampleAmounts } from './sampler';
|
||||
import {
|
||||
AggregationError,
|
||||
CollapsedFill,
|
||||
DexSample,
|
||||
ERC20BridgeSource,
|
||||
Fill,
|
||||
FillData,
|
||||
FillFlags,
|
||||
GetMarketOrdersOpts,
|
||||
NativeCollapsedFill,
|
||||
NativeFillData,
|
||||
OptimizedMarketOrder,
|
||||
OrderDomain,
|
||||
} from './types';
|
||||
|
||||
export { DexOrderSampler } from './sampler';
|
||||
|
||||
const { ZERO_AMOUNT } = constants;
|
||||
const {
|
||||
BUY_SOURCES,
|
||||
DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
ERC20_PROXY_ID,
|
||||
FEE_QUOTE_SOURCES,
|
||||
ONE_ETHER,
|
||||
SELL_SOURCES,
|
||||
} = marketOperationUtilConstants;
|
||||
|
||||
export class MarketOperationUtils {
|
||||
private readonly _createOrderUtils: CreateOrderUtils;
|
||||
private readonly _wethAddress: string;
|
||||
|
||||
constructor(
|
||||
private readonly _sampler: DexOrderSampler,
|
||||
contractAddresses: ContractAddresses,
|
||||
private readonly contractAddresses: ContractAddresses,
|
||||
private readonly _orderDomain: OrderDomain,
|
||||
private readonly _liquidityProviderRegistry: string = NULL_ADDRESS,
|
||||
) {
|
||||
this._createOrderUtils = new CreateOrderUtils(contractAddresses);
|
||||
this._wethAddress = contractAddresses.etherToken;
|
||||
this._wethAddress = contractAddresses.etherToken.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,34 +54,34 @@ export class MarketOperationUtils {
|
||||
if (nativeOrders.length === 0) {
|
||||
throw new Error(AggregationError.EmptyOrders);
|
||||
}
|
||||
const _opts = {
|
||||
...DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
...opts,
|
||||
};
|
||||
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
|
||||
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
|
||||
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
|
||||
// Call the sampler contract.
|
||||
const [
|
||||
fillableAmounts,
|
||||
orderFillableAmounts,
|
||||
liquidityProviderAddress,
|
||||
ethToMakerAssetRate,
|
||||
dexQuotes,
|
||||
] = await this._sampler.executeAsync(
|
||||
// Get native order fillable amounts.
|
||||
DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders),
|
||||
// Get the custom liquidity provider from registry.
|
||||
DexOrderSampler.ops.getLiquidityProviderFromRegistry(
|
||||
this._liquidityProviderRegistry,
|
||||
takerToken,
|
||||
makerToken,
|
||||
takerToken,
|
||||
),
|
||||
makerToken.toLowerCase() === this._wethAddress.toLowerCase()
|
||||
? DexOrderSampler.ops.constant(new BigNumber(1))
|
||||
: DexOrderSampler.ops.getMedianSellRate(
|
||||
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
|
||||
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
|
||||
),
|
||||
makerToken,
|
||||
this._wethAddress,
|
||||
ONE_ETHER,
|
||||
this._liquidityProviderRegistry,
|
||||
),
|
||||
// Get ETH -> maker token price.
|
||||
DexOrderSampler.ops.getMedianSellRate(
|
||||
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
|
||||
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
|
||||
),
|
||||
makerToken,
|
||||
this._wethAddress,
|
||||
ONE_ETHER,
|
||||
this._liquidityProviderRegistry,
|
||||
),
|
||||
// Get sell quotes for taker -> maker.
|
||||
DexOrderSampler.ops.getSellQuotes(
|
||||
difference(SELL_SOURCES, _opts.excludedSources).concat(
|
||||
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
|
||||
@ -106,50 +92,22 @@ export class MarketOperationUtils {
|
||||
this._liquidityProviderRegistry,
|
||||
),
|
||||
);
|
||||
|
||||
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
|
||||
return this._generateOptimizedOrders({
|
||||
orderFillableAmounts,
|
||||
nativeOrders,
|
||||
fillableAmounts,
|
||||
MarketOperation.Sell,
|
||||
);
|
||||
|
||||
const nativeFills = pruneNativeFills(
|
||||
sortFillsByAdjustedRate(
|
||||
createSellPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToMakerAssetRate, _opts),
|
||||
),
|
||||
takerAmount,
|
||||
_opts.dustFractionThreshold,
|
||||
);
|
||||
const dexPaths = createSellPathsFromDexQuotes(dexQuotes, ethToMakerAssetRate, _opts);
|
||||
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, nativeFills);
|
||||
allFills.splice(0, 0, ...nativeFills);
|
||||
}
|
||||
|
||||
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.
|
||||
sortFillsByAdjustedRate(allFills),
|
||||
takerAmount,
|
||||
upperBoundPath,
|
||||
);
|
||||
if (!optimalPath) {
|
||||
throw new Error(AggregationError.NoOptimalPath);
|
||||
}
|
||||
return this._createOrderUtils.createSellOrdersFromPath(
|
||||
this._orderDomain,
|
||||
takerToken,
|
||||
makerToken,
|
||||
collapsePath(optimalPath, false),
|
||||
_opts.bridgeSlippage,
|
||||
dexQuotes,
|
||||
liquidityProviderAddress,
|
||||
);
|
||||
inputToken: takerToken,
|
||||
outputToken: makerToken,
|
||||
side: MarketOperation.Sell,
|
||||
inputAmount: takerAmount,
|
||||
ethToOutputRate: ethToMakerAssetRate,
|
||||
bridgeSlippage: _opts.bridgeSlippage,
|
||||
maxFallbackSlippage: _opts.maxFallbackSlippage,
|
||||
excludedSources: _opts.excludedSources,
|
||||
feeSchedule: _opts.feeSchedule,
|
||||
allowFallback: _opts.allowFallback,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,34 +126,34 @@ export class MarketOperationUtils {
|
||||
if (nativeOrders.length === 0) {
|
||||
throw new Error(AggregationError.EmptyOrders);
|
||||
}
|
||||
const _opts = {
|
||||
...DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
...opts,
|
||||
};
|
||||
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
|
||||
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
|
||||
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
|
||||
// Call the sampler contract.
|
||||
const [
|
||||
fillableAmounts,
|
||||
orderFillableAmounts,
|
||||
liquidityProviderAddress,
|
||||
ethToTakerAssetRate,
|
||||
dexQuotes,
|
||||
] = await this._sampler.executeAsync(
|
||||
// Get native order fillable amounts.
|
||||
DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders),
|
||||
// Get the custom liquidity provider from registry.
|
||||
DexOrderSampler.ops.getLiquidityProviderFromRegistry(
|
||||
this._liquidityProviderRegistry,
|
||||
takerToken,
|
||||
makerToken,
|
||||
takerToken,
|
||||
),
|
||||
takerToken.toLowerCase() === this._wethAddress.toLowerCase()
|
||||
? DexOrderSampler.ops.constant(new BigNumber(1))
|
||||
: DexOrderSampler.ops.getMedianSellRate(
|
||||
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
|
||||
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
|
||||
),
|
||||
takerToken,
|
||||
this._wethAddress,
|
||||
ONE_ETHER,
|
||||
this._liquidityProviderRegistry,
|
||||
),
|
||||
// Get ETH -> taker token price.
|
||||
DexOrderSampler.ops.getMedianSellRate(
|
||||
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
|
||||
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
|
||||
),
|
||||
takerToken,
|
||||
this._wethAddress,
|
||||
ONE_ETHER,
|
||||
this._liquidityProviderRegistry,
|
||||
),
|
||||
// Get buy quotes for taker -> maker.
|
||||
DexOrderSampler.ops.getBuyQuotes(
|
||||
difference(BUY_SOURCES, _opts.excludedSources).concat(
|
||||
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
|
||||
@ -206,19 +164,23 @@ export class MarketOperationUtils {
|
||||
this._liquidityProviderRegistry,
|
||||
),
|
||||
);
|
||||
const signedOrderWithFillableAmounts = this._createBuyOrdersPathFromSamplerResultIfExists(
|
||||
|
||||
return this._generateOptimizedOrders({
|
||||
orderFillableAmounts,
|
||||
nativeOrders,
|
||||
makerAmount,
|
||||
fillableAmounts,
|
||||
dexQuotes,
|
||||
ethToTakerAssetRate,
|
||||
_opts,
|
||||
liquidityProviderAddress,
|
||||
);
|
||||
if (!signedOrderWithFillableAmounts) {
|
||||
throw new Error(AggregationError.NoOptimalPath);
|
||||
}
|
||||
return signedOrderWithFillableAmounts;
|
||||
inputToken: makerToken,
|
||||
outputToken: takerToken,
|
||||
side: MarketOperation.Buy,
|
||||
inputAmount: makerAmount,
|
||||
ethToOutputRate: ethToTakerAssetRate,
|
||||
bridgeSlippage: _opts.bridgeSlippage,
|
||||
maxFallbackSlippage: _opts.maxFallbackSlippage,
|
||||
excludedSources: _opts.excludedSources,
|
||||
feeSchedule: _opts.feeSchedule,
|
||||
allowFallback: _opts.allowFallback,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,10 +202,7 @@ export class MarketOperationUtils {
|
||||
if (batchNativeOrders.length === 0) {
|
||||
throw new Error(AggregationError.EmptyOrders);
|
||||
}
|
||||
const _opts = {
|
||||
...DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
...opts,
|
||||
};
|
||||
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
|
||||
|
||||
const sources = difference(BUY_SOURCES, _opts.excludedSources);
|
||||
const ops = [
|
||||
@ -251,32 +210,118 @@ export class MarketOperationUtils {
|
||||
...batchNativeOrders.map(orders =>
|
||||
DexOrderSampler.ops.getMedianSellRate(
|
||||
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
|
||||
getNativeOrderTokens(orders[0])[1],
|
||||
this._wethAddress,
|
||||
getOrderTokens(orders[0])[1],
|
||||
ONE_ETHER,
|
||||
),
|
||||
),
|
||||
...batchNativeOrders.map((orders, i) =>
|
||||
DexOrderSampler.ops.getBuyQuotes(sources, getOrderTokens(orders[0])[0], getOrderTokens(orders[0])[1], [
|
||||
makerAmounts[i],
|
||||
]),
|
||||
DexOrderSampler.ops.getBuyQuotes(
|
||||
sources,
|
||||
getNativeOrderTokens(orders[0])[0],
|
||||
getNativeOrderTokens(orders[0])[1],
|
||||
[makerAmounts[i]],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
const executeResults = await this._sampler.executeBatchAsync(ops);
|
||||
const batchFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
|
||||
const batchOrderFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
|
||||
const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[];
|
||||
const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
|
||||
|
||||
return batchFillableAmounts.map((fillableAmounts, i) =>
|
||||
this._createBuyOrdersPathFromSamplerResultIfExists(
|
||||
batchNativeOrders[i],
|
||||
makerAmounts[i],
|
||||
fillableAmounts,
|
||||
batchDexQuotes[i],
|
||||
batchEthToTakerAssetRate[i],
|
||||
_opts,
|
||||
),
|
||||
);
|
||||
return batchNativeOrders.map((nativeOrders, i) => {
|
||||
if (nativeOrders.length === 0) {
|
||||
throw new Error(AggregationError.EmptyOrders);
|
||||
}
|
||||
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
|
||||
const orderFillableAmounts = batchOrderFillableAmounts[i];
|
||||
const ethToTakerAssetRate = batchEthToTakerAssetRate[i];
|
||||
const dexQuotes = batchDexQuotes[i];
|
||||
const makerAmount = makerAmounts[i];
|
||||
return this._generateOptimizedOrders({
|
||||
orderFillableAmounts,
|
||||
nativeOrders,
|
||||
dexQuotes,
|
||||
inputToken: makerToken,
|
||||
outputToken: takerToken,
|
||||
side: MarketOperation.Buy,
|
||||
inputAmount: makerAmount,
|
||||
ethToOutputRate: ethToTakerAssetRate,
|
||||
bridgeSlippage: _opts.bridgeSlippage,
|
||||
maxFallbackSlippage: _opts.maxFallbackSlippage,
|
||||
excludedSources: _opts.excludedSources,
|
||||
feeSchedule: _opts.feeSchedule,
|
||||
allowFallback: _opts.allowFallback,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _generateOptimizedOrders(opts: {
|
||||
side: MarketOperation;
|
||||
inputToken: string;
|
||||
outputToken: string;
|
||||
inputAmount: BigNumber;
|
||||
nativeOrders: SignedOrder[];
|
||||
orderFillableAmounts: BigNumber[];
|
||||
dexQuotes: DexSample[][];
|
||||
runLimit?: number;
|
||||
ethToOutputRate?: BigNumber;
|
||||
bridgeSlippage?: number;
|
||||
maxFallbackSlippage?: number;
|
||||
excludedSources?: ERC20BridgeSource[];
|
||||
feeSchedule?: { [source: string]: BigNumber };
|
||||
allowFallback?: boolean;
|
||||
liquidityProviderAddress?: string;
|
||||
}): OptimizedMarketOrder[] {
|
||||
const { inputToken, outputToken, side, inputAmount } = opts;
|
||||
const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
|
||||
// Convert native orders and dex quotes into fill paths.
|
||||
const paths = createFillPaths({
|
||||
side,
|
||||
// Augment native orders with their fillable amounts.
|
||||
orders: createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts),
|
||||
dexQuotes: opts.dexQuotes,
|
||||
targetInput: inputAmount,
|
||||
ethToOutputRate: opts.ethToOutputRate,
|
||||
excludedSources: opts.excludedSources,
|
||||
feeSchedule: opts.feeSchedule,
|
||||
});
|
||||
// Find the optimal path.
|
||||
const optimalPath = findOptimalPath(side, paths, inputAmount, opts.runLimit) || [];
|
||||
// TODO(dorothy-zbornak): Ensure the slippage on the optimal path is <= maxFallbackSlippage
|
||||
// once we decide on a good baseline.
|
||||
if (optimalPath.length === 0) {
|
||||
throw new Error(AggregationError.NoOptimalPath);
|
||||
}
|
||||
// Generate a fallback path if native orders are in the optimal paath.
|
||||
let fallbackPath: Fill[] = [];
|
||||
const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native);
|
||||
if (opts.allowFallback && nativeSubPath.length !== 0) {
|
||||
// The fallback path is, at most, as large as the native path.
|
||||
const fallbackInputAmount = BigNumber.min(inputAmount, getPathSize(nativeSubPath, inputAmount)[0]);
|
||||
fallbackPath =
|
||||
findOptimalPath(side, getFallbackSourcePaths(optimalPath, paths), fallbackInputAmount, opts.runLimit) ||
|
||||
[];
|
||||
const fallbackSlippage = getPathAdjustedSlippage(
|
||||
side,
|
||||
fallbackPath,
|
||||
fallbackInputAmount,
|
||||
getPathAdjustedRate(side, optimalPath, inputAmount),
|
||||
);
|
||||
if (fallbackSlippage > maxFallbackSlippage) {
|
||||
fallbackPath = [];
|
||||
}
|
||||
}
|
||||
return createOrdersFromPath([...optimalPath, ...fallbackPath], {
|
||||
side,
|
||||
inputToken,
|
||||
outputToken,
|
||||
orderDomain: this._orderDomain,
|
||||
contractAddresses: this.contractAddresses,
|
||||
bridgeSlippage: opts.bridgeSlippage || 0,
|
||||
liquidityProviderAddress: opts.liquidityProviderAddress,
|
||||
});
|
||||
}
|
||||
|
||||
private _liquidityProviderSourceIfAvailable(excludedSources: ERC20BridgeSource[]): ERC20BridgeSource[] {
|
||||
@ -285,328 +330,6 @@ export class MarketOperationUtils {
|
||||
? [ERC20BridgeSource.LiquidityProvider]
|
||||
: [];
|
||||
}
|
||||
|
||||
private _createBuyOrdersPathFromSamplerResultIfExists(
|
||||
nativeOrders: SignedOrder[],
|
||||
makerAmount: BigNumber,
|
||||
nativeOrderFillableAmounts: BigNumber[],
|
||||
dexQuotes: DexSample[][],
|
||||
ethToTakerAssetRate: BigNumber,
|
||||
opts: GetMarketOrdersOpts,
|
||||
liquidityProviderAddress?: string,
|
||||
): OptimizedMarketOrder[] | undefined {
|
||||
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
|
||||
nativeOrders,
|
||||
nativeOrderFillableAmounts,
|
||||
MarketOperation.Buy,
|
||||
);
|
||||
const nativeFills = pruneNativeFills(
|
||||
sortFillsByAdjustedRate(
|
||||
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToTakerAssetRate, opts),
|
||||
true,
|
||||
),
|
||||
makerAmount,
|
||||
opts.dustFractionThreshold,
|
||||
);
|
||||
const dexPaths = createBuyPathsFromDexQuotes(dexQuotes, ethToTakerAssetRate, opts);
|
||||
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, nativeFills);
|
||||
allFills.splice(0, 0, ...nativeFills);
|
||||
}
|
||||
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.
|
||||
sortFillsByAdjustedRate(allFills, true),
|
||||
makerAmount,
|
||||
upperBoundPath,
|
||||
);
|
||||
if (!optimalPath) {
|
||||
return undefined;
|
||||
}
|
||||
const [inputToken, outputToken] = getOrderTokens(nativeOrders[0]);
|
||||
return this._createOrderUtils.createBuyOrdersFromPath(
|
||||
this._orderDomain,
|
||||
inputToken,
|
||||
outputToken,
|
||||
collapsePath(optimalPath, true),
|
||||
opts.bridgeSlippage,
|
||||
liquidityProviderAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createSignedOrdersWithFillableAmounts(
|
||||
signedOrders: SignedOrder[],
|
||||
fillableAmounts: BigNumber[],
|
||||
operation: MarketOperation,
|
||||
): SignedOrderWithFillableAmounts[] {
|
||||
return signedOrders
|
||||
.map((order: SignedOrder, i: number) => {
|
||||
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[],
|
||||
ethToOutputAssetRate: BigNumber,
|
||||
opts: GetMarketOrdersOpts,
|
||||
): 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.getMakerAssetAmountSwappedAfterOrderFees(order);
|
||||
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(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,
|
||||
// Every fill from native orders incurs a penalty.
|
||||
fillPenalty: ethToOutputAssetRate.times(opts.fees[ERC20BridgeSource.Native] || 0),
|
||||
fillData: {
|
||||
source: ERC20BridgeSource.Native,
|
||||
order,
|
||||
},
|
||||
});
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function createBuyPathFromNativeOrders(
|
||||
orders: SignedOrderWithFillableAmounts[],
|
||||
ethToOutputAssetRate: BigNumber,
|
||||
opts: GetMarketOrdersOpts,
|
||||
): 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.getMakerAssetAmountSwappedAfterOrderFees(order);
|
||||
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(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,
|
||||
// Every fill from native orders incurs a penalty.
|
||||
// Negated because we try to minimize the output in buys.
|
||||
fillPenalty: ethToOutputAssetRate.times(opts.fees[ERC20BridgeSource.Native] || 0).negated(),
|
||||
fillData: {
|
||||
source: ERC20BridgeSource.Native,
|
||||
order,
|
||||
},
|
||||
});
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function pruneNativeFills(fills: Fill[], fillAmount: BigNumber, dustFractionThreshold: number): Fill[] {
|
||||
const minInput = fillAmount.times(dustFractionThreshold);
|
||||
const pruned = [];
|
||||
let totalInput = ZERO_AMOUNT;
|
||||
for (const fill of fills) {
|
||||
if (totalInput.gte(fillAmount)) {
|
||||
break;
|
||||
}
|
||||
if (fill.input.lt(minInput)) {
|
||||
continue;
|
||||
}
|
||||
totalInput = totalInput.plus(fill.input);
|
||||
pruned.push(fill);
|
||||
}
|
||||
return pruned;
|
||||
}
|
||||
|
||||
function createSellPathsFromDexQuotes(
|
||||
dexQuotes: DexSample[][],
|
||||
ethToOutputAssetRate: BigNumber,
|
||||
opts: GetMarketOrdersOpts,
|
||||
): Fill[][] {
|
||||
return createPathsFromDexQuotes(dexQuotes, ethToOutputAssetRate, opts);
|
||||
}
|
||||
|
||||
function createBuyPathsFromDexQuotes(
|
||||
dexQuotes: DexSample[][],
|
||||
ethToOutputAssetRate: BigNumber,
|
||||
opts: GetMarketOrdersOpts,
|
||||
): Fill[][] {
|
||||
return createPathsFromDexQuotes(
|
||||
dexQuotes,
|
||||
// Negated because we try to minimize the output in buys.
|
||||
ethToOutputAssetRate.negated(),
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
function createPathsFromDexQuotes(
|
||||
dexQuotes: DexSample[][],
|
||||
ethToOutputAssetRate: BigNumber,
|
||||
opts: GetMarketOrdersOpts,
|
||||
): Fill[][] {
|
||||
const paths: Fill[][] = [];
|
||||
for (const quote of dexQuotes) {
|
||||
const path: Fill[] = [];
|
||||
let prevSample: DexSample | undefined;
|
||||
// tslint:disable-next-line: prefer-for-of
|
||||
for (let i = 0; i < quote.length; i++) {
|
||||
const sample = quote[i];
|
||||
// Stop of the sample has zero output, which can occur if the source
|
||||
// cannot fill the full amount.
|
||||
if (sample.output.isZero()) {
|
||||
break;
|
||||
}
|
||||
path.push({
|
||||
input: sample.input.minus(prevSample ? prevSample.input : 0),
|
||||
output: sample.output.minus(prevSample ? prevSample.output : 0),
|
||||
fillPenalty: ZERO_AMOUNT,
|
||||
parent: path.length !== 0 ? path[path.length - 1] : undefined,
|
||||
flags: sourceToFillFlags(sample.source),
|
||||
exclusionMask: opts.noConflicts ? sourceToExclusionMask(sample.source) : 0,
|
||||
fillData: { source: sample.source },
|
||||
});
|
||||
prevSample = quote[i];
|
||||
}
|
||||
// Don't push empty paths.
|
||||
if (path.length > 0) {
|
||||
// Only the first fill in a DEX path incurs a penalty.
|
||||
path[0].fillPenalty = ethToOutputAssetRate.times(opts.fees[path[0].fillData.source] || 0);
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (source === ERC20BridgeSource.LiquidityProvider) {
|
||||
return FillFlags.SourceLiquidityPool;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 = getPathAdjustedOutput(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 collapsePath(path: Fill[], isBuy: boolean): CollapsedFill[] {
|
||||
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
|
||||
for (const fill of path) {
|
||||
const makerAssetAmount = isBuy ? fill.input : fill.output;
|
||||
const takerAssetAmount = isBuy ? fill.output : fill.input;
|
||||
const source = (fill.fillData as FillData).source;
|
||||
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
|
||||
const prevFill = collapsed[collapsed.length - 1];
|
||||
// If the last fill is from the same source, merge them.
|
||||
if (prevFill.source === source) {
|
||||
prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
|
||||
prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
|
||||
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
collapsed.push({
|
||||
source: fill.fillData.source,
|
||||
totalMakerAssetAmount: makerAssetAmount,
|
||||
totalTakerAssetAmount: takerAssetAmount,
|
||||
subFills: [{ makerAssetAmount, takerAssetAmount }],
|
||||
nativeOrder: (fill.fillData as NativeFillData).order,
|
||||
});
|
||||
}
|
||||
return collapsed;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// tslint:disable: max-file-line-count
|
||||
|
@ -0,0 +1,258 @@
|
||||
import { ContractAddresses } from '@0x/contract-addresses';
|
||||
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { AbiEncoder, BigNumber } from '@0x/utils';
|
||||
|
||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||
|
||||
import {
|
||||
DEFAULT_CURVE_OPTS,
|
||||
ERC20_PROXY_ID,
|
||||
NULL_ADDRESS,
|
||||
NULL_BYTES,
|
||||
ONE_HOUR_IN_SECONDS,
|
||||
ONE_SECOND_MS,
|
||||
WALLET_SIGNATURE,
|
||||
ZERO_AMOUNT,
|
||||
} from './constants';
|
||||
import { collapsePath } from './fills';
|
||||
import {
|
||||
AggregationError,
|
||||
CollapsedFill,
|
||||
ERC20BridgeSource,
|
||||
Fill,
|
||||
NativeCollapsedFill,
|
||||
OptimizedMarketOrder,
|
||||
OrderDomain,
|
||||
} from './types';
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export function createDummyOrderForSampler(
|
||||
makerAssetData: string,
|
||||
takerAssetData: string,
|
||||
makerAddress: string,
|
||||
): SignedOrder {
|
||||
return {
|
||||
makerAddress,
|
||||
takerAddress: NULL_ADDRESS,
|
||||
senderAddress: NULL_ADDRESS,
|
||||
feeRecipientAddress: NULL_ADDRESS,
|
||||
salt: ZERO_AMOUNT,
|
||||
expirationTimeSeconds: ZERO_AMOUNT,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export function getNativeOrderTokens(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.toLowerCase()) as [string, string];
|
||||
}
|
||||
|
||||
export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOrder): OptimizedMarketOrder {
|
||||
return {
|
||||
...order,
|
||||
fillableMakerAssetAmount: order.makerAssetAmount,
|
||||
fillableTakerAssetAmount: order.takerAssetAmount,
|
||||
fillableTakerFeeAmount: order.takerFee,
|
||||
fill: {
|
||||
source: ERC20BridgeSource.Native,
|
||||
totalMakerAssetAmount: order.makerAssetAmount,
|
||||
totalTakerAssetAmount: order.takerAssetAmount,
|
||||
subFills: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Augments native orders with fillable amounts and filters out unfillable orders.
|
||||
*/
|
||||
export function createSignedOrdersWithFillableAmounts(
|
||||
side: MarketOperation,
|
||||
orders: SignedOrder[],
|
||||
fillableAmounts: BigNumber[],
|
||||
): SignedOrderWithFillableAmounts[] {
|
||||
return orders
|
||||
.map((order: SignedOrder, i: number) => {
|
||||
const fillableAmount = fillableAmounts[i];
|
||||
const fillableMakerAssetAmount =
|
||||
side === MarketOperation.Buy
|
||||
? fillableAmount
|
||||
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
|
||||
const fillableTakerAssetAmount =
|
||||
side === MarketOperation.Sell
|
||||
? fillableAmount
|
||||
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
|
||||
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
|
||||
return {
|
||||
...order,
|
||||
fillableMakerAssetAmount,
|
||||
fillableTakerAssetAmount,
|
||||
fillableTakerFeeAmount,
|
||||
};
|
||||
})
|
||||
.filter(order => {
|
||||
return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero();
|
||||
});
|
||||
}
|
||||
|
||||
export interface CreateOrderFromPathOpts {
|
||||
side: MarketOperation;
|
||||
inputToken: string;
|
||||
outputToken: string;
|
||||
orderDomain: OrderDomain;
|
||||
contractAddresses: ContractAddresses;
|
||||
bridgeSlippage: number;
|
||||
liquidityProviderAddress?: string;
|
||||
}
|
||||
|
||||
// Convert sell fills into orders.
|
||||
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
|
||||
const collapsedPath = collapsePath(opts.side, path);
|
||||
const orders: OptimizedMarketOrder[] = [];
|
||||
for (const fill of collapsedPath) {
|
||||
if (fill.source === ERC20BridgeSource.Native) {
|
||||
orders.push(createNativeOrder(fill));
|
||||
} else {
|
||||
orders.push(createBridgeOrder(fill, opts));
|
||||
}
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrderFromPathOpts): string {
|
||||
switch (source) {
|
||||
case ERC20BridgeSource.Eth2Dai:
|
||||
return opts.contractAddresses.eth2DaiBridge;
|
||||
case ERC20BridgeSource.Kyber:
|
||||
return opts.contractAddresses.kyberBridge;
|
||||
case ERC20BridgeSource.Uniswap:
|
||||
return opts.contractAddresses.uniswapBridge;
|
||||
case ERC20BridgeSource.CurveUsdcDai:
|
||||
case ERC20BridgeSource.CurveUsdcDaiUsdt:
|
||||
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
|
||||
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
|
||||
return opts.contractAddresses.curveBridge;
|
||||
case ERC20BridgeSource.LiquidityProvider:
|
||||
if (opts.liquidityProviderAddress === undefined) {
|
||||
throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.');
|
||||
}
|
||||
return opts.liquidityProviderAddress;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
throw new Error(AggregationError.NoBridgeForSource);
|
||||
}
|
||||
|
||||
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
||||
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
||||
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
||||
const bridgeAddress = getBridgeAddressFromSource(fill.source, opts);
|
||||
|
||||
let makerAssetData;
|
||||
if (Object.keys(DEFAULT_CURVE_OPTS).includes(fill.source)) {
|
||||
const { curveAddress, tokens, version } = DEFAULT_CURVE_OPTS[fill.source];
|
||||
const fromTokenIdx = tokens.indexOf(takerToken);
|
||||
const toTokenIdx = tokens.indexOf(makerToken);
|
||||
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
|
||||
makerToken,
|
||||
bridgeAddress,
|
||||
createCurveBridgeData(curveAddress, fromTokenIdx, toTokenIdx, version),
|
||||
);
|
||||
} else {
|
||||
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
|
||||
makerToken,
|
||||
bridgeAddress,
|
||||
createBridgeData(takerToken),
|
||||
);
|
||||
}
|
||||
return {
|
||||
makerAddress: bridgeAddress,
|
||||
makerAssetData,
|
||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
|
||||
...createCommonBridgeOrderFields(fill, opts),
|
||||
};
|
||||
}
|
||||
|
||||
function createBridgeData(tokenAddress: string): string {
|
||||
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
|
||||
return encoder.encode({ tokenAddress });
|
||||
}
|
||||
|
||||
function createCurveBridgeData(
|
||||
curveAddress: string,
|
||||
fromTokenIdx: number,
|
||||
toTokenIdx: number,
|
||||
version: number,
|
||||
): string {
|
||||
const curveBridgeDataEncoder = AbiEncoder.create([
|
||||
{ name: 'curveAddress', type: 'address' },
|
||||
{ name: 'fromTokenIdx', type: 'int128' },
|
||||
{ name: 'toTokenIdx', type: 'int128' },
|
||||
{ name: 'version', type: 'int128' },
|
||||
]);
|
||||
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]);
|
||||
}
|
||||
|
||||
type CommonBridgeOrderFields = Pick<
|
||||
OptimizedMarketOrder,
|
||||
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
|
||||
>;
|
||||
|
||||
function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFromPathOpts): CommonBridgeOrderFields {
|
||||
const makerAssetAmountAdjustedWithSlippage =
|
||||
opts.side === MarketOperation.Sell
|
||||
? fill.totalMakerAssetAmount.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN)
|
||||
: fill.totalMakerAssetAmount;
|
||||
const takerAssetAmountAdjustedWithSlippage =
|
||||
opts.side === MarketOperation.Sell
|
||||
? fill.totalTakerAssetAmount
|
||||
: fill.totalTakerAssetAmount.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP);
|
||||
return {
|
||||
fill,
|
||||
takerAddress: NULL_ADDRESS,
|
||||
senderAddress: NULL_ADDRESS,
|
||||
feeRecipientAddress: NULL_ADDRESS,
|
||||
salt: generatePseudoRandomSalt(),
|
||||
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS),
|
||||
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,
|
||||
...opts.orderDomain,
|
||||
};
|
||||
}
|
||||
|
||||
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
|
||||
return {
|
||||
fill: {
|
||||
source: fill.source,
|
||||
totalMakerAssetAmount: fill.totalMakerAssetAmount,
|
||||
totalTakerAssetAmount: fill.totalTakerAssetAmount,
|
||||
subFills: fill.subFills,
|
||||
},
|
||||
...(fill as NativeCollapsedFill).nativeOrder,
|
||||
};
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { MarketOperation } from '../../types';
|
||||
|
||||
import { ZERO_AMOUNT } from './constants';
|
||||
import { getPathSize, isValidPath } from './fills';
|
||||
import { Fill } from './types';
|
||||
|
||||
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs
|
||||
|
||||
/**
|
||||
* Find the optimal mixture of paths that maximizes (for sells) or minimizes
|
||||
* (for buys) output, while meeting the input requirement.
|
||||
*/
|
||||
export function findOptimalPath(
|
||||
side: MarketOperation,
|
||||
paths: Fill[][],
|
||||
targetInput: BigNumber,
|
||||
runLimit?: number,
|
||||
): Fill[] | undefined {
|
||||
let optimalPath = paths[0] || [];
|
||||
// TODO(dorothy-zbornak): Convex paths (like kyber) should technically always be
|
||||
// inserted at the front of the path because a partial fill can invalidate them.
|
||||
for (const path of paths.slice(1)) {
|
||||
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit);
|
||||
}
|
||||
return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined;
|
||||
}
|
||||
|
||||
function mixPaths(
|
||||
side: MarketOperation,
|
||||
pathA: Fill[],
|
||||
pathB: Fill[],
|
||||
targetInput: BigNumber,
|
||||
maxSteps: number = 2 ** 15,
|
||||
): Fill[] {
|
||||
const allFills = [...pathA, ...pathB].sort((a, b) => b.rate.comparedTo(a.rate));
|
||||
let bestPath: Fill[] = [];
|
||||
let bestPathInput = ZERO_AMOUNT;
|
||||
let bestPathRate = ZERO_AMOUNT;
|
||||
let steps = 0;
|
||||
const _isBetterPath = (input: BigNumber, rate: BigNumber) => {
|
||||
if (bestPathInput.lt(targetInput)) {
|
||||
return input.gt(bestPathInput);
|
||||
} else if (input.gte(bestPathInput)) {
|
||||
return rate.gt(bestPathRate);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const _walk = (path: Fill[], input: BigNumber, output: BigNumber) => {
|
||||
steps += 1;
|
||||
const rate = getRate(side, input, output);
|
||||
if (_isBetterPath(input, rate)) {
|
||||
bestPath = path;
|
||||
bestPathInput = input;
|
||||
bestPathRate = rate;
|
||||
}
|
||||
if (input.lt(targetInput)) {
|
||||
for (const fill of allFills) {
|
||||
if (steps >= maxSteps) {
|
||||
break;
|
||||
}
|
||||
const childPath = [...path, fill];
|
||||
if (!isValidPath(childPath)) {
|
||||
continue;
|
||||
}
|
||||
_walk(childPath, input.plus(fill.input), output.plus(fill.adjustedOutput));
|
||||
}
|
||||
}
|
||||
};
|
||||
_walk(bestPath, ZERO_AMOUNT, ZERO_AMOUNT);
|
||||
return bestPath;
|
||||
}
|
||||
|
||||
function isPathComplete(path: Fill[], targetInput: BigNumber): boolean {
|
||||
const [input] = getPathSize(path);
|
||||
return input.gte(targetInput);
|
||||
}
|
||||
|
||||
function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber {
|
||||
if (input.eq(0) || output.eq(0)) {
|
||||
return ZERO_AMOUNT;
|
||||
}
|
||||
if (side === MarketOperation.Sell) {
|
||||
return output.div(input);
|
||||
}
|
||||
return input.div(output);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..';
|
||||
import { constants } from '../../constants';
|
||||
|
||||
import { DEFAULT_CURVE_OPTS } from './constants';
|
||||
import { BatchedOperation, DexSample } from './types';
|
||||
|
||||
/**
|
||||
@ -191,6 +191,9 @@ export const samplerOperations = {
|
||||
takerFillAmount: BigNumber,
|
||||
liquidityProviderRegistryAddress?: string | undefined,
|
||||
): BatchedOperation<BigNumber> {
|
||||
if (makerToken.toLowerCase() === takerToken.toLowerCase()) {
|
||||
return samplerOperations.constant(new BigNumber(1));
|
||||
}
|
||||
const getSellQuotes = samplerOperations.getSellQuotes(
|
||||
sources,
|
||||
makerToken,
|
||||
@ -211,6 +214,7 @@ export const samplerOperations = {
|
||||
}
|
||||
const flatSortedSamples = samples
|
||||
.reduce((acc, v) => acc.concat(...v))
|
||||
.filter(v => !v.output.isZero())
|
||||
.sort((a, b) => a.output.comparedTo(b.output));
|
||||
if (flatSortedSamples.length === 0) {
|
||||
return new BigNumber(0);
|
||||
@ -232,8 +236,8 @@ export const samplerOperations = {
|
||||
},
|
||||
getLiquidityProviderFromRegistry(
|
||||
registryAddress: string,
|
||||
takerToken: string,
|
||||
makerToken: string,
|
||||
takerToken: string,
|
||||
): BatchedOperation<string> {
|
||||
return {
|
||||
encodeCall: contract => {
|
||||
@ -262,8 +266,8 @@ export const samplerOperations = {
|
||||
batchedOperation = samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
|
||||
} else if (source === ERC20BridgeSource.Kyber) {
|
||||
batchedOperation = samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
|
||||
} else if (Object.keys(constants.DEFAULT_CURVE_OPTS).includes(source)) {
|
||||
const { curveAddress, tokens } = constants.DEFAULT_CURVE_OPTS[source];
|
||||
} else if (Object.keys(DEFAULT_CURVE_OPTS).includes(source)) {
|
||||
const { curveAddress, tokens } = DEFAULT_CURVE_OPTS[source];
|
||||
const fromTokenIdx = tokens.indexOf(takerToken);
|
||||
const toTokenIdx = tokens.indexOf(makerToken);
|
||||
if (fromTokenIdx !== -1 && toTokenIdx !== -1) {
|
||||
|
@ -37,9 +37,7 @@ export enum ERC20BridgeSource {
|
||||
}
|
||||
|
||||
// Internal `fillData` field for `Fill` objects.
|
||||
export interface FillData {
|
||||
source: ERC20BridgeSource;
|
||||
}
|
||||
export interface FillData {}
|
||||
|
||||
// `FillData` for native fills.
|
||||
export interface NativeFillData extends FillData {
|
||||
@ -59,11 +57,8 @@ export interface DexSample {
|
||||
* Flags for `Fill` objects.
|
||||
*/
|
||||
export enum FillFlags {
|
||||
SourceNative = 0x1,
|
||||
SourceUniswap = 0x2,
|
||||
SourceEth2Dai = 0x4,
|
||||
SourceKyber = 0x8,
|
||||
SourceLiquidityPool = 0x10,
|
||||
ConflictsWithKyber = 0x1,
|
||||
Kyber = 0x2,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,20 +67,25 @@ export enum FillFlags {
|
||||
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;
|
||||
// Output penalty for this fill.
|
||||
fillPenalty: BigNumber;
|
||||
// The maker/taker rate.
|
||||
rate: BigNumber;
|
||||
// The maker/taker rate, adjusted by fees.
|
||||
adjustedRate: BigNumber;
|
||||
// The output fill amount, ajdusted by fees.
|
||||
adjustedOutput: BigNumber;
|
||||
// Fill that must precede this one. This enforces certain fills to be contiguous.
|
||||
parent?: Fill;
|
||||
// The index of the fill in the original path.
|
||||
index: number;
|
||||
// The source of the fill. See `ERC20BridgeSource`.
|
||||
source: ERC20BridgeSource;
|
||||
// Data associated with this this Fill object. Used to reconstruct orders
|
||||
// from paths.
|
||||
fillData: FillData | NativeFillData;
|
||||
fillData?: FillData | NativeFillData;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,10 +138,6 @@ 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.
|
||||
@ -156,15 +152,16 @@ export interface GetMarketOrdersOpts {
|
||||
* Default is 0.0005 (5 basis points).
|
||||
*/
|
||||
bridgeSlippage: number;
|
||||
/**
|
||||
* The maximum price slippage allowed in the fallback quote. If the slippage
|
||||
* between the optimal quote and the fallback quote is greater than this
|
||||
* percentage, no fallback quote will be provided.
|
||||
*/
|
||||
maxFallbackSlippage: 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;
|
||||
/**
|
||||
* The exponential sampling distribution base.
|
||||
* A value of 1 will result in evenly spaced samples.
|
||||
@ -176,7 +173,16 @@ export interface GetMarketOrdersOpts {
|
||||
/**
|
||||
* Fees for each liquidity source, expressed in gas.
|
||||
*/
|
||||
fees: { [source: string]: BigNumber };
|
||||
feeSchedule: { [source: string]: BigNumber };
|
||||
/**
|
||||
* Estimated gas consumed by each liquidity source.
|
||||
*/
|
||||
gasSchedule: { [source: string]: number };
|
||||
/**
|
||||
* Whether to pad the quote with a redundant fallback quote using different
|
||||
* sources.
|
||||
*/
|
||||
allowFallback: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ import * as _ from 'lodash';
|
||||
|
||||
import { constants } from '../constants';
|
||||
import { OrderPrunerPermittedFeeTypes } from '../types';
|
||||
import { utils } from '../utils/utils';
|
||||
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from '../utils/utils';
|
||||
|
||||
export const orderPrunerUtils = {
|
||||
pruneForUsableSignedOrders(
|
||||
@ -19,9 +19,9 @@ export const orderPrunerUtils = {
|
||||
((permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
|
||||
order.takerFee.eq(constants.ZERO_AMOUNT)) ||
|
||||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) &&
|
||||
utils.isOrderTakerFeePayableWithTakerAsset(order)) ||
|
||||
isOrderTakerFeePayableWithTakerAsset(order)) ||
|
||||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) &&
|
||||
utils.isOrderTakerFeePayableWithMakerAsset(order)))
|
||||
isOrderTakerFeePayableWithMakerAsset(order)))
|
||||
);
|
||||
});
|
||||
return result;
|
||||
|
@ -4,7 +4,7 @@ import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { assert } from './assert';
|
||||
import { utils } from './utils';
|
||||
import { getAdjustedMakerAndTakerAmountsFromTakerFees } from './utils';
|
||||
|
||||
export const sortingUtils = {
|
||||
sortOrders<T extends Order>(orders: T[]): T[] {
|
||||
@ -21,9 +21,7 @@ export const sortingUtils = {
|
||||
};
|
||||
|
||||
function getTakerFeeAdjustedRateOfOrder(order: Order): BigNumber {
|
||||
const [adjustedMakerAssetAmount, adjustedTakerAssetAmount] = utils.getAdjustedMakerAndTakerAmountsFromTakerFees(
|
||||
order,
|
||||
);
|
||||
const [adjustedMakerAssetAmount, adjustedTakerAssetAmount] = getAdjustedMakerAndTakerAmountsFromTakerFees(order);
|
||||
const rate = adjustedTakerAssetAmount.div(adjustedMakerAssetAmount);
|
||||
return rate;
|
||||
}
|
||||
|
@ -19,10 +19,14 @@ import {
|
||||
|
||||
import { fillableAmountsUtils } from './fillable_amounts_utils';
|
||||
import { MarketOperationUtils } from './market_operation_utils';
|
||||
import { CreateOrderUtils } from './market_operation_utils/create_order';
|
||||
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
||||
import { ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types';
|
||||
import { ProtocolFeeUtils } from './protocol_fee_utils';
|
||||
import { utils } from './utils';
|
||||
import {
|
||||
isOrderTakerFeePayableWithMakerAsset,
|
||||
isOrderTakerFeePayableWithTakerAsset,
|
||||
isSupportedAssetDataInOrders,
|
||||
} from './utils';
|
||||
|
||||
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
|
||||
export class SwapQuoteCalculator {
|
||||
@ -37,14 +41,12 @@ export class SwapQuoteCalculator {
|
||||
public async calculateMarketSellSwapQuoteAsync(
|
||||
prunedOrders: SignedOrder[],
|
||||
takerAssetFillAmount: BigNumber,
|
||||
slippagePercentage: number,
|
||||
gasPrice: BigNumber,
|
||||
opts: CalculateSwapQuoteOpts,
|
||||
): Promise<MarketSellSwapQuote> {
|
||||
return (await this._calculateSwapQuoteAsync(
|
||||
prunedOrders,
|
||||
takerAssetFillAmount,
|
||||
slippagePercentage,
|
||||
gasPrice,
|
||||
MarketOperation.Sell,
|
||||
opts,
|
||||
@ -54,14 +56,12 @@ export class SwapQuoteCalculator {
|
||||
public async calculateMarketBuySwapQuoteAsync(
|
||||
prunedOrders: SignedOrder[],
|
||||
takerAssetFillAmount: BigNumber,
|
||||
slippagePercentage: number,
|
||||
gasPrice: BigNumber,
|
||||
opts: CalculateSwapQuoteOpts,
|
||||
): Promise<MarketBuySwapQuote> {
|
||||
return (await this._calculateSwapQuoteAsync(
|
||||
prunedOrders,
|
||||
takerAssetFillAmount,
|
||||
slippagePercentage,
|
||||
gasPrice,
|
||||
MarketOperation.Buy,
|
||||
opts,
|
||||
@ -71,14 +71,12 @@ export class SwapQuoteCalculator {
|
||||
public async calculateBatchMarketBuySwapQuoteAsync(
|
||||
batchPrunedOrders: SignedOrder[][],
|
||||
takerAssetFillAmounts: BigNumber[],
|
||||
slippagePercentage: number,
|
||||
gasPrice: BigNumber,
|
||||
opts: CalculateSwapQuoteOpts,
|
||||
): Promise<Array<MarketBuySwapQuote | undefined>> {
|
||||
return (await this._calculateBatchBuySwapQuoteAsync(
|
||||
batchPrunedOrders,
|
||||
takerAssetFillAmounts,
|
||||
slippagePercentage,
|
||||
gasPrice,
|
||||
MarketOperation.Buy,
|
||||
opts,
|
||||
@ -88,17 +86,13 @@ export class SwapQuoteCalculator {
|
||||
private async _calculateBatchBuySwapQuoteAsync(
|
||||
batchPrunedOrders: SignedOrder[][],
|
||||
assetFillAmounts: BigNumber[],
|
||||
slippagePercentage: number,
|
||||
gasPrice: BigNumber,
|
||||
operation: MarketOperation,
|
||||
opts: CalculateSwapQuoteOpts,
|
||||
): Promise<Array<SwapQuote | undefined>> {
|
||||
const assetFillAmountsWithSlippage = assetFillAmounts.map(a =>
|
||||
a.plus(a.multipliedBy(slippagePercentage).integerValue()),
|
||||
);
|
||||
const batchSignedOrders = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync(
|
||||
batchPrunedOrders,
|
||||
assetFillAmountsWithSlippage,
|
||||
assetFillAmounts,
|
||||
opts,
|
||||
);
|
||||
const batchSwapQuotes = await Promise.all(
|
||||
@ -112,6 +106,7 @@ export class SwapQuoteCalculator {
|
||||
operation,
|
||||
assetFillAmounts[i],
|
||||
gasPrice,
|
||||
opts.gasSchedule,
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
@ -123,25 +118,23 @@ export class SwapQuoteCalculator {
|
||||
private async _calculateSwapQuoteAsync(
|
||||
prunedOrders: SignedOrder[],
|
||||
assetFillAmount: BigNumber,
|
||||
slippagePercentage: number,
|
||||
gasPrice: BigNumber,
|
||||
operation: MarketOperation,
|
||||
opts: CalculateSwapQuoteOpts,
|
||||
): Promise<SwapQuote> {
|
||||
// checks if maker asset is ERC721 or ERC20 and taker asset is ERC20
|
||||
if (!utils.isSupportedAssetDataInOrders(prunedOrders)) {
|
||||
if (!isSupportedAssetDataInOrders(prunedOrders)) {
|
||||
throw Error(SwapQuoterError.AssetDataUnsupported);
|
||||
}
|
||||
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
|
||||
|
||||
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
|
||||
let resultOrders: OptimizedMarketOrder[] = [];
|
||||
|
||||
{
|
||||
// Scale fees by gas price.
|
||||
const _opts = {
|
||||
...opts,
|
||||
fees: _.mapValues(opts.fees, (v, k) => v.times(gasPrice)),
|
||||
fees: _.mapValues(opts.feeSchedule, v => v.times(gasPrice)),
|
||||
};
|
||||
|
||||
const firstOrderMakerAssetData = !!prunedOrders[0]
|
||||
@ -150,20 +143,18 @@ export class SwapQuoteCalculator {
|
||||
|
||||
if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) {
|
||||
// HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable
|
||||
resultOrders = prunedOrders.map(o =>
|
||||
CreateOrderUtils.convertNativeOrderToFullyFillableOptimizedOrders(o),
|
||||
);
|
||||
resultOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o));
|
||||
} else {
|
||||
if (operation === MarketOperation.Buy) {
|
||||
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
prunedOrders,
|
||||
assetFillAmount.plus(slippageBufferAmount),
|
||||
assetFillAmount,
|
||||
_opts,
|
||||
);
|
||||
} else {
|
||||
resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync(
|
||||
prunedOrders,
|
||||
assetFillAmount.plus(slippageBufferAmount),
|
||||
assetFillAmount,
|
||||
_opts,
|
||||
);
|
||||
}
|
||||
@ -179,6 +170,7 @@ export class SwapQuoteCalculator {
|
||||
operation,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
opts.gasSchedule,
|
||||
);
|
||||
}
|
||||
private async _createSwapQuoteAsync(
|
||||
@ -188,22 +180,25 @@ export class SwapQuoteCalculator {
|
||||
operation: MarketOperation,
|
||||
assetFillAmount: BigNumber,
|
||||
gasPrice: BigNumber,
|
||||
gasSchedule: { [source: string]: number },
|
||||
): Promise<SwapQuote> {
|
||||
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
|
||||
resultOrders,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
gasSchedule,
|
||||
operation,
|
||||
);
|
||||
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
|
||||
resultOrders,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
gasSchedule,
|
||||
operation,
|
||||
true,
|
||||
);
|
||||
|
||||
const breakdown = this._getSwapQuoteOrdersBreakdown(resultOrders, operation);
|
||||
const breakdown = getSwapQuoteOrdersBreakdown(resultOrders, operation);
|
||||
|
||||
const quoteBase: SwapQuoteBase = {
|
||||
takerAssetData,
|
||||
@ -236,14 +231,16 @@ export class SwapQuoteCalculator {
|
||||
orders: OptimizedMarketOrder[],
|
||||
assetFillAmount: BigNumber,
|
||||
gasPrice: BigNumber,
|
||||
gasSchedule: { [source: string]: number },
|
||||
operation: MarketOperation,
|
||||
worstCase: boolean = false,
|
||||
): Promise<SwapQuoteInfo> {
|
||||
if (operation === MarketOperation.Buy) {
|
||||
return this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase);
|
||||
} else {
|
||||
return this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase);
|
||||
}
|
||||
return {
|
||||
...(operation === MarketOperation.Buy
|
||||
? await this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)
|
||||
: await this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)),
|
||||
gas: getGasUsedByOrders(orders, gasSchedule),
|
||||
};
|
||||
}
|
||||
|
||||
private async _calculateMarketSellQuoteInfoAsync(
|
||||
@ -337,6 +334,7 @@ export class SwapQuoteCalculator {
|
||||
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
|
||||
makerAssetAmount: totalMakerAssetAmount,
|
||||
protocolFeeInWeiAmount,
|
||||
gas: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -426,45 +424,37 @@ export class SwapQuoteCalculator {
|
||||
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
|
||||
makerAssetAmount: totalMakerAssetAmount,
|
||||
protocolFeeInWeiAmount,
|
||||
gas: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: prefer-function-over-method
|
||||
private _getSwapQuoteOrdersBreakdown(
|
||||
orders: OptimizedMarketOrder[],
|
||||
operation: MarketOperation,
|
||||
): SwapQuoteOrdersBreakdown {
|
||||
// HACK: to shut up linter
|
||||
const breakdown: SwapQuoteOrdersBreakdown = {};
|
||||
|
||||
// total asset amount (accounting for slippage protection)
|
||||
const totalAssetAmount = BigNumber.sum(
|
||||
...[
|
||||
constants.ZERO_AMOUNT,
|
||||
...orders.map(o => (operation === MarketOperation.Buy ? o.makerAssetAmount : o.takerAssetAmount)),
|
||||
],
|
||||
);
|
||||
|
||||
return orders.reduce((acc: SwapQuoteOrdersBreakdown, order: OptimizedMarketOrder): SwapQuoteOrdersBreakdown => {
|
||||
const assetAmount = operation === MarketOperation.Buy ? order.makerAssetAmount : order.takerAssetAmount;
|
||||
const { source } = order.fill;
|
||||
return {
|
||||
...acc,
|
||||
...{
|
||||
[source]: !!acc[source]
|
||||
? acc[source].plus(assetAmount.dividedBy(totalAssetAmount))
|
||||
: assetAmount.dividedBy(totalAssetAmount),
|
||||
},
|
||||
};
|
||||
}, breakdown);
|
||||
function getSwapQuoteOrdersBreakdown(
|
||||
orders: OptimizedMarketOrder[],
|
||||
operation: MarketOperation,
|
||||
): SwapQuoteOrdersBreakdown {
|
||||
const orderAmounts =
|
||||
operation === MarketOperation.Buy
|
||||
? orders.map(o => o.fill.totalMakerAssetAmount)
|
||||
: orders.map(o => o.fill.totalTakerAssetAmount);
|
||||
const amountsBySource: SwapQuoteOrdersBreakdown = {};
|
||||
orders.forEach((o, i) => {
|
||||
const source = o.fill.source;
|
||||
amountsBySource[source] = orderAmounts[i].plus(amountsBySource[source] || 0);
|
||||
});
|
||||
const totalAmount = BigNumber.sum(0, ...orderAmounts);
|
||||
const breakdown: SwapQuoteOrdersBreakdown = {};
|
||||
for (const [source, amount] of Object.entries(amountsBySource)) {
|
||||
breakdown[source] = amount.div(totalAmount);
|
||||
}
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
function getTakerAssetAmountBreakDown(
|
||||
order: SignedOrderWithFillableAmounts,
|
||||
takerAssetAmountWithFees: BigNumber,
|
||||
): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } {
|
||||
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
|
||||
if (isOrderTakerFeePayableWithTakerAsset(order)) {
|
||||
const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee);
|
||||
const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount);
|
||||
const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL);
|
||||
@ -472,7 +462,7 @@ function getTakerAssetAmountBreakDown(
|
||||
takerAssetAmount,
|
||||
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
|
||||
};
|
||||
} else if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
|
||||
} else if (isOrderTakerFeePayableWithMakerAsset(order)) {
|
||||
if (takerAssetAmountWithFees.isZero()) {
|
||||
return {
|
||||
takerAssetAmount: constants.ZERO_AMOUNT,
|
||||
@ -495,3 +485,12 @@ function getTakerAssetAmountBreakDown(
|
||||
takerAssetAmount: takerAssetAmountWithFees,
|
||||
};
|
||||
}
|
||||
|
||||
function getGasUsedByOrders(orders: OptimizedMarketOrder[], gasSchedule: { [source: string]: number }): number {
|
||||
let totalUsage = 0;
|
||||
for (const order of orders) {
|
||||
totalUsage += gasSchedule[order.fill.source] || 0;
|
||||
}
|
||||
return totalUsage;
|
||||
}
|
||||
// tslint:disable: max-file-line-count
|
||||
|
@ -15,9 +15,9 @@ import {
|
||||
SwapQuoteConsumerError,
|
||||
SwapQuoteExecutionOpts,
|
||||
} from '../types';
|
||||
import { utils } from '../utils/utils';
|
||||
|
||||
import { assert } from './assert';
|
||||
import { isExactAssetData } from './utils';
|
||||
|
||||
export const swapQuoteConsumerUtils = {
|
||||
async getTakerAddressOrThrowAsync(
|
||||
@ -66,7 +66,7 @@ export const swapQuoteConsumerUtils = {
|
||||
return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData));
|
||||
},
|
||||
isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean {
|
||||
return utils.isExactAssetData(order.takerAssetData, wethAssetData);
|
||||
return isExactAssetData(order.takerAssetData, wethAssetData);
|
||||
},
|
||||
async getExtensionContractTypeForSwapQuoteAsync(
|
||||
quote: SwapQuote,
|
||||
|
@ -5,103 +5,111 @@ import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
|
||||
import { constants } from '../constants';
|
||||
|
||||
// tslint:disable:no-unnecessary-type-assertion
|
||||
export const utils = {
|
||||
isSupportedAssetDataInOrders(orders: SignedOrder[]): boolean {
|
||||
const firstOrderMakerAssetData = !!orders[0]
|
||||
? assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData)
|
||||
: { assetProxyId: '' };
|
||||
return orders.every(o => {
|
||||
const takerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.takerAssetData);
|
||||
const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.makerAssetData);
|
||||
return (
|
||||
(makerAssetData.assetProxyId === AssetProxyId.ERC20 ||
|
||||
makerAssetData.assetProxyId === AssetProxyId.ERC721) &&
|
||||
takerAssetData.assetProxyId === AssetProxyId.ERC20 &&
|
||||
firstOrderMakerAssetData.assetProxyId === makerAssetData.assetProxyId
|
||||
); // checks that all native order maker assets are of the same type
|
||||
});
|
||||
},
|
||||
numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber {
|
||||
return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy(
|
||||
percentage,
|
||||
);
|
||||
},
|
||||
isOrderTakerFeePayableWithMakerAsset<T extends Order>(order: T): boolean {
|
||||
return !order.takerFee.isZero() && utils.isAssetDataEquivalent(order.takerFeeAssetData, order.makerAssetData);
|
||||
},
|
||||
isOrderTakerFeePayableWithTakerAsset<T extends Order>(order: T): boolean {
|
||||
return !order.takerFee.isZero() && utils.isAssetDataEquivalent(order.takerFeeAssetData, order.takerAssetData);
|
||||
},
|
||||
getAdjustedMakerAndTakerAmountsFromTakerFees<T extends Order>(order: T): [BigNumber, BigNumber] {
|
||||
const adjustedMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)
|
||||
? order.makerAssetAmount.minus(order.takerFee)
|
||||
: order.makerAssetAmount;
|
||||
const adjustedTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order)
|
||||
? order.takerAssetAmount.plus(order.takerFee)
|
||||
: order.takerAssetAmount;
|
||||
return [adjustedMakerAssetAmount, adjustedTakerAssetAmount];
|
||||
},
|
||||
isExactAssetData(expectedAssetData: string, actualAssetData: string): boolean {
|
||||
return expectedAssetData === actualAssetData;
|
||||
},
|
||||
/**
|
||||
* Compare the Asset Data for equivalency. Expected is the asset data the user provided (wanted),
|
||||
* actual is the asset data found or created.
|
||||
*/
|
||||
isAssetDataEquivalent(expectedAssetData: string, actualAssetData: string): boolean {
|
||||
if (utils.isExactAssetData(expectedAssetData, actualAssetData)) {
|
||||
return true;
|
||||
}
|
||||
const decodedExpectedAssetData = assetDataUtils.decodeAssetDataOrThrow(expectedAssetData);
|
||||
const decodedActualAssetData = assetDataUtils.decodeAssetDataOrThrow(actualAssetData);
|
||||
// ERC20 === ERC20, ERC20 === ERC20Bridge
|
||||
if (
|
||||
utils.isERC20EquivalentAssetData(decodedExpectedAssetData) &&
|
||||
utils.isERC20EquivalentAssetData(decodedActualAssetData)
|
||||
) {
|
||||
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
|
||||
return doesTokenAddressMatch;
|
||||
}
|
||||
// ERC1155 === ERC1155
|
||||
if (
|
||||
assetDataUtils.isERC1155TokenAssetData(decodedExpectedAssetData) &&
|
||||
assetDataUtils.isERC1155TokenAssetData(decodedActualAssetData)
|
||||
) {
|
||||
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
|
||||
// IDs may be out of order yet still equivalent
|
||||
// i.e (["a", "b"], [1,2]) === (["b", "a"], [2, 1])
|
||||
// (["a", "b"], [2,1]) !== (["b", "a"], [2, 1])
|
||||
const hasAllIds = decodedExpectedAssetData.tokenIds.every(
|
||||
id => decodedActualAssetData.tokenIds.findIndex(v => id.eq(v)) !== -1,
|
||||
);
|
||||
const hasAllValues = decodedExpectedAssetData.tokenIds.every((id, i) =>
|
||||
decodedExpectedAssetData.tokenValues[i].eq(
|
||||
decodedActualAssetData.tokenValues[decodedActualAssetData.tokenIds.findIndex(v => id.eq(v))],
|
||||
),
|
||||
);
|
||||
// If expected contains callback data, ensure it is present
|
||||
// if actual has callbackdata and expected provided none then ignore it
|
||||
const hasEquivalentCallback =
|
||||
decodedExpectedAssetData.callbackData === NULL_BYTES ||
|
||||
decodedExpectedAssetData.callbackData === decodedActualAssetData.callbackData;
|
||||
return doesTokenAddressMatch && hasAllIds && hasAllValues && hasEquivalentCallback;
|
||||
}
|
||||
// ERC721 === ERC721
|
||||
if (
|
||||
assetDataUtils.isERC721TokenAssetData(decodedExpectedAssetData) ||
|
||||
assetDataUtils.isERC721TokenAssetData(decodedActualAssetData)
|
||||
) {
|
||||
// Asset Data should exactly match for ERC721
|
||||
return utils.isExactAssetData(expectedAssetData, actualAssetData);
|
||||
}
|
||||
// tslint:disable: no-unnecessary-type-assertion completed-docs
|
||||
|
||||
// TODO(dekz): Unsupported cases
|
||||
// ERCXX(token) === MAP(token, staticCall)
|
||||
// MAP(a, b) === MAP(b, a) === MAP(b, a, staticCall)
|
||||
return false;
|
||||
},
|
||||
isERC20EquivalentAssetData(assetData: AssetData): assetData is ERC20AssetData | ERC20BridgeAssetData {
|
||||
return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData);
|
||||
},
|
||||
};
|
||||
export function isSupportedAssetDataInOrders(orders: SignedOrder[]): boolean {
|
||||
const firstOrderMakerAssetData = !!orders[0]
|
||||
? assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData)
|
||||
: { assetProxyId: '' };
|
||||
return orders.every(o => {
|
||||
const takerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.takerAssetData);
|
||||
const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.makerAssetData);
|
||||
return (
|
||||
(makerAssetData.assetProxyId === AssetProxyId.ERC20 ||
|
||||
makerAssetData.assetProxyId === AssetProxyId.ERC721) &&
|
||||
takerAssetData.assetProxyId === AssetProxyId.ERC20 &&
|
||||
firstOrderMakerAssetData.assetProxyId === makerAssetData.assetProxyId
|
||||
); // checks that all native order maker assets are of the same type
|
||||
});
|
||||
}
|
||||
|
||||
export function numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber {
|
||||
return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy(percentage);
|
||||
}
|
||||
|
||||
export function isOrderTakerFeePayableWithMakerAsset<T extends Order>(order: T): boolean {
|
||||
return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.makerAssetData);
|
||||
}
|
||||
|
||||
export function isOrderTakerFeePayableWithTakerAsset<T extends Order>(order: T): boolean {
|
||||
return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.takerAssetData);
|
||||
}
|
||||
|
||||
export function getAdjustedMakerAndTakerAmountsFromTakerFees<T extends Order>(order: T): [BigNumber, BigNumber] {
|
||||
const adjustedMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order)
|
||||
? order.makerAssetAmount.minus(order.takerFee)
|
||||
: order.makerAssetAmount;
|
||||
const adjustedTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order)
|
||||
? order.takerAssetAmount.plus(order.takerFee)
|
||||
: order.takerAssetAmount;
|
||||
return [adjustedMakerAssetAmount, adjustedTakerAssetAmount];
|
||||
}
|
||||
|
||||
export function isExactAssetData(expectedAssetData: string, actualAssetData: string): boolean {
|
||||
return expectedAssetData === actualAssetData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the Asset Data for equivalency. Expected is the asset data the user provided (wanted),
|
||||
* actual is the asset data found or created.
|
||||
*/
|
||||
export function isAssetDataEquivalent(expectedAssetData: string, actualAssetData: string): boolean {
|
||||
if (isExactAssetData(expectedAssetData, actualAssetData)) {
|
||||
return true;
|
||||
}
|
||||
const decodedExpectedAssetData = assetDataUtils.decodeAssetDataOrThrow(expectedAssetData);
|
||||
const decodedActualAssetData = assetDataUtils.decodeAssetDataOrThrow(actualAssetData);
|
||||
// ERC20 === ERC20, ERC20 === ERC20Bridge
|
||||
if (isERC20EquivalentAssetData(decodedExpectedAssetData) && isERC20EquivalentAssetData(decodedActualAssetData)) {
|
||||
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
|
||||
return doesTokenAddressMatch;
|
||||
}
|
||||
// ERC1155 === ERC1155
|
||||
if (
|
||||
assetDataUtils.isERC1155TokenAssetData(decodedExpectedAssetData) &&
|
||||
assetDataUtils.isERC1155TokenAssetData(decodedActualAssetData)
|
||||
) {
|
||||
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
|
||||
// IDs may be out of order yet still equivalent
|
||||
// i.e (["a", "b"], [1,2]) === (["b", "a"], [2, 1])
|
||||
// (["a", "b"], [2,1]) !== (["b", "a"], [2, 1])
|
||||
const hasAllIds = decodedExpectedAssetData.tokenIds.every(
|
||||
id => decodedActualAssetData.tokenIds.findIndex(v => id.eq(v)) !== -1,
|
||||
);
|
||||
const hasAllValues = decodedExpectedAssetData.tokenIds.every((id, i) =>
|
||||
decodedExpectedAssetData.tokenValues[i].eq(
|
||||
decodedActualAssetData.tokenValues[decodedActualAssetData.tokenIds.findIndex(v => id.eq(v))],
|
||||
),
|
||||
);
|
||||
// If expected contains callback data, ensure it is present
|
||||
// if actual has callbackdata and expected provided none then ignore it
|
||||
const hasEquivalentCallback =
|
||||
decodedExpectedAssetData.callbackData === NULL_BYTES ||
|
||||
decodedExpectedAssetData.callbackData === decodedActualAssetData.callbackData;
|
||||
return doesTokenAddressMatch && hasAllIds && hasAllValues && hasEquivalentCallback;
|
||||
}
|
||||
// ERC721 === ERC721
|
||||
if (
|
||||
assetDataUtils.isERC721TokenAssetData(decodedExpectedAssetData) ||
|
||||
assetDataUtils.isERC721TokenAssetData(decodedActualAssetData)
|
||||
) {
|
||||
// Asset Data should exactly match for ERC721
|
||||
return isExactAssetData(expectedAssetData, actualAssetData);
|
||||
}
|
||||
|
||||
// TODO(dekz): Unsupported cases
|
||||
// ERCXX(token) === MAP(token, staticCall)
|
||||
// MAP(a, b) === MAP(b, a) === MAP(b, a, staticCall)
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isERC20EquivalentAssetData(assetData: AssetData): assetData is ERC20AssetData | ERC20BridgeAssetData {
|
||||
return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the difference between two sets.
|
||||
*/
|
||||
export function difference<T>(a: T[], b: T[]): T[] {
|
||||
return a.filter(x => b.indexOf(x) === -1);
|
||||
}
|
||||
|
@ -14,14 +14,11 @@ import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types';
|
||||
import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants as assetSwapperConstants } from '../src/constants';
|
||||
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
||||
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
|
||||
import { BUY_SOURCES, DEFAULT_CURVE_OPTS, SELL_SOURCES } from '../src/utils/market_operation_utils/constants';
|
||||
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
|
||||
import { DexSample, ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
|
||||
|
||||
const { BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants;
|
||||
|
||||
// tslint:disable: custom-no-magic-numbers
|
||||
describe('MarketOperationUtils tests', () => {
|
||||
const CHAIN_ID = 1;
|
||||
@ -81,8 +78,8 @@ describe('MarketOperationUtils tests', () => {
|
||||
case UNISWAP_BRIDGE_ADDRESS.toLowerCase():
|
||||
return ERC20BridgeSource.Uniswap;
|
||||
case CURVE_BRIDGE_ADDRESS.toLowerCase():
|
||||
const curveSource = Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS).filter(
|
||||
k => assetData.indexOf(assetSwapperConstants.DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
|
||||
const curveSource = Object.keys(DEFAULT_CURVE_OPTS).filter(
|
||||
k => assetData.indexOf(DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
|
||||
);
|
||||
return curveSource[0] as ERC20BridgeSource;
|
||||
default:
|
||||
@ -120,20 +117,21 @@ describe('MarketOperationUtils tests', () => {
|
||||
chainId: CHAIN_ID,
|
||||
};
|
||||
|
||||
type GetQuotesOperation = (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => BigNumber[];
|
||||
|
||||
function createGetSellQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
|
||||
return (...args) => {
|
||||
const fillAmounts = args.pop() as BigNumber[];
|
||||
return fillAmounts.map((a, i) => a.times(rates[i]).integerValue());
|
||||
};
|
||||
}
|
||||
|
||||
function createGetBuyQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
|
||||
return (...args) => {
|
||||
const fillAmounts = args.pop() as BigNumber[];
|
||||
return fillAmounts.map((a, i) => a.div(rates[i]).integerValue());
|
||||
};
|
||||
function createSamplesFromRates(source: ERC20BridgeSource, inputs: Numberish[], rates: Numberish[]): DexSample[] {
|
||||
const samples: DexSample[] = [];
|
||||
inputs.forEach((input, i) => {
|
||||
const rate = rates[i];
|
||||
samples.push({
|
||||
source,
|
||||
input: new BigNumber(input),
|
||||
output: new BigNumber(input)
|
||||
.minus(i === 0 ? 0 : samples[i - 1].input)
|
||||
.times(rate)
|
||||
.plus(i === 0 ? 0 : samples[i - 1].output)
|
||||
.integerValue(),
|
||||
});
|
||||
});
|
||||
return samples;
|
||||
}
|
||||
|
||||
type GetMultipleQuotesOperation = (
|
||||
@ -146,13 +144,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
|
||||
function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
|
||||
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
|
||||
return sources.map(s =>
|
||||
fillAmounts.map((a, i) => ({
|
||||
source: s,
|
||||
input: a,
|
||||
output: a.times(rates[s][i]).integerValue(),
|
||||
})),
|
||||
);
|
||||
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
|
||||
};
|
||||
}
|
||||
|
||||
@ -180,13 +172,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
|
||||
function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
|
||||
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
|
||||
return sources.map(s =>
|
||||
fillAmounts.map((a, i) => ({
|
||||
source: s,
|
||||
input: a,
|
||||
output: a.div(rates[s][i]).integerValue(),
|
||||
})),
|
||||
);
|
||||
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))));
|
||||
};
|
||||
}
|
||||
|
||||
@ -264,22 +250,6 @@ describe('MarketOperationUtils tests', () => {
|
||||
[ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0),
|
||||
};
|
||||
|
||||
function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource {
|
||||
const minSourceRates = Object.keys(rates).map(s => _.last(rates[s]) as BigNumber);
|
||||
const bestSourceRate = BigNumber.max(...minSourceRates);
|
||||
let source = Object.keys(rates)[_.findIndex(minSourceRates, t => bestSourceRate.eq(t))] as ERC20BridgeSource;
|
||||
// Native order rates play by different rules.
|
||||
if (source !== ERC20BridgeSource.Native) {
|
||||
const nativeTotalRate = BigNumber.sum(...rates[ERC20BridgeSource.Native]).div(
|
||||
rates[ERC20BridgeSource.Native].length,
|
||||
);
|
||||
if (nativeTotalRate.gt(bestSourceRate)) {
|
||||
source = ERC20BridgeSource.Native;
|
||||
}
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
const DEFAULT_OPS = {
|
||||
getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] {
|
||||
return orders.map(o => o.takerAssetAmount);
|
||||
@ -287,12 +257,6 @@ describe('MarketOperationUtils tests', () => {
|
||||
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
|
||||
return orders.map(o => o.makerAssetAmount);
|
||||
},
|
||||
getKyberSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Kyber]),
|
||||
getUniswapSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
|
||||
getEth2DaiSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
|
||||
getUniswapBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
|
||||
getEth2DaiBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
|
||||
getCurveSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.CurveUsdcDai]),
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
|
||||
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
|
||||
getMedianSellRate: createGetMedianSellRate(1),
|
||||
@ -323,17 +287,18 @@ describe('MarketOperationUtils tests', () => {
|
||||
});
|
||||
|
||||
describe('getMarketSellOrdersAsync()', () => {
|
||||
const FILL_AMOUNT = getRandomInteger(1, 1e18);
|
||||
const FILL_AMOUNT = new BigNumber('100e18');
|
||||
const ORDERS = createOrdersFromSellRates(
|
||||
FILL_AMOUNT,
|
||||
_.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]),
|
||||
);
|
||||
const DEFAULT_OPTS = {
|
||||
numSamples: NUM_SAMPLES,
|
||||
runLimit: 0,
|
||||
sampleDistributionBase: 1,
|
||||
bridgeSlippage: 0,
|
||||
excludedSources: Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||
maxFallbackSlippage: 100,
|
||||
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||
allowFallback: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@ -341,7 +306,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
});
|
||||
|
||||
it('queries `numSamples` samples', async () => {
|
||||
const numSamples = _.random(1, 16);
|
||||
const numSamples = _.random(1, NUM_SAMPLES);
|
||||
let actualNumSamples = 0;
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: (sources, makerToken, takerToken, amounts) => {
|
||||
@ -412,18 +377,6 @@ describe('MarketOperationUtils tests', () => {
|
||||
expect(sourcesPolled.sort()).to.deep.eq(_.without(SELL_SOURCES, ...excludedSources).sort());
|
||||
});
|
||||
|
||||
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
|
||||
const bestSource = findSourceWithMaxOutput(DEFAULT_RATES);
|
||||
expect(bestSource).to.exist('');
|
||||
const improvedOrders = await marketOperationUtils.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 marketOperationUtils.getMarketSellOrdersAsync(
|
||||
// Pass in empty orders to prevent native orders from being used.
|
||||
@ -469,10 +422,9 @@ describe('MarketOperationUtils tests', () => {
|
||||
);
|
||||
expect(improvedOrders).to.not.be.length(0);
|
||||
for (const order of improvedOrders) {
|
||||
const source = getSourceFromAssetData(order.makerAssetData);
|
||||
const expectedMakerAmount = FILL_AMOUNT.times(_.last(DEFAULT_RATES[source]) as BigNumber);
|
||||
const expectedMakerAmount = order.fill.totalMakerAssetAmount;
|
||||
const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber();
|
||||
assertRoughlyEquals(slippage, bridgeSlippage, 8);
|
||||
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
||||
}
|
||||
});
|
||||
|
||||
@ -481,26 +433,26 @@ describe('MarketOperationUtils tests', () => {
|
||||
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];
|
||||
rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Kyber,
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
];
|
||||
expect(orderSources).to.deep.eq(expectedSources);
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
it('excludes Kyber when `noConflicts` enabled and Uniswap or Eth2Dai are used first', async () => {
|
||||
it('Kyber is exclusive against Uniswap and Eth2Dai', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Native] = [0.3, 0.2, 0.1, 0.05];
|
||||
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
|
||||
@ -512,40 +464,15 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
];
|
||||
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.1, 0.05, 0.05, 0.05];
|
||||
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];
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Kyber,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
];
|
||||
expect(orderSources).to.deep.eq(expectedSources);
|
||||
if (orderSources.includes(ERC20BridgeSource.Kyber)) {
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap);
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai);
|
||||
} else {
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Kyber);
|
||||
}
|
||||
});
|
||||
|
||||
const ETH_TO_MAKER_RATE = 1.5;
|
||||
@ -555,12 +482,12 @@ describe('MarketOperationUtils tests', () => {
|
||||
// dropping their effective rates.
|
||||
const nativeFeeRate = 0.06;
|
||||
const rates: RatesBySource = {
|
||||
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91]
|
||||
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, 0.93, 0.92, 0.91]
|
||||
[ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1],
|
||||
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
|
||||
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
|
||||
};
|
||||
const fees = {
|
||||
const feeSchedule = {
|
||||
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
|
||||
.times(nativeFeeRate)
|
||||
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
|
||||
@ -572,32 +499,32 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
];
|
||||
expect(orderSources).to.deep.eq(expectedSources);
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
it('factors in fees for dexes', async () => {
|
||||
// Kyber will have the best rates but will have fees,
|
||||
// dropping its effective rates.
|
||||
const kyberFeeRate = 0.2;
|
||||
const uniswapFeeRate = 0.2;
|
||||
const rates: RatesBySource = {
|
||||
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
|
||||
[ERC20BridgeSource.Uniswap]: [0.1, 0.1, 0.1, 0.1],
|
||||
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
|
||||
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
|
||||
// Effectively [0.8, ~0.5, ~0, ~0]
|
||||
[ERC20BridgeSource.Kyber]: [1, 0.7, 0.2, 0.2],
|
||||
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
|
||||
};
|
||||
const fees = {
|
||||
[ERC20BridgeSource.Kyber]: FILL_AMOUNT.div(4)
|
||||
.times(kyberFeeRate)
|
||||
const feeSchedule = {
|
||||
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
|
||||
.times(uniswapFeeRate)
|
||||
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
|
||||
};
|
||||
replaceSamplerOps({
|
||||
@ -607,11 +534,87 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
|
||||
expect(orderSources).to.deep.eq(expectedSources);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
];
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
it('can mix one concave source', async () => {
|
||||
const rates: RatesBySource = {
|
||||
[ERC20BridgeSource.Kyber]: [0, 0, 0, 0], // Won't use
|
||||
[ERC20BridgeSource.Eth2Dai]: [0.5, 0.85, 0.75, 0.75], // Concave
|
||||
[ERC20BridgeSource.Uniswap]: [0.96, 0.2, 0.1, 0.1],
|
||||
[ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1],
|
||||
};
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
||||
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
ERC20BridgeSource.Native,
|
||||
];
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
it('fallback orders use different sources', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5];
|
||||
rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const firstSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
];
|
||||
const secondSources = [ERC20BridgeSource.Eth2Dai];
|
||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
||||
});
|
||||
|
||||
it('does not create a fallback if below maxFallbackSlippage', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
|
||||
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||
const secondSources: ERC20BridgeSource[] = [];
|
||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
||||
});
|
||||
|
||||
it('is able to create a order from LiquidityProvider', async () => {
|
||||
@ -651,7 +654,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
}),
|
||||
],
|
||||
Web3Wrapper.toBaseUnitAmount(10, 18),
|
||||
{ excludedSources: SELL_SOURCES, numSamples: 4 },
|
||||
{ excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0 },
|
||||
);
|
||||
expect(result.length).to.eql(1);
|
||||
expect(result[0].makerAddress).to.eql(liquidityProviderAddress);
|
||||
@ -666,22 +669,24 @@ describe('MarketOperationUtils tests', () => {
|
||||
expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider);
|
||||
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
|
||||
expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress);
|
||||
expect(getLiquidityProviderParams.makerToken).is.eql(xAsset);
|
||||
expect(getLiquidityProviderParams.takerToken).is.eql(yAsset);
|
||||
expect(getLiquidityProviderParams.makerToken).is.eql(yAsset);
|
||||
expect(getLiquidityProviderParams.takerToken).is.eql(xAsset);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMarketBuyOrdersAsync()', () => {
|
||||
const FILL_AMOUNT = getRandomInteger(1, 1e18);
|
||||
const FILL_AMOUNT = new BigNumber('100e18');
|
||||
const ORDERS = createOrdersFromBuyRates(
|
||||
FILL_AMOUNT,
|
||||
_.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]),
|
||||
);
|
||||
const DEFAULT_OPTS = {
|
||||
numSamples: NUM_SAMPLES,
|
||||
runLimit: 0,
|
||||
sampleDistributionBase: 1,
|
||||
excludedSources: Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||
bridgeSlippage: 0,
|
||||
maxFallbackSlippage: 100,
|
||||
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||
allowFallback: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@ -760,26 +765,6 @@ describe('MarketOperationUtils tests', () => {
|
||||
expect(sourcesPolled).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources));
|
||||
});
|
||||
|
||||
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
|
||||
const bestSource = findSourceWithMaxOutput(
|
||||
_.omit(
|
||||
DEFAULT_RATES,
|
||||
ERC20BridgeSource.Kyber,
|
||||
ERC20BridgeSource.CurveUsdcDai,
|
||||
ERC20BridgeSource.CurveUsdcDaiUsdt,
|
||||
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
|
||||
),
|
||||
);
|
||||
expect(bestSource).to.exist('');
|
||||
const improvedOrders = await marketOperationUtils.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 marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
// Pass in empty orders to prevent native orders from being used.
|
||||
@ -825,10 +810,9 @@ describe('MarketOperationUtils tests', () => {
|
||||
);
|
||||
expect(improvedOrders).to.not.be.length(0);
|
||||
for (const order of improvedOrders) {
|
||||
const source = getSourceFromAssetData(order.makerAssetData);
|
||||
const expectedTakerAmount = FILL_AMOUNT.div(_.last(DEFAULT_RATES[source]) as BigNumber);
|
||||
const expectedTakerAmount = order.fill.totalTakerAssetAmount;
|
||||
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1;
|
||||
assertRoughlyEquals(slippage, bridgeSlippage, 8);
|
||||
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
||||
}
|
||||
});
|
||||
|
||||
@ -843,7 +827,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
@ -852,7 +836,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
];
|
||||
expect(orderSources).to.deep.eq(expectedSources);
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
const ETH_TO_TAKER_RATE = 1.5;
|
||||
@ -867,7 +851,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
|
||||
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
|
||||
};
|
||||
const fees = {
|
||||
const feeSchedule = {
|
||||
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
|
||||
.times(nativeFeeRate)
|
||||
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
|
||||
@ -879,7 +863,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
@ -888,7 +872,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
];
|
||||
expect(orderSources).to.deep.eq(expectedSources);
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
it('factors in fees for dexes', async () => {
|
||||
@ -901,7 +885,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
|
||||
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
|
||||
};
|
||||
const fees = {
|
||||
const feeSchedule = {
|
||||
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
|
||||
.times(uniswapFeeRate)
|
||||
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
|
||||
@ -913,7 +897,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const expectedSources = [
|
||||
@ -921,7 +905,52 @@ describe('MarketOperationUtils tests', () => {
|
||||
ERC20BridgeSource.Eth2Dai,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
];
|
||||
expect(orderSources).to.deep.eq(expectedSources);
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
it('fallback orders use different sources', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5];
|
||||
rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
|
||||
replaceSamplerOps({
|
||||
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const firstSources = [
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
];
|
||||
const secondSources = [ERC20BridgeSource.Eth2Dai];
|
||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
||||
});
|
||||
|
||||
it('does not create a fallback if below maxFallbackSlippage', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01];
|
||||
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
|
||||
replaceSamplerOps({
|
||||
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
||||
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||
const secondSources: ERC20BridgeSource[] = [];
|
||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,7 @@
|
||||
// tslint:disable:max-file-line-count
|
||||
// TODO(dorothy-zbornak): Skipping these tests for now because they're a
|
||||
// nightmare to maintain. We should replace them with simpler unit tests.
|
||||
/*
|
||||
import { constants as devConstants } from '@0x/contracts-test-utils';
|
||||
import { BlockchainLifecycle } from '@0x/dev-utils';
|
||||
import { ContractAddresses, migrateOnceAsync } from '@0x/migrations';
|
||||
@ -8,8 +12,9 @@ import 'mocha';
|
||||
|
||||
import { constants } from '../src/constants';
|
||||
import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types';
|
||||
import { DexOrderSampler, MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
||||
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
|
||||
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
||||
import { DEFAULT_GET_MARKET_ORDERS_OPTS, SELL_SOURCES } from '../src/utils/market_operation_utils/constants';
|
||||
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
|
||||
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
|
||||
import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator';
|
||||
|
||||
@ -33,8 +38,6 @@ const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
|
||||
// );
|
||||
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,
|
||||
@ -64,10 +67,7 @@ function createSamplerFromSignedOrdersWithFillableAmounts(
|
||||
);
|
||||
}
|
||||
|
||||
// tslint:disable:max-file-line-count
|
||||
// tslint:disable:custom-no-magic-numbers
|
||||
// TODO(dorothy-zbornak): Skipping these tests for now because they're a
|
||||
// nightmare to maintain. We should replace them with simpler unit tests.
|
||||
describe.skip('swapQuoteCalculator', () => {
|
||||
let protocolFeeUtils: ProtocolFeeUtils;
|
||||
let contractAddresses: ContractAddresses;
|
||||
@ -905,3 +905,4 @@ describe.skip('swapQuoteCalculator', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
@ -25,6 +25,7 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||
takerAssetAmount: totalTakerAssetAmount,
|
||||
totalTakerAssetAmount,
|
||||
protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice),
|
||||
gas: 200e3,
|
||||
};
|
||||
|
||||
const breakdown = {
|
||||
|
@ -4,7 +4,7 @@ import { BigNumber, NULL_ADDRESS, NULL_BYTES } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import { utils } from '../src/utils/utils';
|
||||
import { isAssetDataEquivalent } from '../src/utils/utils';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
|
||||
@ -16,35 +16,35 @@ describe('utils', () => {
|
||||
describe('ERC20', () => {
|
||||
const [tokenA, tokenB] = tokenUtils.getDummyERC20TokenAddresses();
|
||||
it('should succeed ERC20 to be ERC20Bridge', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC20AssetData(tokenA),
|
||||
assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should succeed ERC20Bridge to be ERC20', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES),
|
||||
assetDataUtils.encodeERC20AssetData(tokenA),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should succeed ERC20 to be ERC20', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC20AssetData(tokenA),
|
||||
assetDataUtils.encodeERC20AssetData(tokenA),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should fail if ERC20Bridge is not the same ERC20 token', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC20AssetData(tokenA),
|
||||
assetDataUtils.encodeERC20BridgeAssetData(tokenB, NULL_ADDRESS, NULL_BYTES),
|
||||
);
|
||||
expect(isEquivalent).to.be.false();
|
||||
});
|
||||
it('should fail if ERC20 is not the same ERC20 token', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC20AssetData(tokenA),
|
||||
assetDataUtils.encodeERC20AssetData(tokenB),
|
||||
);
|
||||
@ -56,28 +56,28 @@ describe('utils', () => {
|
||||
const tokenIdA = new BigNumber(1);
|
||||
const tokenIdB = new BigNumber(2);
|
||||
it('should succeed if ERC721 the same ERC721 token and id', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
|
||||
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should fail if ERC721 is not the same ERC721 token', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
|
||||
assetDataUtils.encodeERC721AssetData(tokenB, tokenIdA),
|
||||
);
|
||||
expect(isEquivalent).to.be.false();
|
||||
});
|
||||
it('should fail if ERC721 is not the same ERC721 id', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
|
||||
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdB),
|
||||
);
|
||||
expect(isEquivalent).to.be.false();
|
||||
});
|
||||
it('should fail if ERC721 is compared with ERC20', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
|
||||
assetDataUtils.encodeERC20AssetData(tokenA),
|
||||
);
|
||||
@ -91,49 +91,49 @@ describe('utils', () => {
|
||||
const valueA = new BigNumber(1);
|
||||
const valueB = new BigNumber(2);
|
||||
it('should succeed if ERC1155 is the same ERC1155 token and id', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES),
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should succeed if ERC1155 is the same ERC1155 token and ids', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should succeed if ERC1155 is the same ERC1155 token and ids in different orders', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should succeed if ERC1155 is the same ERC1155 token and ids with different callback data', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], tokenA),
|
||||
);
|
||||
expect(isEquivalent).to.be.true();
|
||||
});
|
||||
it('should fail if ERC1155 contains different ids', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
|
||||
assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA], [valueB], NULL_BYTES),
|
||||
);
|
||||
expect(isEquivalent).to.be.false();
|
||||
});
|
||||
it('should fail if ERC1155 is a different ERC1155 token', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
|
||||
assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
|
||||
);
|
||||
expect(isEquivalent).to.be.false();
|
||||
});
|
||||
it('should fail if expected ERC1155 has different callback data', () => {
|
||||
const isEquivalent = utils.isAssetDataEquivalent(
|
||||
const isEquivalent = isAssetDataEquivalent(
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], tokenA),
|
||||
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
|
||||
);
|
||||
|
@ -9,6 +9,10 @@
|
||||
{
|
||||
"note": "Fix ERC721 asset support",
|
||||
"pr": 2491
|
||||
},
|
||||
{
|
||||
"note": "Remove `slippagePercentage` SwapQuoter config.",
|
||||
"pr": 2513
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -30,8 +30,6 @@ export const ONE_SECOND_MS = 1000;
|
||||
export const ONE_MINUTE_MS = ONE_SECOND_MS * 60;
|
||||
export const GIT_SHA = process.env.GIT_SHA;
|
||||
export const NODE_ENV = process.env.NODE_ENV;
|
||||
export const ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE = 0.2;
|
||||
export const ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE = 0;
|
||||
export const NPM_PACKAGE_VERSION = process.env.NPM_PACKAGE_VERSION;
|
||||
export const DEFAULT_UNKOWN_ASSET_NAME = '???';
|
||||
export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5;
|
||||
|
@ -4,7 +4,6 @@ import { BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE, ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE } from '../constants';
|
||||
import { Action, actions } from '../redux/actions';
|
||||
import { Asset, QuoteFetchOrigin } from '../types';
|
||||
|
||||
@ -37,10 +36,6 @@ export const swapQuoteUpdater = {
|
||||
}
|
||||
const wethAssetData = await swapQuoter.getEtherTokenAssetDataOrThrowAsync();
|
||||
let newSwapQuote: MarketBuySwapQuote | undefined;
|
||||
const slippagePercentage =
|
||||
asset.metaData.assetProxyId === AssetProxyId.ERC20
|
||||
? ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE
|
||||
: ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE;
|
||||
try {
|
||||
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
|
||||
newSwapQuote = await swapQuoter.getMarketBuySwapQuoteForAssetDataAsync(
|
||||
@ -48,7 +43,6 @@ export const swapQuoteUpdater = {
|
||||
wethAssetData,
|
||||
baseUnitValue,
|
||||
{
|
||||
slippagePercentage,
|
||||
gasPrice: gasInfo.gasPriceInWei,
|
||||
// Only use native orders
|
||||
// excludedSources: [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber, ERC20BridgeSource.Uniswap],
|
||||
|
Loading…
x
Reference in New Issue
Block a user