Refactor TokenAdjacency
and TokenAdjacencyBuilder
[TKR-324] (#517)
* Add a new TokenAdjacencyGraph implementation * Replace old TokenAdjacencyGraph with new implementation * Simplify token adjacency graph in constants.ts * Fix lint error * Update CHANGELOG.json
This commit is contained in:
parent
f7cb7a0f51
commit
b72b8b5ffd
@ -1,4 +1,13 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "16.64.0",
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"note": "Refactor `TokenAdjacency` and `TokenAdjacencyBuilder`",
|
||||||
|
"pr": 517
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "16.63.1",
|
"version": "16.63.1",
|
||||||
"changes": [
|
"changes": [
|
||||||
|
@ -166,9 +166,10 @@ export {
|
|||||||
NativeFillData,
|
NativeFillData,
|
||||||
OptimizedMarketOrder,
|
OptimizedMarketOrder,
|
||||||
SourceQuoteOperation,
|
SourceQuoteOperation,
|
||||||
TokenAdjacencyGraph,
|
|
||||||
UniswapV2FillData,
|
UniswapV2FillData,
|
||||||
} from './utils/market_operation_utils/types';
|
} from './utils/market_operation_utils/types';
|
||||||
|
|
||||||
|
export { TokenAdjacencyGraph, TokenAdjacencyGraphBuilder } from './utils/token_adjacency_graph';
|
||||||
export { IdentityFillAdjustor } from './utils/market_operation_utils/identity_fill_adjustor';
|
export { IdentityFillAdjustor } from './utils/market_operation_utils/identity_fill_adjustor';
|
||||||
export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
|
export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
|
||||||
export {
|
export {
|
||||||
|
@ -17,11 +17,13 @@ import {
|
|||||||
GetMarketOrdersOpts,
|
GetMarketOrdersOpts,
|
||||||
LiquidityProviderRegistry,
|
LiquidityProviderRegistry,
|
||||||
OptimizedMarketOrder,
|
OptimizedMarketOrder,
|
||||||
TokenAdjacencyGraph,
|
|
||||||
} from './utils/market_operation_utils/types';
|
} from './utils/market_operation_utils/types';
|
||||||
export { SamplerMetrics } from './utils/market_operation_utils/types';
|
export { SamplerMetrics } from './utils/market_operation_utils/types';
|
||||||
import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from './utils/quote_report_generator';
|
import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from './utils/quote_report_generator';
|
||||||
import { MetricsProxy } from './utils/quote_requestor';
|
import { MetricsProxy } from './utils/quote_requestor';
|
||||||
|
import { TokenAdjacencyGraph } from './utils/token_adjacency_graph';
|
||||||
|
|
||||||
|
export type Address = string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
|
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
|
||||||
|
@ -3,7 +3,7 @@ import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
|
|||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import { formatBytes32String } from '@ethersproject/strings';
|
import { formatBytes32String } from '@ethersproject/strings';
|
||||||
|
|
||||||
import { TokenAdjacencyGraphBuilder } from '../token_adjacency_graph_builder';
|
import { TokenAdjacencyGraph, TokenAdjacencyGraphBuilder } from '../token_adjacency_graph';
|
||||||
|
|
||||||
import { IdentityFillAdjustor } from './identity_fill_adjustor';
|
import { IdentityFillAdjustor } from './identity_fill_adjustor';
|
||||||
import { SourceFilters } from './source_filters';
|
import { SourceFilters } from './source_filters';
|
||||||
@ -32,7 +32,6 @@ import {
|
|||||||
MultiHopFillData,
|
MultiHopFillData,
|
||||||
PlatypusInfo,
|
PlatypusInfo,
|
||||||
PsmInfo,
|
PsmInfo,
|
||||||
TokenAdjacencyGraph,
|
|
||||||
UniswapV2FillData,
|
UniswapV2FillData,
|
||||||
UniswapV3FillData,
|
UniswapV3FillData,
|
||||||
} from './types';
|
} from './types';
|
||||||
@ -903,65 +902,49 @@ export const DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID = valueByChainId<string[]>(
|
|||||||
// attaching to a default intermediary token (stables or ETH etc) can have a large impact
|
// attaching to a default intermediary token (stables or ETH etc) can have a large impact
|
||||||
export const DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID = valueByChainId<TokenAdjacencyGraph>(
|
export const DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID = valueByChainId<TokenAdjacencyGraph>(
|
||||||
{
|
{
|
||||||
[ChainId.Mainnet]: new TokenAdjacencyGraphBuilder({
|
[ChainId.Mainnet]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Mainnet])
|
||||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Mainnet],
|
|
||||||
})
|
|
||||||
.tap(builder => {
|
.tap(builder => {
|
||||||
// Mirror Protocol
|
// Mirror Protocol
|
||||||
builder.add(MAINNET_TOKENS.MIR, MAINNET_TOKENS.UST);
|
builder.add(MAINNET_TOKENS.MIR, MAINNET_TOKENS.UST);
|
||||||
// Convex and Curve
|
// Convex and Curve
|
||||||
builder.add(MAINNET_TOKENS.cvxCRV, MAINNET_TOKENS.CRV).add(MAINNET_TOKENS.CRV, MAINNET_TOKENS.cvxCRV);
|
builder.addBidirectional(MAINNET_TOKENS.cvxCRV, MAINNET_TOKENS.CRV);
|
||||||
// Convex and FXS
|
// Convex and FXS
|
||||||
builder.add(MAINNET_TOKENS.cvxFXS, MAINNET_TOKENS.FXS).add(MAINNET_TOKENS.FXS, MAINNET_TOKENS.cvxFXS);
|
builder.addBidirectional(MAINNET_TOKENS.cvxFXS, MAINNET_TOKENS.FXS);
|
||||||
// FEI TRIBE liquid in UniV2
|
// FEI TRIBE liquid in UniV2
|
||||||
builder.add(MAINNET_TOKENS.FEI, MAINNET_TOKENS.TRIBE).add(MAINNET_TOKENS.TRIBE, MAINNET_TOKENS.FEI);
|
builder.addBidirectional(MAINNET_TOKENS.FEI, MAINNET_TOKENS.TRIBE);
|
||||||
// FRAX ecosystem
|
// FRAX ecosystem
|
||||||
builder.add(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.FXS).add(MAINNET_TOKENS.FXS, MAINNET_TOKENS.FRAX);
|
builder.addBidirectional(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.FXS);
|
||||||
builder.add(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.OHM).add(MAINNET_TOKENS.OHM, MAINNET_TOKENS.FRAX);
|
builder.addBidirectional(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.OHM);
|
||||||
// REDACTED CARTEL
|
// REDACTED CARTEL
|
||||||
builder
|
builder.addBidirectional(MAINNET_TOKENS.OHMV2, MAINNET_TOKENS.BTRFLY);
|
||||||
.add(MAINNET_TOKENS.OHMV2, MAINNET_TOKENS.BTRFLY)
|
|
||||||
.add(MAINNET_TOKENS.BTRFLY, MAINNET_TOKENS.OHMV2);
|
|
||||||
// Lido
|
// Lido
|
||||||
builder
|
builder.addBidirectional(MAINNET_TOKENS.stETH, MAINNET_TOKENS.wstETH);
|
||||||
.add(MAINNET_TOKENS.stETH, MAINNET_TOKENS.wstETH)
|
|
||||||
.add(MAINNET_TOKENS.wstETH, MAINNET_TOKENS.stETH);
|
|
||||||
})
|
})
|
||||||
// Build
|
// Build
|
||||||
.build(),
|
.build(),
|
||||||
[ChainId.BSC]: new TokenAdjacencyGraphBuilder({
|
[ChainId.BSC]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.BSC]).build(),
|
||||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.BSC],
|
[ChainId.Polygon]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Polygon])
|
||||||
}).build(),
|
|
||||||
[ChainId.Polygon]: new TokenAdjacencyGraphBuilder({
|
|
||||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Polygon],
|
|
||||||
})
|
|
||||||
.tap(builder => {
|
.tap(builder => {
|
||||||
builder.add(POLYGON_TOKENS.QUICK, POLYGON_TOKENS.ANY).add(POLYGON_TOKENS.ANY, POLYGON_TOKENS.QUICK);
|
builder.addBidirectional(POLYGON_TOKENS.QUICK, POLYGON_TOKENS.ANY);
|
||||||
})
|
})
|
||||||
.build(),
|
.build(),
|
||||||
[ChainId.Avalanche]: new TokenAdjacencyGraphBuilder({
|
[ChainId.Avalanche]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Avalanche])
|
||||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Avalanche],
|
|
||||||
})
|
|
||||||
.tap(builder => {
|
.tap(builder => {
|
||||||
// Synapse nETH/aWETH pool
|
// Synapse nETH/aWETH pool
|
||||||
builder
|
builder.addBidirectional(AVALANCHE_TOKENS.aWETH, AVALANCHE_TOKENS.nETH);
|
||||||
.add(AVALANCHE_TOKENS.aWETH, AVALANCHE_TOKENS.nETH)
|
|
||||||
.add(AVALANCHE_TOKENS.nETH, AVALANCHE_TOKENS.aWETH);
|
|
||||||
// Trader Joe MAG/MIM pool
|
// Trader Joe MAG/MIM pool
|
||||||
builder.add(AVALANCHE_TOKENS.MIM, AVALANCHE_TOKENS.MAG).add(AVALANCHE_TOKENS.MAG, AVALANCHE_TOKENS.MIM);
|
builder.addBidirectional(AVALANCHE_TOKENS.MIM, AVALANCHE_TOKENS.MAG);
|
||||||
})
|
})
|
||||||
.build(),
|
.build(),
|
||||||
[ChainId.Fantom]: new TokenAdjacencyGraphBuilder({
|
[ChainId.Fantom]: new TokenAdjacencyGraphBuilder(
|
||||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Fantom],
|
DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Fantom],
|
||||||
}).build(),
|
).build(),
|
||||||
[ChainId.Celo]: new TokenAdjacencyGraphBuilder({
|
[ChainId.Celo]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Celo]).build(),
|
||||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Celo],
|
[ChainId.Optimism]: new TokenAdjacencyGraphBuilder(
|
||||||
}).build(),
|
DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Optimism],
|
||||||
[ChainId.Optimism]: new TokenAdjacencyGraphBuilder({
|
).build(),
|
||||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Optimism],
|
|
||||||
}).build(),
|
|
||||||
},
|
},
|
||||||
new TokenAdjacencyGraphBuilder({ default: [] }).build(),
|
TokenAdjacencyGraph.getEmptyGraph(),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NATIVE_FEE_TOKEN_BY_CHAIN_ID = valueByChainId<string>(
|
export const NATIVE_FEE_TOKEN_BY_CHAIN_ID = valueByChainId<string>(
|
||||||
@ -2604,7 +2587,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice
|
|||||||
allowFallback: true,
|
allowFallback: true,
|
||||||
shouldGenerateQuoteReport: true,
|
shouldGenerateQuoteReport: true,
|
||||||
shouldIncludePriceComparisonsReport: false,
|
shouldIncludePriceComparisonsReport: false,
|
||||||
tokenAdjacencyGraph: { default: [] },
|
tokenAdjacencyGraph: TokenAdjacencyGraph.getEmptyGraph(),
|
||||||
neonRouterNumSamples: 14,
|
neonRouterNumSamples: 14,
|
||||||
fillAdjustor: new IdentityFillAdjustor(),
|
fillAdjustor: new IdentityFillAdjustor(),
|
||||||
};
|
};
|
||||||
|
@ -12,26 +12,8 @@ import {
|
|||||||
FillAdjustor,
|
FillAdjustor,
|
||||||
MarketSideLiquidity,
|
MarketSideLiquidity,
|
||||||
MultiHopFillData,
|
MultiHopFillData,
|
||||||
TokenAdjacencyGraph,
|
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a token pair, returns the intermediate tokens to consider for two-hop routes.
|
|
||||||
*/
|
|
||||||
export function getIntermediateTokens(
|
|
||||||
makerToken: string,
|
|
||||||
takerToken: string,
|
|
||||||
tokenAdjacencyGraph: TokenAdjacencyGraph,
|
|
||||||
): string[] {
|
|
||||||
const intermediateTokens = _.union(
|
|
||||||
_.get(tokenAdjacencyGraph, takerToken, tokenAdjacencyGraph.default),
|
|
||||||
_.get(tokenAdjacencyGraph, makerToken, tokenAdjacencyGraph.default),
|
|
||||||
);
|
|
||||||
return _.uniqBy(intermediateTokens, a => a.toLowerCase()).filter(
|
|
||||||
token => token.toLowerCase() !== makerToken.toLowerCase() && token.toLowerCase() !== takerToken.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the best two-hop quote and the fee-adjusted rate of that quote.
|
* Returns the best two-hop quote and the fee-adjusted rate of that quote.
|
||||||
*/
|
*/
|
||||||
|
@ -3,10 +3,11 @@ import { BigNumber, NULL_BYTES } from '@0x/utils';
|
|||||||
|
|
||||||
import { SamplerOverrides } from '../../types';
|
import { SamplerOverrides } from '../../types';
|
||||||
import { ERC20BridgeSamplerContract } from '../../wrappers';
|
import { ERC20BridgeSamplerContract } from '../../wrappers';
|
||||||
|
import { TokenAdjacencyGraph } from '../token_adjacency_graph';
|
||||||
|
|
||||||
import { BancorService } from './bancor_service';
|
import { BancorService } from './bancor_service';
|
||||||
import { PoolsCacheMap, SamplerOperations } from './sampler_operations';
|
import { PoolsCacheMap, SamplerOperations } from './sampler_operations';
|
||||||
import { BatchedOperation, LiquidityProviderRegistry, TokenAdjacencyGraph } from './types';
|
import { BatchedOperation, LiquidityProviderRegistry } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate sample amounts up to `maxFillAmount`.
|
* Generate sample amounts up to `maxFillAmount`.
|
||||||
|
@ -7,6 +7,7 @@ import { AaveV2Sampler } from '../../noop_samplers/AaveV2Sampler';
|
|||||||
import { GeistSampler } from '../../noop_samplers/GeistSampler';
|
import { GeistSampler } from '../../noop_samplers/GeistSampler';
|
||||||
import { SamplerCallResult, SignedNativeOrder } from '../../types';
|
import { SamplerCallResult, SignedNativeOrder } from '../../types';
|
||||||
import { ERC20BridgeSamplerContract } from '../../wrappers';
|
import { ERC20BridgeSamplerContract } from '../../wrappers';
|
||||||
|
import { TokenAdjacencyGraph } from '../token_adjacency_graph';
|
||||||
|
|
||||||
import { AaveV2ReservesCache } from './aave_reserves_cache';
|
import { AaveV2ReservesCache } from './aave_reserves_cache';
|
||||||
import { BancorService } from './bancor_service';
|
import { BancorService } from './bancor_service';
|
||||||
@ -53,7 +54,6 @@ import {
|
|||||||
} from './constants';
|
} from './constants';
|
||||||
import { getGeistInfoForPair } from './geist_utils';
|
import { getGeistInfoForPair } from './geist_utils';
|
||||||
import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
|
import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
|
||||||
import { getIntermediateTokens } from './multihop_utils';
|
|
||||||
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache';
|
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache';
|
||||||
import { BalancerV2SwapInfoCache } from './pools_cache/balancer_v2_utils_new';
|
import { BalancerV2SwapInfoCache } from './pools_cache/balancer_v2_utils_new';
|
||||||
import { SamplerContractOperation } from './sampler_contract_operation';
|
import { SamplerContractOperation } from './sampler_contract_operation';
|
||||||
@ -94,7 +94,6 @@ import {
|
|||||||
ShellFillData,
|
ShellFillData,
|
||||||
SourceQuoteOperation,
|
SourceQuoteOperation,
|
||||||
SourcesWithPoolsCache,
|
SourcesWithPoolsCache,
|
||||||
TokenAdjacencyGraph,
|
|
||||||
UniswapV2FillData,
|
UniswapV2FillData,
|
||||||
UniswapV3FillData,
|
UniswapV3FillData,
|
||||||
VelodromeFillData,
|
VelodromeFillData,
|
||||||
@ -140,7 +139,7 @@ export class SamplerOperations {
|
|||||||
public readonly chainId: ChainId,
|
public readonly chainId: ChainId,
|
||||||
protected readonly _samplerContract: ERC20BridgeSamplerContract,
|
protected readonly _samplerContract: ERC20BridgeSamplerContract,
|
||||||
poolsCaches?: PoolsCacheMap,
|
poolsCaches?: PoolsCacheMap,
|
||||||
protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [] },
|
protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = TokenAdjacencyGraph.getEmptyGraph(),
|
||||||
liquidityProviderRegistry: LiquidityProviderRegistry = {},
|
liquidityProviderRegistry: LiquidityProviderRegistry = {},
|
||||||
bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined,
|
bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined,
|
||||||
) {
|
) {
|
||||||
@ -810,7 +809,7 @@ export class SamplerOperations {
|
|||||||
if (_sources.length === 0) {
|
if (_sources.length === 0) {
|
||||||
return SamplerOperations.constant([]);
|
return SamplerOperations.constant([]);
|
||||||
}
|
}
|
||||||
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, this.tokenAdjacencyGraph);
|
const intermediateTokens = this.tokenAdjacencyGraph.getIntermediateTokens(makerToken, takerToken);
|
||||||
const subOps = intermediateTokens.map(intermediateToken => {
|
const subOps = intermediateTokens.map(intermediateToken => {
|
||||||
const firstHopOps = this._getSellQuoteOperations(_sources, intermediateToken, takerToken, [ZERO_AMOUNT]);
|
const firstHopOps = this._getSellQuoteOperations(_sources, intermediateToken, takerToken, [ZERO_AMOUNT]);
|
||||||
const secondHopOps = this._getSellQuoteOperations(_sources, makerToken, intermediateToken, [ZERO_AMOUNT]);
|
const secondHopOps = this._getSellQuoteOperations(_sources, makerToken, intermediateToken, [ZERO_AMOUNT]);
|
||||||
@ -865,7 +864,7 @@ export class SamplerOperations {
|
|||||||
if (_sources.length === 0) {
|
if (_sources.length === 0) {
|
||||||
return SamplerOperations.constant([]);
|
return SamplerOperations.constant([]);
|
||||||
}
|
}
|
||||||
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, this.tokenAdjacencyGraph);
|
const intermediateTokens = this.tokenAdjacencyGraph.getIntermediateTokens(makerToken, takerToken);
|
||||||
const subOps = intermediateTokens.map(intermediateToken => {
|
const subOps = intermediateTokens.map(intermediateToken => {
|
||||||
const firstHopOps = this._getBuyQuoteOperations(_sources, intermediateToken, takerToken, [
|
const firstHopOps = this._getBuyQuoteOperations(_sources, intermediateToken, takerToken, [
|
||||||
new BigNumber(0),
|
new BigNumber(0),
|
||||||
@ -1325,9 +1324,13 @@ export class SamplerOperations {
|
|||||||
if (makerToken.toLowerCase() === nativeToken.toLowerCase()) {
|
if (makerToken.toLowerCase() === nativeToken.toLowerCase()) {
|
||||||
return SamplerOperations.constant(new BigNumber(1));
|
return SamplerOperations.constant(new BigNumber(1));
|
||||||
}
|
}
|
||||||
const subOps = this._getSellQuoteOperations(sources, makerToken, nativeToken, [nativeFillAmount], {
|
const subOps = this._getSellQuoteOperations(
|
||||||
default: [],
|
sources,
|
||||||
});
|
makerToken,
|
||||||
|
nativeToken,
|
||||||
|
[nativeFillAmount],
|
||||||
|
TokenAdjacencyGraph.getEmptyGraph(),
|
||||||
|
);
|
||||||
return this._createBatch(
|
return this._createBatch(
|
||||||
subOps,
|
subOps,
|
||||||
(samples: BigNumber[][]) => {
|
(samples: BigNumber[][]) => {
|
||||||
@ -1423,7 +1426,7 @@ export class SamplerOperations {
|
|||||||
): SourceQuoteOperation[] {
|
): SourceQuoteOperation[] {
|
||||||
// Find the adjacent tokens in the provided token adjacency graph,
|
// Find the adjacent tokens in the provided token adjacency graph,
|
||||||
// e.g if this is DAI->USDC we may check for DAI->WETH->USDC
|
// e.g if this is DAI->USDC we may check for DAI->WETH->USDC
|
||||||
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph);
|
const intermediateTokens = tokenAdjacencyGraph.getIntermediateTokens(makerToken, takerToken);
|
||||||
// Drop out MultiHop and Native as we do not query those here.
|
// Drop out MultiHop and Native as we do not query those here.
|
||||||
const _sources = SELL_SOURCE_FILTER_BY_CHAIN_ID[this.chainId]
|
const _sources = SELL_SOURCE_FILTER_BY_CHAIN_ID[this.chainId]
|
||||||
.exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native])
|
.exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native])
|
||||||
@ -1767,7 +1770,7 @@ export class SamplerOperations {
|
|||||||
): SourceQuoteOperation[] {
|
): SourceQuoteOperation[] {
|
||||||
// Find the adjacent tokens in the provided token adjacency graph,
|
// Find the adjacent tokens in the provided token adjacency graph,
|
||||||
// e.g if this is DAI->USDC we may check for DAI->WETH->USDC
|
// e.g if this is DAI->USDC we may check for DAI->WETH->USDC
|
||||||
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, this.tokenAdjacencyGraph);
|
const intermediateTokens = this.tokenAdjacencyGraph.getIntermediateTokens(makerToken, takerToken);
|
||||||
const _sources = BATCH_SOURCE_FILTERS.getAllowed(sources);
|
const _sources = BATCH_SOURCE_FILTERS.getAllowed(sources);
|
||||||
return _.flatten(
|
return _.flatten(
|
||||||
_sources.map((source): SourceQuoteOperation | SourceQuoteOperation[] => {
|
_sources.map((source): SourceQuoteOperation | SourceQuoteOperation[] => {
|
||||||
|
@ -10,6 +10,7 @@ import { NativeOrderWithFillableAmounts, RfqFirmQuoteValidator, RfqRequestOpts }
|
|||||||
import { QuoteRequestor, V4RFQIndicativeQuoteMM } from '../../utils/quote_requestor';
|
import { QuoteRequestor, V4RFQIndicativeQuoteMM } from '../../utils/quote_requestor';
|
||||||
import { IRfqClient } from '../irfq_client';
|
import { IRfqClient } from '../irfq_client';
|
||||||
import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from '../quote_report_generator';
|
import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from '../quote_report_generator';
|
||||||
|
import { TokenAdjacencyGraph } from '../token_adjacency_graph';
|
||||||
|
|
||||||
import { SourceFilters } from './source_filters';
|
import { SourceFilters } from './source_filters';
|
||||||
|
|
||||||
@ -642,11 +643,6 @@ export interface RawQuotes {
|
|||||||
dexQuotes: Array<Array<DexSample<FillData>>>;
|
dexQuotes: Array<Array<DexSample<FillData>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenAdjacencyGraph {
|
|
||||||
[token: string]: string[];
|
|
||||||
default: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LiquidityProviderRegistry {
|
export interface LiquidityProviderRegistry {
|
||||||
[address: string]: {
|
[address: string]: {
|
||||||
tokens: string[];
|
tokens: string[];
|
||||||
|
84
packages/asset-swapper/src/utils/token_adjacency_graph.ts
Normal file
84
packages/asset-swapper/src/utils/token_adjacency_graph.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import { Address } from '../types';
|
||||||
|
|
||||||
|
export class TokenAdjacencyGraph {
|
||||||
|
private readonly _graph: Map<Address, Address[]>;
|
||||||
|
private readonly _defaultTokens: readonly Address[];
|
||||||
|
|
||||||
|
public static getEmptyGraph(): TokenAdjacencyGraph {
|
||||||
|
return new TokenAdjacencyGraphBuilder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefer using {@link TokenAdjacencyGraphBuilder}. */
|
||||||
|
constructor(graph: Map<Address, Address[]>, defaultTokens: readonly Address[]) {
|
||||||
|
this._graph = graph;
|
||||||
|
this._defaultTokens = defaultTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAdjacentTokens(fromToken: Address): readonly Address[] {
|
||||||
|
return this._graph.get(fromToken.toLowerCase()) || this._defaultTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Given a token pair, returns the intermediate tokens to consider for two-hop routes. */
|
||||||
|
public getIntermediateTokens(takerToken: Address, makerToken: Address): Address[] {
|
||||||
|
// NOTE: it seems it should be a union of `to` tokens of `takerToken` and `from` tokens of `makerToken`,
|
||||||
|
// leaving it as same as the initial implementation for now.
|
||||||
|
return _.union(this.getAdjacentTokens(takerToken), this.getAdjacentTokens(makerToken)).filter(
|
||||||
|
token => token !== takerToken.toLowerCase() && token !== makerToken.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line: max-classes-per-file
|
||||||
|
export class TokenAdjacencyGraphBuilder {
|
||||||
|
private readonly _graph: Map<Address, Address[]>;
|
||||||
|
private readonly _defaultTokens: readonly Address[];
|
||||||
|
|
||||||
|
constructor(defaultTokens: readonly string[] = []) {
|
||||||
|
this._graph = new Map();
|
||||||
|
this._defaultTokens = defaultTokens.map(addr => addr.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(fromToken: Address, toToken: Address): TokenAdjacencyGraphBuilder {
|
||||||
|
const fromLower = fromToken.toLowerCase();
|
||||||
|
const toLower = toToken.toLowerCase();
|
||||||
|
|
||||||
|
if (fromLower === toLower) {
|
||||||
|
throw new Error(`from token (${fromToken}) must be different from to token (${toToken})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._graph.has(fromLower)) {
|
||||||
|
this._graph.set(fromLower, [...this._defaultTokens]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toTokens = this._graph.get(fromLower)!;
|
||||||
|
if (!toTokens.includes(toLower)) {
|
||||||
|
toTokens.push(toLower);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addBidirectional(tokenA: Address, tokenB: Address): TokenAdjacencyGraphBuilder {
|
||||||
|
return this.add(tokenA, tokenB).add(tokenB, tokenA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addCompleteSubgraph(tokens: Address[]): TokenAdjacencyGraphBuilder {
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
for (let j = i + 1; j < tokens.length; j++) {
|
||||||
|
this.addBidirectional(tokens[i], tokens[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public tap(cb: (graph: TokenAdjacencyGraphBuilder) => void): TokenAdjacencyGraphBuilder {
|
||||||
|
cb(this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): TokenAdjacencyGraph {
|
||||||
|
return new TokenAdjacencyGraph(this._graph, this._defaultTokens);
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
import _ = require('lodash');
|
|
||||||
|
|
||||||
import { TokenAdjacencyGraph } from './market_operation_utils/types';
|
|
||||||
|
|
||||||
export class TokenAdjacencyGraphBuilder {
|
|
||||||
constructor(private readonly tokenAdjacency: TokenAdjacencyGraph) {}
|
|
||||||
|
|
||||||
public add(from: string, to: string | string[]): TokenAdjacencyGraphBuilder {
|
|
||||||
if (!this.tokenAdjacency[from]) {
|
|
||||||
this.tokenAdjacency[from] = [...this.tokenAdjacency.default];
|
|
||||||
}
|
|
||||||
this.tokenAdjacency[from] = [...(Array.isArray(to) ? to : [to]), ...this.tokenAdjacency[from]];
|
|
||||||
this.tokenAdjacency[from] = _.uniqBy(this.tokenAdjacency[from], a => a.toLowerCase());
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public tap(cb: (builder: TokenAdjacencyGraphBuilder) => void): TokenAdjacencyGraphBuilder {
|
|
||||||
cb(this);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public build(): TokenAdjacencyGraph {
|
|
||||||
return this.tokenAdjacency;
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,8 @@ import * as _ from 'lodash';
|
|||||||
|
|
||||||
import { SignedOrder } from '../src/types';
|
import { SignedOrder } from '../src/types';
|
||||||
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
|
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
|
||||||
import { ERC20BridgeSource, TokenAdjacencyGraph } from '../src/utils/market_operation_utils/types';
|
import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
|
||||||
|
import { TokenAdjacencyGraphBuilder } from '../src/utils/token_adjacency_graph';
|
||||||
|
|
||||||
import { MockSamplerContract } from './utils/mock_sampler_contract';
|
import { MockSamplerContract } from './utils/mock_sampler_contract';
|
||||||
import { generatePseudoRandomSalt } from './utils/utils';
|
import { generatePseudoRandomSalt } from './utils/utils';
|
||||||
@ -29,7 +30,7 @@ describe('DexSampler tests', () => {
|
|||||||
const wethAddress = getContractAddressesForChainOrThrow(CHAIN_ID).etherToken;
|
const wethAddress = getContractAddressesForChainOrThrow(CHAIN_ID).etherToken;
|
||||||
const exchangeProxyAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchangeProxy;
|
const exchangeProxyAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchangeProxy;
|
||||||
|
|
||||||
const tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [wethAddress] };
|
const tokenAdjacencyGraph = new TokenAdjacencyGraphBuilder([wethAddress]).build();
|
||||||
|
|
||||||
describe('getSampleAmounts()', () => {
|
describe('getSampleAmounts()', () => {
|
||||||
const FILL_AMOUNT = getRandomInteger(1, 1e18);
|
const FILL_AMOUNT = getRandomInteger(1, 1e18);
|
||||||
|
@ -15,7 +15,7 @@ import { Pool } from 'balancer-labs-sor-v1/dist/types';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as TypeMoq from 'typemoq';
|
import * as TypeMoq from 'typemoq';
|
||||||
|
|
||||||
import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src';
|
import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder, TokenAdjacencyGraph } from '../src';
|
||||||
import { Integrator } from '../src/types';
|
import { Integrator } from '../src/types';
|
||||||
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
||||||
import {
|
import {
|
||||||
@ -39,7 +39,6 @@ import {
|
|||||||
MarketSideLiquidity,
|
MarketSideLiquidity,
|
||||||
OptimizedMarketBridgeOrder,
|
OptimizedMarketBridgeOrder,
|
||||||
OptimizerResultWithReport,
|
OptimizerResultWithReport,
|
||||||
TokenAdjacencyGraph,
|
|
||||||
} from '../src/utils/market_operation_utils/types';
|
} from '../src/utils/market_operation_utils/types';
|
||||||
|
|
||||||
const MAKER_TOKEN = randomAddress();
|
const MAKER_TOKEN = randomAddress();
|
||||||
@ -57,7 +56,7 @@ const DEFAULT_EXCLUDED = SELL_SOURCE_FILTER_BY_CHAIN_ID[ChainId.Mainnet].sources
|
|||||||
);
|
);
|
||||||
const BUY_SOURCES = BUY_SOURCE_FILTER_BY_CHAIN_ID[ChainId.Mainnet].sources;
|
const BUY_SOURCES = BUY_SOURCE_FILTER_BY_CHAIN_ID[ChainId.Mainnet].sources;
|
||||||
const SELL_SOURCES = SELL_SOURCE_FILTER_BY_CHAIN_ID[ChainId.Mainnet].sources;
|
const SELL_SOURCES = SELL_SOURCE_FILTER_BY_CHAIN_ID[ChainId.Mainnet].sources;
|
||||||
const TOKEN_ADJACENCY_GRAPH: TokenAdjacencyGraph = { default: [] };
|
const TOKEN_ADJACENCY_GRAPH = TokenAdjacencyGraph.getEmptyGraph();
|
||||||
|
|
||||||
const SIGNATURE = { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign };
|
const SIGNATURE = { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign };
|
||||||
const FOO_INTEGRATOR: Integrator = {
|
const FOO_INTEGRATOR: Integrator = {
|
||||||
|
108
packages/asset-swapper/test/token_adjacency_graph_test.ts
Normal file
108
packages/asset-swapper/test/token_adjacency_graph_test.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import * as chai from 'chai';
|
||||||
|
import 'mocha';
|
||||||
|
|
||||||
|
import { TokenAdjacencyGraphBuilder } from '../src/utils/token_adjacency_graph';
|
||||||
|
|
||||||
|
import { chaiSetup } from './utils/chai_setup';
|
||||||
|
|
||||||
|
chaiSetup.configure();
|
||||||
|
const expect = chai.expect;
|
||||||
|
|
||||||
|
describe('TokenAdjacencyGraphBuilder and TokenAdjacencyGraph', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('sanitizes passed default tokens to lower case', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['DEFAULT_1', 'DEFAULT_2']).build();
|
||||||
|
|
||||||
|
expect(graph.getAdjacentTokens('random_token')).to.deep.eq(['default_1', 'default_2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('add', () => {
|
||||||
|
it('adds a new token path to the graph', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1', 'default_2']).add('token_a', 'token_b').build();
|
||||||
|
|
||||||
|
expect(graph.getAdjacentTokens('token_a')).to.deep.eq(['default_1', 'default_2', 'token_b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds lower-cased token path to the graph', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1', 'default_2']).add('TOKEN_A', 'TOKEN_B').build();
|
||||||
|
|
||||||
|
expect(graph.getAdjacentTokens('token_a')).to.deep.eq(['default_1', 'default_2', 'token_b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores an existing to token', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder()
|
||||||
|
.add('token_a', 'token_b')
|
||||||
|
.add('token_a', 'token_b')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
expect(graph.getAdjacentTokens('token_a')).to.deep.eq(['token_b']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addBidirectional', () => {
|
||||||
|
it('adds a bidirectional path to the graph', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1']).addBidirectional('token_a', 'token_b').build();
|
||||||
|
|
||||||
|
expect(graph.getAdjacentTokens('token_a')).to.deep.eq(['default_1', 'token_b']);
|
||||||
|
expect(graph.getAdjacentTokens('token_b')).to.deep.eq(['default_1', 'token_a']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addCompleteSubgraph', () => {
|
||||||
|
it('adds a complete subgraph to the graph', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1'])
|
||||||
|
.addCompleteSubgraph(['token_a', 'token_b', 'token_c', 'token_d'])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
expect(graph.getAdjacentTokens('token_a')).to.deep.eq(['default_1', 'token_b', 'token_c', 'token_d']);
|
||||||
|
expect(graph.getAdjacentTokens('token_b')).to.deep.eq(['default_1', 'token_a', 'token_c', 'token_d']);
|
||||||
|
expect(graph.getAdjacentTokens('token_c')).to.deep.eq(['default_1', 'token_a', 'token_b', 'token_d']);
|
||||||
|
expect(graph.getAdjacentTokens('token_d')).to.deep.eq(['default_1', 'token_a', 'token_b', 'token_c']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tap', () => {
|
||||||
|
it('applies callback correctly', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1'])
|
||||||
|
.tap(g => {
|
||||||
|
g.add('token_a', 'token_b');
|
||||||
|
g.add('token_c', 'token_d');
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
expect(graph.getAdjacentTokens('token_a')).to.deep.eq(['default_1', 'token_b']);
|
||||||
|
expect(graph.getAdjacentTokens('token_c')).to.deep.eq(['default_1', 'token_d']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getIntermediateTokens', () => {
|
||||||
|
it('returns intermediate tokens without a duplicate ', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1'])
|
||||||
|
.add('token_a', 'token_b')
|
||||||
|
.add('token_c', 'token_b')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
expect(graph.getIntermediateTokens('token_a', 'token_c')).to.deep.eq(['default_1', 'token_b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns intermediate tokens after lower-casing taker and maker tokens', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1'])
|
||||||
|
.add('token_a', 'token_b')
|
||||||
|
.add('token_c', 'token_d')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
expect(graph.getIntermediateTokens('TOKEN_a', 'token_C')).to.deep.eq(['default_1', 'token_b', 'token_d']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns intermediate tokens excluding taker token or maker token ', async () => {
|
||||||
|
const graph = new TokenAdjacencyGraphBuilder(['default_1'])
|
||||||
|
.addBidirectional('token_a', 'token_b')
|
||||||
|
.addBidirectional('token_b', 'token_c')
|
||||||
|
.addBidirectional('token_c', 'token_a')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
expect(graph.getIntermediateTokens('token_a', 'token_c')).to.deep.eq(['default_1', 'token_b']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user