diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index f5f758141a..93f1af57d2 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "16.64.0", + "changes": [ + { + "note": "Refactor `TokenAdjacency` and `TokenAdjacencyBuilder`", + "pr": 517 + } + ] + }, { "version": "16.63.1", "changes": [ diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 78ff8bec14..a9f56cb9d8 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -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 { diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index 9ef5e8830e..68667c633f 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -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). diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 83e12e3913..29aa44dd0e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -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( // 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( { - [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( @@ -2604,7 +2587,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit 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. */ diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts index 3024de2ace..472a7c5eec 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts @@ -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`. diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index 79cd22fdf1..b9686b8c5c 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -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 = 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[] => { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index abae2c72a3..3e802bacb5 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -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>>; } -export interface TokenAdjacencyGraph { - [token: string]: string[]; - default: string[]; -} - export interface LiquidityProviderRegistry { [address: string]: { tokens: string[]; diff --git a/packages/asset-swapper/src/utils/token_adjacency_graph.ts b/packages/asset-swapper/src/utils/token_adjacency_graph.ts new file mode 100644 index 0000000000..97dc7470ea --- /dev/null +++ b/packages/asset-swapper/src/utils/token_adjacency_graph.ts @@ -0,0 +1,84 @@ +import * as _ from 'lodash'; + +import { Address } from '../types'; + +export class TokenAdjacencyGraph { + private readonly _graph: Map; + private readonly _defaultTokens: readonly Address[]; + + public static getEmptyGraph(): TokenAdjacencyGraph { + return new TokenAdjacencyGraphBuilder().build(); + } + + /** Prefer using {@link TokenAdjacencyGraphBuilder}. */ + constructor(graph: Map, 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; + 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); + } +} diff --git a/packages/asset-swapper/src/utils/token_adjacency_graph_builder.ts b/packages/asset-swapper/src/utils/token_adjacency_graph_builder.ts deleted file mode 100644 index c08dfc0cf5..0000000000 --- a/packages/asset-swapper/src/utils/token_adjacency_graph_builder.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/asset-swapper/test/dex_sampler_test.ts b/packages/asset-swapper/test/dex_sampler_test.ts index b7da4c4a4a..f4adb7673f 100644 --- a/packages/asset-swapper/test/dex_sampler_test.ts +++ b/packages/asset-swapper/test/dex_sampler_test.ts @@ -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); diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 7d01bef5ef..874fcc92d9 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -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 = { diff --git a/packages/asset-swapper/test/token_adjacency_graph_test.ts b/packages/asset-swapper/test/token_adjacency_graph_test.ts new file mode 100644 index 0000000000..cfaf9ecfb2 --- /dev/null +++ b/packages/asset-swapper/test/token_adjacency_graph_test.ts @@ -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']); + }); + }); +});