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",
|
||||
"changes": [
|
||||
|
@ -166,9 +166,10 @@ export {
|
||||
NativeFillData,
|
||||
OptimizedMarketOrder,
|
||||
SourceQuoteOperation,
|
||||
TokenAdjacencyGraph,
|
||||
UniswapV2FillData,
|
||||
} 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 { ProtocolFeeUtils } from './utils/protocol_fee_utils';
|
||||
export {
|
||||
|
@ -17,11 +17,13 @@ import {
|
||||
GetMarketOrdersOpts,
|
||||
LiquidityProviderRegistry,
|
||||
OptimizedMarketOrder,
|
||||
TokenAdjacencyGraph,
|
||||
} from './utils/market_operation_utils/types';
|
||||
export { SamplerMetrics } from './utils/market_operation_utils/types';
|
||||
import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from './utils/quote_report_generator';
|
||||
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).
|
||||
|
@ -3,7 +3,7 @@ import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
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 { SourceFilters } from './source_filters';
|
||||
@ -32,7 +32,6 @@ import {
|
||||
MultiHopFillData,
|
||||
PlatypusInfo,
|
||||
PsmInfo,
|
||||
TokenAdjacencyGraph,
|
||||
UniswapV2FillData,
|
||||
UniswapV3FillData,
|
||||
} 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
|
||||
export const DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID = valueByChainId<TokenAdjacencyGraph>(
|
||||
{
|
||||
[ChainId.Mainnet]: new TokenAdjacencyGraphBuilder({
|
||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Mainnet],
|
||||
})
|
||||
[ChainId.Mainnet]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Mainnet])
|
||||
.tap(builder => {
|
||||
// Mirror Protocol
|
||||
builder.add(MAINNET_TOKENS.MIR, MAINNET_TOKENS.UST);
|
||||
// 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
|
||||
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
|
||||
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
|
||||
builder.add(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.FXS).add(MAINNET_TOKENS.FXS, MAINNET_TOKENS.FRAX);
|
||||
builder.add(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.OHM).add(MAINNET_TOKENS.OHM, MAINNET_TOKENS.FRAX);
|
||||
builder.addBidirectional(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.FXS);
|
||||
builder.addBidirectional(MAINNET_TOKENS.FRAX, MAINNET_TOKENS.OHM);
|
||||
// REDACTED CARTEL
|
||||
builder
|
||||
.add(MAINNET_TOKENS.OHMV2, MAINNET_TOKENS.BTRFLY)
|
||||
.add(MAINNET_TOKENS.BTRFLY, MAINNET_TOKENS.OHMV2);
|
||||
builder.addBidirectional(MAINNET_TOKENS.OHMV2, MAINNET_TOKENS.BTRFLY);
|
||||
// Lido
|
||||
builder
|
||||
.add(MAINNET_TOKENS.stETH, MAINNET_TOKENS.wstETH)
|
||||
.add(MAINNET_TOKENS.wstETH, MAINNET_TOKENS.stETH);
|
||||
builder.addBidirectional(MAINNET_TOKENS.stETH, MAINNET_TOKENS.wstETH);
|
||||
})
|
||||
// Build
|
||||
.build(),
|
||||
[ChainId.BSC]: new TokenAdjacencyGraphBuilder({
|
||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.BSC],
|
||||
}).build(),
|
||||
[ChainId.Polygon]: new TokenAdjacencyGraphBuilder({
|
||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Polygon],
|
||||
})
|
||||
[ChainId.BSC]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.BSC]).build(),
|
||||
[ChainId.Polygon]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Polygon])
|
||||
.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(),
|
||||
[ChainId.Avalanche]: new TokenAdjacencyGraphBuilder({
|
||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Avalanche],
|
||||
})
|
||||
[ChainId.Avalanche]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Avalanche])
|
||||
.tap(builder => {
|
||||
// Synapse nETH/aWETH pool
|
||||
builder
|
||||
.add(AVALANCHE_TOKENS.aWETH, AVALANCHE_TOKENS.nETH)
|
||||
.add(AVALANCHE_TOKENS.nETH, AVALANCHE_TOKENS.aWETH);
|
||||
builder.addBidirectional(AVALANCHE_TOKENS.aWETH, AVALANCHE_TOKENS.nETH);
|
||||
// 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(),
|
||||
[ChainId.Fantom]: new TokenAdjacencyGraphBuilder({
|
||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Fantom],
|
||||
}).build(),
|
||||
[ChainId.Celo]: new TokenAdjacencyGraphBuilder({
|
||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Celo],
|
||||
}).build(),
|
||||
[ChainId.Optimism]: new TokenAdjacencyGraphBuilder({
|
||||
default: DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Optimism],
|
||||
}).build(),
|
||||
[ChainId.Fantom]: new TokenAdjacencyGraphBuilder(
|
||||
DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Fantom],
|
||||
).build(),
|
||||
[ChainId.Celo]: new TokenAdjacencyGraphBuilder(DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID[ChainId.Celo]).build(),
|
||||
[ChainId.Optimism]: new TokenAdjacencyGraphBuilder(
|
||||
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>(
|
||||
@ -2604,7 +2587,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice
|
||||
allowFallback: true,
|
||||
shouldGenerateQuoteReport: true,
|
||||
shouldIncludePriceComparisonsReport: false,
|
||||
tokenAdjacencyGraph: { default: [] },
|
||||
tokenAdjacencyGraph: TokenAdjacencyGraph.getEmptyGraph(),
|
||||
neonRouterNumSamples: 14,
|
||||
fillAdjustor: new IdentityFillAdjustor(),
|
||||
};
|
||||
|
@ -12,26 +12,8 @@ import {
|
||||
FillAdjustor,
|
||||
MarketSideLiquidity,
|
||||
MultiHopFillData,
|
||||
TokenAdjacencyGraph,
|
||||
} 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.
|
||||
*/
|
||||
|
@ -3,10 +3,11 @@ import { BigNumber, NULL_BYTES } from '@0x/utils';
|
||||
|
||||
import { SamplerOverrides } from '../../types';
|
||||
import { ERC20BridgeSamplerContract } from '../../wrappers';
|
||||
import { TokenAdjacencyGraph } from '../token_adjacency_graph';
|
||||
|
||||
import { BancorService } from './bancor_service';
|
||||
import { PoolsCacheMap, SamplerOperations } from './sampler_operations';
|
||||
import { BatchedOperation, LiquidityProviderRegistry, TokenAdjacencyGraph } from './types';
|
||||
import { BatchedOperation, LiquidityProviderRegistry } from './types';
|
||||
|
||||
/**
|
||||
* Generate sample amounts up to `maxFillAmount`.
|
||||
|
@ -7,6 +7,7 @@ import { AaveV2Sampler } from '../../noop_samplers/AaveV2Sampler';
|
||||
import { GeistSampler } from '../../noop_samplers/GeistSampler';
|
||||
import { SamplerCallResult, SignedNativeOrder } from '../../types';
|
||||
import { ERC20BridgeSamplerContract } from '../../wrappers';
|
||||
import { TokenAdjacencyGraph } from '../token_adjacency_graph';
|
||||
|
||||
import { AaveV2ReservesCache } from './aave_reserves_cache';
|
||||
import { BancorService } from './bancor_service';
|
||||
@ -53,7 +54,6 @@ import {
|
||||
} from './constants';
|
||||
import { getGeistInfoForPair } from './geist_utils';
|
||||
import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
|
||||
import { getIntermediateTokens } from './multihop_utils';
|
||||
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache';
|
||||
import { BalancerV2SwapInfoCache } from './pools_cache/balancer_v2_utils_new';
|
||||
import { SamplerContractOperation } from './sampler_contract_operation';
|
||||
@ -94,7 +94,6 @@ import {
|
||||
ShellFillData,
|
||||
SourceQuoteOperation,
|
||||
SourcesWithPoolsCache,
|
||||
TokenAdjacencyGraph,
|
||||
UniswapV2FillData,
|
||||
UniswapV3FillData,
|
||||
VelodromeFillData,
|
||||
@ -140,7 +139,7 @@ export class SamplerOperations {
|
||||
public readonly chainId: ChainId,
|
||||
protected readonly _samplerContract: ERC20BridgeSamplerContract,
|
||||
poolsCaches?: PoolsCacheMap,
|
||||
protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [] },
|
||||
protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = TokenAdjacencyGraph.getEmptyGraph(),
|
||||
liquidityProviderRegistry: LiquidityProviderRegistry = {},
|
||||
bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined,
|
||||
) {
|
||||
@ -810,7 +809,7 @@ export class SamplerOperations {
|
||||
if (_sources.length === 0) {
|
||||
return SamplerOperations.constant([]);
|
||||
}
|
||||
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, this.tokenAdjacencyGraph);
|
||||
const intermediateTokens = this.tokenAdjacencyGraph.getIntermediateTokens(makerToken, takerToken);
|
||||
const subOps = intermediateTokens.map(intermediateToken => {
|
||||
const firstHopOps = this._getSellQuoteOperations(_sources, intermediateToken, takerToken, [ZERO_AMOUNT]);
|
||||
const secondHopOps = this._getSellQuoteOperations(_sources, makerToken, intermediateToken, [ZERO_AMOUNT]);
|
||||
@ -865,7 +864,7 @@ export class SamplerOperations {
|
||||
if (_sources.length === 0) {
|
||||
return SamplerOperations.constant([]);
|
||||
}
|
||||
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, this.tokenAdjacencyGraph);
|
||||
const intermediateTokens = this.tokenAdjacencyGraph.getIntermediateTokens(makerToken, takerToken);
|
||||
const subOps = intermediateTokens.map(intermediateToken => {
|
||||
const firstHopOps = this._getBuyQuoteOperations(_sources, intermediateToken, takerToken, [
|
||||
new BigNumber(0),
|
||||
@ -1325,9 +1324,13 @@ export class SamplerOperations {
|
||||
if (makerToken.toLowerCase() === nativeToken.toLowerCase()) {
|
||||
return SamplerOperations.constant(new BigNumber(1));
|
||||
}
|
||||
const subOps = this._getSellQuoteOperations(sources, makerToken, nativeToken, [nativeFillAmount], {
|
||||
default: [],
|
||||
});
|
||||
const subOps = this._getSellQuoteOperations(
|
||||
sources,
|
||||
makerToken,
|
||||
nativeToken,
|
||||
[nativeFillAmount],
|
||||
TokenAdjacencyGraph.getEmptyGraph(),
|
||||
);
|
||||
return this._createBatch(
|
||||
subOps,
|
||||
(samples: BigNumber[][]) => {
|
||||
@ -1423,7 +1426,7 @@ export class SamplerOperations {
|
||||
): SourceQuoteOperation[] {
|
||||
// Find the adjacent tokens in the provided token adjacency graph,
|
||||
// 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.
|
||||
const _sources = SELL_SOURCE_FILTER_BY_CHAIN_ID[this.chainId]
|
||||
.exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native])
|
||||
@ -1767,7 +1770,7 @@ export class SamplerOperations {
|
||||
): SourceQuoteOperation[] {
|
||||
// Find the adjacent tokens in the provided token adjacency graph,
|
||||
// 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);
|
||||
return _.flatten(
|
||||
_sources.map((source): SourceQuoteOperation | SourceQuoteOperation[] => {
|
||||
|
@ -10,6 +10,7 @@ import { NativeOrderWithFillableAmounts, RfqFirmQuoteValidator, RfqRequestOpts }
|
||||
import { QuoteRequestor, V4RFQIndicativeQuoteMM } from '../../utils/quote_requestor';
|
||||
import { IRfqClient } from '../irfq_client';
|
||||
import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from '../quote_report_generator';
|
||||
import { TokenAdjacencyGraph } from '../token_adjacency_graph';
|
||||
|
||||
import { SourceFilters } from './source_filters';
|
||||
|
||||
@ -642,11 +643,6 @@ export interface RawQuotes {
|
||||
dexQuotes: Array<Array<DexSample<FillData>>>;
|
||||
}
|
||||
|
||||
export interface TokenAdjacencyGraph {
|
||||
[token: string]: string[];
|
||||
default: string[];
|
||||
}
|
||||
|
||||
export interface LiquidityProviderRegistry {
|
||||
[address: 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 { 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 { generatePseudoRandomSalt } from './utils/utils';
|
||||
@ -29,7 +30,7 @@ describe('DexSampler tests', () => {
|
||||
const wethAddress = getContractAddressesForChainOrThrow(CHAIN_ID).etherToken;
|
||||
const exchangeProxyAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchangeProxy;
|
||||
|
||||
const tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [wethAddress] };
|
||||
const tokenAdjacencyGraph = new TokenAdjacencyGraphBuilder([wethAddress]).build();
|
||||
|
||||
describe('getSampleAmounts()', () => {
|
||||
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 TypeMoq from 'typemoq';
|
||||
|
||||
import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src';
|
||||
import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder, TokenAdjacencyGraph } from '../src';
|
||||
import { Integrator } from '../src/types';
|
||||
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
||||
import {
|
||||
@ -39,7 +39,6 @@ import {
|
||||
MarketSideLiquidity,
|
||||
OptimizedMarketBridgeOrder,
|
||||
OptimizerResultWithReport,
|
||||
TokenAdjacencyGraph,
|
||||
} from '../src/utils/market_operation_utils/types';
|
||||
|
||||
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 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 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