diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 05bb71e461..07df81beb3 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -33,6 +33,10 @@ { "note": "Add Balancer V2 integration", "pr": 206 + }, + { + "note": "Re-work the PoolCache for Balancer et al", + "pr": 226 } ] }, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 50e7f0273d..3b296f6acb 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -46,7 +46,6 @@ import { OptimizerResult, OptimizerResultWithReport, OrderDomain, - SourcesWithPoolsCache, } from './types'; // tslint:disable:boolean-naming @@ -101,15 +100,6 @@ export class MarketOperationUtils { const quoteSourceFilters = this._sellSources.merge(requestFilters); const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); - // Can't sample Balancer or Cream on-chain without the pools cache - const sourcesWithStaleCaches: SourcesWithPoolsCache[] = (Object.keys( - this._sampler.poolsCaches, - ) as SourcesWithPoolsCache[]).filter(s => !this._sampler.poolsCaches[s].isFresh(takerToken, makerToken)); - // tslint:disable-next-line:promise-function-async - const cacheRefreshPromises: Array> = sourcesWithStaleCaches.map(s => - this._sampler.poolsCaches[s].getFreshPoolsForPairAsync(takerToken, makerToken), - ); - // Used to determine whether the tx origin is an EOA or a contract const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; @@ -142,6 +132,10 @@ export class MarketOperationUtils { ), this._sampler.isAddressContract(txOrigin), ); + + // Refresh the cached pools asynchronously if required + void this._refreshPoolCacheIfRequiredAsync(takerToken, makerToken); + const [ [ tokenDecimals, @@ -152,7 +146,7 @@ export class MarketOperationUtils { rawTwoHopQuotes, isTxOriginContract, ], - ] = await Promise.all([samplerPromise, Promise.all(cacheRefreshPromises)]); + ] = await Promise.all([samplerPromise]); // Filter out any invalid two hop quotes where we couldn't find a route const twoHopQuotes = rawTwoHopQuotes.filter( @@ -207,15 +201,6 @@ export class MarketOperationUtils { const quoteSourceFilters = this._buySources.merge(requestFilters); const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); - // Can't sample Balancer or Cream on-chain without the pools cache - const sourcesWithStaleCaches: SourcesWithPoolsCache[] = (Object.keys( - this._sampler.poolsCaches, - ) as SourcesWithPoolsCache[]).filter(s => !this._sampler.poolsCaches[s].isFresh(takerToken, makerToken)); - // tslint:disable-next-line:promise-function-async - const cacheRefreshPromises: Array> = sourcesWithStaleCaches.map(s => - this._sampler.poolsCaches[s].getFreshPoolsForPairAsync(takerToken, makerToken), - ); - // Used to determine whether the tx origin is an EOA or a contract const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; @@ -249,6 +234,9 @@ export class MarketOperationUtils { this._sampler.isAddressContract(txOrigin), ); + // Refresh the cached pools asynchronously if required + void this._refreshPoolCacheIfRequiredAsync(takerToken, makerToken); + const [ [ tokenDecimals, @@ -259,7 +247,7 @@ export class MarketOperationUtils { rawTwoHopQuotes, isTxOriginContract, ], - ] = await Promise.all([samplerPromise, Promise.all(cacheRefreshPromises)]); + ] = await Promise.all([samplerPromise]); // Filter out any invalid two hop quotes where we couldn't find a route const twoHopQuotes = rawTwoHopQuotes.filter( @@ -691,6 +679,17 @@ export class MarketOperationUtils { } return { ...optimizerResult, quoteReport }; } + + private async _refreshPoolCacheIfRequiredAsync(takerToken: string, makerToken: string): Promise { + void Promise.all( + Object.values(this._sampler.poolsCaches).map(async cache => { + if (cache.isFresh(takerToken, makerToken)) { + return Promise.resolve([]); + } + return cache.getFreshPoolsForPairAsync(takerToken, makerToken); + }), + ); + } } // tslint:disable: max-file-line-count diff --git a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_utils.ts index 2099526d7d..dbd451397e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_utils.ts @@ -16,6 +16,7 @@ interface BalancerPoolResponse { tokensList: string[]; totalWeight: string; } + export class BalancerPoolsCache extends PoolsCache { constructor( private readonly _subgraphUrl: string = BALANCER_SUBGRAPH_URL, @@ -63,7 +64,8 @@ export class BalancerPoolsCache extends PoolsCache { const poolData = parsePoolData([pool], from, to); fromToPools[from][to].push(poolData[0]); // Cache this as we progress through - this._cachePoolsForPair(from, to, fromToPools[from][to]); + const expiresAt = Date.now() + this._cacheTimeMs; + this._cachePoolsForPair(from, to, fromToPools[from][to], expiresAt); } catch { // soldier on } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_v2_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_v2_utils.ts index 0a5f60da8c..d0c6d23234 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_v2_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_v2_utils.ts @@ -58,25 +58,29 @@ export class BalancerV2PoolsCache extends PoolsCache { } } `; - const { pools } = await request(this.subgraphUrl, query); - return pools.map((pool: any) => { - const tToken = pool.tokens.find((t: any) => t.address === takerToken); - const mToken = pool.tokens.find((t: any) => t.address === makerToken); - const swap = pool.swaps[0]; - const tokenAmountOut = swap ? swap.tokenAmountOut : undefined; - const tokenAmountIn = swap ? swap.tokenAmountIn : undefined; - const spotPrice = - tokenAmountOut && tokenAmountIn ? new BigNumber(tokenAmountOut).div(tokenAmountIn) : undefined; // TODO: xianny check + try { + const { pools } = await request(this.subgraphUrl, query); + return pools.map((pool: any) => { + const tToken = pool.tokens.find((t: any) => t.address === takerToken); + const mToken = pool.tokens.find((t: any) => t.address === makerToken); + const swap = pool.swaps[0]; + const tokenAmountOut = swap ? swap.tokenAmountOut : undefined; + const tokenAmountIn = swap ? swap.tokenAmountIn : undefined; + const spotPrice = + tokenAmountOut && tokenAmountIn ? new BigNumber(tokenAmountOut).div(tokenAmountIn) : undefined; // TODO: xianny check - return { - id: pool.id, - balanceIn: new BigNumber(tToken.balance), - balanceOut: new BigNumber(mToken.balance), - weightIn: new BigNumber(tToken.weight), - weightOut: new BigNumber(mToken.weight), - swapFee: new BigNumber(pool.swapFee), - spotPrice, - }; - }); + return { + id: pool.id, + balanceIn: new BigNumber(tToken.balance), + balanceOut: new BigNumber(mToken.balance), + weightIn: new BigNumber(tToken.weight), + weightOut: new BigNumber(mToken.weight), + swapFee: new BigNumber(pool.swapFee), + spotPrice, + }; + }); + } catch (e) { + return []; + } } } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/pools_cache.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/pools_cache.ts index ddf65b2c2c..03ef57f088 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/pools_cache.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/pools_cache.ts @@ -1,18 +1,26 @@ import { Pool } from '@balancer-labs/sor/dist/types'; + +import { ONE_HOUR_IN_SECONDS, ONE_SECOND_MS } from '../constants'; export { Pool }; export interface CacheValue { - timestamp: number; + expiresAt: number; pools: Pool[]; } // tslint:disable:custom-no-magic-numbers -const FIVE_SECONDS_MS = 5 * 1000; -const ONE_DAY_MS = 24 * 60 * 60 * 1000; +// Cache results for 30mins +const DEFAULT_CACHE_TIME_MS = (ONE_HOUR_IN_SECONDS / 2) * ONE_SECOND_MS; const DEFAULT_TIMEOUT_MS = 1000; // tslint:enable:custom-no-magic-numbers export abstract class PoolsCache { - constructor(protected readonly _cache: { [key: string]: CacheValue }) {} + protected static _isExpired(value: CacheValue): boolean { + return Date.now() >= value.expiresAt; + } + constructor( + protected readonly _cache: { [key: string]: CacheValue }, + protected readonly _cacheTimeMs: number = DEFAULT_CACHE_TIME_MS, + ) {} public async getFreshPoolsForPairAsync( takerToken: string, @@ -26,46 +34,43 @@ export abstract class PoolsCache { public getCachedPoolAddressesForPair( takerToken: string, makerToken: string, - cacheExpiryMs?: number, + ignoreExpired: boolean = true, ): string[] | undefined { const key = JSON.stringify([takerToken, makerToken]); const value = this._cache[key]; - if (cacheExpiryMs === undefined) { + if (ignoreExpired) { return value === undefined ? [] : value.pools.map(pool => pool.id); } - const minTimestamp = Date.now() - cacheExpiryMs; - if (value === undefined || value.timestamp < minTimestamp) { + if (!value) { return undefined; - } else { - return value.pools.map(pool => pool.id); } + if (PoolsCache._isExpired(value)) { + return undefined; + } + return (value || []).pools.map(pool => pool.id); } public isFresh(takerToken: string, makerToken: string): boolean { - const cached = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS); - return cached !== undefined && cached.length > 0; + const cached = this.getCachedPoolAddressesForPair(takerToken, makerToken, false); + return cached !== undefined; } - protected async _getAndSaveFreshPoolsForPairAsync( - takerToken: string, - makerToken: string, - cacheExpiryMs: number = FIVE_SECONDS_MS, - ): Promise { + protected async _getAndSaveFreshPoolsForPairAsync(takerToken: string, makerToken: string): Promise { const key = JSON.stringify([takerToken, makerToken]); const value = this._cache[key]; - const minTimestamp = Date.now() - cacheExpiryMs; - if (value === undefined || value.timestamp < minTimestamp) { + if (value === undefined || value.expiresAt >= Date.now()) { const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); - this._cachePoolsForPair(takerToken, makerToken, pools); + const expiresAt = Date.now() + this._cacheTimeMs; + this._cachePoolsForPair(takerToken, makerToken, pools, expiresAt); } return this._cache[key].pools; } - protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[]): void { + protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[], expiresAt: number): void { const key = JSON.stringify([takerToken, makerToken]); this._cache[key] = { pools, - timestamp: Date.now(), + expiresAt, }; }