diff --git a/package-lock.json b/package-lock.json index 24e2652a..ab6082b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@curvefi/api", - "version": "2.66.30", + "version": "2.66.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@curvefi/api", - "version": "2.66.30", + "version": "2.66.31", "license": "MIT", "dependencies": { "@curvefi/ethcall": "^6.0.13", diff --git a/package.json b/package.json index bd949eec..cc880e24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@curvefi/api", - "version": "2.66.30", + "version": "2.66.31", "description": "JavaScript library for curve.finance", "main": "lib/index.js", "author": "Macket", diff --git a/src/cached.ts b/src/cached.ts index 26ae7b83..40c02433 100644 --- a/src/cached.ts +++ b/src/cached.ts @@ -1,25 +1,80 @@ -import memoize from "memoizee"; import {IDict, IExtendedPoolDataFromApi, INetworkName, IPoolType} from "./interfaces.js"; import {createCrvApyDict, createUsdPricesDict, uncached_getAllPoolsFromApi} from './external-api.js' +/** + * Memoizes a function that returns a promise. + * Custom function instead of `memoizee` because we want to be able to set the cache manually based on server data. + * @param fn The function that returns a promise and will be memoized + * @param maxAge The maximum age of the cache in milliseconds + * @param createKey A function that creates a key for the cache based on the arguments passed to the function + * @returns A memoized `fn` function that includes a `set` method to set the cache manually + */ +const memoize = Promise>(fn: TFunc, { + maxAge, + createKey = (list) => list.toString(), +}: { + maxAge: number, + createKey?: (args: TParams) => string +}) => { + const cache: Record> = {}; + const timeouts: Record> = {}; + + const setCache = (key: string, promise?: Promise) => { + if (promise) { + cache[key] = promise; + } else if (key in cache) { + delete cache[key]; + } + if (key in timeouts) { + clearTimeout(timeouts[key]); + delete timeouts[key] + } + }; + + const scheduleCleanup = (key: string) => timeouts[key] = setTimeout(() => { + delete timeouts[key]; + delete cache[key]; + }, maxAge); + + const cachedFn = async (...args: TParams): Promise => { + const key = createKey(args); + if (key in cache) { + return cache[key]; + } + const promise = fn(...args); + setCache(key, promise); + try { + const result = await promise; + scheduleCleanup(key) + return result; + } catch (e) { + delete cache[key]; + throw e; + } + }; + + cachedFn.set = (result: TResult, ...args: TParams) => { + const key = createKey(args); + setCache(key, Promise.resolve(result)); + scheduleCleanup(key); + } + + return cachedFn as TFunc & { set: (result: TResult, ...args: TParams) => void }; +} + +const createCache = (poolsDict: Record) => { + const poolLists = Object.values(poolsDict) + const usdPrices = createUsdPricesDict(poolLists); + const crvApy = createCrvApyDict(poolLists) + return {poolsDict, poolLists, usdPrices, crvApy}; +}; + /** * This function is used to cache the data fetched from the API and the data derived from it. * Note: do not expose this function to the outside world, instead encapsulate it in a function that returns the data you need. */ -const _getCachedData = memoize( - async (network: INetworkName, isLiteChain: boolean) => { - const poolsDict = await uncached_getAllPoolsFromApi(network, isLiteChain); - const poolLists = Object.values(poolsDict) - const usdPrices = createUsdPricesDict(poolLists); - const crvApy = createCrvApyDict(poolLists) - return { poolsDict, poolLists, usdPrices, crvApy }; - }, - { - promise: true, - maxAge: 5 * 60 * 1000, // 5m - primitive: true, - } -) +const _getCachedData = memoize(async (network: INetworkName, isLiteChain: boolean) => + createCache(await uncached_getAllPoolsFromApi(network, isLiteChain)), {maxAge: 1000 * 60 * 5 /* 5 minutes */}) export const _getPoolsFromApi = async (network: INetworkName, poolType: IPoolType, isLiteChain = false): Promise => { @@ -27,6 +82,10 @@ export const _getPoolsFromApi = return poolsDict[poolType] } +export const _setPoolsFromApi = + (network: INetworkName, isLiteChain: boolean, data: Record): void => + _getCachedData.set(createCache(data), network, isLiteChain) + export const _getAllPoolsFromApi = async (network: INetworkName, isLiteChain: boolean): Promise => { const {poolLists} = await _getCachedData(network, isLiteChain); return poolLists diff --git a/src/curve.ts b/src/curve.ts index 3e593f40..31dcb00d 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -16,7 +16,17 @@ import {getFactoryPoolData} from "./factory/factory.js"; import {getFactoryPoolsDataFromApi} from "./factory/factory-api.js"; import {getCryptoFactoryPoolData} from "./factory/factory-crypto.js"; import {getTricryptoFactoryPoolData} from "./factory/factory-tricrypto.js"; -import {Abi, IChainId, ICurve, IDict, IFactoryPoolType, INetworkConstants, IPoolData} from "./interfaces"; +import { + IPoolData, + IDict, + ICurve, + IChainId, + IFactoryPoolType, + Abi, + INetworkConstants, + IPoolType, + IExtendedPoolDataFromApi, +} from "./interfaces"; import ERC20Abi from './constants/abis/ERC20.json' with {type: 'json'}; import cERC20Abi from './constants/abis/cERC20.json' with {type: 'json'}; import yERC20Abi from './constants/abis/yERC20.json' with {type: 'json'}; @@ -61,7 +71,7 @@ import {_getHiddenPools} from "./external-api.js"; import {L2Networks} from "./constants/L2Networks.js"; import {getTwocryptoFactoryPoolData} from "./factory/factory-twocrypto.js"; import {getNetworkConstants} from "./utils.js"; - +import {_setPoolsFromApi} from "./cached"; export const OLD_CHAINS = [1, 10, 56, 100, 137, 250, 1284, 2222, 8453, 42161, 42220, 43114, 1313161554]; // these chains have non-ng pools @@ -149,7 +159,7 @@ export class Curve implements ICurve { async init( providerType: 'JsonRpc' | 'Web3' | 'Infura' | 'Alchemy' | 'NoRPC', providerSettings: { url?: string, privateKey?: string, batchMaxCount? : number } | { externalProvider: ethers.Eip1193Provider } | { network?: Networkish, apiKey?: string } | 'NoRPC', - options: { gasPrice?: number, maxFeePerGas?: number, maxPriorityFeePerGas?: number, chainId?: number } = {} // gasPrice in Gwei + options: { gasPrice?: number, maxFeePerGas?: number, maxPriorityFeePerGas?: number, chainId?: number, poolsData?: Record } = {} // gasPrice in Gwei ): Promise { this.provider = null!; this.signer = null; @@ -294,6 +304,10 @@ export class Curve implements ICurve { } this.feeData = { gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas }; + if (options.poolsData) { + _setPoolsFromApi(this.constants.NETWORK_NAME, this.isLiteChain, options.poolsData); + } + await this.updateFeeData(); for (const pool of Object.values({...this.constants.POOLS_DATA, ...this.constants.LLAMMAS_DATA})) {