feat: [asset-swapper] more hops via token adjacency (#24)

* feat: [asset-swapper] more hops via token adjacency

* fix lint

* CHANGELOG
This commit is contained in:
Jacob Evans 2020-11-03 07:48:37 +10:00 committed by GitHub
parent f4709ed1cb
commit e10a81023a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 135 additions and 56 deletions

View File

@ -1,10 +1,14 @@
[
{
"version": "4.9.0",
"version": "5.0.0",
"changes": [
{
"note": "Support multiple `Shells` by supplying the `pool` address",
"pr": 17
},
{
"note": "Make use of Token Adjacency in more places. Moved as a parameter for the quote",
"pr": 24
}
]
},

View File

@ -14,7 +14,7 @@ import {
SwapQuoteRequestOpts,
SwapQuoterOpts,
} from './types';
import { DEFAULT_GET_MARKET_ORDERS_OPTS } from './utils/market_operation_utils/constants';
import { DEFAULT_GET_MARKET_ORDERS_OPTS, TOKENS } from './utils/market_operation_utils/constants';
const ETH_GAS_STATION_API_URL = 'https://ethgasstation.info/api/ethgasAPI.json';
const NULL_BYTES = '0x';
@ -42,6 +42,7 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000);
// default 50% buffer for selecting native orders to be aggregated with other sources
const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5;
const DEFAULT_INTERMEDIATE_TOKENS = [TOKENS.WETH, TOKENS.USDT, TOKENS.DAI, TOKENS.USDC];
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
chainId: ChainId.Mainnet,
orderRefreshIntervalMs: 10000, // 10 seconds
@ -125,6 +126,7 @@ export const constants = {
ONE_SECOND_MS,
ONE_MINUTE_MS,
DEFAULT_SWAP_QUOTER_OPTS,
DEFAULT_INTERMEDIATE_TOKENS,
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,

View File

@ -166,7 +166,6 @@ export class SwapQuoter {
samplerGasLimit,
liquidityProviderRegistryAddress,
rfqt,
tokenAdjacencyGraph,
} = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isValidOrderbook('orderbook', orderbook);
@ -215,7 +214,6 @@ export class SwapQuoter {
exchangeAddress: this._contractAddresses.exchange,
},
liquidityProviderRegistryAddress,
tokenAdjacencyGraph,
);
this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils);
}

View File

@ -4,12 +4,7 @@ import { TakerRequestQueryParams } from '@0x/quote-server';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import {
ERC20BridgeSource,
GetMarketOrdersOpts,
OptimizedMarketOrder,
TokenAdjacencyGraph,
} from './utils/market_operation_utils/types';
import { ERC20BridgeSource, GetMarketOrdersOpts, OptimizedMarketOrder } from './utils/market_operation_utils/types';
import { QuoteReport } from './utils/quote_report_generator';
/**
@ -315,7 +310,6 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
ethGasStationUrl?: string;
rfqt?: SwapQuoterRfqtOpts;
samplerOverrides?: SamplerOverrides;
tokenAdjacencyGraph?: TokenAdjacencyGraph;
}
/**

View File

@ -459,4 +459,5 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
exchangeProxyOverhead: () => ZERO_AMOUNT,
allowFallback: true,
shouldGenerateQuoteReport: false,
tokenAdjacencyGraph: {},
};

View File

@ -41,7 +41,6 @@ import {
OptimizerResult,
OptimizerResultWithReport,
OrderDomain,
TokenAdjacencyGraph,
} from './types';
// tslint:disable:boolean-naming
@ -109,7 +108,6 @@ export class MarketOperationUtils {
private readonly contractAddresses: AssetSwapperContractAddresses,
private readonly _orderDomain: OrderDomain,
private readonly _liquidityProviderRegistry: string = NULL_ADDRESS,
private readonly _tokenAdjacencyGraph: TokenAdjacencyGraph = {},
) {
this._wethAddress = contractAddresses.etherToken.toLowerCase();
this._multiBridge = contractAddresses.multiBridge.toLowerCase();
@ -182,7 +180,6 @@ export class MarketOperationUtils {
makerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._liquidityProviderRegistry,
this._multiBridge,
),
@ -192,7 +189,6 @@ export class MarketOperationUtils {
takerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._liquidityProviderRegistry,
this._multiBridge,
),
@ -203,6 +199,7 @@ export class MarketOperationUtils {
takerToken,
sampleAmounts,
this._wethAddress,
_opts.tokenAdjacencyGraph,
this._liquidityProviderRegistry,
this._multiBridge,
),
@ -211,8 +208,8 @@ export class MarketOperationUtils {
makerToken,
takerToken,
takerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
_opts.tokenAdjacencyGraph,
this._liquidityProviderRegistry,
),
);
@ -333,7 +330,6 @@ export class MarketOperationUtils {
makerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._liquidityProviderRegistry,
this._multiBridge,
),
@ -343,7 +339,6 @@ export class MarketOperationUtils {
takerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._liquidityProviderRegistry,
this._multiBridge,
),
@ -354,6 +349,7 @@ export class MarketOperationUtils {
takerToken,
sampleAmounts,
this._wethAddress,
_opts.tokenAdjacencyGraph,
this._liquidityProviderRegistry,
),
this._sampler.getTwoHopBuyQuotes(
@ -361,8 +357,8 @@ export class MarketOperationUtils {
makerToken,
takerToken,
makerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
_opts.tokenAdjacencyGraph,
this._liquidityProviderRegistry,
),
);
@ -493,6 +489,7 @@ export class MarketOperationUtils {
getNativeOrderTokens(orders[0])[1],
[makerAmounts[i]],
this._wethAddress,
_opts.tokenAdjacencyGraph,
),
),
];

View File

@ -34,7 +34,7 @@ export function getIntermediateTokens(
[wethAddress],
);
}
return intermediateTokens.filter(
return _.uniqBy(intermediateTokens, a => a.toLowerCase()).filter(
token => token.toLowerCase() !== makerToken.toLowerCase() && token.toLowerCase() !== takerToken.toLowerCase(),
);
}

View File

@ -669,8 +669,8 @@ export class SamplerOperations {
makerToken: string,
takerToken: string,
sellAmount: BigNumber,
tokenAdjacencyGraph: TokenAdjacencyGraph,
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
liquidityProviderRegistryAddress?: string,
): BatchedOperation<Array<DexSample<MultiHopFillData>>> {
const _sources = TWO_HOP_SOURCE_FILTERS.getAllowed(sources);
@ -685,6 +685,7 @@ export class SamplerOperations {
takerToken,
[ZERO_AMOUNT],
wethAddress,
tokenAdjacencyGraph,
liquidityProviderRegistryAddress,
);
const secondHopOps = this._getSellQuoteOperations(
@ -693,6 +694,7 @@ export class SamplerOperations {
intermediateToken,
[ZERO_AMOUNT],
wethAddress,
tokenAdjacencyGraph,
liquidityProviderRegistryAddress,
);
return new SamplerContractOperation({
@ -745,8 +747,8 @@ export class SamplerOperations {
makerToken: string,
takerToken: string,
buyAmount: BigNumber,
tokenAdjacencyGraph: TokenAdjacencyGraph,
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
liquidityProviderRegistryAddress?: string,
): BatchedOperation<Array<DexSample<MultiHopFillData>>> {
const _sources = TWO_HOP_SOURCE_FILTERS.getAllowed(sources);
@ -761,6 +763,7 @@ export class SamplerOperations {
takerToken,
[new BigNumber(0)],
wethAddress,
tokenAdjacencyGraph, // TODO is this a bad idea?
liquidityProviderRegistryAddress,
);
const secondHopOps = this._getBuyQuoteOperations(
@ -769,6 +772,7 @@ export class SamplerOperations {
intermediateToken,
[new BigNumber(0)],
wethAddress,
tokenAdjacencyGraph,
liquidityProviderRegistryAddress,
);
return new SamplerContractOperation({
@ -932,7 +936,6 @@ export class SamplerOperations {
makerToken: string,
takerToken: string,
takerFillAmount: BigNumber,
wethAddress: string,
liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string,
): BatchedOperation<BigNumber> {
@ -944,7 +947,8 @@ export class SamplerOperations {
makerToken,
takerToken,
[takerFillAmount],
wethAddress,
NULL_ADDRESS, // weth address
{}, // token adjacency
liquidityProviderRegistryAddress,
multiBridgeAddress,
);
@ -988,6 +992,7 @@ export class SamplerOperations {
takerToken: string,
takerFillAmounts: BigNumber[],
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string,
): BatchedOperation<DexSample[][]> {
@ -997,6 +1002,7 @@ export class SamplerOperations {
takerToken,
takerFillAmounts,
wethAddress,
tokenAdjacencyGraph,
liquidityProviderRegistryAddress,
multiBridgeAddress,
);
@ -1029,6 +1035,7 @@ export class SamplerOperations {
takerToken: string,
makerFillAmounts: BigNumber[],
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
liquidityProviderRegistryAddress?: string,
): BatchedOperation<DexSample[][]> {
const subOps = this._getBuyQuoteOperations(
@ -1037,6 +1044,7 @@ export class SamplerOperations {
takerToken,
makerFillAmounts,
wethAddress,
tokenAdjacencyGraph,
liquidityProviderRegistryAddress,
);
return {
@ -1068,6 +1076,7 @@ export class SamplerOperations {
takerToken: string,
takerFillAmounts: BigNumber[],
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
liquidityProviderRegistryAddress?: string,
multiBridgeAddress?: string,
): SourceQuoteOperation[] {
@ -1076,6 +1085,11 @@ export class SamplerOperations {
)
.exclude(multiBridgeAddress || multiBridgeAddress === NULL_ADDRESS ? [] : [ERC20BridgeSource.MultiBridge])
.getAllowed(sources);
// Find the adjacent tokens in the provided tooken adjacency graph,
// e.g if this is DAI->USDC we may check for DAI->WETH->USDC
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress);
return _.flatten(
_sources.map(
(source): SourceQuoteOperation | SourceQuoteOperation[] => {
@ -1086,25 +1100,17 @@ export class SamplerOperations {
return this.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
case ERC20BridgeSource.UniswapV2:
const ops = [this.getUniswapV2SellQuotes([takerToken, makerToken], takerFillAmounts)];
if (takerToken !== wethAddress && makerToken !== wethAddress) {
ops.push(
this.getUniswapV2SellQuotes(
[takerToken, wethAddress, makerToken],
takerFillAmounts,
),
);
}
intermediateTokens.forEach(t => {
ops.push(this.getUniswapV2SellQuotes([takerToken, t, makerToken], takerFillAmounts));
});
return ops;
case ERC20BridgeSource.SushiSwap:
const sushiOps = [this.getSushiSwapSellQuotes([takerToken, makerToken], takerFillAmounts)];
if (takerToken !== wethAddress && makerToken !== wethAddress) {
intermediateTokens.forEach(t => {
sushiOps.push(
this.getSushiSwapSellQuotes(
[takerToken, wethAddress, makerToken],
takerFillAmounts,
),
this.getSushiSwapSellQuotes([takerToken, t, makerToken], takerFillAmounts),
);
}
});
return sushiOps;
case ERC20BridgeSource.Kyber:
return getKyberReserveIdsForPair(takerToken, makerToken).map(reserveId =>
@ -1211,11 +1217,17 @@ export class SamplerOperations {
takerToken: string,
makerFillAmounts: BigNumber[],
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
liquidityProviderRegistryAddress?: string,
): SourceQuoteOperation[] {
const _sources = BATCH_SOURCE_FILTERS.exclude(
liquidityProviderRegistryAddress ? [] : [ERC20BridgeSource.LiquidityProvider],
).getAllowed(sources);
// Find the adjacent tokens in the provided tooken adjacency graph,
// e.g if this is DAI->USDC we may check for DAI->WETH->USDC
const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress);
return _.flatten(
_sources.map(
(source): SourceQuoteOperation | SourceQuoteOperation[] => {
@ -1226,19 +1238,17 @@ export class SamplerOperations {
return this.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts);
case ERC20BridgeSource.UniswapV2:
const ops = [this.getUniswapV2BuyQuotes([takerToken, makerToken], makerFillAmounts)];
if (takerToken !== wethAddress && makerToken !== wethAddress) {
ops.push(
this.getUniswapV2BuyQuotes([takerToken, wethAddress, makerToken], makerFillAmounts),
);
}
intermediateTokens.forEach(t => {
ops.push(this.getUniswapV2BuyQuotes([takerToken, t, makerToken], makerFillAmounts));
});
return ops;
case ERC20BridgeSource.SushiSwap:
const sushiOps = [this.getSushiSwapBuyQuotes([takerToken, makerToken], makerFillAmounts)];
if (takerToken !== wethAddress && makerToken !== wethAddress) {
intermediateTokens.forEach(t => {
sushiOps.push(
this.getSushiSwapBuyQuotes([takerToken, wethAddress, makerToken], makerFillAmounts),
this.getSushiSwapBuyQuotes([takerToken, t, makerToken], makerFillAmounts),
);
}
});
return sushiOps;
case ERC20BridgeSource.Kyber:
return getKyberReserveIdsForPair(takerToken, makerToken).map(reserveId =>

View File

@ -317,6 +317,11 @@ export interface GetMarketOrdersOpts {
* Whether to generate a quote report
*/
shouldGenerateQuoteReport: boolean;
/**
* Token addresses with a list of adjacent intermediary tokens to consider
* hopping to. E.g DAI->USDC via an adjacent token WETH
*/
tokenAdjacencyGraph: TokenAdjacencyGraph;
}
/**

View File

@ -18,7 +18,7 @@ import {
computeBalancerSellQuote,
} from '../src/utils/market_operation_utils/balancer_utils';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
import { ERC20BridgeSource, TokenAdjacencyGraph } from '../src/utils/market_operation_utils/types';
import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache';
import { MockBancorService } from './utils/mock_bancor_service';
@ -36,6 +36,8 @@ describe('DexSampler tests', () => {
const wethAddress = getContractAddressesForChainOrThrow(CHAIN_ID).etherToken;
const exchangeAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchange;
const tokenAdjacencyGraph: TokenAdjacencyGraph = {};
describe('getSampleAmounts()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const NUM_SAMPLES = 16;
@ -175,6 +177,7 @@ describe('DexSampler tests', () => {
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
tokenAdjacencyGraph,
registry,
),
);
@ -211,6 +214,7 @@ describe('DexSampler tests', () => {
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
tokenAdjacencyGraph,
registry,
),
);
@ -252,7 +256,8 @@ describe('DexSampler tests', () => {
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
randomAddress(),
wethAddress,
tokenAdjacencyGraph,
randomAddress(),
multiBridge,
),
@ -419,6 +424,7 @@ describe('DexSampler tests', () => {
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
tokenAdjacencyGraph,
),
);
const expectedQuotes = sources.map(s =>
@ -563,6 +569,7 @@ describe('DexSampler tests', () => {
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
tokenAdjacencyGraph,
),
);
const expectedQuotes = sources.map(s =>

View File

@ -41,6 +41,7 @@ import {
GetMarketOrdersOpts,
MarketSideLiquidity,
NativeFillData,
TokenAdjacencyGraph,
} from '../src/utils/market_operation_utils/types';
const MAKER_TOKEN = randomAddress();
@ -64,6 +65,7 @@ const DEFAULT_EXCLUDED = [
];
const BUY_SOURCES = BUY_SOURCE_FILTER.sources;
const SELL_SOURCES = SELL_SOURCE_FILTER.sources;
const TOKEN_ADJACENCY_GRAPH: TokenAdjacencyGraph = {};
// tslint:disable: custom-no-magic-numbers promise-function-async
describe('MarketOperationUtils tests', () => {
@ -219,6 +221,7 @@ describe('MarketOperationUtils tests', () => {
takerToken: string,
fillAmounts: BigNumber[],
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
liquidityProviderAddress?: string,
) => DexSample[][];
@ -248,6 +251,7 @@ describe('MarketOperationUtils tests', () => {
takerToken: string,
fillAmounts: BigNumber[],
wethAddress: string,
tokenAdjacencyGraph: TokenAdjacencyGraph = TOKEN_ADJACENCY_GRAPH,
liquidityProviderAddress?: string,
) => {
liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress;
@ -258,6 +262,7 @@ describe('MarketOperationUtils tests', () => {
takerToken,
fillAmounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
liquidityProviderAddress,
);
};
@ -548,7 +553,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length;
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getSellQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -563,7 +575,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getSellQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
getTwoHopSellQuotes: (...args: any[]) => {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
@ -647,7 +666,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getSellQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ...args: any[]) => {
if (sources.length !== 0) {
@ -686,7 +712,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getSellQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ...args: any[]) => {
if (sources.length !== 0) {
@ -1429,7 +1462,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length;
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getBuyQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -1444,7 +1484,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getBuyQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
@ -1531,7 +1578,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getBuyQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
@ -1570,7 +1624,14 @@ describe('MarketOperationUtils tests', () => {
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getBuyQuotes(
sources,
makerToken,
takerToken,
amounts,
wethAddress,
TOKEN_ADJACENCY_GRAPH,
);
},
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {