Refactor asset-swapper
This commit is contained in:
parent
f089f5d87f
commit
c6d738ed0c
@ -39,7 +39,7 @@
|
|||||||
"publish:private": "yarn build && gitpkg publish"
|
"publish:private": "yarn build && gitpkg publish"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter",
|
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature",
|
||||||
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
|
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
|
||||||
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json"
|
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json"
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,7 @@ import * as ISimpleFunctionRegistryFeature from '../generated-artifacts/ISimpleF
|
|||||||
import * as ITokenSpenderFeature from '../generated-artifacts/ITokenSpenderFeature.json';
|
import * as ITokenSpenderFeature from '../generated-artifacts/ITokenSpenderFeature.json';
|
||||||
import * as ITransformERC20Feature from '../generated-artifacts/ITransformERC20Feature.json';
|
import * as ITransformERC20Feature from '../generated-artifacts/ITransformERC20Feature.json';
|
||||||
import * as IZeroEx from '../generated-artifacts/IZeroEx.json';
|
import * as IZeroEx from '../generated-artifacts/IZeroEx.json';
|
||||||
|
import * as LiquidityProviderFeature from '../generated-artifacts/LiquidityProviderFeature.json';
|
||||||
import * as LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json';
|
import * as LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json';
|
||||||
import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json';
|
import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json';
|
||||||
import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json';
|
import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json';
|
||||||
@ -52,4 +53,5 @@ export const artifacts = {
|
|||||||
MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact,
|
MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact,
|
||||||
LogMetadataTransformer: LogMetadataTransformer as ContractArtifact,
|
LogMetadataTransformer: LogMetadataTransformer as ContractArtifact,
|
||||||
BridgeAdapter: BridgeAdapter as ContractArtifact,
|
BridgeAdapter: BridgeAdapter as ContractArtifact,
|
||||||
|
LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact,
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ export * from '../generated-wrappers/i_token_spender_feature';
|
|||||||
export * from '../generated-wrappers/i_transform_erc20_feature';
|
export * from '../generated-wrappers/i_transform_erc20_feature';
|
||||||
export * from '../generated-wrappers/i_zero_ex';
|
export * from '../generated-wrappers/i_zero_ex';
|
||||||
export * from '../generated-wrappers/initial_migration';
|
export * from '../generated-wrappers/initial_migration';
|
||||||
|
export * from '../generated-wrappers/liquidity_provider_feature';
|
||||||
export * from '../generated-wrappers/log_metadata_transformer';
|
export * from '../generated-wrappers/log_metadata_transformer';
|
||||||
export * from '../generated-wrappers/meta_transactions_feature';
|
export * from '../generated-wrappers/meta_transactions_feature';
|
||||||
export * from '../generated-wrappers/ownable_feature';
|
export * from '../generated-wrappers/ownable_feature';
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"generated-artifacts/ITransformERC20Feature.json",
|
"generated-artifacts/ITransformERC20Feature.json",
|
||||||
"generated-artifacts/IZeroEx.json",
|
"generated-artifacts/IZeroEx.json",
|
||||||
"generated-artifacts/InitialMigration.json",
|
"generated-artifacts/InitialMigration.json",
|
||||||
|
"generated-artifacts/LiquidityProviderFeature.json",
|
||||||
"generated-artifacts/LogMetadataTransformer.json",
|
"generated-artifacts/LogMetadataTransformer.json",
|
||||||
"generated-artifacts/MetaTransactionsFeature.json",
|
"generated-artifacts/MetaTransactionsFeature.json",
|
||||||
"generated-artifacts/OwnableFeature.json",
|
"generated-artifacts/OwnableFeature.json",
|
||||||
|
@ -119,6 +119,7 @@ export {
|
|||||||
SwapQuoterRfqtOpts,
|
SwapQuoterRfqtOpts,
|
||||||
} from './types';
|
} from './types';
|
||||||
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
|
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
|
||||||
|
export { SOURCE_FLAGS } from './utils/market_operation_utils/constants';
|
||||||
export {
|
export {
|
||||||
Parameters,
|
Parameters,
|
||||||
SamplerContractCall,
|
SamplerContractCall,
|
||||||
@ -136,7 +137,6 @@ export {
|
|||||||
FeeSchedule,
|
FeeSchedule,
|
||||||
Fill,
|
Fill,
|
||||||
FillData,
|
FillData,
|
||||||
FillFlags,
|
|
||||||
GetMarketOrdersRfqtOpts,
|
GetMarketOrdersRfqtOpts,
|
||||||
KyberFillData,
|
KyberFillData,
|
||||||
LiquidityProviderFillData,
|
LiquidityProviderFillData,
|
||||||
|
@ -58,6 +58,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
|
|||||||
sampleDistributionBase: 1.05,
|
sampleDistributionBase: 1.05,
|
||||||
feeSchedule: {},
|
feeSchedule: {},
|
||||||
gasSchedule: {},
|
gasSchedule: {},
|
||||||
|
exchangeProxyOverhead: () => ZERO_AMOUNT,
|
||||||
allowFallback: true,
|
allowFallback: true,
|
||||||
shouldBatchBridgeOrders: true,
|
shouldBatchBridgeOrders: true,
|
||||||
shouldGenerateQuoteReport: false,
|
shouldGenerateQuoteReport: false,
|
||||||
@ -68,6 +69,11 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
|
|||||||
*/
|
*/
|
||||||
export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
|
export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
|
||||||
|
|
||||||
|
export const SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } = Object.assign(
|
||||||
|
{},
|
||||||
|
...Object.values(ERC20BridgeSource).map((source: ERC20BridgeSource, index) => ({ [source]: 1 << index })),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mainnet Curve configuration
|
* Mainnet Curve configuration
|
||||||
*/
|
*/
|
||||||
|
@ -3,15 +3,15 @@ import { BigNumber, hexUtils } from '@0x/utils';
|
|||||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||||
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
|
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
|
||||||
|
|
||||||
import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
|
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
|
||||||
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types';
|
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types';
|
||||||
|
|
||||||
// tslint:disable: prefer-for-of no-bitwise completed-docs
|
// tslint:disable: prefer-for-of no-bitwise completed-docs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create fill paths from orders and dex quotes.
|
* Create `Fill` objects from orders and dex quotes.
|
||||||
*/
|
*/
|
||||||
export function createFillPaths(opts: {
|
export function createFills(opts: {
|
||||||
side: MarketOperation;
|
side: MarketOperation;
|
||||||
orders?: SignedOrderWithFillableAmounts[];
|
orders?: SignedOrderWithFillableAmounts[];
|
||||||
dexQuotes?: DexSample[][];
|
dexQuotes?: DexSample[][];
|
||||||
@ -28,30 +28,50 @@ export function createFillPaths(opts: {
|
|||||||
const dexQuotes = opts.dexQuotes || [];
|
const dexQuotes = opts.dexQuotes || [];
|
||||||
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT;
|
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT;
|
||||||
const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT;
|
const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT;
|
||||||
// Create native fill paths.
|
// Create native fills.
|
||||||
const nativePath = nativeOrdersToPath(side, orders, opts.targetInput, ethToOutputRate, ethToInputRate, feeSchedule);
|
const nativeFills = nativeOrdersToFills(
|
||||||
// Create DEX fill paths.
|
side,
|
||||||
const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule);
|
orders,
|
||||||
return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources);
|
opts.targetInput,
|
||||||
|
ethToOutputRate,
|
||||||
|
ethToInputRate,
|
||||||
|
feeSchedule,
|
||||||
|
);
|
||||||
|
// Create DEX fills.
|
||||||
|
const dexFills = dexQuotes.map(singleSourceSamples =>
|
||||||
|
dexSamplesToFills(side, singleSourceSamples, ethToOutputRate, ethToInputRate, feeSchedule),
|
||||||
|
);
|
||||||
|
return [...dexFills, nativeFills]
|
||||||
|
.map(p => clipFillsToInput(p, opts.targetInput))
|
||||||
|
.filter(fills => hasLiquidity(fills) && !excludedSources.includes(fills[0].source));
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterPaths(paths: Fill[][], excludedSources: ERC20BridgeSource[]): Fill[][] {
|
function clipFillsToInput(fills: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] {
|
||||||
return paths.filter(path => {
|
const clipped: Fill[] = [];
|
||||||
if (path.length === 0) {
|
let input = ZERO_AMOUNT;
|
||||||
return false;
|
for (const fill of fills) {
|
||||||
|
if (input.gte(targetInput)) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
const [input, output] = getPathSize(path);
|
input = input.plus(fill.input);
|
||||||
if (input.eq(0) || output.eq(0)) {
|
clipped.push(fill);
|
||||||
return false;
|
}
|
||||||
}
|
return clipped;
|
||||||
if (excludedSources.includes(path[0].source)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nativeOrdersToPath(
|
function hasLiquidity(fills: Fill[]): boolean {
|
||||||
|
if (fills.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const totalInput = BigNumber.sum(...fills.map(fill => fill.input));
|
||||||
|
const totalOutput = BigNumber.sum(...fills.map(fill => fill.output));
|
||||||
|
if (totalInput.isZero() || totalOutput.isZero()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nativeOrdersToFills(
|
||||||
side: MarketOperation,
|
side: MarketOperation,
|
||||||
orders: SignedOrderWithFillableAmounts[],
|
orders: SignedOrderWithFillableAmounts[],
|
||||||
targetInput: BigNumber = POSITIVE_INF,
|
targetInput: BigNumber = POSITIVE_INF,
|
||||||
@ -61,7 +81,7 @@ function nativeOrdersToPath(
|
|||||||
): Fill[] {
|
): Fill[] {
|
||||||
const sourcePathId = hexUtils.random();
|
const sourcePathId = hexUtils.random();
|
||||||
// Create a single path from all orders.
|
// Create a single path from all orders.
|
||||||
let path: Array<Fill & { adjustedRate: BigNumber }> = [];
|
let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
|
||||||
for (const order of orders) {
|
for (const order of orders) {
|
||||||
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
|
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
|
||||||
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
|
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
|
||||||
@ -87,13 +107,13 @@ function nativeOrdersToPath(
|
|||||||
if (adjustedRate.lte(0)) {
|
if (adjustedRate.lte(0)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
path.push({
|
fills.push({
|
||||||
sourcePathId,
|
sourcePathId,
|
||||||
adjustedRate,
|
adjustedRate,
|
||||||
adjustedOutput,
|
adjustedOutput,
|
||||||
input: clippedInput,
|
input: clippedInput,
|
||||||
output: clippedOutput,
|
output: clippedOutput,
|
||||||
flags: 0,
|
flags: SOURCE_FLAGS[ERC20BridgeSource.Native],
|
||||||
index: 0, // TBD
|
index: 0, // TBD
|
||||||
parent: undefined, // TBD
|
parent: undefined, // TBD
|
||||||
source: ERC20BridgeSource.Native,
|
source: ERC20BridgeSource.Native,
|
||||||
@ -101,240 +121,56 @@ function nativeOrdersToPath(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Sort by descending adjusted rate.
|
// Sort by descending adjusted rate.
|
||||||
path = path.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate));
|
fills = fills.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate));
|
||||||
// Re-index fills.
|
// Re-index fills.
|
||||||
for (let i = 0; i < path.length; ++i) {
|
for (let i = 0; i < fills.length; ++i) {
|
||||||
path[i].parent = i === 0 ? undefined : path[i - 1];
|
fills[i].parent = i === 0 ? undefined : fills[i - 1];
|
||||||
path[i].index = i;
|
fills[i].index = i;
|
||||||
}
|
}
|
||||||
return path;
|
return fills;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dexQuotesToPaths(
|
function dexSamplesToFills(
|
||||||
side: MarketOperation,
|
side: MarketOperation,
|
||||||
dexQuotes: DexSample[][],
|
samples: DexSample[],
|
||||||
ethToOutputRate: BigNumber,
|
ethToOutputRate: BigNumber,
|
||||||
|
ethToInputRate: BigNumber,
|
||||||
fees: FeeSchedule,
|
fees: FeeSchedule,
|
||||||
): Fill[][] {
|
): Fill[] {
|
||||||
const paths: Fill[][] = [];
|
const sourcePathId = hexUtils.random();
|
||||||
for (let quote of dexQuotes) {
|
const fills: Fill[] = [];
|
||||||
const sourcePathId = hexUtils.random();
|
// Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves
|
||||||
const path: Fill[] = [];
|
// We need not worry about Kyber fills going to UniswapReserve as the input amount
|
||||||
// Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves
|
// we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input
|
||||||
// We need not worry about Kyber fills going to UniswapReserve as the input amount
|
// and we only fill [2,3] on Kyber (as 1 returns 0 output)
|
||||||
// we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input
|
samples = samples.filter(q => !q.output.isZero());
|
||||||
// and we only fill [2,3] on Kyber (as 1 returns 0 output)
|
for (let i = 0; i < samples.length; i++) {
|
||||||
quote = quote.filter(q => !q.output.isZero());
|
const sample = samples[i];
|
||||||
for (let i = 0; i < quote.length; i++) {
|
const prevSample = i === 0 ? undefined : samples[i - 1];
|
||||||
const sample = quote[i];
|
const { source, fillData } = sample;
|
||||||
const prevSample = i === 0 ? undefined : quote[i - 1];
|
const input = sample.input.minus(prevSample ? prevSample.input : 0);
|
||||||
const { source, fillData } = sample;
|
const output = sample.output.minus(prevSample ? prevSample.output : 0);
|
||||||
const input = sample.input.minus(prevSample ? prevSample.input : 0);
|
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData);
|
||||||
const output = sample.output.minus(prevSample ? prevSample.output : 0);
|
let penalty = ZERO_AMOUNT;
|
||||||
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData);
|
if (i === 0) {
|
||||||
const penalty =
|
// Only the first fill in a DEX path incurs a penalty.
|
||||||
i === 0 // Only the first fill in a DEX path incurs a penalty.
|
penalty = !ethToOutputRate.isZero()
|
||||||
? ethToOutputRate.times(fee)
|
? ethToOutputRate.times(fee)
|
||||||
: ZERO_AMOUNT;
|
: ethToInputRate.times(fee).times(output.dividedToIntegerBy(input));
|
||||||
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
|
|
||||||
|
|
||||||
path.push({
|
|
||||||
sourcePathId,
|
|
||||||
input,
|
|
||||||
output,
|
|
||||||
adjustedOutput,
|
|
||||||
source,
|
|
||||||
fillData,
|
|
||||||
index: i,
|
|
||||||
parent: i !== 0 ? path[path.length - 1] : undefined,
|
|
||||||
flags: sourceToFillFlags(source),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
paths.push(path);
|
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTwoHopAdjustedRate(
|
fills.push({
|
||||||
side: MarketOperation,
|
sourcePathId,
|
||||||
twoHopQuote: DexSample<MultiHopFillData>,
|
input,
|
||||||
targetInput: BigNumber,
|
output,
|
||||||
ethToOutputRate: BigNumber,
|
adjustedOutput,
|
||||||
fees: FeeSchedule = {},
|
source,
|
||||||
): BigNumber {
|
fillData,
|
||||||
const { output, input, fillData } = twoHopQuote;
|
index: i,
|
||||||
if (input.isLessThan(targetInput) || output.isZero()) {
|
parent: i !== 0 ? fills[fills.length - 1] : undefined,
|
||||||
return ZERO_AMOUNT;
|
flags: SOURCE_FLAGS[source],
|
||||||
}
|
|
||||||
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData));
|
|
||||||
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
|
|
||||||
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sourceToFillFlags(source: ERC20BridgeSource): number {
|
|
||||||
switch (source) {
|
|
||||||
case ERC20BridgeSource.Uniswap:
|
|
||||||
return FillFlags.ConflictsWithMultiBridge;
|
|
||||||
case ERC20BridgeSource.LiquidityProvider:
|
|
||||||
return FillFlags.ConflictsWithMultiBridge;
|
|
||||||
case ERC20BridgeSource.MultiBridge:
|
|
||||||
return FillFlags.MultiBridge;
|
|
||||||
default:
|
|
||||||
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);
|
|
||||||
if (di.gt(0)) {
|
|
||||||
input = input.plus(di);
|
|
||||||
// Penalty does not get interpolated.
|
|
||||||
const penalty = fill.adjustedOutput.minus(fill.output);
|
|
||||||
output = output.plus(fill.output.times(di.div(fill.input)).plus(penalty));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
input = input.plus(fill.input);
|
|
||||||
output = output.plus(fill.adjustedOutput);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [input.integerValue(), output.integerValue()];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidPath(path: Fill[], skipDuplicateCheck: boolean = false): 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!skipDuplicateCheck) {
|
|
||||||
// Fill must not be duplicated.
|
|
||||||
for (let j = 0; j < i; ++j) {
|
|
||||||
if (path[i] === path[j]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flags |= path[i].flags;
|
|
||||||
}
|
|
||||||
return arePathFlagsAllowed(flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function arePathFlagsAllowed(flags: number): boolean {
|
|
||||||
const multiBridgeConflict = FillFlags.MultiBridge | FillFlags.ConflictsWithMultiBridge;
|
|
||||||
return (flags & multiBridgeConflict) !== multiBridgeConflict;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(path: Fill[]): CollapsedFill[] {
|
|
||||||
const collapsed: CollapsedFill[] = [];
|
|
||||||
for (const fill of path) {
|
|
||||||
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.sourcePathId === fill.sourcePathId) {
|
|
||||||
prevFill.input = prevFill.input.plus(fill.input);
|
|
||||||
prevFill.output = prevFill.output.plus(fill.output);
|
|
||||||
prevFill.fillData = fill.fillData;
|
|
||||||
prevFill.subFills.push(fill);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collapsed.push({
|
|
||||||
sourcePathId: fill.sourcePathId,
|
|
||||||
source: fill.source,
|
|
||||||
fillData: fill.fillData,
|
|
||||||
input: fill.input,
|
|
||||||
output: fill.output,
|
|
||||||
subFills: [fill],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return collapsed;
|
return fills;
|
||||||
}
|
|
||||||
|
|
||||||
export function getPathAdjustedCompleteRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber {
|
|
||||||
const [input, output] = getPathAdjustedSize(path, targetInput);
|
|
||||||
return getCompleteRate(side, input, output, targetInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPathAdjustedRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber {
|
|
||||||
const [input, output] = getPathAdjustedSize(path, targetInput);
|
|
||||||
return getRate(side, input, 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCompleteRate(
|
|
||||||
side: MarketOperation,
|
|
||||||
input: BigNumber,
|
|
||||||
output: BigNumber,
|
|
||||||
targetInput: BigNumber,
|
|
||||||
): BigNumber {
|
|
||||||
if (input.eq(0) || output.eq(0) || targetInput.eq(0)) {
|
|
||||||
return ZERO_AMOUNT;
|
|
||||||
}
|
|
||||||
// Penalize paths that fall short of the entire input amount by a factor of
|
|
||||||
// input / targetInput => (i / t)
|
|
||||||
if (side === MarketOperation.Sell) {
|
|
||||||
// (o / i) * (i / t) => (o / t)
|
|
||||||
return output.div(targetInput);
|
|
||||||
}
|
|
||||||
// (i / o) * (i / t)
|
|
||||||
return input.div(output).times(input.div(targetInput));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber {
|
|
||||||
if (input.eq(0) || output.eq(0)) {
|
|
||||||
return ZERO_AMOUNT;
|
|
||||||
}
|
|
||||||
return side === MarketOperation.Sell ? output.div(input) : input.div(output);
|
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,12 @@ import {
|
|||||||
FEE_QUOTE_SOURCES,
|
FEE_QUOTE_SOURCES,
|
||||||
ONE_ETHER,
|
ONE_ETHER,
|
||||||
SELL_SOURCE_FILTER,
|
SELL_SOURCE_FILTER,
|
||||||
|
SOURCE_FLAGS,
|
||||||
ZERO_AMOUNT,
|
ZERO_AMOUNT,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills';
|
import { createFills } from './fills';
|
||||||
import { getBestTwoHopQuote } from './multihop_utils';
|
import { getBestTwoHopQuote } from './multihop_utils';
|
||||||
import {
|
import {
|
||||||
createOrdersFromPath,
|
|
||||||
createOrdersFromTwoHopSample,
|
createOrdersFromTwoHopSample,
|
||||||
createSignedOrdersFromRfqtIndicativeQuotes,
|
createSignedOrdersFromRfqtIndicativeQuotes,
|
||||||
createSignedOrdersWithFillableAmounts,
|
createSignedOrdersWithFillableAmounts,
|
||||||
@ -32,12 +32,12 @@ import {
|
|||||||
AggregationError,
|
AggregationError,
|
||||||
DexSample,
|
DexSample,
|
||||||
ERC20BridgeSource,
|
ERC20BridgeSource,
|
||||||
|
ExchangeProxyOverhead,
|
||||||
FeeSchedule,
|
FeeSchedule,
|
||||||
GetMarketOrdersOpts,
|
GetMarketOrdersOpts,
|
||||||
MarketSideLiquidity,
|
MarketSideLiquidity,
|
||||||
OptimizedMarketOrder,
|
OptimizedMarketOrder,
|
||||||
OptimizerResult,
|
OptimizerResult,
|
||||||
OptimizerResultWithReport,
|
|
||||||
OrderDomain,
|
OrderDomain,
|
||||||
TokenAdjacencyGraph,
|
TokenAdjacencyGraph,
|
||||||
} from './types';
|
} from './types';
|
||||||
@ -359,7 +359,7 @@ export class MarketOperationUtils {
|
|||||||
nativeOrders: SignedOrder[],
|
nativeOrders: SignedOrder[],
|
||||||
takerAmount: BigNumber,
|
takerAmount: BigNumber,
|
||||||
opts?: Partial<GetMarketOrdersOpts>,
|
opts?: Partial<GetMarketOrdersOpts>,
|
||||||
): Promise<OptimizerResultWithReport> {
|
): Promise<OptimizerResult> {
|
||||||
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
|
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
|
||||||
const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts);
|
const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts);
|
||||||
const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
|
const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
|
||||||
@ -396,7 +396,7 @@ export class MarketOperationUtils {
|
|||||||
nativeOrders: SignedOrder[],
|
nativeOrders: SignedOrder[],
|
||||||
makerAmount: BigNumber,
|
makerAmount: BigNumber,
|
||||||
opts?: Partial<GetMarketOrdersOpts>,
|
opts?: Partial<GetMarketOrdersOpts>,
|
||||||
): Promise<OptimizerResultWithReport> {
|
): Promise<OptimizerResult> {
|
||||||
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
|
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
|
||||||
const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts);
|
const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts);
|
||||||
const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
|
const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
|
||||||
@ -526,6 +526,7 @@ export class MarketOperationUtils {
|
|||||||
maxFallbackSlippage?: number;
|
maxFallbackSlippage?: number;
|
||||||
excludedSources?: ERC20BridgeSource[];
|
excludedSources?: ERC20BridgeSource[];
|
||||||
feeSchedule?: FeeSchedule;
|
feeSchedule?: FeeSchedule;
|
||||||
|
exchangeProxyOverhead?: ExchangeProxyOverhead;
|
||||||
allowFallback?: boolean;
|
allowFallback?: boolean;
|
||||||
shouldBatchBridgeOrders?: boolean;
|
shouldBatchBridgeOrders?: boolean;
|
||||||
},
|
},
|
||||||
@ -554,8 +555,8 @@ export class MarketOperationUtils {
|
|||||||
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
|
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert native orders and dex quotes into fill paths.
|
// Convert native orders and dex quotes into `Fill` objects.
|
||||||
const paths = createFillPaths({
|
const fills = createFills({
|
||||||
side,
|
side,
|
||||||
// Augment native orders with their fillable amounts.
|
// Augment native orders with their fillable amounts.
|
||||||
orders: [
|
orders: [
|
||||||
@ -571,11 +572,16 @@ export class MarketOperationUtils {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Find the optimal path.
|
// Find the optimal path.
|
||||||
let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || [];
|
const optimizerOpts = {
|
||||||
if (optimalPath.length === 0) {
|
ethToOutputRate,
|
||||||
|
ethToInputRate,
|
||||||
|
exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT),
|
||||||
|
};
|
||||||
|
let optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts);
|
||||||
|
if (optimalPath === undefined) {
|
||||||
throw new Error(AggregationError.NoOptimalPath);
|
throw new Error(AggregationError.NoOptimalPath);
|
||||||
}
|
}
|
||||||
const optimalPathRate = getPathAdjustedRate(side, optimalPath, inputAmount);
|
const optimalPathRate = optimalPath.adjustedRate();
|
||||||
|
|
||||||
const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
|
const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
|
||||||
marketSideLiquidity,
|
marketSideLiquidity,
|
||||||
@ -583,39 +589,35 @@ export class MarketOperationUtils {
|
|||||||
);
|
);
|
||||||
if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) {
|
if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) {
|
||||||
const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts);
|
const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts);
|
||||||
return { optimizedOrders: twoHopOrders, liquidityDelivered: bestTwoHopQuote, isTwoHop: true };
|
return {
|
||||||
|
optimizedOrders: twoHopOrders,
|
||||||
|
liquidityDelivered: bestTwoHopQuote,
|
||||||
|
sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a fallback path if native orders are in the optimal path.
|
// Generate a fallback path if native orders are in the optimal path.
|
||||||
const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native);
|
const nativeFills = optimalPath.fills.filter(f => f.source === ERC20BridgeSource.Native);
|
||||||
if (opts.allowFallback && nativeSubPath.length !== 0) {
|
if (opts.allowFallback && nativeFills.length !== 0) {
|
||||||
// We create a fallback path that is exclusive of Native liquidity
|
// We create a fallback path that is exclusive of Native liquidity
|
||||||
// This is the optimal on-chain path for the entire input amount
|
// This is the optimal on-chain path for the entire input amount
|
||||||
const nonNativePaths = paths.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native);
|
const nonNativeFills = fills.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native);
|
||||||
const nonNativeOptimalPath =
|
const nonNativeOptimalPath = await findOptimalPathAsync(side, nonNativeFills, inputAmount, opts.runLimit);
|
||||||
(await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || [];
|
|
||||||
// Calculate the slippage of on-chain sources compared to the most optimal path
|
// Calculate the slippage of on-chain sources compared to the most optimal path
|
||||||
const fallbackSlippage = getPathAdjustedSlippage(side, nonNativeOptimalPath, inputAmount, optimalPathRate);
|
if (
|
||||||
if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) {
|
nonNativeOptimalPath !== undefined &&
|
||||||
// If the last fill is Native and penultimate is not, then the intention was to partial fill
|
(nativeFills.length === optimalPath.fills.length ||
|
||||||
// In this case we drop it entirely as we can't handle a failure at the end and we don't
|
nonNativeOptimalPath.adjustedSlippage(optimalPathRate) <= maxFallbackSlippage)
|
||||||
// want to fully fill when it gets prepended to the front below
|
) {
|
||||||
const [last, penultimateIfExists] = optimalPath.slice().reverse();
|
optimalPath.addFallback(nonNativeOptimalPath);
|
||||||
const lastNativeFillIfExists =
|
|
||||||
last.source === ERC20BridgeSource.Native &&
|
|
||||||
penultimateIfExists &&
|
|
||||||
penultimateIfExists.source !== ERC20BridgeSource.Native
|
|
||||||
? last
|
|
||||||
: undefined;
|
|
||||||
// By prepending native paths to the front they cannot split on-chain sources and incur
|
|
||||||
// an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber]
|
|
||||||
// In the previous step we dropped any hanging Native partial fills, as to not fully fill
|
|
||||||
optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts);
|
const collapsedPath = optimalPath.collapse(orderOpts);
|
||||||
const liquidityDelivered = _.flatten(optimizedOrders.map(order => order.fills));
|
return {
|
||||||
return { optimizedOrders, liquidityDelivered, isTwoHop: false };
|
optimizedOrders: collapsedPath.orders,
|
||||||
|
liquidityDelivered: collapsedPath.collapsedFills,
|
||||||
|
sourceFlags: collapsedPath.sourceFlags,
|
||||||
|
} as OptimizerResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { ZERO_AMOUNT } from './constants';
|
import { ZERO_AMOUNT } from './constants';
|
||||||
import { getTwoHopAdjustedRate } from './fills';
|
import { getTwoHopAdjustedRate } from './rate_utils';
|
||||||
import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types';
|
import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +16,6 @@ import {
|
|||||||
WALLET_SIGNATURE,
|
WALLET_SIGNATURE,
|
||||||
ZERO_AMOUNT,
|
ZERO_AMOUNT,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { collapsePath } from './fills';
|
|
||||||
import { getMultiBridgeIntermediateToken } from './multibridge_utils';
|
import { getMultiBridgeIntermediateToken } from './multibridge_utils';
|
||||||
import {
|
import {
|
||||||
AggregationError,
|
AggregationError,
|
||||||
@ -26,7 +25,6 @@ import {
|
|||||||
CurveFillData,
|
CurveFillData,
|
||||||
DexSample,
|
DexSample,
|
||||||
ERC20BridgeSource,
|
ERC20BridgeSource,
|
||||||
Fill,
|
|
||||||
KyberFillData,
|
KyberFillData,
|
||||||
LiquidityProviderFillData,
|
LiquidityProviderFillData,
|
||||||
MooniswapFillData,
|
MooniswapFillData,
|
||||||
@ -155,37 +153,6 @@ export interface CreateOrderFromPathOpts {
|
|||||||
shouldBatchBridgeOrders: boolean;
|
shouldBatchBridgeOrders: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert sell fills into orders.
|
|
||||||
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
|
|
||||||
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
|
||||||
const collapsedPath = collapsePath(path);
|
|
||||||
const orders: OptimizedMarketOrder[] = [];
|
|
||||||
for (let i = 0; i < collapsedPath.length; ) {
|
|
||||||
if (collapsedPath[i].source === ERC20BridgeSource.Native) {
|
|
||||||
orders.push(createNativeOrder(collapsedPath[i] as NativeCollapsedFill));
|
|
||||||
++i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If there are contiguous bridge orders, we can batch them together.
|
|
||||||
const contiguousBridgeFills = [collapsedPath[i]];
|
|
||||||
for (let j = i + 1; j < collapsedPath.length; ++j) {
|
|
||||||
if (collapsedPath[j].source === ERC20BridgeSource.Native) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
contiguousBridgeFills.push(collapsedPath[j]);
|
|
||||||
}
|
|
||||||
// Always use DexForwarderBridge unless configured not to
|
|
||||||
if (!opts.shouldBatchBridgeOrders) {
|
|
||||||
orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
|
|
||||||
i += contiguousBridgeFills.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return orders;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOrdersFromTwoHopSample(
|
export function createOrdersFromTwoHopSample(
|
||||||
sample: DexSample<MultiHopFillData>,
|
sample: DexSample<MultiHopFillData>,
|
||||||
opts: CreateOrderFromPathOpts,
|
opts: CreateOrderFromPathOpts,
|
||||||
@ -248,7 +215,7 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath
|
|||||||
throw new Error(AggregationError.NoBridgeForSource);
|
throw new Error(AggregationError.NoBridgeForSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBridgeOrder(
|
export function createBridgeOrder(
|
||||||
fill: CollapsedFill,
|
fill: CollapsedFill,
|
||||||
makerToken: string,
|
makerToken: string,
|
||||||
takerToken: string,
|
takerToken: string,
|
||||||
@ -362,7 +329,7 @@ function createBridgeOrder(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
export function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
||||||
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
||||||
let totalMakerAssetAmount = ZERO_AMOUNT;
|
let totalMakerAssetAmount = ZERO_AMOUNT;
|
||||||
let totalTakerAssetAmount = ZERO_AMOUNT;
|
let totalTakerAssetAmount = ZERO_AMOUNT;
|
||||||
@ -403,7 +370,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
|
export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
|
||||||
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
||||||
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
||||||
return [makerToken, takerToken];
|
return [makerToken, takerToken];
|
||||||
@ -525,7 +492,7 @@ function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder {
|
export function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder {
|
||||||
return {
|
return {
|
||||||
fills: [fill],
|
fills: [fill],
|
||||||
...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion
|
...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion
|
||||||
|
287
packages/asset-swapper/src/utils/market_operation_utils/path.ts
Normal file
287
packages/asset-swapper/src/utils/market_operation_utils/path.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import { BigNumber } from '@0x/utils';
|
||||||
|
|
||||||
|
import { MarketOperation } from '../../types';
|
||||||
|
|
||||||
|
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
|
||||||
|
import {
|
||||||
|
createBatchedBridgeOrder,
|
||||||
|
createBridgeOrder,
|
||||||
|
createNativeOrder,
|
||||||
|
CreateOrderFromPathOpts,
|
||||||
|
getMakerTakerTokens,
|
||||||
|
} from './orders';
|
||||||
|
import { getCompleteRate, getRate } from './rate_utils';
|
||||||
|
import {
|
||||||
|
CollapsedFill,
|
||||||
|
ERC20BridgeSource,
|
||||||
|
ExchangeProxyOverhead,
|
||||||
|
Fill,
|
||||||
|
NativeCollapsedFill,
|
||||||
|
OptimizedMarketOrder,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// tslint:disable: prefer-for-of no-bitwise completed-docs
|
||||||
|
|
||||||
|
export interface PathSize {
|
||||||
|
input: BigNumber;
|
||||||
|
output: BigNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PathPenaltyOpts {
|
||||||
|
ethToOutputRate: BigNumber;
|
||||||
|
ethToInputRate: BigNumber;
|
||||||
|
exchangeProxyOverhead: ExchangeProxyOverhead;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = {
|
||||||
|
ethToOutputRate: ZERO_AMOUNT,
|
||||||
|
ethToInputRate: ZERO_AMOUNT,
|
||||||
|
exchangeProxyOverhead: () => ZERO_AMOUNT,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Path {
|
||||||
|
public collapsedFills?: ReadonlyArray<CollapsedFill>;
|
||||||
|
public orders?: OptimizedMarketOrder[];
|
||||||
|
public sourceFlags: number = 0;
|
||||||
|
protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT };
|
||||||
|
protected _adjustedSize: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT };
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
protected readonly side: MarketOperation,
|
||||||
|
public fills: ReadonlyArray<Fill>,
|
||||||
|
protected readonly targetInput: BigNumber,
|
||||||
|
public readonly pathPenaltyOpts: PathPenaltyOpts,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
side: MarketOperation,
|
||||||
|
fills: ReadonlyArray<Fill>,
|
||||||
|
targetInput: BigNumber = POSITIVE_INF,
|
||||||
|
pathPenaltyOpts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
|
||||||
|
): Path {
|
||||||
|
const path = new Path(side, fills, targetInput, pathPenaltyOpts);
|
||||||
|
fills.forEach(fill => {
|
||||||
|
path.sourceFlags |= fill.flags;
|
||||||
|
path._addFillSize(fill);
|
||||||
|
});
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static clone(base: Path): Path {
|
||||||
|
const clonedPath = new Path(base.side, base.fills.slice(), base.targetInput, base.pathPenaltyOpts);
|
||||||
|
clonedPath.sourceFlags = base.sourceFlags;
|
||||||
|
clonedPath._size = { ...base._size };
|
||||||
|
clonedPath._adjustedSize = { ...base._adjustedSize };
|
||||||
|
clonedPath.collapsedFills = base.collapsedFills === undefined ? undefined : base.collapsedFills.slice();
|
||||||
|
clonedPath.orders = base.orders === undefined ? undefined : base.orders.slice();
|
||||||
|
return clonedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public append(fill: Fill): this {
|
||||||
|
(this.fills as Fill[]).push(fill);
|
||||||
|
this.sourceFlags |= fill.flags;
|
||||||
|
this._addFillSize(fill);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addFallback(fallback: Path): this {
|
||||||
|
// If the last fill is Native and penultimate is not, then the intention was to partial fill
|
||||||
|
// In this case we drop it entirely as we can't handle a failure at the end and we don't
|
||||||
|
// want to fully fill when it gets prepended to the front below
|
||||||
|
const [last, penultimateIfExists] = this.fills.slice().reverse();
|
||||||
|
const lastNativeFillIfExists =
|
||||||
|
last.source === ERC20BridgeSource.Native &&
|
||||||
|
penultimateIfExists &&
|
||||||
|
penultimateIfExists.source !== ERC20BridgeSource.Native
|
||||||
|
? last
|
||||||
|
: undefined;
|
||||||
|
// By prepending native paths to the front they cannot split on-chain sources and incur
|
||||||
|
// an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber]
|
||||||
|
// In the previous step we dropped any hanging Native partial fills, as to not fully fill
|
||||||
|
const nativeFills = this.fills.filter(f => f.source === ERC20BridgeSource.Native);
|
||||||
|
this.fills = [...nativeFills.filter(f => f !== lastNativeFillIfExists), ...fallback.fills];
|
||||||
|
// Recompute the source flags
|
||||||
|
this.sourceFlags = this.fills.reduce((flags, fill) => (flags |= fill.flags), 0);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public collapse(opts: CreateOrderFromPathOpts): CollapsedPath {
|
||||||
|
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
||||||
|
const collapsedFills = this.collapsedFills === undefined ? this._collapseFills() : this.collapsedFills;
|
||||||
|
this.orders = [];
|
||||||
|
for (let i = 0; i < collapsedFills.length; ) {
|
||||||
|
if (collapsedFills[i].source === ERC20BridgeSource.Native) {
|
||||||
|
this.orders.push(createNativeOrder(collapsedFills[i] as NativeCollapsedFill));
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If there are contiguous bridge orders, we can batch them together.
|
||||||
|
const contiguousBridgeFills = [collapsedFills[i]];
|
||||||
|
for (let j = i + 1; j < collapsedFills.length; ++j) {
|
||||||
|
if (collapsedFills[j].source === ERC20BridgeSource.Native) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
contiguousBridgeFills.push(collapsedFills[j]);
|
||||||
|
}
|
||||||
|
// Always use DexForwarderBridge unless configured not to
|
||||||
|
if (!opts.shouldBatchBridgeOrders) {
|
||||||
|
this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
this.orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
|
||||||
|
i += contiguousBridgeFills.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this as CollapsedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public size(): PathSize {
|
||||||
|
return this._size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public adjustedSize(): PathSize {
|
||||||
|
const { input, output } = this._adjustedSize;
|
||||||
|
const { exchangeProxyOverhead, ethToOutputRate, ethToInputRate } = this.pathPenaltyOpts;
|
||||||
|
const gasOverhead = exchangeProxyOverhead(this.sourceFlags);
|
||||||
|
const pathPenalty = !ethToOutputRate.isZero()
|
||||||
|
? ethToOutputRate.times(gasOverhead)
|
||||||
|
: ethToInputRate.times(gasOverhead).times(output.dividedToIntegerBy(input));
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public adjustedCompleteRate(): BigNumber {
|
||||||
|
const { input, output } = this.adjustedSize();
|
||||||
|
return getCompleteRate(this.side, input, output, this.targetInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
public adjustedRate(): BigNumber {
|
||||||
|
const { input, output } = this.adjustedSize();
|
||||||
|
return getRate(this.side, input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public adjustedSlippage(maxRate: BigNumber): number {
|
||||||
|
if (maxRate.eq(0)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const totalRate = this.adjustedRate();
|
||||||
|
const rateChange = maxRate.minus(totalRate);
|
||||||
|
return rateChange.div(maxRate).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isBetterThan(other: Path): boolean {
|
||||||
|
if (!this.targetInput.isEqualTo(other.targetInput)) {
|
||||||
|
throw new Error(`Target input mismatch: ${this.targetInput} !== ${other.targetInput}`);
|
||||||
|
}
|
||||||
|
const { targetInput } = this;
|
||||||
|
const { input } = this._size;
|
||||||
|
const { input: otherInput } = other._size;
|
||||||
|
if (input.isLessThan(targetInput) || otherInput.isLessThan(targetInput)) {
|
||||||
|
return input.isGreaterThan(otherInput);
|
||||||
|
} else {
|
||||||
|
return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate());
|
||||||
|
}
|
||||||
|
// if (otherInput.isLessThan(targetInput)) {
|
||||||
|
// return input.isGreaterThan(otherInput);
|
||||||
|
// } else if (input.isGreaterThanOrEqualTo(targetInput)) {
|
||||||
|
// return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate());
|
||||||
|
// }
|
||||||
|
// return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isComplete(): boolean {
|
||||||
|
const { input } = this._size;
|
||||||
|
return input.gte(this.targetInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isValid(skipDuplicateCheck: boolean = false): boolean {
|
||||||
|
for (let i = 0; i < this.fills.length; ++i) {
|
||||||
|
// Fill must immediately follow its parent.
|
||||||
|
if (this.fills[i].parent) {
|
||||||
|
if (i === 0 || this.fills[i - 1] !== this.fills[i].parent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!skipDuplicateCheck) {
|
||||||
|
// Fill must not be duplicated.
|
||||||
|
for (let j = 0; j < i; ++j) {
|
||||||
|
if (this.fills[i] === this.fills[j]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doSourcesConflict(this.sourceFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isValidNextFill(fill: Fill): boolean {
|
||||||
|
if (this.fills.length === 0) {
|
||||||
|
return !fill.parent;
|
||||||
|
}
|
||||||
|
if (this.fills[this.fills.length - 1] === fill.parent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (fill.parent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return doSourcesConflict(this.sourceFlags | fill.flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _collapseFills(): ReadonlyArray<CollapsedFill> {
|
||||||
|
this.collapsedFills = [];
|
||||||
|
for (const fill of this.fills) {
|
||||||
|
const source = fill.source;
|
||||||
|
if (this.collapsedFills.length !== 0 && source !== ERC20BridgeSource.Native) {
|
||||||
|
const prevFill = this.collapsedFills[this.collapsedFills.length - 1];
|
||||||
|
// If the last fill is from the same source, merge them.
|
||||||
|
if (prevFill.sourcePathId === fill.sourcePathId) {
|
||||||
|
prevFill.input = prevFill.input.plus(fill.input);
|
||||||
|
prevFill.output = prevFill.output.plus(fill.output);
|
||||||
|
prevFill.fillData = fill.fillData;
|
||||||
|
prevFill.subFills.push(fill);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(this.collapsedFills as CollapsedFill[]).push({
|
||||||
|
sourcePathId: fill.sourcePathId,
|
||||||
|
source: fill.source,
|
||||||
|
fillData: fill.fillData,
|
||||||
|
input: fill.input,
|
||||||
|
output: fill.output,
|
||||||
|
subFills: [fill],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.collapsedFills;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addFillSize(fill: Fill): void {
|
||||||
|
if (this._size.input.plus(fill.input).isGreaterThan(this.targetInput)) {
|
||||||
|
const remainingInput = this.targetInput.minus(this._size.input);
|
||||||
|
const scaledFillOutput = fill.output.times(remainingInput.div(fill.input));
|
||||||
|
this._size.input = this.targetInput;
|
||||||
|
this._size.output = this._size.output.plus(scaledFillOutput);
|
||||||
|
// Penalty does not get interpolated.
|
||||||
|
const penalty = fill.adjustedOutput.minus(fill.output);
|
||||||
|
this._adjustedSize.input = this.targetInput;
|
||||||
|
this._adjustedSize.output = this._adjustedSize.output.plus(scaledFillOutput).plus(penalty);
|
||||||
|
} else {
|
||||||
|
this._size.input = this._size.input.plus(fill.input);
|
||||||
|
this._size.output = this._size.output.plus(fill.output);
|
||||||
|
this._adjustedSize.input = this._adjustedSize.input.plus(fill.input);
|
||||||
|
this._adjustedSize.output = this._adjustedSize.output.plus(fill.adjustedOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollapsedPath extends Path {
|
||||||
|
readonly collapsedFills: ReadonlyArray<CollapsedFill>;
|
||||||
|
readonly orders: OptimizedMarketOrder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MULTIBRIDGE_SOURCES = SOURCE_FLAGS.LiquidityProvider | SOURCE_FLAGS.Uniswap;
|
||||||
|
export function doSourcesConflict(flags: number): boolean {
|
||||||
|
const multiBridgeConflict = flags & SOURCE_FLAGS.MultiBridge && flags & MULTIBRIDGE_SOURCES;
|
||||||
|
return !multiBridgeConflict;
|
||||||
|
}
|
@ -1,17 +1,9 @@
|
|||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { MarketOperation } from '../../types';
|
import { MarketOperation } from '../../types';
|
||||||
|
|
||||||
import { ZERO_AMOUNT } from './constants';
|
import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path';
|
||||||
import {
|
|
||||||
arePathFlagsAllowed,
|
|
||||||
getCompleteRate,
|
|
||||||
getPathAdjustedCompleteRate,
|
|
||||||
getPathAdjustedRate,
|
|
||||||
getPathAdjustedSize,
|
|
||||||
getPathSize,
|
|
||||||
isValidPath,
|
|
||||||
} from './fills';
|
|
||||||
import { Fill } from './types';
|
import { Fill } from './types';
|
||||||
|
|
||||||
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise
|
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise
|
||||||
@ -19,134 +11,93 @@ import { Fill } from './types';
|
|||||||
const RUN_LIMIT_DECAY_FACTOR = 0.5;
|
const RUN_LIMIT_DECAY_FACTOR = 0.5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the optimal mixture of paths that maximizes (for sells) or minimizes
|
* Find the optimal mixture of fills that maximizes (for sells) or minimizes
|
||||||
* (for buys) output, while meeting the input requirement.
|
* (for buys) output, while meeting the input requirement.
|
||||||
*/
|
*/
|
||||||
export async function findOptimalPathAsync(
|
export async function findOptimalPathAsync(
|
||||||
side: MarketOperation,
|
side: MarketOperation,
|
||||||
paths: Fill[][],
|
fills: Fill[][],
|
||||||
targetInput: BigNumber,
|
targetInput: BigNumber,
|
||||||
runLimit: number = 2 ** 8,
|
runLimit: number = 2 ** 8,
|
||||||
): Promise<Fill[] | undefined> {
|
opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
|
||||||
// Sort paths by descending adjusted completed rate.
|
): Promise<Path | undefined> {
|
||||||
const sortedPaths = paths
|
const rates = rateBySourcePathId(side, fills, targetInput);
|
||||||
.slice(0)
|
const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts));
|
||||||
.sort((a, b) =>
|
// Sort fill arrays by descending adjusted completed rate.
|
||||||
getPathAdjustedCompleteRate(side, b, targetInput).comparedTo(
|
const sortedPaths = paths.sort((a, b) => b.adjustedCompleteRate().comparedTo(a.adjustedCompleteRate()));
|
||||||
getPathAdjustedCompleteRate(side, a, targetInput),
|
if (sortedPaths.length === 0) {
|
||||||
),
|
return undefined;
|
||||||
);
|
}
|
||||||
let optimalPath = sortedPaths[0] || [];
|
let optimalPath = sortedPaths[0];
|
||||||
for (const [i, path] of sortedPaths.slice(1).entries()) {
|
for (const [i, path] of sortedPaths.slice(1).entries()) {
|
||||||
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i);
|
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i, rates);
|
||||||
// Yield to event loop.
|
// Yield to event loop.
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined;
|
return optimalPath.isComplete() ? optimalPath : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mixPaths(
|
function mixPaths(
|
||||||
side: MarketOperation,
|
side: MarketOperation,
|
||||||
pathA: Fill[],
|
pathA: Path,
|
||||||
pathB: Fill[],
|
pathB: Path,
|
||||||
targetInput: BigNumber,
|
targetInput: BigNumber,
|
||||||
maxSteps: number,
|
maxSteps: number,
|
||||||
): Fill[] {
|
rateBySourcePathId: { [id: string]: BigNumber },
|
||||||
|
): Path {
|
||||||
const _maxSteps = Math.max(maxSteps, 32);
|
const _maxSteps = Math.max(maxSteps, 32);
|
||||||
let steps = 0;
|
let steps = 0;
|
||||||
// We assume pathA is the better of the two initially.
|
// We assume pathA is the better of the two initially.
|
||||||
let bestPath: Fill[] = pathA;
|
let bestPath: Path = pathA;
|
||||||
let [bestPathInput, bestPathOutput] = getPathAdjustedSize(pathA, targetInput);
|
|
||||||
let bestPathRate = getCompleteRate(side, bestPathInput, bestPathOutput, targetInput);
|
const _walk = (path: Path, remainingFills: Fill[]) => {
|
||||||
const _isBetterPath = (input: BigNumber, rate: BigNumber) => {
|
|
||||||
if (bestPathInput.lt(targetInput)) {
|
|
||||||
return input.gt(bestPathInput);
|
|
||||||
} else if (input.gte(targetInput)) {
|
|
||||||
return rate.gt(bestPathRate);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
const _walk = (path: Fill[], input: BigNumber, output: BigNumber, flags: number, remainingFills: Fill[]) => {
|
|
||||||
steps += 1;
|
steps += 1;
|
||||||
const rate = getCompleteRate(side, input, output, targetInput);
|
if (path.isBetterThan(bestPath)) {
|
||||||
if (_isBetterPath(input, rate)) {
|
|
||||||
bestPath = path;
|
bestPath = path;
|
||||||
bestPathInput = input;
|
|
||||||
bestPathOutput = output;
|
|
||||||
bestPathRate = rate;
|
|
||||||
}
|
}
|
||||||
const remainingInput = targetInput.minus(input);
|
const remainingInput = targetInput.minus(path.size().input);
|
||||||
if (remainingInput.gt(0)) {
|
if (remainingInput.isGreaterThan(0)) {
|
||||||
for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) {
|
for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) {
|
||||||
const fill = remainingFills[i];
|
const fill = remainingFills[i];
|
||||||
// Only walk valid paths.
|
// Only walk valid paths.
|
||||||
if (!isValidNextPathFill(path, flags, fill)) {
|
if (!path.isValidNextFill(fill)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Remove this fill from the next list of candidate fills.
|
// Remove this fill from the next list of candidate fills.
|
||||||
const nextRemainingFills = remainingFills.slice();
|
const nextRemainingFills = remainingFills.slice();
|
||||||
nextRemainingFills.splice(i, 1);
|
nextRemainingFills.splice(i, 1);
|
||||||
// Recurse.
|
// Recurse.
|
||||||
_walk(
|
_walk(Path.clone(path).append(fill), nextRemainingFills);
|
||||||
[...path, fill],
|
|
||||||
input.plus(BigNumber.min(remainingInput, fill.input)),
|
|
||||||
output.plus(
|
|
||||||
// Clip the output of the next fill to the remaining
|
|
||||||
// input.
|
|
||||||
clipFillAdjustedOutput(fill, remainingInput),
|
|
||||||
),
|
|
||||||
flags | fill.flags,
|
|
||||||
nextRemainingFills,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const allFills = [...pathA, ...pathB];
|
const allFills = [...pathA.fills, ...pathB.fills];
|
||||||
const sources = allFills.filter(f => f.index === 0).map(f => f.sourcePathId);
|
|
||||||
const rateBySource = Object.assign(
|
|
||||||
{},
|
|
||||||
...sources.map(s => ({
|
|
||||||
[s]: getPathAdjustedRate(side, allFills.filter(f => f.sourcePathId === s), targetInput),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
// Sort subpaths by rate and keep fills contiguous to improve our
|
// Sort subpaths by rate and keep fills contiguous to improve our
|
||||||
// chances of walking ideal, valid paths first.
|
// chances of walking ideal, valid paths first.
|
||||||
const sortedFills = allFills.sort((a, b) => {
|
const sortedFills = allFills.sort((a, b) => {
|
||||||
if (a.sourcePathId !== b.sourcePathId) {
|
if (a.sourcePathId !== b.sourcePathId) {
|
||||||
return rateBySource[b.sourcePathId].comparedTo(rateBySource[a.sourcePathId]);
|
return rateBySourcePathId[b.sourcePathId].comparedTo(rateBySourcePathId[a.sourcePathId]);
|
||||||
}
|
}
|
||||||
return a.index - b.index;
|
return a.index - b.index;
|
||||||
});
|
});
|
||||||
_walk([], ZERO_AMOUNT, ZERO_AMOUNT, 0, sortedFills);
|
_walk(Path.create(side, [], targetInput, pathA.pathPenaltyOpts), sortedFills);
|
||||||
if (!isValidPath(bestPath)) {
|
if (!bestPath.isValid()) {
|
||||||
throw new Error('nooope');
|
throw new Error('nooope');
|
||||||
}
|
}
|
||||||
return bestPath;
|
return bestPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidNextPathFill(path: Fill[], pathFlags: number, fill: Fill): boolean {
|
function rateBySourcePathId(
|
||||||
if (path.length === 0) {
|
side: MarketOperation,
|
||||||
return !fill.parent;
|
fills: Fill[][],
|
||||||
}
|
targetInput: BigNumber,
|
||||||
if (path[path.length - 1] === fill.parent) {
|
): { [id: string]: BigNumber } {
|
||||||
return true;
|
const flattenedFills = _.flatten(fills);
|
||||||
}
|
const sourcePathIds = flattenedFills.filter(f => f.index === 0).map(f => f.sourcePathId);
|
||||||
if (fill.parent) {
|
return Object.assign(
|
||||||
return false;
|
{},
|
||||||
}
|
...sourcePathIds.map(s => ({
|
||||||
return arePathFlagsAllowed(pathFlags | fill.flags);
|
[s]: Path.create(side, flattenedFills.filter(f => f.sourcePathId === s), targetInput).adjustedRate(),
|
||||||
}
|
})),
|
||||||
|
);
|
||||||
function isPathComplete(path: Fill[], targetInput: BigNumber): boolean {
|
|
||||||
const [input] = getPathSize(path);
|
|
||||||
return input.gte(targetInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clipFillAdjustedOutput(fill: Fill, remainingInput: BigNumber): BigNumber {
|
|
||||||
if (fill.input.lte(remainingInput)) {
|
|
||||||
return fill.adjustedOutput;
|
|
||||||
}
|
|
||||||
// Penalty does not get interpolated.
|
|
||||||
const penalty = fill.adjustedOutput.minus(fill.output);
|
|
||||||
return remainingInput.times(fill.output.div(fill.input)).plus(penalty);
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import { BigNumber } from '@0x/utils';
|
||||||
|
|
||||||
|
import { MarketOperation } from '../../types';
|
||||||
|
|
||||||
|
import { ZERO_AMOUNT } from './constants';
|
||||||
|
import { DexSample, ERC20BridgeSource, FeeSchedule, MultiHopFillData } from './types';
|
||||||
|
|
||||||
|
export function getTwoHopAdjustedRate(
|
||||||
|
side: MarketOperation,
|
||||||
|
twoHopQuote: DexSample<MultiHopFillData>,
|
||||||
|
targetInput: BigNumber,
|
||||||
|
ethToOutputRate: BigNumber,
|
||||||
|
fees: FeeSchedule = {},
|
||||||
|
): BigNumber {
|
||||||
|
const { output, input, fillData } = twoHopQuote;
|
||||||
|
if (input.isLessThan(targetInput) || output.isZero()) {
|
||||||
|
return ZERO_AMOUNT;
|
||||||
|
}
|
||||||
|
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData));
|
||||||
|
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
|
||||||
|
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCompleteRate(
|
||||||
|
side: MarketOperation,
|
||||||
|
input: BigNumber,
|
||||||
|
output: BigNumber,
|
||||||
|
targetInput: BigNumber,
|
||||||
|
): BigNumber {
|
||||||
|
if (input.eq(0) || output.eq(0) || targetInput.eq(0)) {
|
||||||
|
return ZERO_AMOUNT;
|
||||||
|
}
|
||||||
|
// Penalize paths that fall short of the entire input amount by a factor of
|
||||||
|
// input / targetInput => (i / t)
|
||||||
|
if (side === MarketOperation.Sell) {
|
||||||
|
// (o / i) * (i / t) => (o / t)
|
||||||
|
return output.div(targetInput);
|
||||||
|
}
|
||||||
|
// (i / o) * (i / t)
|
||||||
|
return input.div(output).times(input.div(targetInput));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber {
|
||||||
|
if (input.eq(0) || output.eq(0)) {
|
||||||
|
return ZERO_AMOUNT;
|
||||||
|
}
|
||||||
|
return side === MarketOperation.Sell ? output.div(input) : input.div(output);
|
||||||
|
}
|
@ -156,16 +156,6 @@ export interface DexSample<TFillData extends FillData = FillData> extends Source
|
|||||||
output: BigNumber;
|
output: BigNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Flags for `Fill` objects.
|
|
||||||
*/
|
|
||||||
export enum FillFlags {
|
|
||||||
ConflictsWithKyber = 0x1,
|
|
||||||
Kyber = 0x2,
|
|
||||||
ConflictsWithMultiBridge = 0x4,
|
|
||||||
MultiBridge = 0x8,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a node on a fill path.
|
* Represents a node on a fill path.
|
||||||
*/
|
*/
|
||||||
@ -174,8 +164,8 @@ export interface Fill<TFillData extends FillData = FillData> extends SourceInfo<
|
|||||||
// This is generated when the path is generated and is useful to distinguish
|
// This is generated when the path is generated and is useful to distinguish
|
||||||
// paths that have the same `source` IDs but are distinct (e.g., Curves).
|
// paths that have the same `source` IDs but are distinct (e.g., Curves).
|
||||||
sourcePathId: string;
|
sourcePathId: string;
|
||||||
// See `FillFlags`.
|
// See `SOURCE_FLAGS`.
|
||||||
flags: FillFlags;
|
flags: number;
|
||||||
// Input fill amount (taker asset amount in a sell, maker asset amount in a buy).
|
// Input fill amount (taker asset amount in a sell, maker asset amount in a buy).
|
||||||
input: BigNumber;
|
input: BigNumber;
|
||||||
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
|
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
|
||||||
@ -234,6 +224,7 @@ export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts {
|
|||||||
|
|
||||||
export type FeeEstimate = (fillData?: FillData) => number | BigNumber;
|
export type FeeEstimate = (fillData?: FillData) => number | BigNumber;
|
||||||
export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>;
|
export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>;
|
||||||
|
export type ExchangeProxyOverhead = (sourceFlags: number) => BigNumber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
|
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
|
||||||
@ -288,6 +279,7 @@ export interface GetMarketOrdersOpts {
|
|||||||
* Estimated gas consumed by each liquidity source.
|
* Estimated gas consumed by each liquidity source.
|
||||||
*/
|
*/
|
||||||
gasSchedule: FeeSchedule;
|
gasSchedule: FeeSchedule;
|
||||||
|
exchangeProxyOverhead: ExchangeProxyOverhead;
|
||||||
/**
|
/**
|
||||||
* Whether to pad the quote with a redundant fallback quote using different
|
* Whether to pad the quote with a redundant fallback quote using different
|
||||||
* sources. Defaults to `true`.
|
* sources. Defaults to `true`.
|
||||||
@ -321,11 +313,8 @@ export interface SourceQuoteOperation<TFillData extends FillData = FillData>
|
|||||||
|
|
||||||
export interface OptimizerResult {
|
export interface OptimizerResult {
|
||||||
optimizedOrders: OptimizedMarketOrder[];
|
optimizedOrders: OptimizedMarketOrder[];
|
||||||
isTwoHop: boolean;
|
sourceFlags: number;
|
||||||
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
|
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptimizerResultWithReport extends OptimizerResult {
|
|
||||||
quoteReport?: QuoteReport;
|
quoteReport?: QuoteReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ export function generateQuoteReport(
|
|||||||
multiHopQuotes: Array<DexSample<MultiHopFillData>>,
|
multiHopQuotes: Array<DexSample<MultiHopFillData>>,
|
||||||
nativeOrders: SignedOrder[],
|
nativeOrders: SignedOrder[],
|
||||||
orderFillableAmounts: BigNumber[],
|
orderFillableAmounts: BigNumber[],
|
||||||
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>,
|
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
|
||||||
quoteRequestor?: QuoteRequestor,
|
quoteRequestor?: QuoteRequestor,
|
||||||
): QuoteReport {
|
): QuoteReport {
|
||||||
const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
|
const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
|
||||||
@ -101,7 +101,9 @@ export function generateQuoteReport(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)];
|
sourcesDelivered = [
|
||||||
|
_multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
sourcesConsidered,
|
sourcesConsidered,
|
||||||
|
@ -349,7 +349,7 @@ function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteI
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] {
|
function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] {
|
||||||
const fills: CollapsedFill[] = [];
|
const fills: CollapsedFill[] = [];
|
||||||
for (const o of orders) {
|
for (const o of orders) {
|
||||||
fills.push(...o.fills);
|
fills.push(...o.fills);
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import { MarketOperationUtils } from './market_operation_utils';
|
import { MarketOperationUtils } from './market_operation_utils';
|
||||||
|
import { SOURCE_FLAGS } from './market_operation_utils/constants';
|
||||||
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
||||||
import {
|
import {
|
||||||
ERC20BridgeSource,
|
ERC20BridgeSource,
|
||||||
@ -130,70 +131,74 @@ export class SwapQuoteCalculator {
|
|||||||
|
|
||||||
let optimizedOrders: OptimizedMarketOrder[];
|
let optimizedOrders: OptimizedMarketOrder[];
|
||||||
let quoteReport: QuoteReport | undefined;
|
let quoteReport: QuoteReport | undefined;
|
||||||
let isTwoHop = false;
|
let sourceFlags: number = 0;
|
||||||
|
|
||||||
{
|
// Scale fees by gas price.
|
||||||
// Scale fees by gas price.
|
const _opts: GetMarketOrdersOpts = {
|
||||||
const _opts: GetMarketOrdersOpts = {
|
...opts,
|
||||||
...opts,
|
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) =>
|
||||||
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) =>
|
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
|
||||||
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
|
),
|
||||||
),
|
exchangeProxyOverhead: (sourceFlags: number) => gasPrice.times(opts.exchangeProxyOverhead!(sourceFlags)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const firstOrderMakerAssetData = !!prunedOrders[0]
|
const firstOrderMakerAssetData = !!prunedOrders[0]
|
||||||
? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData)
|
? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData)
|
||||||
: { assetProxyId: '' };
|
: { assetProxyId: '' };
|
||||||
|
|
||||||
if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) {
|
if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) {
|
||||||
// HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable
|
// HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable
|
||||||
optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o));
|
optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o));
|
||||||
|
} else {
|
||||||
|
if (operation === MarketOperation.Buy) {
|
||||||
|
const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync(
|
||||||
|
prunedOrders,
|
||||||
|
assetFillAmount,
|
||||||
|
_opts,
|
||||||
|
);
|
||||||
|
optimizedOrders = buyResult.optimizedOrders;
|
||||||
|
quoteReport = buyResult.quoteReport;
|
||||||
|
sourceFlags = buyResult.sourceFlags;
|
||||||
} else {
|
} else {
|
||||||
if (operation === MarketOperation.Buy) {
|
const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
|
||||||
const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync(
|
prunedOrders,
|
||||||
prunedOrders,
|
assetFillAmount,
|
||||||
assetFillAmount,
|
_opts,
|
||||||
_opts,
|
);
|
||||||
);
|
optimizedOrders = sellResult.optimizedOrders;
|
||||||
optimizedOrders = buyResult.optimizedOrders;
|
quoteReport = sellResult.quoteReport;
|
||||||
quoteReport = buyResult.quoteReport;
|
sourceFlags = sellResult.sourceFlags;
|
||||||
isTwoHop = buyResult.isTwoHop;
|
|
||||||
} else {
|
|
||||||
const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
|
|
||||||
prunedOrders,
|
|
||||||
assetFillAmount,
|
|
||||||
_opts,
|
|
||||||
);
|
|
||||||
optimizedOrders = sellResult.optimizedOrders;
|
|
||||||
quoteReport = sellResult.quoteReport;
|
|
||||||
isTwoHop = sellResult.isTwoHop;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// assetData information for the result
|
// assetData information for the result
|
||||||
const { makerAssetData, takerAssetData } = prunedOrders[0];
|
const { makerAssetData, takerAssetData } = prunedOrders[0];
|
||||||
return isTwoHop
|
const swapQuote =
|
||||||
? createTwoHopSwapQuote(
|
sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]
|
||||||
makerAssetData,
|
? createTwoHopSwapQuote(
|
||||||
takerAssetData,
|
makerAssetData,
|
||||||
optimizedOrders,
|
takerAssetData,
|
||||||
operation,
|
optimizedOrders,
|
||||||
assetFillAmount,
|
operation,
|
||||||
gasPrice,
|
assetFillAmount,
|
||||||
opts.gasSchedule,
|
gasPrice,
|
||||||
quoteReport,
|
opts.gasSchedule,
|
||||||
)
|
quoteReport,
|
||||||
: createSwapQuote(
|
)
|
||||||
makerAssetData,
|
: createSwapQuote(
|
||||||
takerAssetData,
|
makerAssetData,
|
||||||
optimizedOrders,
|
takerAssetData,
|
||||||
operation,
|
optimizedOrders,
|
||||||
assetFillAmount,
|
operation,
|
||||||
gasPrice,
|
assetFillAmount,
|
||||||
opts.gasSchedule,
|
gasPrice,
|
||||||
quoteReport,
|
opts.gasSchedule,
|
||||||
);
|
quoteReport,
|
||||||
|
);
|
||||||
|
const exchangeProxyOverhead = _opts.exchangeProxyOverhead(sourceFlags).toNumber();
|
||||||
|
swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead;
|
||||||
|
swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead;
|
||||||
|
return swapQuote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import {
|
|||||||
SELL_SOURCE_FILTER,
|
SELL_SOURCE_FILTER,
|
||||||
ZERO_AMOUNT,
|
ZERO_AMOUNT,
|
||||||
} from '../src/utils/market_operation_utils/constants';
|
} from '../src/utils/market_operation_utils/constants';
|
||||||
import { createFillPaths } from '../src/utils/market_operation_utils/fills';
|
import { createFills } from '../src/utils/market_operation_utils/fills';
|
||||||
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
|
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
|
||||||
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
|
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
|
||||||
import {
|
import {
|
||||||
@ -1299,7 +1299,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
|
|
||||||
it('batches contiguous bridge sources', async () => {
|
it('batches contiguous bridge sources', async () => {
|
||||||
const rates: RatesBySource = { ...ZERO_RATES };
|
const rates: RatesBySource = { ...ZERO_RATES };
|
||||||
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01];
|
rates[ERC20BridgeSource.Native] = [0.3, 0.01, 0.01, 0.01];
|
||||||
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01];
|
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01];
|
||||||
rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01];
|
rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01];
|
||||||
replaceSamplerOps({
|
replaceSamplerOps({
|
||||||
@ -1318,14 +1318,14 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
expect(improvedOrders).to.be.length(2);
|
expect(improvedOrders).to.be.length(2);
|
||||||
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
|
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
|
||||||
expect(orderFillSources).to.deep.eq([
|
expect(orderFillSources).to.deep.eq([
|
||||||
[ERC20BridgeSource.Native],
|
|
||||||
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],
|
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],
|
||||||
|
[ERC20BridgeSource.Native],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createFillPaths', () => {
|
describe('createFills', () => {
|
||||||
const takerAssetAmount = new BigNumber(5000000);
|
const takerAssetAmount = new BigNumber(5000000);
|
||||||
const ethToOutputRate = new BigNumber(0.5);
|
const ethToOutputRate = new BigNumber(0.5);
|
||||||
// tslint:disable-next-line:no-object-literal-type-assertion
|
// tslint:disable-next-line:no-object-literal-type-assertion
|
||||||
@ -1359,7 +1359,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('penalizes native fill based on target amount when target is smaller', () => {
|
it('penalizes native fill based on target amount when target is smaller', () => {
|
||||||
const path = createFillPaths({
|
const path = createFills({
|
||||||
side: MarketOperation.Sell,
|
side: MarketOperation.Sell,
|
||||||
orders,
|
orders,
|
||||||
dexQuotes: [],
|
dexQuotes: [],
|
||||||
@ -1372,7 +1372,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('penalizes native fill based on available amount when target is larger', () => {
|
it('penalizes native fill based on available amount when target is larger', () => {
|
||||||
const path = createFillPaths({
|
const path = createFills({
|
||||||
side: MarketOperation.Sell,
|
side: MarketOperation.Sell,
|
||||||
orders,
|
orders,
|
||||||
dexQuotes: [],
|
dexQuotes: [],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user