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:
Kyu 2022-07-18 13:02:56 -07:00 committed by GitHub
parent f7cb7a0f51
commit b72b8b5ffd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 251 additions and 107 deletions

View File

@ -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": [

View File

@ -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 {

View File

@ -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).

View File

@ -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(),
}; };

View File

@ -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.
*/ */

View File

@ -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`.

View File

@ -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[] => {

View File

@ -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[];

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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 = {

View 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']);
});
});
});