diff --git a/src/cached.ts b/src/cached.ts new file mode 100644 index 00000000..8f6ca58e --- /dev/null +++ b/src/cached.ts @@ -0,0 +1,46 @@ +import memoize from "memoizee"; +import {IDict, IExtendedPoolDataFromApi, INetworkName, IPoolType} from "./interfaces.js"; +import {uncached_getAllPoolsFromApi, createCrvApyDict, createUsdPricesDict} from './external-api.js' +import {curve} from "./curve"; + +/** + * 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, + } +) + +export const _getPoolsFromApi = + async (network: INetworkName, poolType: IPoolType, isLiteChain = false): Promise => { + const {poolsDict} = await _getCachedData(network, isLiteChain); + return poolsDict[poolType] + } + +export const _getAllPoolsFromApi = async (network: INetworkName, isLiteChain = false): Promise => { + const {poolLists} = await _getCachedData(network, isLiteChain); + return poolLists +} + +export const _getUsdPricesFromApi = async (): Promise> => { + const network = curve.constants.NETWORK_NAME; + const {usdPrices} = await _getCachedData(network, false); + return usdPrices +} + +export const _getCrvApyFromApi = async (): Promise> => { + const network = curve.constants.NETWORK_NAME; + const {crvApy} = await _getCachedData(network, false); + return crvApy +} diff --git a/src/external-api.ts b/src/external-api.ts index 5b93d097..4b67f1f2 100644 --- a/src/external-api.ts +++ b/src/external-api.ts @@ -12,38 +12,108 @@ import { } from "./interfaces"; -export const _getPoolsFromApi = memoize( - async (network: INetworkName, poolType: IPoolType, isLiteChain = false): Promise => { - const api = isLiteChain ? "https://api-core.curve.fi/v1/" : "https://api.curve.fi/api"; - const url = `${api}/getPools/${network}/${poolType}`; - return await fetchData(url) ?? { poolData: [], tvl: 0, tvlAll: 0 }; - }, - { - promise: true, - maxAge: 5 * 60 * 1000, // 5m +const uncached_getPoolsFromApi = async (network: INetworkName, poolType: IPoolType, isLiteChain = false): Promise => { + const api = isLiteChain ? "https://api-core.curve.fi/v1/" : "https://api.curve.fi/api"; + const url = `${api}/getPools/${network}/${poolType}`; + return await fetchData(url) ?? { poolData: [], tvl: 0, tvlAll: 0 }; +} + +const getPoolTypes = (isLiteChain: boolean) => isLiteChain ? ["factory-twocrypto", "factory-tricrypto", "factory-stable-ng"] as const : + ["main", "crypto", "factory", "factory-crvusd", "factory-eywa", "factory-crypto", "factory-twocrypto", "factory-tricrypto", "factory-stable-ng"] as const; + +export const uncached_getAllPoolsFromApi = async (network: INetworkName, isLiteChain = false): Promise> => + Object.fromEntries( + await Promise.all(getPoolTypes(isLiteChain).map(async (poolType) => { + const data = await uncached_getPoolsFromApi(network, poolType, isLiteChain); + return [poolType, data]; + })) + ) + +export const createUsdPricesDict = (allTypesExtendedPoolData: IExtendedPoolDataFromApi[]): IDict => { + const priceDict: IDict[]> = {}; + const priceDictByMaxTvl: IDict = {}; + + for (const extendedPoolData of allTypesExtendedPoolData) { + for (const pool of extendedPoolData.poolData) { + const lpTokenAddress = pool.lpTokenAddress ?? pool.address; + const totalSupply = pool.totalSupply / (10 ** 18); + if(lpTokenAddress.toLowerCase() in priceDict) { + priceDict[lpTokenAddress.toLowerCase()].push({ + price: pool.usdTotal && totalSupply ? pool.usdTotal / totalSupply : 0, + tvl: pool.usdTotal, + }) + } else { + priceDict[lpTokenAddress.toLowerCase()] = [] + priceDict[lpTokenAddress.toLowerCase()].push({ + price: pool.usdTotal && totalSupply ? pool.usdTotal / totalSupply : 0, + tvl: pool.usdTotal, + }) + } + + for (const coin of pool.coins) { + if (typeof coin.usdPrice === "number") { + if(coin.address.toLowerCase() in priceDict) { + priceDict[coin.address.toLowerCase()].push({ + price: coin.usdPrice, + tvl: pool.usdTotal, + }) + } else { + priceDict[coin.address.toLowerCase()] = [] + priceDict[coin.address.toLowerCase()].push({ + price: coin.usdPrice, + tvl: pool.usdTotal, + }) + } + } + } + + for (const coin of pool.gaugeRewards ?? []) { + if (typeof coin.tokenPrice === "number") { + if(coin.tokenAddress.toLowerCase() in priceDict) { + priceDict[coin.tokenAddress.toLowerCase()].push({ + price: coin.tokenPrice, + tvl: pool.usdTotal, + }); + } else { + priceDict[coin.tokenAddress.toLowerCase()] = [] + priceDict[coin.tokenAddress.toLowerCase()].push({ + price: coin.tokenPrice, + tvl: pool.usdTotal, + }); + } + } + } + } + } + + for(const address in priceDict) { + if (priceDict[address].length) { + const maxTvlItem = priceDict[address].reduce((prev, current) => +current.tvl > +prev.tvl ? current : prev); + priceDictByMaxTvl[address] = maxTvlItem.price + } else { + priceDictByMaxTvl[address] = 0 + } } -) -export const _getAllPoolsFromApi = async (network: INetworkName, isLiteChain = false): Promise => { - if(isLiteChain) { - return await Promise.all([ - _getPoolsFromApi(network, "factory-twocrypto", isLiteChain), - _getPoolsFromApi(network, "factory-tricrypto", isLiteChain), - _getPoolsFromApi(network, "factory-stable-ng", isLiteChain), - ]); - } else { - return await Promise.all([ - _getPoolsFromApi(network, "main", isLiteChain), - _getPoolsFromApi(network, "crypto", isLiteChain), - _getPoolsFromApi(network, "factory", isLiteChain), - _getPoolsFromApi(network, "factory-crvusd", isLiteChain), - _getPoolsFromApi(network, "factory-eywa", isLiteChain), - _getPoolsFromApi(network, "factory-crypto", isLiteChain), - _getPoolsFromApi(network, "factory-twocrypto", isLiteChain), - _getPoolsFromApi(network, "factory-tricrypto", isLiteChain), - _getPoolsFromApi(network, "factory-stable-ng", isLiteChain), - ]); + return priceDictByMaxTvl +} + +export const createCrvApyDict = (allTypesExtendedPoolData: IExtendedPoolDataFromApi[]): IDict<[number, number]> => { + const apyDict: IDict<[number, number]> = {}; + + for (const extendedPoolData of allTypesExtendedPoolData) { + for (const pool of extendedPoolData.poolData) { + if (pool.gaugeAddress) { + if (!pool.gaugeCrvApy) { + apyDict[pool.gaugeAddress.toLowerCase()] = [0, 0]; + } else { + apyDict[pool.gaugeAddress.toLowerCase()] = [pool.gaugeCrvApy[0] ?? 0, pool.gaugeCrvApy[1] ?? 0]; + } + } + } } + + return apyDict } export const _getSubgraphData = memoize( diff --git a/src/factory/factory-api.ts b/src/factory/factory-api.ts index ba51ae5c..8760e540 100644 --- a/src/factory/factory-api.ts +++ b/src/factory/factory-api.ts @@ -8,7 +8,7 @@ import twocryptoFactorySwapABI from "../constants/abis/factory-twocrypto/factory import tricryptoFactorySwapABI from "../constants/abis/factory-tricrypto/factory-tricrypto-pool.json" with { type: 'json' }; import tricryptoFactoryEthDisabledSwapABI from "../constants/abis/factory-tricrypto/factory-tricrypto-pool-eth-disabled.json" with { type: 'json' }; import { getPoolIdByAddress, setFactoryZapContracts } from "./common.js"; -import { _getPoolsFromApi } from "../external-api.js"; +import { _getPoolsFromApi } from "../cached.js"; import {assetTypeNameHandler, getPoolName, isStableNgPool} from "../utils.js"; import StableNgBasePoolZapABI from "../constants/abis/stable-ng-base-pool-zap.json" with { type: 'json' }; import MetaStableSwapNGABI from "../constants/abis/factory-stable-ng/meta-stableswap-ng.json" with { type: 'json' }; diff --git a/src/pools/subClasses/corePool.ts b/src/pools/subClasses/corePool.ts index 3a4bee73..1134dfd2 100644 --- a/src/pools/subClasses/corePool.ts +++ b/src/pools/subClasses/corePool.ts @@ -69,7 +69,11 @@ export class CorePool implements ICorePool { inApi: boolean; constructor(id: string) { - const poolData = curve.getPoolsData()[id]; + const poolsData = curve.getPoolsData(); + if (!poolsData[id]) { + throw new Error(`Pool ${id} not found. Available pools: ${Object.keys(poolsData).join(', ')}`); + } + const poolData = poolsData[id]; this.id = id; this.name = poolData.name; diff --git a/src/pools/subClasses/statsPool.ts b/src/pools/subClasses/statsPool.ts index 1505c01c..36b4e55b 100644 --- a/src/pools/subClasses/statsPool.ts +++ b/src/pools/subClasses/statsPool.ts @@ -1,11 +1,10 @@ import { curve } from "../../curve.js"; import {IPoolType, IReward} from '../../interfaces.js'; -import {_getPoolsFromApi} from '../../external-api.js'; +import {_getPoolsFromApi,_getCrvApyFromApi} from '../../cached.js'; import { _getUsdRate, BN, toBN, - _getCrvApyFromApi, _getRewardsFromApi, getVolumeApiController, } from '../../utils.js'; diff --git a/src/pools/utils.ts b/src/pools/utils.ts index c7f44405..eb84c07b 100644 --- a/src/pools/utils.ts +++ b/src/pools/utils.ts @@ -2,7 +2,7 @@ import { getPool } from "./poolConstructor.js"; import { IDict } from "../interfaces"; import { curve } from "../curve.js"; import { _getRewardsFromApi, _getUsdRate, _setContracts, toBN } from "../utils.js"; -import { _getAllPoolsFromApi } from "../external-api.js"; +import { _getAllPoolsFromApi } from "../cached.js"; import ERC20Abi from "../constants/abis/ERC20.json" with { type: 'json' }; // _userLpBalance: { address: { poolId: { _lpBalance: 0, time: 0 } } } diff --git a/src/utils.ts b/src/utils.ts index ff898c9e..438a4f99 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,13 +15,13 @@ import { } from './interfaces'; import {curve} from "./curve.js"; import { - _getAllPoolsFromApi, _getCurveLiteNetworks, _getFactoryAPYs, _getLiteNetworksData, _getSubgraphData, _getVolumes, } from "./external-api.js"; +import {_getAllPoolsFromApi, _getUsdPricesFromApi} from "./cached.js"; import ERC20Abi from './constants/abis/ERC20.json' with { type: 'json' }; import {L2Networks} from './constants/L2Networks.js'; import {volumeNetworks} from "./constants/volumeNetworks.js"; @@ -316,97 +316,6 @@ export const getPoolIdBySwapAddress = (swapAddress: string): string => { return poolIds[0][0]; } -export const _getUsdPricesFromApi = async (): Promise> => { - const network = curve.constants.NETWORK_NAME; - const allTypesExtendedPoolData = await _getAllPoolsFromApi(network, curve.isLiteChain); - const priceDict: IDict[]> = {}; - const priceDictByMaxTvl: IDict = {}; - - for (const extendedPoolData of allTypesExtendedPoolData) { - for (const pool of extendedPoolData.poolData) { - const lpTokenAddress = pool.lpTokenAddress ?? pool.address; - const totalSupply = pool.totalSupply / (10 ** 18); - if(lpTokenAddress.toLowerCase() in priceDict) { - priceDict[lpTokenAddress.toLowerCase()].push({ - price: pool.usdTotal && totalSupply ? pool.usdTotal / totalSupply : 0, - tvl: pool.usdTotal, - }) - } else { - priceDict[lpTokenAddress.toLowerCase()] = [] - priceDict[lpTokenAddress.toLowerCase()].push({ - price: pool.usdTotal && totalSupply ? pool.usdTotal / totalSupply : 0, - tvl: pool.usdTotal, - }) - } - - for (const coin of pool.coins) { - if (typeof coin.usdPrice === "number") { - if(coin.address.toLowerCase() in priceDict) { - priceDict[coin.address.toLowerCase()].push({ - price: coin.usdPrice, - tvl: pool.usdTotal, - }) - } else { - priceDict[coin.address.toLowerCase()] = [] - priceDict[coin.address.toLowerCase()].push({ - price: coin.usdPrice, - tvl: pool.usdTotal, - }) - } - } - } - - for (const coin of pool.gaugeRewards ?? []) { - if (typeof coin.tokenPrice === "number") { - if(coin.tokenAddress.toLowerCase() in priceDict) { - priceDict[coin.tokenAddress.toLowerCase()].push({ - price: coin.tokenPrice, - tvl: pool.usdTotal, - }); - } else { - priceDict[coin.tokenAddress.toLowerCase()] = [] - priceDict[coin.tokenAddress.toLowerCase()].push({ - price: coin.tokenPrice, - tvl: pool.usdTotal, - }); - } - } - } - } - } - - for(const address in priceDict) { - if (priceDict[address].length) { - const maxTvlItem = priceDict[address].reduce((prev, current) => +current.tvl > +prev.tvl ? current : prev); - priceDictByMaxTvl[address] = maxTvlItem.price - } else { - priceDictByMaxTvl[address] = 0 - } - } - - return priceDictByMaxTvl -} - -export const _getCrvApyFromApi = async (): Promise> => { - const network = curve.constants.NETWORK_NAME; - const allTypesExtendedPoolData = await _getAllPoolsFromApi(network, curve.isLiteChain); - const apyDict: IDict<[number, number]> = {}; - - for (const extendedPoolData of allTypesExtendedPoolData) { - for (const pool of extendedPoolData.poolData) { - if (pool.gaugeAddress) { - if (!pool.gaugeCrvApy) { - apyDict[pool.gaugeAddress.toLowerCase()] = [0, 0]; - } else { - apyDict[pool.gaugeAddress.toLowerCase()] = [pool.gaugeCrvApy[0] ?? 0, pool.gaugeCrvApy[1] ?? 0]; - } - } - } - } - - return apyDict -} - export const _getRewardsFromApi = async (): Promise> => { const network = curve.constants.NETWORK_NAME; const allTypesExtendedPoolData = await _getAllPoolsFromApi(network, curve.isLiteChain);