From f64a4916a773f29f7dc966e7199550c818efb476 Mon Sep 17 00:00:00 2001 From: franzns <93920061+franzns@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:15:32 +0200 Subject: [PATCH] Pricing revamp (#180) * price handler helper * linear * price handlers * remove unused * fix * pass chain as param * more chain passing * remove unused * redo updates * remove unused * adding retrieval of historical prices * fix historical pricing --------- Co-authored-by: gmbronco <83549293+gmbronco@users.noreply.github.com> --- modules/actions/pool/update-on-chain-data.ts | 7 +- modules/beets/beets.service.ts | 2 +- modules/coingecko/coingecko-types.ts | 12 - modules/coingecko/coingecko.service.ts | 247 -------------- modules/datastudio/datastudio.service.ts | 10 +- modules/network/arbitrum.ts | 11 - modules/network/avalanche.ts | 11 - modules/network/base.ts | 11 - modules/network/fantom.ts | 23 -- modules/network/gnosis.ts | 11 - modules/network/mainnet.ts | 11 - modules/network/network-config-types.ts | 2 - modules/network/optimism.ts | 17 - modules/network/polygon.ts | 11 - modules/network/sepolia.ts | 9 - modules/network/zkevm.ts | 11 - .../fantom/masterchef-farm-apr.service.ts | 8 +- .../fantom/reliquary-farm-apr.service.ts | 2 +- .../fantom/spooky-swap-apr.service.ts | 2 +- .../ve-bal-gauge-apr.service.ts | 2 +- .../apr-data-sources/yb-tokens-apr.service.ts | 3 +- modules/pool/lib/pool-apr-updater.service.ts | 17 +- .../pool/lib/pool-on-chain-data.service.ts | 14 +- modules/pool/lib/pool-snapshot.service.ts | 20 +- modules/pool/lib/pool-swap.service.ts | 8 +- modules/pool/lib/pool-usd-data.service.ts | 6 +- modules/pool/pool.gql | 4 +- modules/pool/pool.resolvers.ts | 8 +- modules/pool/pool.service.ts | 12 +- modules/protocol/protocol.service.ts | 6 +- .../sources/transformers/swaps-transformer.ts | 4 +- modules/token/lib/coingecko-data.service.ts | 321 +++++++----------- .../beets-price-handler.service.ts | 82 ++--- .../bpt-price-handler.service.ts | 67 +--- .../clqdr-price-handler.service.ts | 52 +-- .../coingecko-price-handler.service.ts | 161 +++++---- .../fallback-price-handler.service.ts | 67 ++++ .../fbeets-price-handler.service.ts | 57 ++-- ...ear-wrapped-token-price-handler.service.ts | 65 ++-- .../price-handler-helper.ts | 65 ++++ .../swaps-price-handler.service.ts | 153 ++++----- modules/token/lib/token-price.service.ts | 182 +++++----- modules/token/token-types.ts | 16 +- modules/token/token.gql | 16 +- modules/token/token.prisma | 8 +- modules/token/token.resolvers.ts | 77 ++--- modules/token/token.service.ts | 66 ++-- modules/user/user.resolvers.ts | 6 +- .../migration.sql | 17 + prisma/schema.prisma | 8 +- worker/job-handlers.ts | 40 ++- 51 files changed, 817 insertions(+), 1231 deletions(-) delete mode 100644 modules/coingecko/coingecko-types.ts delete mode 100644 modules/coingecko/coingecko.service.ts create mode 100644 modules/token/lib/token-price-handlers/fallback-price-handler.service.ts create mode 100644 modules/token/lib/token-price-handlers/price-handler-helper.ts create mode 100644 prisma/migrations/20240229153000_change_tokenprice_table/migration.sql diff --git a/modules/actions/pool/update-on-chain-data.ts b/modules/actions/pool/update-on-chain-data.ts index 4183d2074..1d16dd1bb 100644 --- a/modules/actions/pool/update-on-chain-data.ts +++ b/modules/actions/pool/update-on-chain-data.ts @@ -205,8 +205,11 @@ export async function updateOnChainDataForPools( balanceUSD: poolToken.address === pool.address ? 0 - : tokenService.getPriceForToken(tokenPricesForCurrentChain, poolToken.address) * - parseFloat(balance), + : tokenService.getPriceForToken( + tokenPricesForCurrentChain, + poolToken.address, + chain, + ) * parseFloat(balance), }, }), ); diff --git a/modules/beets/beets.service.ts b/modules/beets/beets.service.ts index cddbb5058..326d5bbfe 100644 --- a/modules/beets/beets.service.ts +++ b/modules/beets/beets.service.ts @@ -15,7 +15,7 @@ export class BeetsService { public async getBeetsPrice(): Promise { const tokenPrices = await tokenService.getTokenPrices(); - return tokenService.getPriceForToken(tokenPrices, networkContext.data.beets!.address).toString(); + return tokenService.getPriceForToken(tokenPrices, networkContext.data.beets!.address, 'FANTOM').toString(); } } diff --git a/modules/coingecko/coingecko-types.ts b/modules/coingecko/coingecko-types.ts deleted file mode 100644 index 6551fd0aa..000000000 --- a/modules/coingecko/coingecko-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type Price = { usd: number }; -export type CoingeckoPriceResponse = { [id: string]: Price }; -export type TokenPrices = { [address: string]: Price }; - -export interface HistoricalPriceResponse { - market_caps: number[][]; - prices: number[][]; - total_volumes: number[][]; -} - -export type HistoricalPrice = { timestamp: number; price: number }; -export type TokenHistoricalPrices = { [address: string]: HistoricalPrice[] }; diff --git a/modules/coingecko/coingecko.service.ts b/modules/coingecko/coingecko.service.ts deleted file mode 100644 index c3da74188..000000000 --- a/modules/coingecko/coingecko.service.ts +++ /dev/null @@ -1,247 +0,0 @@ -import axios, { AxiosError } from 'axios'; -import { twentyFourHoursInSecs } from '../common/time'; -import _ from 'lodash'; -import moment from 'moment-timezone'; -import { tokenService } from '../token/token.service'; -import { TokenDefinition } from '../token/token-types'; -import { isAddress } from 'ethers/lib/utils'; -import { RateLimiter } from 'limiter'; -import { networkContext } from '../network/network-context.service'; -import { - CoingeckoPriceResponse, - HistoricalPrice, - HistoricalPriceResponse, - Price, - TokenPrices, -} from './coingecko-types'; -import { env } from '../../app/env'; -import { DeploymentEnv } from '../network/network-config-types'; - -interface MappedToken { - platform: string; - address: string; - originalAddress?: string; -} - -interface CoingeckoTokenMarketData { - id: string; - symbol: string; - name: string; - image: string; - current_price: number; - market_cap: number; - market_cap_rank: number; - fully_diluted_valuation: number | null; - total_volume: number; - high_24h: number; - low_24h: number; - price_change_24h: number; - price_change_percentage_24h: number; - market_cap_change_24h: number; - market_cap_change_percentage_24h: number; - circulating_supply: number; - total_supply: number; - max_supply: number | null; - ath: number; - ath_change_percentage: number; - ath_date: Date; - atl: number; - atl_change_percentage: number; - atl_date: Date; - roi: null; - last_updated: Date; - price_change_percentage_14d_in_currency: number; - price_change_percentage_1h_in_currency: number; - price_change_percentage_24h_in_currency: number; - price_change_percentage_30d_in_currency: number; - price_change_percentage_7d_in_currency: number; -} - -interface CoinId { - id: string; - symbol: string; - name: string; - platforms: Record; -} - -/* coingecko has a rate limit of 10-50req/minute - https://www.coingecko.com/en/api/pricing: - Our free API has a rate limit of 10-50 calls per minute, - if you exceed that limit you will be blocked until the next 1 minute window. - Do revise your queries to ensure that you do not exceed our limits should - that happen. - -*/ -const tokensPerMinute = env.COINGECKO_API_KEY ? 10 : 3; -const requestRateLimiter = new RateLimiter({ tokensPerInterval: tokensPerMinute, interval: 'minute' }); -//max 10 addresses per request because of URI size limit, pro is max 180 because of URI limit -const addressChunkSize = env.COINGECKO_API_KEY ? 180 : 20; - -export class CoingeckoService { - private readonly baseUrl: string; - private readonly fiatParam: string; - private readonly apiKeyParam: string; - - constructor() { - this.baseUrl = env.COINGECKO_API_KEY - ? 'https://pro-api.coingecko.com/api/v3' - : 'https://api.coingecko.com/api/v3'; - this.fiatParam = 'usd'; - this.apiKeyParam = env.COINGECKO_API_KEY ? `&x_cg_pro_api_key=${env.COINGECKO_API_KEY}` : ''; - } - - public async getNativeAssetPrice(): Promise { - try { - const response = await this.get( - `/simple/price?ids=${networkContext.data.coingecko.nativeAssetId}&vs_currencies=${this.fiatParam}`, - ); - return response[networkContext.data.coingecko.nativeAssetId]; - } catch (error) { - //console.error('Unable to fetch Ether price', error); - throw error; - } - } - - /** - * Rate limit for the CoinGecko API is 10 calls each second per IP address. - */ - public async getTokenPrices(addresses: string[]): Promise { - const addressesPerRequest = addressChunkSize; - try { - // if (addresses.length / addressesPerRequest > tokensPerMinute) - // throw new Error('Too many requests for rate limit.'); - - const tokenDefinitions = await tokenService.getTokenDefinitions([networkContext.chain]); - const mapped = addresses.map((address) => this.getMappedTokenDetails(address, tokenDefinitions)); - const groupedByPlatform = _.groupBy(mapped, 'platform'); - - const requests: Promise[] = []; - - _.forEach(groupedByPlatform, (tokens, platform) => { - const mappedAddresses = tokens.map((token) => token.address); - const pageCount = Math.ceil(mappedAddresses.length / addressesPerRequest); - const pages = Array.from(Array(pageCount).keys()); - - pages.forEach((page) => { - const addressString = mappedAddresses.slice( - addressesPerRequest * page, - addressesPerRequest * (page + 1), - ); - const endpoint = `/simple/token_price/${platform}?contract_addresses=${addressString}&vs_currencies=${this.fiatParam}`; - const request = this.get(endpoint); - requests.push(request); - }); - }); - - const paginatedResults = await Promise.all(requests); - const results = this.parsePaginatedTokens(paginatedResults, mapped); - - return results; - } catch (error: any) { - throw new Error(`Unable to fetch token prices - ${error.message} - ${error.statusCode}`); - } - } - - public async getTokenHistoricalPrices(address: string, days: number): Promise { - const now = Math.floor(Date.now() / 1000); - const end = now; - const start = end - days * twentyFourHoursInSecs; - const tokenDefinitions = await tokenService.getTokenDefinitions([networkContext.chain]); - const mapped = this.getMappedTokenDetails(address, tokenDefinitions); - - const endpoint = `/coins/${mapped.platform}/contract/${mapped.address}/market_chart/range?vs_currency=${this.fiatParam}&from=${start}&to=${end}`; - - const result = await this.get(endpoint); - - return result.prices.map((item) => ({ - //anchor to the start of the hour - timestamp: - moment - .unix(item[0] / 1000) - .startOf('hour') - .unix() * 1000, - price: item[1], - })); - } - - private parsePaginatedTokens(paginatedResults: TokenPrices[], mappedTokens: MappedToken[]): TokenPrices { - const results = paginatedResults.reduce((result, page) => ({ ...result, ...page }), {}); - const prices: TokenPrices = _.mapKeys(results, (val, address) => address); - - const resultAddresses = Object.keys(results); - for (const mappedToken of mappedTokens) { - if (mappedToken.originalAddress) { - const resultAddress = resultAddresses.find( - (address) => address.toLowerCase() === mappedToken.address.toLowerCase(), - ); - if (!resultAddress) { - console.warn(`Matching address for original address ${mappedToken.originalAddress} not found`); - } else { - prices[mappedToken.originalAddress] = results[resultAddress]; - } - } - } - - return prices; - } - - /** - * Support instances where a token address is not supported by the platform id, provide the option to use a different platform - */ - public getMappedTokenDetails(address: string, tokens: TokenDefinition[]): MappedToken { - const token = tokens.find((token) => token.address.toLowerCase() === address.toLowerCase()); - if (token && token.coingeckoPlatformId && token.coingeckoContractAddress) { - return { - platform: token.coingeckoPlatformId, - address: isAddress(token.coingeckoContractAddress) - ? token.coingeckoContractAddress.toLowerCase() - : token.coingeckoContractAddress, - originalAddress: address.toLowerCase(), - }; - } - - return { - platform: networkContext.data.coingecko.platformId, - address: address.toLowerCase(), - }; - } - - public async getMarketDataForTokenIds(tokenIds: string[]): Promise { - const endpoint = `/coins/markets?vs_currency=${this.fiatParam}&ids=${tokenIds}&per_page=250&page=1&sparkline=false&price_change_percentage=1h%2C24h%2C7d%2C14d%2C30d`; - - return this.get(endpoint); - } - - public async getCoinCandlestickData( - tokenId: string, - days: 1 | 30, - ): Promise<[number, number, number, number, number][]> { - const endpoint = `/coins/${tokenId}/ohlc?vs_currency=usd&days=${days}`; - - return this.get(endpoint); - } - - public async getCoinIdList(): Promise { - const endpoint = `/coins/list?include_platform=true`; - return this.get(endpoint); - } - - private async get(endpoint: string): Promise { - const remainingRequests = await requestRateLimiter.removeTokens(1); - console.log('Remaining coingecko requests', remainingRequests); - let response; - try { - response = await axios.get(this.baseUrl + endpoint + this.apiKeyParam); - } catch (err: any | AxiosError) { - if (axios.isAxiosError(err)) { - if (err.response?.status === 429) { - throw Error(`Coingecko ratelimit: ${err}`); - } - } - throw err; - } - return response.data; - } -} - -export const coingeckoService = new CoingeckoService(); diff --git a/modules/datastudio/datastudio.service.ts b/modules/datastudio/datastudio.service.ts index dcb0397f8..2c2caeec3 100644 --- a/modules/datastudio/datastudio.service.ts +++ b/modules/datastudio/datastudio.service.ts @@ -12,11 +12,12 @@ import { oneDayInSeconds, secondsPerDay } from '../common/time'; import { isComposableStablePool, isWeightedPoolV2 } from '../pool/lib/pool-utils'; import { networkContext } from '../network/network-context.service'; import { DeploymentEnv } from '../network/network-config-types'; +import { Chain } from '@prisma/client'; export class DatastudioService { constructor(private readonly secretsManager: SecretsManager, private readonly jwtClientHelper: GoogleJwtClient) {} - public async feedPoolData() { + public async feedPoolData(chain: Chain) { const privateKey = await this.secretsManager.getSecret('backend-v3-datafeed-privatekey'); const jwtClient = await this.jwtClientHelper.getAuthorizedSheetsClient(privateKey); @@ -89,7 +90,7 @@ export class DatastudioService { dynamicData: { totalLiquidity: { gte: 5000 }, }, - chain: networkContext.chain, + chain: chain, }, include: { dynamicData: true, @@ -265,7 +266,8 @@ export class DatastudioService { } const rewardsPerDay = parseFloat(rewarder.rewardPerSecond) * secondsPerDay; const rewardsValuePerDay = - tokenService.getPriceForToken(tokenPrices, rewarder.tokenAddress) * rewardsPerDay; + tokenService.getPriceForToken(tokenPrices, rewarder.tokenAddress, chain) * + rewardsPerDay; if (rewardsPerDay > 0) { allEmissionDataRows.push([ endOfYesterday.format('DD MMM YYYY'), @@ -309,7 +311,7 @@ export class DatastudioService { } const rewardsPerDay = parseFloat(reward.rewardPerSecond) * secondsPerDay; const rewardsValuePerDay = - tokenService.getPriceForToken(tokenPrices, reward.tokenAddress) * rewardsPerDay; + tokenService.getPriceForToken(tokenPrices, reward.tokenAddress, chain) * rewardsPerDay; if (rewardsPerDay > 0) { allEmissionDataRows.push([ endOfYesterday.format('DD MMM YYYY'), diff --git a/modules/network/arbitrum.ts b/modules/network/arbitrum.ts index 38bc3f43e..b3257b156 100644 --- a/modules/network/arbitrum.ts +++ b/modules/network/arbitrum.ts @@ -6,15 +6,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { env } from '../../app/env'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -227,12 +222,6 @@ export const arbitrumNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [arbitrumNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, arbitrumNetworkData.bal!.address)], - tokenPriceHandlers: [ - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/network/avalanche.ts b/modules/network/avalanche.ts index 0711cce29..9474f7e20 100644 --- a/modules/network/avalanche.ts +++ b/modules/network/avalanche.ts @@ -6,15 +6,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { env } from '../../app/env'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -202,12 +197,6 @@ export const avalancheNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [avalancheNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, avalancheNetworkData.bal!.address)], - tokenPriceHandlers: [ - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/network/base.ts b/modules/network/base.ts index c3d882d0d..f9c2f634e 100644 --- a/modules/network/base.ts +++ b/modules/network/base.ts @@ -5,15 +5,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; import { env } from '../../app/env'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -125,12 +120,6 @@ export const baseNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [baseNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, baseNetworkData.bal!.address)], - tokenPriceHandlers: [ - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/network/fantom.ts b/modules/network/fantom.ts index 10fbb2397..51295a6ce 100644 --- a/modules/network/fantom.ts +++ b/modules/network/fantom.ts @@ -1,7 +1,5 @@ import { BigNumber, ethers } from 'ethers'; import { DeploymentEnv, NetworkConfig, NetworkData } from './network-config-types'; -import { SpookySwapAprService } from '../pool/lib/apr-data-sources/fantom/spooky-swap-apr.service'; -import { tokenService } from '../token/token.service'; import { PhantomStableAprService } from '../pool/lib/apr-data-sources/phantom-stable-apr.service'; import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool-apr.service'; import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; @@ -11,18 +9,10 @@ import { MasterChefStakingService } from '../pool/lib/staking/master-chef-stakin import { masterchefService } from '../subgraphs/masterchef-subgraph/masterchef.service'; import { ReliquaryStakingService } from '../pool/lib/staking/reliquary-staking.service'; import { reliquarySubgraphService } from '../subgraphs/reliquary-subgraph/reliquary.service'; -import { BeetsPriceHandlerService } from '../token/lib/token-price-handlers/beets-price-handler.service'; -import { FbeetsPriceHandlerService } from '../token/lib/token-price-handlers/fbeets-price-handler.service'; -import { ClqdrPriceHandlerService } from '../token/lib/token-price-handlers/clqdr-price-handler.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncMasterchefFarmBalanceService } from '../user/lib/user-sync-masterchef-farm-balance.service'; import { UserSyncReliquaryFarmBalanceService } from '../user/lib/user-sync-reliquary-farm-balance.service'; import { every } from '../../worker/intervals'; import { SanityContentService } from '../content/sanity-content.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; import { env } from '../../app/env'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { BeetswarsGaugeVotingAprService } from '../pool/lib/apr-data-sources/fantom/beetswars-gauge-voting-apr'; @@ -101,7 +91,6 @@ const fantomNetworkData: NetworkData = { protocolToken: 'beets', beets: { address: '0xf24bcf4d1e507740041c9cfd2dddb29585adce1e', - beetsPriceProviderRpcUrl: 'https://rpc.ftm.tools', }, sftmx: { stakingContractAddress: '0xb458bfc855ab504a8a327720fcef98886065529b', @@ -300,18 +289,6 @@ export const fantomNetworkConfig: NetworkConfig = { new MasterChefStakingService(masterchefService, fantomNetworkData.masterchef!.excludedFarmIds), new ReliquaryStakingService(fantomNetworkData.reliquary!.address, reliquarySubgraphService), ], - tokenPriceHandlers: [ - new BeetsPriceHandlerService( - fantomNetworkData.beets!.address, - fantomNetworkData.beets!.beetsPriceProviderRpcUrl, - ), - new FbeetsPriceHandlerService(fantomNetworkData.fbeets!.address, fantomNetworkData.fbeets!.poolId), - new ClqdrPriceHandlerService(), - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [ new UserSyncMasterchefFarmBalanceService( fantomNetworkData.fbeets!.address, diff --git a/modules/network/gnosis.ts b/modules/network/gnosis.ts index 667868194..fa1e089ad 100644 --- a/modules/network/gnosis.ts +++ b/modules/network/gnosis.ts @@ -6,15 +6,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { env } from '../../app/env'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -132,12 +127,6 @@ export const gnosisNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [gnosisNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, gnosisNetworkData.bal!.address)], - tokenPriceHandlers: [ - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/network/mainnet.ts b/modules/network/mainnet.ts index da3f67508..a43108519 100644 --- a/modules/network/mainnet.ts +++ b/modules/network/mainnet.ts @@ -6,15 +6,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { env } from '../../app/env'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -386,12 +381,6 @@ export const mainnetNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [data.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, data.bal!.address)], - tokenPriceHandlers: [ - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService(data.subgraphs.balancer, 1), diff --git a/modules/network/network-config-types.ts b/modules/network/network-config-types.ts index 34cc493f6..4360ce55c 100644 --- a/modules/network/network-config-types.ts +++ b/modules/network/network-config-types.ts @@ -16,7 +16,6 @@ export interface NetworkConfig { poolStakingServices: PoolStakingService[]; poolAprServices: PoolAprService[]; userStakedBalanceServices: UserStakedBalanceService[]; - tokenPriceHandlers: TokenPriceHandler[]; provider: BaseProvider; workerJobs: WorkerJob[]; services: NetworkServices; @@ -79,7 +78,6 @@ export interface NetworkData { protocolToken: 'beets' | 'bal'; beets?: { address: string; - beetsPriceProviderRpcUrl: string; }; fbeets?: { address: string; diff --git a/modules/network/optimism.ts b/modules/network/optimism.ts index 71c18d35a..ffa4d10c0 100644 --- a/modules/network/optimism.ts +++ b/modules/network/optimism.ts @@ -6,16 +6,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BeetsPriceHandlerService } from '../token/lib/token-price-handlers/beets-price-handler.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { SanityContentService } from '../content/sanity-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { env } from '../../app/env'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -60,7 +54,6 @@ const optimismNetworkData: NetworkData = { protocolToken: 'beets', beets: { address: '0xb4bc46bc6cb217b59ea8f4530bae26bf69f677f0', - beetsPriceProviderRpcUrl: 'https://rpc.ftm.tools', }, bal: { address: '0xfe8b128ba8c78aabc59d4c64cee7ff28e9379921', @@ -278,16 +271,6 @@ export const optimismNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [optimismNetworkData.beets!.address, optimismNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, optimismNetworkData.bal!.address)], - tokenPriceHandlers: [ - new BeetsPriceHandlerService( - optimismNetworkData.beets!.address, - optimismNetworkData.beets!.beetsPriceProviderRpcUrl, - ), - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/network/polygon.ts b/modules/network/polygon.ts index 5088611af..513f75bbe 100644 --- a/modules/network/polygon.ts +++ b/modules/network/polygon.ts @@ -6,15 +6,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { env } from '../../app/env'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -252,12 +247,6 @@ export const polygonNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [polygonNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, polygonNetworkData.bal!.address)], - tokenPriceHandlers: [ - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/network/sepolia.ts b/modules/network/sepolia.ts index 390a95d1d..07e7722ad 100644 --- a/modules/network/sepolia.ts +++ b/modules/network/sepolia.ts @@ -6,9 +6,6 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; @@ -31,12 +28,6 @@ export const sepoliaNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [sepoliaNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, sepoliaNetworkData.bal!.address)], - tokenPriceHandlers: [ - // new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/network/zkevm.ts b/modules/network/zkevm.ts index 544fb1091..95dc24929 100644 --- a/modules/network/zkevm.ts +++ b/modules/network/zkevm.ts @@ -6,15 +6,10 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; -import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; -import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; import { UserSyncGaugeBalanceService } from '../user/lib/user-sync-gauge-balance.service'; import { every } from '../../worker/intervals'; import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; -import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; -import { coingeckoService } from '../coingecko/coingecko.service'; import { env } from '../../app/env'; import { YbTokensAprService } from '../pool/lib/apr-data-sources/yb-tokens-apr.service'; import { BalancerSubgraphService } from '../subgraphs/balancer-subgraph/balancer-subgraph.service'; @@ -167,12 +162,6 @@ export const zkevmNetworkConfig: NetworkConfig = { new GaugeAprService(tokenService, [zkevmNetworkData.bal!.address]), ], poolStakingServices: [new GaugeStakingService(gaugeSubgraphService, zkevmNetworkData.bal!.address)], - tokenPriceHandlers: [ - new CoingeckoPriceHandlerService(coingeckoService), - new BptPriceHandlerService(), - new LinearWrappedTokenPriceHandlerService(), - new SwapsPriceHandlerService(), - ], userStakedBalanceServices: [new UserSyncGaugeBalanceService()], services: { balancerSubgraphService: new BalancerSubgraphService( diff --git a/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts index b06f1a8cf..66e501bb8 100644 --- a/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts @@ -92,7 +92,7 @@ export class MasterchefFarmAprService implements PoolAprService { return []; } - const beetsPrice = tokenService.getPriceForToken(tokenPrices, this.beetsAddress); + const beetsPrice = tokenService.getPriceForToken(tokenPrices, this.beetsAddress, networkContext.chain); const beetsPerBlock = Number(parseInt(farm.masterChef.beetsPerBlock) / 1e18) * FARM_EMISSIONS_PERCENT; const beetsPerYear = beetsPerBlock * blocksPerYear; const farmBeetsPerYear = (parseInt(farm.allocPoint) / parseInt(farm.masterChef.totalAllocPoint)) * beetsPerYear; @@ -122,7 +122,11 @@ export class MasterchefFarmAprService implements PoolAprService { }, }, }); - const rewardTokenPrice = tokenService.getPriceForToken(tokenPrices, rewardToken.token); + const rewardTokenPrice = tokenService.getPriceForToken( + tokenPrices, + rewardToken.token, + networkContext.chain, + ); const rewardTokenPerYear = parseFloat(farmRewarder.rewardPerSecond) * secondsPerYear; const rewardTokenValuePerYear = rewardTokenPrice * rewardTokenPerYear; const rewardApr = rewardTokenValuePerYear / farmTvl > 0 ? rewardTokenValuePerYear / farmTvl : 0; diff --git a/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts index 6cea39e57..104e6bfde 100644 --- a/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts @@ -58,7 +58,7 @@ export class ReliquaryFarmAprService implements PoolAprService { const totalLiquidity = pool.dynamicData?.totalLiquidity || 0; const pricePerShare = totalLiquidity / totalShares; - const beetsPrice = tokenService.getPriceForToken(tokenPrices, this.beetsAddress); + const beetsPrice = tokenService.getPriceForToken(tokenPrices, this.beetsAddress, networkContext.chain); const farmBeetsPerYear = parseFloat(farm.beetsPerSecond) * secondsPerYear; const beetsValuePerYear = beetsPrice * farmBeetsPerYear; diff --git a/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts index 3004a2210..d9b139213 100644 --- a/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts @@ -47,7 +47,7 @@ export class SpookySwapAprService implements PoolAprService { const linearData = pool.typeData as LinearData; const wrappedToken = pool.tokens[linearData.wrappedIndex]; - const tokenPrice = this.tokenService.getPriceForToken(tokenPrices, this.booAddress); + const tokenPrice = this.tokenService.getPriceForToken(tokenPrices, this.booAddress, networkContext.chain); const wrappedTokens = parseFloat(wrappedToken.dynamicData?.balance || '0'); const priceRate = parseFloat(wrappedToken.dynamicData?.priceRate || '1.0'); const poolWrappedLiquidity = wrappedTokens * priceRate * tokenPrice; diff --git a/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts b/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts index d264ef6ab..ff6b59c25 100644 --- a/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts @@ -58,7 +58,7 @@ export class GaugeAprService implements PoolAprService { // Get token rewards per year with data needed for the DB const rewards = await Promise.allSettled( gauge.rewards.map(async ({ tokenAddress, rewardPerSecond }) => { - const price = this.tokenService.getPriceForToken(tokenPrices, tokenAddress); + const price = this.tokenService.getPriceForToken(tokenPrices, tokenAddress, networkContext.chain); if (!price) { return Promise.reject(`Price not found for ${tokenAddress}`); } diff --git a/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts b/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts index c6791a8df..5f4f9400e 100644 --- a/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/yb-tokens-apr.service.ts @@ -7,6 +7,7 @@ import { YbAprHandlers, TokenApr } from './yb-apr-handlers'; import { tokenService } from '../../../token/token.service'; import { collectsYieldFee } from '../pool-utils'; import { YbAprConfig } from '../../../network/apr-config-types'; +import { networkContext } from '../../../network/network-context.service'; export class YbTokensAprService implements PoolAprService { private ybTokensAprHandlers: YbAprHandlers; @@ -60,7 +61,7 @@ export class YbTokensAprService implements PoolAprService { continue; } - const tokenPrice = tokenService.getPriceForToken(tokenPrices, token.address); + const tokenPrice = tokenService.getPriceForToken(tokenPrices, token.address, networkContext.chain); const tokenBalance = token.dynamicData?.balance; const tokenLiquidity = tokenPrice * parseFloat(tokenBalance || '0'); diff --git a/modules/pool/lib/pool-apr-updater.service.ts b/modules/pool/lib/pool-apr-updater.service.ts index 4f7fd330b..cde36dd2c 100644 --- a/modules/pool/lib/pool-apr-updater.service.ts +++ b/modules/pool/lib/pool-apr-updater.service.ts @@ -5,6 +5,7 @@ import { PoolAprService } from '../pool-types'; import _ from 'lodash'; import { prismaBulkExecuteOperations } from '../../../prisma/prisma-util'; import { networkContext } from '../../network/network-context.service'; +import { Chain } from '@prisma/client'; export class PoolAprUpdaterService { constructor() {} @@ -13,10 +14,10 @@ export class PoolAprUpdaterService { return networkContext.config.poolAprServices; } - public async updatePoolAprs() { + public async updatePoolAprs(chain: Chain) { const pools = await prisma.prismaPool.findMany({ ...poolWithTokens, - where: { chain: networkContext.chain }, + where: { chain: chain }, }); const failedAprServices = []; @@ -31,7 +32,7 @@ export class PoolAprUpdaterService { } const aprItems = await prisma.prismaPoolAprItem.findMany({ - where: { chain: networkContext.chain }, + where: { chain: chain }, select: { poolId: true, apr: true }, }); @@ -42,7 +43,7 @@ export class PoolAprUpdaterService { for (const poolId in grouped) { operations.push( prisma.prismaPoolDynamicData.update({ - where: { id_chain: { id: poolId, chain: networkContext.chain } }, + where: { id_chain: { id: poolId, chain: chain } }, data: { apr: _.sumBy(grouped[poolId], (item) => item.apr) }, }), ); @@ -54,9 +55,9 @@ export class PoolAprUpdaterService { } } - public async reloadAllPoolAprs() { - await prisma.prismaPoolAprRange.deleteMany({ where: { chain: networkContext.chain } }); - await prisma.prismaPoolAprItem.deleteMany({ where: { chain: networkContext.chain } }); - await this.updatePoolAprs(); + public async reloadAllPoolAprs(chain: Chain) { + await prisma.prismaPoolAprRange.deleteMany({ where: { chain: chain } }); + await prisma.prismaPoolAprItem.deleteMany({ where: { chain: chain } }); + await this.updatePoolAprs(chain); } } diff --git a/modules/pool/lib/pool-on-chain-data.service.ts b/modules/pool/lib/pool-on-chain-data.service.ts index 326fed052..c8295051b 100644 --- a/modules/pool/lib/pool-on-chain-data.service.ts +++ b/modules/pool/lib/pool-on-chain-data.service.ts @@ -274,8 +274,11 @@ export class PoolOnChainDataService { balanceUSD: poolToken.address === pool.address ? 0 - : this.tokenService.getPriceForToken(tokenPrices, poolToken.address) * - parseFloat(balance), + : this.tokenService.getPriceForToken( + tokenPrices, + poolToken.address, + this.options.chain, + ) * parseFloat(balance), }, update: { blockNumber, @@ -285,8 +288,11 @@ export class PoolOnChainDataService { balanceUSD: poolToken.address === pool.address ? 0 - : this.tokenService.getPriceForToken(tokenPrices, poolToken.address) * - parseFloat(balance), + : this.tokenService.getPriceForToken( + tokenPrices, + poolToken.address, + this.options.chain, + ) * parseFloat(balance), }, }), ); diff --git a/modules/pool/lib/pool-snapshot.service.ts b/modules/pool/lib/pool-snapshot.service.ts index 8e61477ca..06c216ed6 100644 --- a/modules/pool/lib/pool-snapshot.service.ts +++ b/modules/pool/lib/pool-snapshot.service.ts @@ -10,14 +10,13 @@ import _ from 'lodash'; import { Chain, PrismaPoolSnapshot } from '@prisma/client'; import { prismaBulkExecuteOperations } from '../../../prisma/prisma-util'; import { prismaPoolWithExpandedNesting } from '../../../prisma/prisma-types'; -import { CoingeckoService } from '../../coingecko/coingecko.service'; import { blocksSubgraphService } from '../../subgraphs/blocks-subgraph/blocks-subgraph.service'; import { sleep } from '../../common/promise'; import { networkContext } from '../../network/network-context.service'; -import { TokenHistoricalPrices } from '../../coingecko/coingecko-types'; +import { CoingeckoDataService, TokenHistoricalPrices } from '../../token/lib/coingecko-data.service'; export class PoolSnapshotService { - constructor(private readonly coingeckoService: CoingeckoService) {} + constructor(private readonly coingeckoService: CoingeckoDataService) {} private get balancerSubgraphService() { return networkContext.config.services.balancerSubgraphService; @@ -194,18 +193,9 @@ export class PoolSnapshotService { }, }); if (priceForDays.length === 0) { - try { - tokenPriceMap[token.address] = await this.coingeckoService.getTokenHistoricalPrices( - token.address, - numDays, - ); - await sleep(5000); - } catch (error: any) { - console.error( - `Error getting historical prices form coingecko, skipping token ${token.address}. Error:`, - error.message, - ); - } + console.error( + `No historical price in DB for to create pool snapshots. Skipping token ${token.address}.`, + ); } else { tokenPriceMap[token.address] = priceForDays; } diff --git a/modules/pool/lib/pool-swap.service.ts b/modules/pool/lib/pool-swap.service.ts index 1c975a2f9..5aa4e5b54 100644 --- a/modules/pool/lib/pool-swap.service.ts +++ b/modules/pool/lib/pool-swap.service.ts @@ -224,8 +224,8 @@ export class PoolSwapService { skipDuplicates: true, data: swaps.map((swap) => { let valueUSD = 0; - const tokenInPrice = this.tokenService.getPriceForToken(tokenPrices, swap.tokenIn); - const tokenOutPrice = this.tokenService.getPriceForToken(tokenPrices, swap.tokenOut); + const tokenInPrice = this.tokenService.getPriceForToken(tokenPrices, swap.tokenIn, this.chain); + const tokenOutPrice = this.tokenService.getPriceForToken(tokenPrices, swap.tokenOut, this.chain); if (tokenInPrice > 0) { valueUSD = tokenInPrice * parseFloat(swap.tokenAmountIn); @@ -350,8 +350,8 @@ export class PoolSwapService { tokenAmountOut: endSwap.tokenAmountOut, tx: startSwap.tx, valueUSD: endSwap.valueUSD, - tokenInPrice: tokenService.getPriceForToken(tokenPrices, startSwap.tokenIn), - tokenOutPrice: tokenService.getPriceForToken(tokenPrices, endSwap.tokenOut), + tokenInPrice: tokenService.getPriceForToken(tokenPrices, startSwap.tokenIn, this.chain), + tokenOutPrice: tokenService.getPriceForToken(tokenPrices, endSwap.tokenOut, this.chain), }, }), ...batchSwaps.map((swap, index) => diff --git a/modules/pool/lib/pool-usd-data.service.ts b/modules/pool/lib/pool-usd-data.service.ts index 5bdcf946a..b370bb5e3 100644 --- a/modules/pool/lib/pool-usd-data.service.ts +++ b/modules/pool/lib/pool-usd-data.service.ts @@ -57,7 +57,7 @@ export class PoolUsdDataService { token.address === pool.address ? 0 : parseFloat(token.dynamicData?.balance || '0') * - this.tokenService.getPriceForToken(tokenPrices, token.address), + this.tokenService.getPriceForToken(tokenPrices, token.address, this.chain), })); const totalLiquidity = _.sumBy(balanceUSDs, (item) => item.balanceUSD); @@ -115,7 +115,7 @@ export class PoolUsdDataService { public async updateLiquidity24hAgoForAllPools() { const block24hAgo = await this.blockSubgraphService.getBlockFrom24HoursAgo(); - const tokenPrices24hAgo = await this.tokenService.getTokenPriceFrom24hAgo(); + const tokenPrices24hAgo = await this.tokenService.getTokenPriceFrom24hAgo(this.chain); const subgraphPools = await this.balancerSubgraphService.getAllPools( { block: { number: parseInt(block24hAgo.number) } }, @@ -131,7 +131,7 @@ export class PoolUsdDataService { token.address === pool.address ? 0 : parseFloat(token.balance || '0') * - this.tokenService.getPriceForToken(tokenPrices24hAgo, token.address), + this.tokenService.getPriceForToken(tokenPrices24hAgo, token.address, this.chain), })); const totalLiquidity = Math.max( _.sumBy(balanceUSDs, (item) => item.balanceUSD), diff --git a/modules/pool/pool.gql b/modules/pool/pool.gql index 508d58431..5037b7a09 100644 --- a/modules/pool/pool.gql +++ b/modules/pool/pool.gql @@ -36,9 +36,9 @@ extend type Mutation { poolUpdateVolumeAndFeeValuesForAllPools: String! poolSyncSwapsForLast48Hours: String! poolSyncSanityPoolData: String! - poolUpdateAprs: String! + poolUpdateAprs(chain: GqlChain!): String! poolSyncPoolAllTokensRelationship: String! - poolReloadAllPoolAprs: String! + poolReloadAllPoolAprs(chain: GqlChain!): String! poolSyncTotalShares: String! poolReloadStakingForAllPools(stakingTypes: [GqlPoolStakingType!]!): String! poolSyncStakingForPools: String! diff --git a/modules/pool/pool.resolvers.ts b/modules/pool/pool.resolvers.ts index 7bf0519f3..f7fa7d834 100644 --- a/modules/pool/pool.resolvers.ts +++ b/modules/pool/pool.resolvers.ts @@ -163,10 +163,10 @@ const balancerResolvers: Resolvers = { return 'success'; }, - poolUpdateAprs: async (parent, {}, context) => { + poolUpdateAprs: async (parent, { chain }, context) => { isAdminRoute(context); - await poolService.updatePoolAprs(); + await poolService.updatePoolAprs(chain); return 'success'; }, @@ -177,10 +177,10 @@ const balancerResolvers: Resolvers = { return 'success'; }, - poolReloadAllPoolAprs: async (parent, {}, context) => { + poolReloadAllPoolAprs: async (parent, { chain }, context) => { isAdminRoute(context); - await poolService.reloadAllPoolAprs(); + await poolService.reloadAllPoolAprs(chain); return 'success'; }, diff --git a/modules/pool/pool.service.ts b/modules/pool/pool.service.ts index 93ae434c6..451187fd1 100644 --- a/modules/pool/pool.service.ts +++ b/modules/pool/pool.service.ts @@ -20,7 +20,6 @@ import { QueryPoolGetPoolsArgs, QueryPoolGetSwapsArgs, } from '../../schema'; -import { coingeckoService } from '../coingecko/coingecko.service'; import { blocksSubgraphService } from '../subgraphs/blocks-subgraph/blocks-subgraph.service'; import { tokenService } from '../token/token.service'; import { userService } from '../user/user.service'; @@ -37,6 +36,7 @@ import { networkContext } from '../network/network-context.service'; import { reliquarySubgraphService } from '../subgraphs/reliquary-subgraph/reliquary.service'; import { ReliquarySnapshotService } from './lib/reliquary-snapshot.service'; import { ContentService } from '../content/content-types'; +import { coingeckoDataService } from '../token/lib/coingecko-data.service'; export class PoolService { constructor( @@ -254,16 +254,16 @@ export class PoolService { await Promise.all(this.poolStakingServices.map((stakingService) => stakingService.syncStakingForPools())); } - public async updatePoolAprs() { - await this.poolAprUpdaterService.updatePoolAprs(); + public async updatePoolAprs(chain: Chain) { + await this.poolAprUpdaterService.updatePoolAprs(chain); } public async syncChangedPools() { await this.poolSyncService.syncChangedPools(); } - public async reloadAllPoolAprs() { - await this.poolAprUpdaterService.reloadAllPoolAprs(); + public async reloadAllPoolAprs(chain: Chain) { + await this.poolAprUpdaterService.reloadAllPoolAprs(chain); } public async updateLiquidity24hAgoForAllPools() { @@ -493,6 +493,6 @@ export const poolService = new PoolService( new PoolAprUpdaterService(), new PoolSyncService(), new PoolSwapService(tokenService), - new PoolSnapshotService(coingeckoService), + new PoolSnapshotService(coingeckoDataService), new ReliquarySnapshotService(reliquarySubgraphService), ); diff --git a/modules/protocol/protocol.service.ts b/modules/protocol/protocol.service.ts index 106d58969..5f774f9d9 100644 --- a/modules/protocol/protocol.service.ts +++ b/modules/protocol/protocol.service.ts @@ -156,7 +156,11 @@ export class ProtocolService { } const tokenprices = await tokenService.getTokenPrices(AllNetworkConfigs[chainId].data.chain.prismaId); - const ftmPrice = tokenService.getPriceForToken(tokenprices, AllNetworkConfigs[chainId].data.weth.address); + const ftmPrice = tokenService.getPriceForToken( + tokenprices, + AllNetworkConfigs[chainId].data.weth.address, + AllNetworkConfigs[chainId].data.chain.prismaId, + ); if (AllNetworkConfigs[chainId].data.sftmx) { const stakingData = await prisma.prismaSftmxStakingData.findUniqueOrThrow({ diff --git a/modules/sources/transformers/swaps-transformer.ts b/modules/sources/transformers/swaps-transformer.ts index 3e5e4f961..9100b403b 100644 --- a/modules/sources/transformers/swaps-transformer.ts +++ b/modules/sources/transformers/swaps-transformer.ts @@ -7,8 +7,8 @@ export async function swapsTransformer(swaps: SwapFragment[], chain: Chain): Pro return swaps.map((swap) => { let valueUSD = 0; - const tokenInPrice = tokenService.getPriceForToken(tokenPrices, swap.tokenIn); // TODO need to get price close to swap timestamp - const tokenOutPrice = tokenService.getPriceForToken(tokenPrices, swap.tokenOut); // TODO need to get price close to swap timestamp + const tokenInPrice = tokenService.getPriceForToken(tokenPrices, swap.tokenIn, chain); // TODO need to get price close to swap timestamp + const tokenOutPrice = tokenService.getPriceForToken(tokenPrices, swap.tokenOut, chain); // TODO need to get price close to swap timestamp if (tokenInPrice > 0) { valueUSD = tokenInPrice * parseFloat(swap.tokenAmountIn); diff --git a/modules/token/lib/coingecko-data.service.ts b/modules/token/lib/coingecko-data.service.ts index 28e57b7f4..db4ef46d5 100644 --- a/modules/token/lib/coingecko-data.service.ts +++ b/modules/token/lib/coingecko-data.service.ts @@ -1,145 +1,90 @@ import { prisma } from '../../../prisma/prisma-client'; import _ from 'lodash'; -import moment from 'moment-timezone'; -import { prismaBulkExecuteOperations } from '../../../prisma/prisma-util'; -import { timestampRoundedUpToNearestHour } from '../../common/time'; -import { CoingeckoService } from '../../coingecko/coingecko.service'; import { networkContext } from '../../network/network-context.service'; -import { AllNetworkConfigs } from '../../network/network-config'; -import { PrismaToken } from '.prisma/client'; +import { env } from '../../../app/env'; +import { RateLimiter } from 'limiter'; +import axios, { AxiosError } from 'axios'; + +type Price = { usd: number }; +interface HistoricalPriceResponse { + market_caps: number[][]; + prices: number[][]; + total_volumes: number[][]; +} -export class CoingeckoDataService { - constructor(private readonly conigeckoService: CoingeckoService) {} - - public async syncCoingeckoPricesForAllChains() { - const timestamp = timestampRoundedUpToNearestHour(); - - const tokensWithIds = await prisma.prismaToken.findMany({ - where: { coingeckoTokenId: { not: null } }, - orderBy: { dynamicData: { updatedAt: 'asc' } }, - }); - - // need to filter any excluded tokens from the network configs - const allNetworkConfigs = Object.keys(AllNetworkConfigs); - const includedTokensWithIds: PrismaToken[] = []; - for (const chainId of allNetworkConfigs) { - const excludedAddresses = AllNetworkConfigs[chainId].data.coingecko.excludedTokenAddresses; - const chain = AllNetworkConfigs[chainId].data.chain; - - includedTokensWithIds.push( - ...tokensWithIds.filter( - (token) => token.chain === chain.prismaId && !excludedAddresses.includes(token.address), - ), - ); - } +type HistoricalPrice = { timestamp: number; price: number }; +export type TokenHistoricalPrices = { [address: string]: HistoricalPrice[] }; + +interface CoingeckoTokenMarketData { + id: string; + symbol: string; + name: string; + image: string; + current_price: number; + market_cap: number; + market_cap_rank: number; + fully_diluted_valuation: number | null; + total_volume: number; + high_24h: number; + low_24h: number; + price_change_24h: number; + price_change_percentage_24h: number; + market_cap_change_24h: number; + market_cap_change_percentage_24h: number; + circulating_supply: number; + total_supply: number; + max_supply: number | null; + ath: number; + ath_change_percentage: number; + ath_date: Date; + atl: number; + atl_change_percentage: number; + atl_date: Date; + roi: null; + last_updated: Date; + price_change_percentage_14d_in_currency: number; + price_change_percentage_1h_in_currency: number; + price_change_percentage_24h_in_currency: number; + price_change_percentage_30d_in_currency: number; + price_change_percentage_7d_in_currency: number; +} - // don't price beets via coingecko for now - const filteredTokens = includedTokensWithIds.filter((token) => token.coingeckoTokenId !== 'beethoven-x'); - - const uniqueTokensWithIds = _.uniqBy(filteredTokens, 'coingeckoTokenId'); - - const chunks = _.chunk(uniqueTokensWithIds, 250); //max page size is 250 - - for (const chunk of chunks) { - const response = await this.conigeckoService.getMarketDataForTokenIds( - chunk.map((item) => item.coingeckoTokenId || ''), - ); - let operations: any[] = []; - - for (const item of response) { - const tokensToUpdate = includedTokensWithIds.filter((token) => token.coingeckoTokenId === item.id); - for (const tokenToUpdate of tokensToUpdate) { - // only update if we have a new price and if we have a price at all - if (moment(item.last_updated).isAfter(moment().subtract(10, 'minutes')) && item.current_price) { - const data = { - price: item.current_price, - ath: item.ath ?? undefined, - atl: item.atl ?? undefined, - marketCap: item.market_cap ?? undefined, - fdv: item.fully_diluted_valuation ?? undefined, - high24h: item.high_24h ?? undefined, - low24h: item.low_24h ?? undefined, - priceChange24h: item.price_change_24h ?? undefined, - priceChangePercent24h: item.price_change_percentage_24h ?? undefined, - priceChangePercent7d: item.price_change_percentage_7d_in_currency ?? undefined, - priceChangePercent14d: item.price_change_percentage_14d_in_currency ?? undefined, - priceChangePercent30d: item.price_change_percentage_30d_in_currency ?? undefined, - updatedAt: item.last_updated, - }; - - operations.push( - prisma.prismaTokenDynamicData.upsert({ - where: { - tokenAddress_chain: { - tokenAddress: tokenToUpdate.address, - chain: tokenToUpdate.chain, - }, - }, - update: data, - create: { - coingeckoId: item.id, - tokenAddress: tokenToUpdate.address, - chain: tokenToUpdate.chain, - ...data, - }, - }), - ); - - // update current price and price data, for every chain - operations.push( - prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { - tokenAddress: tokenToUpdate.address, - timestamp, - chain: tokenToUpdate.chain, - }, - }, - update: { price: item.current_price, close: item.current_price }, - create: { - tokenAddress: tokenToUpdate.address, - chain: tokenToUpdate.chain, - timestamp, - price: item.current_price, - high: item.current_price, - low: item.current_price, - open: item.current_price, - close: item.current_price, - coingecko: true, - }, - }), - ); - - operations.push( - prisma.prismaTokenCurrentPrice.upsert({ - where: { - tokenAddress_chain: { - tokenAddress: tokenToUpdate.address, - chain: tokenToUpdate.chain, - }, - }, - update: { price: item.current_price }, - create: { - tokenAddress: tokenToUpdate.address, - chain: tokenToUpdate.chain, - timestamp, - price: item.current_price, - coingecko: true, - }, - }), - ); - } - } - } +interface CoinId { + id: string; + symbol: string; + name: string; + platforms: Record; +} - await Promise.all(operations); - } +/* coingecko has a rate limit of 10-50req/minute + https://www.coingecko.com/en/api/pricing: + Our free API has a rate limit of 10-50 calls per minute, + if you exceed that limit you will be blocked until the next 1 minute window. + Do revise your queries to ensure that you do not exceed our limits should + that happen. + +*/ +const tokensPerMinute = env.COINGECKO_API_KEY ? 10 : 3; +const requestRateLimiter = new RateLimiter({ tokensPerInterval: tokensPerMinute, interval: 'minute' }); +//max 10 addresses per request because of URI size limit, pro is max 180 because of URI limit +const addressChunkSize = env.COINGECKO_API_KEY ? 180 : 20; +export class CoingeckoDataService { + private readonly baseUrl: string; + private readonly fiatParam: string; + private readonly apiKeyParam: string; + + constructor() { + this.baseUrl = env.COINGECKO_API_KEY + ? 'https://pro-api.coingecko.com/api/v3' + : 'https://api.coingecko.com/api/v3'; + this.fiatParam = 'usd'; + this.apiKeyParam = env.COINGECKO_API_KEY ? `&x_cg_pro_api_key=${env.COINGECKO_API_KEY}` : ''; } public async syncCoingeckoIds() { const allTokens = await prisma.prismaToken.findMany({ where: { chain: networkContext.chain } }); - const coinIds = await this.conigeckoService.getCoinIdList(); + const coinIds = await this.getCoinIdList(); for (const token of allTokens) { const coinId = coinIds.find((coinId) => { @@ -160,81 +105,55 @@ export class CoingeckoDataService { } } - public async initChartData(tokenAddress: string) { - const latestTimestamp = timestampRoundedUpToNearestHour(); - tokenAddress = tokenAddress.toLowerCase(); + // public async getTokenHistoricalPrices(address: string, days: number): Promise { + // const now = Math.floor(Date.now() / 1000); + // const end = now; + // const start = end - days * twentyFourHoursInSecs; + // const tokenDefinitions = await tokenService.getTokenDefinitions([networkContext.chain]); + // const mapped = this.getMappedTokenDetails(address, tokenDefinitions); - const operations: any[] = []; - const token = await prisma.prismaToken.findUnique({ - where: { - address_chain: { - address: tokenAddress, - chain: networkContext.chain, - }, - }, - }); + // const endpoint = `/coins/${mapped.platform}/contract/${mapped.address}/market_chart/range?vs_currency=${this.fiatParam}&from=${start}&to=${end}`; - if (!token || !token.coingeckoTokenId) { - throw new Error('Missing token or token is missing coingecko token id'); - } + // const result = await this.get(endpoint); - const monthData = await this.conigeckoService.getCoinCandlestickData(token.coingeckoTokenId, 30); - const twentyFourHourData = await this.conigeckoService.getCoinCandlestickData(token.coingeckoTokenId, 1); + // return result.prices.map((item) => ({ + // //anchor to the start of the hour + // timestamp: + // moment + // .unix(item[0] / 1000) + // .startOf('hour') + // .unix() * 1000, + // price: item[1], + // })); + // } - //merge 30 min data into hourly data - const hourlyData = Object.values( - _.groupBy(twentyFourHourData, (item) => timestampRoundedUpToNearestHour(moment.unix(item[0] / 1000))), - ).map((hourData) => { - if (hourData.length === 1) { - const item = hourData[0]; - item[0] = timestampRoundedUpToNearestHour(moment.unix(item[0] / 1000)) * 1000; + public async getMarketDataForTokenIds(tokenIds: string[]): Promise { + const endpoint = `/coins/markets?vs_currency=${this.fiatParam}&ids=${tokenIds}&per_page=250&page=1&sparkline=false&price_change_percentage=1h%2C24h%2C7d%2C14d%2C30d`; - return item; - } + return this.get(endpoint); + } - const thirty = hourData[0]; - const hour = hourData[1]; - - return [hour[0], thirty[1], Math.max(thirty[2], hour[2]), Math.min(thirty[3], hour[3]), hour[4]]; - }); - - operations.push(prisma.prismaTokenPrice.deleteMany({ where: { tokenAddress, chain: networkContext.chain } })); - - operations.push( - prisma.prismaTokenPrice.createMany({ - data: monthData - .filter((item) => item[0] / 1000 <= latestTimestamp) - .map((item) => ({ - tokenAddress, - chain: networkContext.chain, - timestamp: item[0] / 1000, - open: item[1], - high: item[2], - low: item[3], - close: item[4], - price: item[4], - coingecko: true, - })), - }), - ); - - operations.push( - prisma.prismaTokenPrice.createMany({ - data: hourlyData.map((item) => ({ - tokenAddress, - chain: networkContext.chain, - timestamp: Math.floor(item[0] / 1000), - open: item[1], - high: item[2], - low: item[3], - close: item[4], - price: item[4], - coingecko: true, - })), - skipDuplicates: true, - }), - ); - - await prismaBulkExecuteOperations(operations, true); + private async getCoinIdList(): Promise { + const endpoint = `/coins/list?include_platform=true`; + return this.get(endpoint); + } + + private async get(endpoint: string): Promise { + const remainingRequests = await requestRateLimiter.removeTokens(1); + console.log('Remaining coingecko requests', remainingRequests); + let response; + try { + response = await axios.get(this.baseUrl + endpoint + this.apiKeyParam); + } catch (err: any | AxiosError) { + if (axios.isAxiosError(err)) { + if (err.response?.status === 429) { + throw Error(`Coingecko ratelimit: ${err}`); + } + } + throw err; + } + return response.data; } } + +export const coingeckoDataService = new CoingeckoDataService(); diff --git a/modules/token/lib/token-price-handlers/beets-price-handler.service.ts b/modules/token/lib/token-price-handlers/beets-price-handler.service.ts index 913253d2e..8ecec317c 100644 --- a/modules/token/lib/token-price-handlers/beets-price-handler.service.ts +++ b/modules/token/lib/token-price-handlers/beets-price-handler.service.ts @@ -9,29 +9,41 @@ import { AddressZero } from '@ethersproject/constants'; import VaultAbi from '../../../pool/abi/Vault.json'; import { ethers } from 'ethers'; import { formatFixed } from '@ethersproject/bignumber'; -import { networkContext } from '../../../network/network-context.service'; -import { time } from 'console'; +import { tokenAndPrice, updatePrices } from './price-handler-helper'; +import { Chain } from '@prisma/client'; export class BeetsPriceHandlerService implements TokenPriceHandler { public readonly exitIfFails = false; public readonly id = 'BeetsPriceHandlerService'; - constructor(private readonly beetsAddress: string, private readonly beetsRpcProvider: string) {} - public async getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise { - return [this.beetsAddress]; + private readonly beetsFtmAddress = '0xF24Bcf4d1e507740041C9cFd2DddB29585aDCe1e'; + private readonly wftmFtmAddress = '0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83'; + private readonly freshBeetsPoolId = '0x9e4341acef4147196e99d648c5e43b3fc9d026780002000000000000000005ec'; + private readonly VaultFtmAddress = '0x20dd72Ed959b6147912C2e529F0a0C651c33c9ce'; + private readonly beetsAddressOptimism = '0xb4bc46bc6cb217b59ea8f4530bae26bf69f677f0'; + private readonly beetsRpcProvider = 'https://rpc.ftm.tools'; + + private getAcceptedTokens(tokens: PrismaTokenWithTypes[]): PrismaTokenWithTypes[] { + return tokens.filter( + (token) => + (token.chain === 'FANTOM' && token.address === this.beetsFtmAddress) || + (token.chain === 'OPTIMISM' && token.address === this.beetsAddressOptimism), + ); } - public async updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise { + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { + const acceptedTokens = this.getAcceptedTokens(tokens); + const updatedTokens: PrismaTokenWithTypes[] = []; + const tokenAndPrices: tokenAndPrice[] = []; const timestamp = timestampRoundedUpToNearestHour(); - const beetsFtmAddress = '0xF24Bcf4d1e507740041C9cFd2DddB29585aDCe1e'; - const wftmFtmAddress = '0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83'; - const freshBeetsPoolId = '0x9e4341acef4147196e99d648c5e43b3fc9d026780002000000000000000005ec'; - const VaultFtmAddress = '0x20dd72Ed959b6147912C2e529F0a0C651c33c9ce'; - const assets: string[] = [beetsFtmAddress, wftmFtmAddress]; + const assets: string[] = [this.beetsFtmAddress, this.wftmFtmAddress]; const swaps: SwapV2[] = [ { - poolId: freshBeetsPoolId, + poolId: this.freshBeetsPoolId, assetInIndex: 0, assetOutIndex: 1, amount: fp(1).toString(), @@ -40,7 +52,7 @@ export class BeetsPriceHandlerService implements TokenPriceHandler { ]; const vaultContract = new Contract( - VaultFtmAddress, + this.VaultFtmAddress, VaultAbi, new ethers.providers.JsonRpcProvider(this.beetsRpcProvider), ); @@ -54,7 +66,7 @@ export class BeetsPriceHandlerService implements TokenPriceHandler { let tokenOutAmountScaled = '0'; try { const deltas = await vaultContract.queryBatchSwap(SwapTypes.SwapExactIn, swaps, assets, funds); - tokenOutAmountScaled = deltas[assets.indexOf(wftmFtmAddress)] ?? '0'; + tokenOutAmountScaled = deltas[assets.indexOf(this.wftmFtmAddress)] ?? '0'; } catch (err) { console.log(`queryBatchSwapTokensIn error: `, err); } @@ -65,46 +77,20 @@ export class BeetsPriceHandlerService implements TokenPriceHandler { const ftmPrice = await prisma.prismaTokenCurrentPrice.findUniqueOrThrow({ where: { - tokenAddress_chain: { tokenAddress: wftmFtmAddress.toLowerCase(), chain: 'FANTOM' }, + tokenAddress_chain: { tokenAddress: this.wftmFtmAddress.toLowerCase(), chain: 'FANTOM' }, }, }); const beetsPrice = ftmPrice.price * Math.abs(parseFloat(formatFixed(tokenOutAmountScaled, 18))); - await prisma.prismaTokenCurrentPrice.upsert({ - where: { - tokenAddress_chain: { tokenAddress: this.beetsAddress, chain: networkContext.chain }, - }, - update: { price: beetsPrice }, - create: { - tokenAddress: this.beetsAddress, - chain: networkContext.chain, - timestamp, - price: beetsPrice, - }, - }); + for (const token of acceptedTokens) { + tokenAndPrices.push({ address: token.address, chain: token.chain, price: beetsPrice }); - await prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { - tokenAddress: this.beetsAddress, - timestamp, - chain: networkContext.chain, - }, - }, - update: { price: beetsPrice, close: beetsPrice }, - create: { - tokenAddress: this.beetsAddress, - chain: networkContext.chain, - timestamp, - price: beetsPrice, - high: beetsPrice, - low: beetsPrice, - open: beetsPrice, - close: beetsPrice, - }, - }); + updatedTokens.push(token); + } + + await updatePrices(this.id, tokenAndPrices, timestamp); - return [this.beetsAddress]; + return updatedTokens; } } diff --git a/modules/token/lib/token-price-handlers/bpt-price-handler.service.ts b/modules/token/lib/token-price-handlers/bpt-price-handler.service.ts index b034a9d88..9ecc5ad45 100644 --- a/modules/token/lib/token-price-handlers/bpt-price-handler.service.ts +++ b/modules/token/lib/token-price-handlers/bpt-price-handler.service.ts @@ -2,29 +2,32 @@ import { TokenPriceHandler } from '../../token-types'; import { PrismaTokenWithTypes } from '../../../../prisma/prisma-types'; import { timestampRoundedUpToNearestHour } from '../../../common/time'; import { prisma } from '../../../../prisma/prisma-client'; -import { networkContext } from '../../../network/network-context.service'; +import { Chain } from '@prisma/client'; +import { tokenAndPrice, updatePrices } from './price-handler-helper'; export class BptPriceHandlerService implements TokenPriceHandler { public readonly exitIfFails = false; public readonly id = 'BptPriceHandlerService'; - public async getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise { - return tokens - .filter((token) => token.types.includes('BPT') || token.types.includes('PHANTOM_BPT')) - .map((token) => token.address); + private getAcceptedTokens(tokens: PrismaTokenWithTypes[]): PrismaTokenWithTypes[] { + return tokens.filter((token) => token.types.includes('BPT') || token.types.includes('PHANTOM_BPT')); } - public async updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise { + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { + const acceptedTokens = this.getAcceptedTokens(tokens); const timestamp = timestampRoundedUpToNearestHour(); const pools = await prisma.prismaPool.findMany({ - where: { dynamicData: { totalLiquidity: { gt: 0.1 } }, chain: networkContext.chain }, + where: { dynamicData: { totalLiquidity: { gt: 0.1 } }, chain: { in: chains } }, include: { dynamicData: true }, }); - let updated: string[] = []; - let operations: any[] = []; + const updated: PrismaTokenWithTypes[] = []; + const tokenAndPrices: tokenAndPrice[] = []; - for (const token of tokens) { - const pool = pools.find((pool) => pool.address === token.address); + for (const token of acceptedTokens) { + const pool = pools.find((pool) => pool.address === token.address && pool.chain === token.chain); if ( pool?.dynamicData && @@ -32,48 +35,12 @@ export class BptPriceHandlerService implements TokenPriceHandler { parseFloat(pool.dynamicData.totalShares) !== 0 ) { const price = pool.dynamicData.totalLiquidity / parseFloat(pool.dynamicData.totalShares); - - updated.push(token.address); - - operations.push( - await prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { - tokenAddress: token.address, - timestamp, - chain: networkContext.chain, - }, - }, - update: { price: price, close: price }, - create: { - tokenAddress: token.address, - chain: networkContext.chain, - timestamp, - price, - high: price, - low: price, - open: price, - close: price, - }, - }), - ); - - operations.push( - prisma.prismaTokenCurrentPrice.upsert({ - where: { tokenAddress_chain: { tokenAddress: token.address, chain: networkContext.chain } }, - update: { price: price }, - create: { - tokenAddress: token.address, - chain: networkContext.chain, - timestamp, - price, - }, - }), - ); + tokenAndPrices.push({ address: token.address, chain: token.chain, price: price }); + updated.push(token); } } - await Promise.all(operations); + await updatePrices(this.id, tokenAndPrices, timestamp); return updated; } diff --git a/modules/token/lib/token-price-handlers/clqdr-price-handler.service.ts b/modules/token/lib/token-price-handlers/clqdr-price-handler.service.ts index bea161667..7c12e18b7 100644 --- a/modules/token/lib/token-price-handlers/clqdr-price-handler.service.ts +++ b/modules/token/lib/token-price-handlers/clqdr-price-handler.service.ts @@ -7,6 +7,8 @@ import { ethers } from 'ethers'; import { formatFixed } from '@ethersproject/bignumber'; import PriceRateProviderAbi from '../../abi/CLQDRPerpetualEscrowTokenRateProvider.json'; import { networkContext } from '../../../network/network-context.service'; +import { tokenAndPrice, updatePrices } from './price-handler-helper'; +import { Chain } from '@prisma/client'; export class ClqdrPriceHandlerService implements TokenPriceHandler { public readonly exitIfFails = false; @@ -15,13 +17,20 @@ export class ClqdrPriceHandlerService implements TokenPriceHandler { private readonly lqdrAddress = '0x10b620b2dbac4faa7d7ffd71da486f5d44cd86f9'; private readonly clqdrPriceRateProviderAddress = '0x1a148871bf262451f34f13cbcb7917b4fe59cb32'; - public async getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise { - return [this.clqdrAddress]; + private getAcceptedTokens(tokens: PrismaTokenWithTypes[]): PrismaTokenWithTypes[] { + return tokens.filter((token) => token.chain === 'FANTOM' && token.address === this.clqdrAddress); } - public async updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise { + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { const timestamp = timestampRoundedUpToNearestHour(); + const acceptedTokens = this.getAcceptedTokens(tokens); + const updatedTokens: PrismaTokenWithTypes[] = []; + const tokenAndPrices: tokenAndPrice[] = []; + const clqdrPriceRateProviderContract = new Contract( this.clqdrPriceRateProviderAddress, PriceRateProviderAbi, @@ -54,38 +63,13 @@ export class ClqdrPriceHandlerService implements TokenPriceHandler { const clqdrPrice = lqdrPrice.price * clqdrRate; - await prisma.prismaTokenCurrentPrice.upsert({ - where: { tokenAddress_chain: { tokenAddress: this.clqdrAddress, chain: networkContext.chain } }, - update: { price: clqdrPrice }, - create: { - tokenAddress: this.clqdrAddress, - chain: networkContext.chain, - timestamp, - price: clqdrPrice, - }, - }); + for (const token of acceptedTokens) { + tokenAndPrices.push({ address: token.address, chain: token.chain, price: clqdrPrice }); + updatedTokens.push(token); + } - await prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { - tokenAddress: this.clqdrAddress, - timestamp, - chain: networkContext.chain, - }, - }, - update: { price: clqdrPrice, close: clqdrPrice }, - create: { - tokenAddress: this.clqdrAddress, - chain: networkContext.chain, - timestamp, - price: clqdrPrice, - high: clqdrPrice, - low: clqdrPrice, - open: clqdrPrice, - close: clqdrPrice, - }, - }); + await updatePrices(this.id, tokenAndPrices, timestamp); - return [this.clqdrAddress]; + return updatedTokens; } } diff --git a/modules/token/lib/token-price-handlers/coingecko-price-handler.service.ts b/modules/token/lib/token-price-handlers/coingecko-price-handler.service.ts index 47b75f03a..4b4fc218b 100644 --- a/modules/token/lib/token-price-handlers/coingecko-price-handler.service.ts +++ b/modules/token/lib/token-price-handlers/coingecko-price-handler.service.ts @@ -2,91 +2,110 @@ import { TokenPriceHandler } from '../../token-types'; import { PrismaTokenWithTypes } from '../../../../prisma/prisma-types'; import { prisma } from '../../../../prisma/prisma-client'; import { timestampRoundedUpToNearestHour } from '../../../common/time'; -import { CoingeckoService } from '../../../coingecko/coingecko.service'; -import { networkContext } from '../../../network/network-context.service'; +import { AllNetworkConfigs } from '../../../network/network-config'; +import { Chain } from '@prisma/client'; +import _ from 'lodash'; +import { coingeckoDataService } from '../coingecko-data.service'; +import { tokenAndPrice, updatePrices } from './price-handler-helper'; +import { prismaBulkExecuteOperations } from '../../../../prisma/prisma-util'; export class CoingeckoPriceHandlerService implements TokenPriceHandler { public readonly exitIfFails = true; public readonly id = 'CoingeckoPriceHandlerService'; - constructor(private readonly coingeckoService: CoingeckoService) {} - - public async getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise { - return tokens - .filter( - (token) => - !token.types.includes('BPT') && - !token.types.includes('PHANTOM_BPT') && - !token.types.includes('LINEAR_WRAPPED_TOKEN') && - !token.coingeckoTokenId && - !networkContext.data.coingecko.excludedTokenAddresses.includes(token.address), - ) - .map((token) => token.address); + private getAcceptedTokens(tokens: PrismaTokenWithTypes[]): PrismaTokenWithTypes[] { + // excluded via network config + const excludedFromCoingecko: { address: string; chain: Chain }[] = []; + for (const chain in AllNetworkConfigs) { + const config = AllNetworkConfigs[chain]; + config.data.coingecko.excludedTokenAddresses.forEach((address) => + excludedFromCoingecko.push({ address: address, chain: config.data.chain.prismaId }), + ); + } + return tokens.filter( + (token) => + !excludedFromCoingecko.find( + (excluded) => excluded.address === token.address && excluded.chain === token.chain, + ) && + //excluded via content service + !token.excludedFromCoingecko && + token.coingeckoTokenId, + ); } - public async updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise { + // we update based on coingecko ID + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { + const acceptedTokens = this.getAcceptedTokens(tokens); + const timestamp = timestampRoundedUpToNearestHour(); - const tokensUpdated: string[] = []; + const updated: PrismaTokenWithTypes[] = []; + const tokenAndPrices: tokenAndPrice[] = []; - const tokenAddresses = tokens.map((item) => item.address); + const uniqueTokensWithIds = _.uniqBy(acceptedTokens, 'coingeckoTokenId'); - const tokenPricesByAddress = await this.coingeckoService.getTokenPrices(tokenAddresses); + const chunks = _.chunk(uniqueTokensWithIds, 250); //max page size is 250 - let operations: any[] = []; - for (let tokenAddress of Object.keys(tokenPricesByAddress)) { - const priceUsd = tokenPricesByAddress[tokenAddress].usd; - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const exists = tokenAddresses.includes(normalizedTokenAddress); - if (!exists) { - console.log('skipping token', normalizedTokenAddress); - } - if (exists && priceUsd) { - operations.push( - prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { - tokenAddress: normalizedTokenAddress, - timestamp, - chain: networkContext.chain, - }, - }, - update: { price: priceUsd, close: priceUsd }, - create: { - tokenAddress: normalizedTokenAddress, - chain: networkContext.chain, - timestamp, - price: priceUsd, - high: priceUsd, - low: priceUsd, - open: priceUsd, - close: priceUsd, - coingecko: true, - }, - }), - ); + for (const chunk of chunks) { + const response = await coingeckoDataService.getMarketDataForTokenIds( + chunk.map((item) => item.coingeckoTokenId || ''), + ); + let operations: any[] = []; - operations.push( - prisma.prismaTokenCurrentPrice.upsert({ - where: { - tokenAddress_chain: { tokenAddress: normalizedTokenAddress, chain: networkContext.chain }, - }, - update: { price: priceUsd }, - create: { - tokenAddress: normalizedTokenAddress, - chain: networkContext.chain, - timestamp, - price: priceUsd, - coingecko: true, - }, - }), - ); + for (const item of response) { + const tokensToUpdate = acceptedTokens.filter((token) => token.coingeckoTokenId === item.id); + for (const tokenToUpdate of tokensToUpdate) { + // if we have a price at all + if (item.current_price) { + const data = { + price: item.current_price, + ath: item.ath ?? item.current_price, + atl: item.atl ?? item.current_price, + marketCap: item.market_cap ?? undefined, + fdv: item.fully_diluted_valuation ?? undefined, + high24h: item.high_24h ?? item.current_price, + low24h: item.low_24h ?? item.current_price, + priceChange24h: item.price_change_24h ?? 0, + priceChangePercent24h: item.price_change_percentage_24h ?? 0, + priceChangePercent7d: item.price_change_percentage_7d_in_currency ?? undefined, + priceChangePercent14d: item.price_change_percentage_14d_in_currency ?? undefined, + priceChangePercent30d: item.price_change_percentage_30d_in_currency ?? undefined, + updatedAt: item.last_updated, + }; - tokensUpdated.push(normalizedTokenAddress); - } - } + operations.push( + prisma.prismaTokenDynamicData.upsert({ + where: { + tokenAddress_chain: { + tokenAddress: tokenToUpdate.address, + chain: tokenToUpdate.chain, + }, + }, + update: data, + create: { + coingeckoId: item.id, + tokenAddress: tokenToUpdate.address, + chain: tokenToUpdate.chain, + ...data, + }, + }), + ); - await Promise.all(operations); + tokenAndPrices.push({ + address: tokenToUpdate.address, + chain: tokenToUpdate.chain, + price: item.current_price, + }); + updated.push(tokenToUpdate); + } + } + } + await updatePrices(this.id, tokenAndPrices, timestamp); - return tokensUpdated; + await prismaBulkExecuteOperations(operations); + } + return updated; } } diff --git a/modules/token/lib/token-price-handlers/fallback-price-handler.service.ts b/modules/token/lib/token-price-handlers/fallback-price-handler.service.ts new file mode 100644 index 000000000..78ada8ea0 --- /dev/null +++ b/modules/token/lib/token-price-handlers/fallback-price-handler.service.ts @@ -0,0 +1,67 @@ +import { TokenPriceHandler } from '../../token-types'; +import { PrismaTokenWithTypes } from '../../../../prisma/prisma-types'; +import { prisma } from '../../../../prisma/prisma-client'; +import { timestampRoundedUpToNearestHour } from '../../../common/time'; +import { Chain } from '@prisma/client'; +import { prismaBulkExecuteOperations } from '../../../../prisma/prisma-util'; + +export class FallbackHandlerService implements TokenPriceHandler { + public readonly exitIfFails = false; + public readonly id = 'FallbackHandlerService'; + + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { + const timestamp = timestampRoundedUpToNearestHour(); + const updated: PrismaTokenWithTypes[] = []; + + const operations: any[] = []; + for (const chain of chains) { + const acceptedTokensForChain = tokens.filter((token) => token.chain === chain); + const tokenAddresses = acceptedTokensForChain.map((token) => token.address); + + const tokenPrices = await prisma.prismaTokenCurrentPrice.findMany({ + where: { chain: chain, tokenAddress: { in: tokenAddresses } }, + }); + + for (const token of acceptedTokensForChain) { + const price = tokenPrices.find((tokenPrice) => tokenPrice.tokenAddress === token.address); + if (price) { + operations.push( + prisma.prismaTokenPrice.upsert({ + where: { + tokenAddress_timestamp_chain: { + tokenAddress: token.address, + timestamp: timestamp, + chain: token.chain, + }, + }, + update: { + price: price.price, + close: price.price, + updatedBy: this.id, + }, + create: { + tokenAddress: token.address, + chain: token.chain, + timestamp: timestamp, + price: price.price, + high: price.price, + low: price.price, + open: price.price, + close: price.price, + updatedBy: this.id, + }, + }), + ); + updated.push(token); + } + } + } + + prismaBulkExecuteOperations(operations); + + return updated; + } +} diff --git a/modules/token/lib/token-price-handlers/fbeets-price-handler.service.ts b/modules/token/lib/token-price-handlers/fbeets-price-handler.service.ts index e697ded52..03be01b87 100644 --- a/modules/token/lib/token-price-handlers/fbeets-price-handler.service.ts +++ b/modules/token/lib/token-price-handlers/fbeets-price-handler.service.ts @@ -3,27 +3,36 @@ import { PrismaTokenWithTypes } from '../../../../prisma/prisma-types'; import { timestampRoundedUpToNearestHour } from '../../../common/time'; import { prisma } from '../../../../prisma/prisma-client'; import _ from 'lodash'; -import { networkContext } from '../../../network/network-context.service'; +import { AllNetworkConfigs } from '../../../network/network-config'; +import { tokenAndPrice, updatePrices } from './price-handler-helper'; +import { Chain } from '@prisma/client'; export class FbeetsPriceHandlerService implements TokenPriceHandler { - constructor(private readonly fbeetsAddress: string, private readonly fbeetsPoolId: string) {} public readonly exitIfFails = false; public readonly id = 'FbeetsPriceHandlerService'; - public async getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise { - return [this.fbeetsAddress]; + private getAcceptedTokens(tokens: PrismaTokenWithTypes[]): PrismaTokenWithTypes[] { + const fbeetsAddress = AllNetworkConfigs['250'].data.fbeets!.address; + return tokens.filter((token) => token.chain === 'FANTOM' && token.address === fbeetsAddress); } - public async updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise { + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { + const fbeetsAddress = AllNetworkConfigs['250'].data.fbeets!.address; + const fbeetsPoolId = AllNetworkConfigs['250'].data.fbeets!.poolId; + const acceptedTokens = this.getAcceptedTokens(tokens); + const tokenAndPrices: tokenAndPrice[] = []; + const timestamp = timestampRoundedUpToNearestHour(); - const fbeetsAddress = this.fbeetsAddress; const fbeets = await prisma.prismaFbeets.findFirst({}); const pool = await prisma.prismaPool.findUnique({ - where: { id_chain: { id: this.fbeetsPoolId, chain: networkContext.chain } }, + where: { id_chain: { id: fbeetsPoolId, chain: 'FANTOM' } }, include: { dynamicData: true, tokens: { include: { dynamicData: true, token: true } } }, }); const tokenPrices = await prisma.prismaTokenCurrentPrice.findMany({ - where: { tokenAddress: { in: pool?.tokens.map((token) => token.address) }, chain: networkContext.chain }, + where: { tokenAddress: { in: pool?.tokens.map((token) => token.address) }, chain: 'FANTOM' }, }); if (!fbeets || !pool || tokenPrices.length !== pool.tokens.length) { @@ -44,34 +53,14 @@ export class FbeetsPriceHandlerService implements TokenPriceHandler { }), ); - await prisma.prismaTokenCurrentPrice.upsert({ - where: { tokenAddress_chain: { tokenAddress: fbeetsAddress, chain: networkContext.chain } }, - update: { price: fbeetsPrice }, - create: { - tokenAddress: fbeetsAddress, - chain: networkContext.chain, - timestamp, - price: fbeetsPrice, - }, + tokenAndPrices.push({ + address: fbeetsAddress, + chain: 'FANTOM', + price: fbeetsPrice, }); - await prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { tokenAddress: fbeetsAddress, timestamp, chain: networkContext.chain }, - }, - update: { price: fbeetsPrice, close: fbeetsPrice }, - create: { - tokenAddress: fbeetsAddress, - chain: networkContext.chain, - timestamp, - price: fbeetsPrice, - high: fbeetsPrice, - low: fbeetsPrice, - open: fbeetsPrice, - close: fbeetsPrice, - }, - }); + await updatePrices(this.id, tokenAndPrices, timestamp); - return [this.fbeetsAddress]; + return acceptedTokens; } } diff --git a/modules/token/lib/token-price-handlers/linear-wrapped-token-price-handler.service.ts b/modules/token/lib/token-price-handlers/linear-wrapped-token-price-handler.service.ts index 9fafe4df5..eeede9248 100644 --- a/modules/token/lib/token-price-handlers/linear-wrapped-token-price-handler.service.ts +++ b/modules/token/lib/token-price-handlers/linear-wrapped-token-price-handler.service.ts @@ -2,32 +2,39 @@ import { TokenPriceHandler } from '../../token-types'; import { PrismaTokenWithTypes } from '../../../../prisma/prisma-types'; import { prisma } from '../../../../prisma/prisma-client'; import { timestampRoundedUpToNearestHour } from '../../../common/time'; -import { networkContext } from '../../../network/network-context.service'; import { LinearData } from '../../../pool/subgraph-mapper'; +import { tokenAndPrice, updatePrices } from './price-handler-helper'; +import { Chain } from '@prisma/client'; export class LinearWrappedTokenPriceHandlerService implements TokenPriceHandler { public readonly exitIfFails = false; public readonly id = 'LinearWrappedTokenPriceHandlerService'; - public async getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise { - return tokens.filter((token) => token.types.includes('LINEAR_WRAPPED_TOKEN')).map((token) => token.address); + private getAcceptedTokens(tokens: PrismaTokenWithTypes[]): PrismaTokenWithTypes[] { + return tokens.filter((token) => token.types.includes('LINEAR_WRAPPED_TOKEN')); } - public async updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise { - let operations: any[] = []; - const tokensUpdated: string[] = []; + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { + const acceptedTokens = this.getAcceptedTokens(tokens); + + const tokensUpdated: PrismaTokenWithTypes[] = []; + const tokenAndPrices: tokenAndPrice[] = []; const timestamp = timestampRoundedUpToNearestHour(); + const pools = await prisma.prismaPool.findMany({ where: { type: 'LINEAR', - chain: networkContext.chain, + chain: { in: chains }, categories: { none: { category: 'BLACK_LISTED' } }, }, include: { tokens: { orderBy: { index: 'asc' }, include: { dynamicData: true } } }, }); const mainTokenPrices = await prisma.prismaTokenPrice.findMany({ where: { - chain: networkContext.chain, + chain: { in: chains }, tokenAddress: { in: pools.map((pool) => pool.tokens[(pool.typeData as LinearData)?.mainIndex || 0].address), }, @@ -35,7 +42,7 @@ export class LinearWrappedTokenPriceHandlerService implements TokenPriceHandler }, }); - for (const token of tokens) { + for (const token of acceptedTokens) { const pool = pools.find( (pool) => (pool.typeData as LinearData) && @@ -53,48 +60,14 @@ export class LinearWrappedTokenPriceHandlerService implements TokenPriceHandler if (mainTokenPrice && wrappedToken.dynamicData) { const price = mainTokenPrice.price * parseFloat(wrappedToken.dynamicData.priceRate); - operations.push( - prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { - tokenAddress: token.address, - timestamp, - chain: networkContext.chain, - }, - }, - update: { price, close: price }, - create: { - tokenAddress: token.address, - chain: networkContext.chain, - timestamp, - price, - high: price, - low: price, - open: price, - close: price, - }, - }), - ); - - operations.push( - prisma.prismaTokenCurrentPrice.upsert({ - where: { tokenAddress_chain: { tokenAddress: token.address, chain: networkContext.chain } }, - update: { price: price }, - create: { - tokenAddress: token.address, - chain: networkContext.chain, - timestamp, - price, - }, - }), - ); + tokenAndPrices.push({ address: token.address, chain: token.chain, price: price }); - tokensUpdated.push(token.address); + tokensUpdated.push(token); } } } - await Promise.all(operations); + await updatePrices(this.id, tokenAndPrices, timestamp); return tokensUpdated; } diff --git a/modules/token/lib/token-price-handlers/price-handler-helper.ts b/modules/token/lib/token-price-handlers/price-handler-helper.ts new file mode 100644 index 000000000..324c03059 --- /dev/null +++ b/modules/token/lib/token-price-handlers/price-handler-helper.ts @@ -0,0 +1,65 @@ +import { Chain } from '@prisma/client'; +import { prisma } from '../../../../prisma/prisma-client'; +import { prismaBulkExecuteOperations } from '../../../../prisma/prisma-util'; + +export type tokenAndPrice = { + address: string; + chain: Chain; + price: number; +}; + +export async function updatePrices(handlerId: string, tokens: tokenAndPrice[], hourlyTimestamp: number) { + const operations: any[] = []; + + for (const token of tokens) { + // update or create hourly price + operations.push( + await prisma.prismaTokenPrice.upsert({ + where: { + tokenAddress_timestamp_chain: { + tokenAddress: token.address, + timestamp: hourlyTimestamp, + chain: token.chain, + }, + }, + update: { + price: token.price, + close: token.price, + updatedBy: handlerId, + }, + create: { + tokenAddress: token.address, + chain: token.chain, + timestamp: hourlyTimestamp, + price: token.price, + high: token.price, + low: token.price, + open: token.price, + close: token.price, + updatedBy: handlerId, + }, + }), + ); + + // create or update current price + operations.push( + prisma.prismaTokenCurrentPrice.upsert({ + where: { tokenAddress_chain: { tokenAddress: token.address, chain: token.chain } }, + update: { + price: token.price, + timestamp: hourlyTimestamp, + updatedBy: handlerId, + }, + create: { + tokenAddress: token.address, + chain: token.chain, + timestamp: hourlyTimestamp, + price: token.price, + updatedBy: handlerId, + }, + }), + ); + } + + await prismaBulkExecuteOperations(operations); +} diff --git a/modules/token/lib/token-price-handlers/swaps-price-handler.service.ts b/modules/token/lib/token-price-handlers/swaps-price-handler.service.ts index b5cd89d55..7d5f8fcb8 100644 --- a/modules/token/lib/token-price-handlers/swaps-price-handler.service.ts +++ b/modules/token/lib/token-price-handlers/swaps-price-handler.service.ts @@ -3,116 +3,89 @@ import { PrismaTokenWithTypes } from '../../../../prisma/prisma-types'; import { prisma } from '../../../../prisma/prisma-client'; import { timestampRoundedUpToNearestHour } from '../../../common/time'; import moment from 'moment-timezone'; -import { networkContext } from '../../../network/network-context.service'; import _ from 'lodash'; +import { tokenAndPrice, updatePrices } from './price-handler-helper'; +import { Chain } from '@prisma/client'; export class SwapsPriceHandlerService implements TokenPriceHandler { public readonly exitIfFails = false; public readonly id = 'SwapsPriceHandlerService'; - public async getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise { - return tokens - .filter( - (token) => - !token.types.includes('BPT') && - !token.types.includes('PHANTOM_BPT') && - !token.types.includes('LINEAR_WRAPPED_TOKEN') && - (!token.coingeckoTokenId || - networkContext.data.coingecko.excludedTokenAddresses.includes(token.address)), - ) - .map((token) => token.address); + private getAcceptedTokens(tokens: PrismaTokenWithTypes[]): PrismaTokenWithTypes[] { + return tokens.filter( + (token) => + !token.types.includes('BPT') && + !token.types.includes('PHANTOM_BPT') && + !token.types.includes('LINEAR_WRAPPED_TOKEN'), + ); } - public async updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise { - let operations: any[] = []; - const tokensUpdated: string[] = []; + public async updatePricesForTokens( + tokens: PrismaTokenWithTypes[], + chains: Chain[], + ): Promise { + const acceptedTokens = this.getAcceptedTokens(tokens); + + const updated: PrismaTokenWithTypes[] = []; + const tokenAndPrices: tokenAndPrice[] = []; + const timestamp = timestampRoundedUpToNearestHour(); - const tokenAddresses = tokens.map((token) => token.address); - const swaps = await prisma.prismaPoolSwap.findMany({ - where: { - chain: networkContext.chain, - timestamp: { gt: moment().unix() - 900 }, //only search for the last 15 minutes - OR: [{ tokenIn: { in: tokenAddresses } }, { tokenOut: { in: tokenAddresses } }], - }, - orderBy: { timestamp: 'desc' }, - }); - const otherTokenAddresses = [ - ...swaps.filter((swap) => !tokenAddresses.includes(swap.tokenIn)).map((swap) => swap.tokenIn), - ...swaps.filter((swap) => !tokenAddresses.includes(swap.tokenOut)).map((swap) => swap.tokenOut), - ]; - const tokenPrices = await prisma.prismaTokenPrice.findMany({ - where: { chain: networkContext.chain, timestamp, tokenAddress: { in: otherTokenAddresses } }, - }); - for (const token of tokens) { - const tokenSwaps = swaps.filter( - (swap) => swap.tokenIn === token.address || swap.tokenOut === token.address, - ); + for (const chain of chains) { + const acceptedTokensForChain = acceptedTokens.filter((token) => token.chain === chain); + const tokenAddresses = acceptedTokensForChain.map((token) => token.address); - for (const tokenSwap of tokenSwaps) { - const tokenSide: 'token-in' | 'token-out' = - tokenSwap.tokenIn === token.address ? 'token-in' : 'token-out'; - const tokenAmount = parseFloat( - tokenSide === 'token-in' ? tokenSwap.tokenAmountIn : tokenSwap.tokenAmountOut, - ); - const otherToken = tokenSide === 'token-in' ? tokenSwap.tokenOut : tokenSwap.tokenIn; - const otherTokenAmount = parseFloat( - tokenSide === 'token-in' ? tokenSwap.tokenAmountOut : tokenSwap.tokenAmountIn, - ); - const otherTokenPrice = tokenPrices.find((tokenPrice) => tokenPrice.tokenAddress === otherToken); + const swaps = await prisma.prismaPoolSwap.findMany({ + where: { + chain: chain, + timestamp: { gt: moment().unix() - 900 }, //only search for the last 15 minutes + OR: [{ tokenIn: { in: tokenAddresses } }, { tokenOut: { in: tokenAddresses } }], + }, + orderBy: { timestamp: 'desc' }, + }); + const otherTokenAddresses = [ + ...swaps.filter((swap) => !tokenAddresses.includes(swap.tokenIn)).map((swap) => swap.tokenIn), + ...swaps.filter((swap) => !tokenAddresses.includes(swap.tokenOut)).map((swap) => swap.tokenOut), + ]; + const tokenPrices = await prisma.prismaTokenPrice.findMany({ + where: { chain: chain, timestamp, tokenAddress: { in: otherTokenAddresses } }, + }); - if (otherTokenPrice) { - const otherTokenValue = otherTokenPrice.price * otherTokenAmount; - const price = otherTokenValue / tokenAmount; + for (const token of acceptedTokensForChain) { + const tokenSwaps = swaps.filter( + (swap) => swap.tokenIn === token.address || swap.tokenOut === token.address, + ); - operations.push( - prisma.prismaTokenPrice.upsert({ - where: { - tokenAddress_timestamp_chain: { - tokenAddress: token.address, - timestamp, - chain: networkContext.chain, - }, - }, - update: { price, close: price }, - create: { - tokenAddress: token.address, - chain: networkContext.chain, - timestamp, - price, - high: price, - low: price, - open: price, - close: price, - }, - }), + for (const tokenSwap of tokenSwaps) { + const tokenSide: 'token-in' | 'token-out' = + tokenSwap.tokenIn === token.address ? 'token-in' : 'token-out'; + const tokenAmount = parseFloat( + tokenSide === 'token-in' ? tokenSwap.tokenAmountIn : tokenSwap.tokenAmountOut, ); - - operations.push( - prisma.prismaTokenCurrentPrice.upsert({ - where: { - tokenAddress_chain: { - tokenAddress: token.address, - chain: networkContext.chain, - }, - }, - update: { price: price }, - create: { - tokenAddress: token.address, - chain: networkContext.chain, - timestamp, - price, - }, - }), + const otherToken = tokenSide === 'token-in' ? tokenSwap.tokenOut : tokenSwap.tokenIn; + const otherTokenAmount = parseFloat( + tokenSide === 'token-in' ? tokenSwap.tokenAmountOut : tokenSwap.tokenAmountIn, ); + const otherTokenPrice = tokenPrices.find((tokenPrice) => tokenPrice.tokenAddress === otherToken); + + if (otherTokenPrice) { + const otherTokenValue = otherTokenPrice.price * otherTokenAmount; + const price = otherTokenValue / tokenAmount; + + tokenAndPrices.push({ + address: token.address, + chain: token.chain, + price: price, + }); - tokensUpdated.push(token.address); + updated.push(token); + } } } } - await Promise.all(operations); + await updatePrices(this.id, tokenAndPrices, timestamp); - return tokensUpdated; + return updated; } } diff --git a/modules/token/lib/token-price.service.ts b/modules/token/lib/token-price.service.ts index 0d274ff1a..e7a225678 100644 --- a/modules/token/lib/token-price.service.ts +++ b/modules/token/lib/token-price.service.ts @@ -1,27 +1,33 @@ import { TokenPriceHandler, TokenPriceItem } from '../token-types'; import { prisma } from '../../../prisma/prisma-client'; import _ from 'lodash'; -import { secondsPerDay, timestampRoundedUpToNearestHour } from '../../common/time'; +import { timestampRoundedUpToNearestHour } from '../../common/time'; import { Chain, PrismaTokenCurrentPrice, PrismaTokenPrice } from '@prisma/client'; import moment from 'moment-timezone'; import { GqlTokenChartDataRange } from '../../../schema'; import { Cache, CacheClass } from 'memory-cache'; import * as Sentry from '@sentry/node'; -import { networkContext } from '../../network/network-context.service'; -import { TokenHistoricalPrices } from '../../coingecko/coingecko-types'; import { AllNetworkConfigsKeyedOnChain } from '../../network/network-config'; - -const TOKEN_HISTORICAL_PRICES_CACHE_KEY = `token-historical-prices`; -const NESTED_BPT_HISTORICAL_PRICES_CACHE_KEY = `nested-bpt-historical-prices`; +import { FbeetsPriceHandlerService } from './token-price-handlers/fbeets-price-handler.service'; +import { ClqdrPriceHandlerService } from './token-price-handlers/clqdr-price-handler.service'; +import { CoingeckoPriceHandlerService } from './token-price-handlers/coingecko-price-handler.service'; +import { FallbackHandlerService } from './token-price-handlers/fallback-price-handler.service'; +import { LinearWrappedTokenPriceHandlerService } from './token-price-handlers/linear-wrapped-token-price-handler.service'; +import { BptPriceHandlerService } from './token-price-handlers/bpt-price-handler.service'; +import { SwapsPriceHandlerService } from './token-price-handlers/swaps-price-handler.service'; +import { PrismaTokenWithTypes } from '../../../prisma/prisma-types'; export class TokenPriceService { cache: CacheClass = new Cache(); - - constructor() {} - - private get handlers(): TokenPriceHandler[] { - return networkContext.config.tokenPriceHandlers; - } + private readonly priceHandlers: TokenPriceHandler[] = [ + new FbeetsPriceHandlerService(), + new ClqdrPriceHandlerService(), + new CoingeckoPriceHandlerService(), + new BptPriceHandlerService(), + new LinearWrappedTokenPriceHandlerService(), + new SwapsPriceHandlerService(), + new FallbackHandlerService(), + ]; public async getWhiteListedCurrentTokenPrices(chains: Chain[]): Promise { const tokenPrices = await prisma.prismaTokenCurrentPrice.findMany({ @@ -49,100 +55,61 @@ export class TokenPriceService { return tokenPrices; } - public async getCurrentTokenPrices(chain = networkContext.chain): Promise { + public async getCurrentTokenPrices(chains: Chain[]): Promise { const tokenPrices = await prisma.prismaTokenCurrentPrice.findMany({ - where: { chain: chain }, + where: { chain: { in: chains } }, orderBy: { timestamp: 'desc' }, distinct: ['tokenAddress'], }); - const wethPrice = tokenPrices.find( - (tokenPrice) => tokenPrice.tokenAddress === AllNetworkConfigsKeyedOnChain[chain].data.weth.address, - ); - - if (wethPrice) { - tokenPrices.push({ - ...wethPrice, - tokenAddress: AllNetworkConfigsKeyedOnChain[chain].data.eth.address, - }); - } + // also add ETH price (0xeee..) + this.addNativeEthPrice(chains, tokenPrices); return tokenPrices.filter((tokenPrice) => tokenPrice.price > 0.000000001); } - public async getTokenPriceFrom24hAgo(): Promise { + public async getTokenPricesFrom24hAgo(chains: Chain[]): Promise { const oneDayAgo = moment().subtract(24, 'hours').unix(); const twoDaysAgo = moment().subtract(48, 'hours').unix(); - console.time(`TokenPrice load 24hrs ago - ${networkContext.chain}`); + console.time(`TokenPrice load 24hrs ago - ${chains}`); const tokenPrices = await prisma.prismaTokenPrice.findMany({ orderBy: { timestamp: 'desc' }, - where: { timestamp: { lte: oneDayAgo, gte: twoDaysAgo }, chain: networkContext.chain }, + where: { timestamp: { lte: oneDayAgo, gte: twoDaysAgo }, chain: { in: chains } }, }); const distinctTokenPrices = tokenPrices.filter( - (price, i, self) => self.findIndex((t) => t.tokenAddress === price.tokenAddress) === i, + (price, i, self) => + self.findIndex((t) => t.tokenAddress === price.tokenAddress && t.chain === price.chain) === i, ); - console.timeEnd(`TokenPrice load 24hrs ago - ${networkContext.chain}`); - - const wethPrice = distinctTokenPrices.find( - (tokenPrice) => tokenPrice.tokenAddress === networkContext.data.weth.address, - ); + console.timeEnd(`TokenPrice load 24hrs ago - ${chains}`); - if (wethPrice) { - distinctTokenPrices.push({ - ...wethPrice, - tokenAddress: networkContext.data.eth.address, - }); - } + // also add ETH price (0xeee..) + this.addNativeEthPrice(chains, distinctTokenPrices); return distinctTokenPrices .filter((tokenPrice) => tokenPrice.price > 0.000000001) .map((tokenPrice) => ({ id: `${tokenPrice.tokenAddress}-${tokenPrice.timestamp}`, ...tokenPrice, + updatedBy: null, })); } - public getPriceForToken(tokenPrices: PrismaTokenCurrentPrice[], tokenAddress: string): number { + public getPriceForToken(tokenPrices: PrismaTokenCurrentPrice[], tokenAddress: string, chain: Chain): number { const tokenPrice = tokenPrices.find( - (tokenPrice) => tokenPrice.tokenAddress.toLowerCase() === tokenAddress.toLowerCase(), + (tokenPrice) => + tokenPrice.tokenAddress.toLowerCase() === tokenAddress.toLowerCase() && tokenPrice.chain === chain, ); return tokenPrice?.price || 0; } - public async getHistoricalTokenPrices(chain: Chain): Promise { - const memCached = this.cache.get( - `${TOKEN_HISTORICAL_PRICES_CACHE_KEY}:${chain}`, - ) as TokenHistoricalPrices | null; - - if (memCached) { - return memCached; - } - - const tokenPrices: TokenHistoricalPrices = await this.cache.get( - `${TOKEN_HISTORICAL_PRICES_CACHE_KEY}:${chain}`, - ); - const nestedBptPrices: TokenHistoricalPrices = await this.cache.get( - `${NESTED_BPT_HISTORICAL_PRICES_CACHE_KEY}:${chain}`, - ); - - if (tokenPrices) { - this.cache.put( - `${TOKEN_HISTORICAL_PRICES_CACHE_KEY}:${chain}`, - { ...tokenPrices, ...nestedBptPrices }, - 60000, - ); - } - - //don't try to refetch the cache, it takes way too long - return { ...tokenPrices, ...nestedBptPrices }; - } - - public async updateTokenPrices(): Promise { + // should this be called for all chains in general? + // thinking about coingecko requests as we should update those prices once for all chains + public async updateAllTokenPrices(chains: Chain[]): Promise { const tokens = await prisma.prismaToken.findMany({ - where: { chain: networkContext.chain }, + where: { chain: { in: chains } }, include: { types: true, }, @@ -153,17 +120,15 @@ export class TokenPriceService { types: token.types.map((type) => type.type), })); - for (const handler of this.handlers) { - const accepted = await handler.getAcceptedTokens(tokensWithTypes); - const acceptedTokens = tokensWithTypes.filter((token) => accepted.includes(token.address)); - let updated: string[] = []; + for (const handler of this.priceHandlers) { + // const accepted = await handler.getAcceptedTokens(tokensWithTypes); + // const acceptedTokens = tokensWithTypes.filter((token) => accepted.includes(token.address)); + let updated: PrismaTokenWithTypes[] = []; try { - updated = await handler.updatePricesForTokens(acceptedTokens); + updated = await handler.updatePricesForTokens(tokensWithTypes, chains); } catch (e) { - console.error( - `TokenPriceHanlder failed. Chain: ${networkContext.chain}, ID: ${handler.id}, Error: ${e}`, - ); + console.error(`TokenPriceHanlder failed. ID: ${handler.id}, Error: ${e}`); Sentry.captureException(e, (scope) => { scope.setTag('handler.exitIfFails', handler.exitIfFails); return scope; @@ -174,29 +139,39 @@ export class TokenPriceService { } //remove any updated tokens from the list for the next handler - tokensWithTypes = tokensWithTypes.filter((token) => !updated.includes(token.address)); + tokensWithTypes = tokensWithTypes.filter((token) => { + return !updated.some((updatedToken) => { + return token.address === updatedToken.address && token.chain === updatedToken.chain; + }); + }); } - await this.updateCandleStickData(); - - //we only keep token prices for the last 24 hours - //const yesterday = moment().subtract(1, 'day').unix(); - //await prisma.prismaTokenPrice.deleteMany({ where: { timestamp: { lt: yesterday } } }); + for (const chain of chains) { + await this.updateCandleStickData(chain); + } } - public async getDataForRange( - tokenAddress: string, + public async getTokenPricesForRange( + tokenAddresses: string[], range: GqlTokenChartDataRange, chain: Chain, ): Promise { const startTimestamp = this.getStartTimestampFromRange(range); return prisma.prismaTokenPrice.findMany({ - where: { tokenAddress, timestamp: { gt: startTimestamp }, chain: chain }, + where: { tokenAddress: { in: tokenAddresses }, timestamp: { gt: startTimestamp }, chain: chain }, orderBy: { timestamp: 'asc' }, }); } + public async getTokenPriceForRange( + tokenAddress: string, + range: GqlTokenChartDataRange, + chain: Chain, + ): Promise { + return this.getTokenPricesForRange([tokenAddress], range, chain); + } + public async getRelativeDataForRange( tokenIn: string, tokenOut: string, @@ -236,12 +211,14 @@ export class TokenPriceService { public async deleteTokenPrice({ timestamp, tokenAddress, + chain, }: { tokenAddress: string; timestamp: number; + chain: Chain; }): Promise { const response = await prisma.prismaTokenPrice.delete({ - where: { tokenAddress_timestamp_chain: { tokenAddress, timestamp, chain: networkContext.chain } }, + where: { tokenAddress_timestamp_chain: { tokenAddress, timestamp, chain: chain } }, }); return !!response; @@ -255,6 +232,10 @@ export class TokenPriceService { return moment().subtract(30, 'days').unix(); case 'NINETY_DAY': return moment().subtract(90, 'days').unix(); + case 'ONE_HUNDRED_EIGHTY_DAY': + return moment().subtract(180, 'days').unix(); + case 'ONE_YEAR': + return moment().subtract(365, 'days').unix(); default: return moment().subtract(7, 'days').unix(); } @@ -268,10 +249,10 @@ export class TokenPriceService { return deleted; } - private async updateCandleStickData() { + private async updateCandleStickData(chain: Chain) { const timestamp = timestampRoundedUpToNearestHour(); const tokenPrices = await prisma.prismaTokenPrice.findMany({ - where: { timestamp, chain: networkContext.chain }, + where: { timestamp, chain: chain }, }); let operations: any[] = []; @@ -282,7 +263,7 @@ export class TokenPriceService { tokenAddress_timestamp_chain: { tokenAddress: tokenPrice.tokenAddress, timestamp, - chain: networkContext.chain, + chain: chain, }, }, data: { @@ -295,4 +276,21 @@ export class TokenPriceService { await Promise.all(operations); } + + private addNativeEthPrice(chains: Chain[], tokenPrices: { tokenAddress: string; chain: Chain }[]) { + for (const chain of chains) { + const wethPrice = tokenPrices.find( + (tokenPrice) => + tokenPrice.tokenAddress === AllNetworkConfigsKeyedOnChain[chain].data.weth.address && + tokenPrice.chain === chain, + ); + + if (wethPrice) { + tokenPrices.push({ + ...wethPrice, + tokenAddress: AllNetworkConfigsKeyedOnChain[chain].data.eth.address, + }); + } + } + } } diff --git a/modules/token/token-types.ts b/modules/token/token-types.ts index 1268a5210..032d81949 100644 --- a/modules/token/token-types.ts +++ b/modules/token/token-types.ts @@ -6,17 +6,15 @@ export interface TokenPriceHandler { id: string; /** - * Determines what tokens this price handler is capable of fetching a price for + * Updates prices for the provided tokens, returning an array of the tokens + * actually updated. It create three updates: + * - current price in the TokenCurrentPrice Table + * - Hourly price as an entry in the TokenPrice table with timestamp rounded to the nearest hour + * - Daily price as an entry in the TokenPrice table with timestamp at midnight today (closing price) * @param tokens tokens needing prices + * @param chains all the chains that the tokens are from */ - getAcceptedTokens(tokens: PrismaTokenWithTypes[]): Promise; - - /** - * Updates prices for the provided tokens, returning an array of addresses of the tokens - * actually updated. - * @param tokens tokens needing prices - */ - updatePricesForTokens(tokens: PrismaTokenWithTypes[]): Promise; + updatePricesForTokens(tokens: PrismaTokenWithTypes[], chains: Chain[]): Promise; } export interface TokenDefinition { diff --git a/modules/token/token.gql b/modules/token/token.gql index 8cddc4a5a..782fae2b1 100644 --- a/modules/token/token.gql +++ b/modules/token/token.gql @@ -1,7 +1,11 @@ extend type Query { tokenGetTokens(chains: [GqlChain!]): [GqlToken!]! tokenGetCurrentPrices(chains: [GqlChain!]): [GqlTokenPrice!]! - tokenGetHistoricalPrices(addresses: [String!]!, chain: GqlChain): [GqlHistoricalTokenPrice!]! + tokenGetHistoricalPrices( + addresses: [String!]! + chain: GqlChain! + range: GqlTokenChartDataRange! + ): [GqlHistoricalTokenPrice!]! tokenGetTokensDynamicData(addresses: [String!]!, chain: GqlChain): [GqlTokenDynamicData!]! tokenGetTokenDynamicData(address: String!, chain: GqlChain): GqlTokenDynamicData tokenGetRelativePriceChartData( @@ -22,16 +26,13 @@ extend type Query { ): [GqlTokenCandlestickChartDataItem!]! tokenGetTokenData(address: String!, chain: GqlChain): GqlTokenData tokenGetTokensData(addresses: [String!]!): [GqlTokenData!]! - tokenGetProtocolTokenPrice: AmountHumanReadable! + tokenGetProtocolTokenPrice(chain: GqlChain): AmountHumanReadable! } extend type Mutation { - tokenReloadTokenPrices: Boolean + tokenReloadTokenPrices(chains: [GqlChain!]!): Boolean tokenSyncTokenDefinitions: String! - tokenSyncTokenDynamicData: String! tokenSyncLatestFxPrices(chain: GqlChain!): String! - tokenInitChartData(tokenAddress: String!): String! - tokenDeletePrice(tokenAddress: String!, timestamp: Int!): Boolean! tokenDeleteTokenType(tokenAddress: String!, type: GqlTokenType!): String! tokenReloadAllTokenTypes: String! } @@ -44,6 +45,7 @@ type GqlTokenPrice { type GqlHistoricalTokenPrice { address: String! + chain: GqlChain! prices: [GqlHistoricalTokenPriceEntry!]! } @@ -92,6 +94,8 @@ enum GqlTokenChartDataRange { SEVEN_DAY THIRTY_DAY NINETY_DAY + ONE_HUNDRED_EIGHTY_DAY + ONE_YEAR } type GqlTokenCandlestickChartDataItem { diff --git a/modules/token/token.prisma b/modules/token/token.prisma index fa3336786..645465881 100644 --- a/modules/token/token.prisma +++ b/modules/token/token.prisma @@ -18,10 +18,12 @@ model PrismaToken { coingeckoPlatformId String? coingeckoContractAddress String? coingeckoTokenId String? + excludedFromCoingecko Boolean @default(false) - dynamicData PrismaTokenDynamicData? currentPrice PrismaTokenCurrentPrice? + dynamicData PrismaTokenDynamicData? prices PrismaTokenPrice[] + types PrismaTokenType[] expandedPools PrismaPoolExpandedTokens[] @@ -40,9 +42,9 @@ model PrismaTokenCurrentPrice { chain Chain updatedAt DateTime @updatedAt + updatedBy String? timestamp Int price Float - coingecko Boolean? } model PrismaTokenPrice { @@ -52,9 +54,9 @@ model PrismaTokenPrice { token PrismaToken @relation(fields:[tokenAddress, chain], references: [address, chain]) chain Chain updatedAt DateTime @updatedAt + updatedBy String? timestamp Int price Float - coingecko Boolean? high Float low Float diff --git a/modules/token/token.resolvers.ts b/modules/token/token.resolvers.ts index 8c02b0491..fc060c734 100644 --- a/modules/token/token.resolvers.ts +++ b/modules/token/token.resolvers.ts @@ -1,4 +1,4 @@ -import { Resolvers } from '../../schema'; +import { GqlHistoricalTokenPrice, Resolvers } from '../../schema'; import _ from 'lodash'; import { isAdminRoute } from '../auth/auth-context'; import { tokenService } from './token.service'; @@ -32,23 +32,23 @@ const resolvers: Resolvers = { chain: price.chain, })); }, - tokenGetHistoricalPrices: async (parent, { addresses, chain }, context) => { - const currentChain = headerChain(); - if (!chain && currentChain) { - chain = currentChain; - } else if (!chain) { - throw new Error('tokenGetHistoricalPrices error: Provide "chain" param'); + tokenGetHistoricalPrices: async (parent, { addresses, chain, range }, context) => { + const data = await tokenService.getTokenPricesForRange(addresses, range, chain); + + const grouped = _.groupBy(data, 'tokenAddress'); + + const result: GqlHistoricalTokenPrice[] = []; + for (const address in grouped) { + result.push({ + address: address, + chain: grouped[address][0].chain, + prices: grouped[address].map((entry) => ({ + timestamp: `${entry.timestamp}`, + price: entry.price, + })), + }); } - const tokenPrices = await tokenService.getHistoricalTokenPrices(chain); - const filtered = _.pickBy(tokenPrices, (entries, address) => addresses.includes(address)); - - return _.map(filtered, (entries, address) => ({ - address, - prices: entries.map((entry) => ({ - timestamp: `${entry.timestamp}`, - price: entry.price, - })), - })); + return result; }, tokenGetTokenDynamicData: async (parent, { address, chain }, context) => { const currentChain = headerChain(); @@ -74,7 +74,7 @@ const resolvers: Resolvers = { if (!chain && currentChain) { chain = currentChain; } else if (!chain) { - throw new Error('tokenGetRelativePriceChartData error: Provide "chain" param'); + throw new Error('tokenGetTokensDynamicData error: Provide "chain" param'); } const items = await tokenService.getTokensDynamicData(addresses, chain); @@ -91,9 +91,9 @@ const resolvers: Resolvers = { if (!chain && currentChain) { chain = currentChain; } else if (!chain) { - throw new Error('tokenGetRelativePriceChartData error: Provide "chain" param'); + throw new Error('tokenGetPriceChartData error: Provide "chain" param'); } - const data = await tokenService.getDataForRange(address, range, chain); + const data = await tokenService.getTokenPriceForRange(address, range, chain); return data.map((item) => ({ id: `${address}-${item.timestamp}`, @@ -123,7 +123,7 @@ const resolvers: Resolvers = { } else if (!chain) { throw new Error('tokenGetCandlestickChartData error: Provide "chain" param'); } - const data = await tokenService.getDataForRange(address, range, chain); + const data = await tokenService.getTokenPriceForRange(address, range, chain); return data.map((item) => ({ id: `${address}-${item.timestamp}`, @@ -139,7 +139,7 @@ const resolvers: Resolvers = { if (!chain && currentChain) { chain = currentChain; } else if (!chain) { - throw new Error('tokenGetRelativePriceChartData error: Provide "chain" param'); + throw new Error('tokenGetTokenData error: Provide "chain" param'); } const token = await tokenService.getToken(address, chain); if (token) { @@ -155,15 +155,21 @@ const resolvers: Resolvers = { const tokens = await tokenService.getTokens(addresses); return tokens.map((token) => ({ ...token, id: token.address, tokenAddress: token.address })); }, - tokenGetProtocolTokenPrice: async (parent, {}, context) => { - return tokenService.getProtocolTokenPrice(); + tokenGetProtocolTokenPrice: async (parent, { chain }, context) => { + const currentChain = headerChain(); + if (!chain && currentChain) { + chain = currentChain; + } else if (!chain) { + throw new Error('tokenGetProtocolTokenPrice error: Provide "chain" param'); + } + return tokenService.getProtocolTokenPrice(chain); }, }, Mutation: { - tokenReloadTokenPrices: async (parent, {}, context) => { + tokenReloadTokenPrices: async (parent, { chains }, context) => { isAdminRoute(context); - await tokenService.updateTokenPrices(); + await tokenService.updateTokenPrices(chains); return true; }, @@ -174,13 +180,6 @@ const resolvers: Resolvers = { return 'success'; }, - tokenSyncTokenDynamicData: async (parent, {}, context) => { - isAdminRoute(context); - - await tokenService.syncCoingeckoPricesForAllChains(); - - return 'success'; - }, tokenSyncLatestFxPrices: async (parent, { chain }, context) => { isAdminRoute(context); const subgraphUrl = AllNetworkConfigsKeyedOnChain[chain].data.subgraphs.balancer; @@ -189,18 +188,6 @@ const resolvers: Resolvers = { return 'success'; }, - tokenInitChartData: async (parent, { tokenAddress }, context) => { - isAdminRoute(context); - - await tokenService.initChartData(tokenAddress); - - return 'success'; - }, - tokenDeletePrice: async (parent, args, context) => { - isAdminRoute(context); - - return tokenService.deleteTokenPrice(args); - }, tokenDeleteTokenType: async (parent, args, context) => { isAdminRoute(context); diff --git a/modules/token/token.service.ts b/modules/token/token.service.ts index 9c7a548bb..7b2672ebb 100644 --- a/modules/token/token.service.ts +++ b/modules/token/token.service.ts @@ -4,11 +4,11 @@ import { TokenPriceService } from './lib/token-price.service'; import { Chain, PrismaToken, PrismaTokenCurrentPrice, PrismaTokenDynamicData, PrismaTokenPrice } from '@prisma/client'; import { CoingeckoDataService } from './lib/coingecko-data.service'; import { Cache, CacheClass } from 'memory-cache'; -import { GqlTokenChartDataRange, MutationTokenDeletePriceArgs, MutationTokenDeleteTokenTypeArgs } from '../../schema'; -import { coingeckoService } from '../coingecko/coingecko.service'; +import { GqlTokenChartDataRange, MutationTokenDeleteTokenTypeArgs } from '../../schema'; import { networkContext } from '../network/network-context.service'; import { Dictionary } from 'lodash'; import { AllNetworkConfigsKeyedOnChain } from '../network/network-config'; +import { chainIdToChain } from '../network/chain-id-to-chain'; const TOKEN_PRICES_CACHE_KEY = `token:prices:current`; const TOKEN_PRICES_24H_AGO_CACHE_KEY = `token:prices:24h-ago`; @@ -83,14 +83,18 @@ export class TokenService { })); } - public async updateTokenPrices(): Promise { - return this.tokenPriceService.updateTokenPrices(); + public async updateTokenPrices(chainIds: string[]): Promise { + const chains: Chain[] = []; + for (const chainId of chainIds) { + chains.push(chainIdToChain[chainId]); + } + return this.tokenPriceService.updateAllTokenPrices(chains); } public async getTokenPrices(chain = networkContext.chain): Promise { let tokenPrices = this.cache.get(`${TOKEN_PRICES_CACHE_KEY}:${chain}`); if (!tokenPrices) { - tokenPrices = await this.tokenPriceService.getCurrentTokenPrices(chain); + tokenPrices = await this.tokenPriceService.getCurrentTokenPrices([chain]); this.cache.put(`${TOKEN_PRICES_CACHE_KEY}:${chain}`, tokenPrices, 30 * 1000); } return tokenPrices; @@ -121,22 +125,18 @@ export class TokenService { return this.tokenPriceService.getWhiteListedCurrentTokenPrices(chains); } - public async getProtocolTokenPrice(): Promise { + public async getProtocolTokenPrice(chain: Chain): Promise { const tokenPrices = await tokenService.getTokenPrices(); if (networkContext.data.protocolToken === 'bal') { - return tokenService.getPriceForToken(tokenPrices, networkContext.data.bal!.address).toString(); + return tokenService.getPriceForToken(tokenPrices, networkContext.data.bal!.address, chain).toString(); } else { - return tokenService.getPriceForToken(tokenPrices, networkContext.data.beets!.address).toString(); + return tokenService.getPriceForToken(tokenPrices, networkContext.data.beets!.address, chain).toString(); } } - public getPriceForToken(tokenPrices: PrismaTokenCurrentPrice[], tokenAddress: string): number { - return this.tokenPriceService.getPriceForToken(tokenPrices, tokenAddress); - } - - public async syncCoingeckoPricesForAllChains(): Promise { - await this.coingeckoDataService.syncCoingeckoPricesForAllChains(); + public getPriceForToken(tokenPrices: PrismaTokenCurrentPrice[], tokenAddress: string, chain: Chain): number { + return this.tokenPriceService.getPriceForToken(tokenPrices, tokenAddress, chain); } public async getTokenDynamicData(tokenAddress: string, chain: Chain): Promise { @@ -181,12 +181,20 @@ export class TokenService { return dynamicData; } - public async getDataForRange( + public async getTokenPricesForRange( + tokenAddress: string[], + range: GqlTokenChartDataRange, + chain: Chain, + ): Promise { + return this.tokenPriceService.getTokenPricesForRange(tokenAddress, range, chain); + } + + public async getTokenPriceForRange( tokenAddress: string, range: GqlTokenChartDataRange, chain: Chain, ): Promise { - return this.tokenPriceService.getDataForRange(tokenAddress, range, chain); + return this.tokenPriceService.getTokenPricesForRange([tokenAddress], range, chain); } public async getRelativeDataForRange( @@ -198,35 +206,19 @@ export class TokenService { return this.tokenPriceService.getRelativeDataForRange(tokenIn, tokenOut, range, chain); } - public async initChartData(tokenAddress: string) { - await this.coingeckoDataService.initChartData(tokenAddress); - } - - public async getTokenPriceFrom24hAgo(): Promise { - let tokenPrices24hAgo = this.cache.get(`${TOKEN_PRICES_24H_AGO_CACHE_KEY}:${networkContext.chain}`); + public async getTokenPriceFrom24hAgo(chain: Chain): Promise { + let tokenPrices24hAgo = this.cache.get(`${TOKEN_PRICES_24H_AGO_CACHE_KEY}:${chain}`); if (!tokenPrices24hAgo) { - tokenPrices24hAgo = await this.tokenPriceService.getTokenPriceFrom24hAgo(); - this.cache.put( - `${TOKEN_PRICES_24H_AGO_CACHE_KEY}:${networkContext.chain}`, - tokenPrices24hAgo, - 60 * 15 * 1000, - ); + tokenPrices24hAgo = await this.tokenPriceService.getTokenPricesFrom24hAgo([chain]); + this.cache.put(`${TOKEN_PRICES_24H_AGO_CACHE_KEY}:${chain}`, tokenPrices24hAgo, 60 * 15 * 1000); } return tokenPrices24hAgo; } - public async getHistoricalTokenPrices(chain: Chain) { - return this.tokenPriceService.getHistoricalTokenPrices(chain); - } - public async purgeOldTokenPricesForAllChains() { return this.tokenPriceService.purgeOldTokenPricesForAllChains(); } - public async deleteTokenPrice(args: MutationTokenDeletePriceArgs) { - return this.tokenPriceService.deleteTokenPrice(args); - } - public async deleteTokenType({ tokenAddress, type }: MutationTokenDeleteTokenTypeArgs) { await prisma.prismaTokenType.delete({ where: { @@ -246,4 +238,4 @@ export class TokenService { } } -export const tokenService = new TokenService(new TokenPriceService(), new CoingeckoDataService(coingeckoService)); +export const tokenService = new TokenService(new TokenPriceService(), new CoingeckoDataService()); diff --git a/modules/user/user.resolvers.ts b/modules/user/user.resolvers.ts index 8912e1186..7750d639c 100644 --- a/modules/user/user.resolvers.ts +++ b/modules/user/user.resolvers.ts @@ -19,7 +19,11 @@ const resolvers: Resolvers = { return balances.map((balance) => ({ ...balance, - tokenPrice: tokenService.getPriceForToken(tokenPrices[balance.chain] || [], balance.tokenAddress), + tokenPrice: tokenService.getPriceForToken( + tokenPrices[balance.chain] || [], + balance.tokenAddress, + balance.chain, + ), })); }, userGetPoolJoinExits: async (parent, { first, skip, poolId, chain, address }, context) => { diff --git a/prisma/migrations/20240229153000_change_tokenprice_table/migration.sql b/prisma/migrations/20240229153000_change_tokenprice_table/migration.sql new file mode 100644 index 000000000..eb7abe534 --- /dev/null +++ b/prisma/migrations/20240229153000_change_tokenprice_table/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the column `coingecko` on the `PrismaTokenCurrentPrice` table. All the data in the column will be lost. + - You are about to drop the column `coingecko` on the `PrismaTokenPrice` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "PrismaToken" ADD COLUMN "excludedFromCoingecko" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "PrismaTokenCurrentPrice" DROP COLUMN "coingecko", +ADD COLUMN "updatedBy" TEXT; + +-- AlterTable +ALTER TABLE "PrismaTokenPrice" DROP COLUMN "coingecko", +ADD COLUMN "updatedBy" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index eed145411..0d155eef8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -610,10 +610,12 @@ model PrismaToken { coingeckoPlatformId String? coingeckoContractAddress String? coingeckoTokenId String? + excludedFromCoingecko Boolean @default(false) - dynamicData PrismaTokenDynamicData? currentPrice PrismaTokenCurrentPrice? + dynamicData PrismaTokenDynamicData? prices PrismaTokenPrice[] + types PrismaTokenType[] expandedPools PrismaPoolExpandedTokens[] @@ -632,9 +634,9 @@ model PrismaTokenCurrentPrice { chain Chain updatedAt DateTime @updatedAt + updatedBy String? timestamp Int price Float - coingecko Boolean? } model PrismaTokenPrice { @@ -644,9 +646,9 @@ model PrismaTokenPrice { token PrismaToken @relation(fields:[tokenAddress, chain], references: [address, chain]) chain Chain updatedAt DateTime @updatedAt + updatedBy String? timestamp Int price Float - coingecko Boolean? high Float low Float diff --git a/worker/job-handlers.ts b/worker/job-handlers.ts index 8481f9a33..e21700aa3 100644 --- a/worker/job-handlers.ts +++ b/worker/job-handlers.ts @@ -18,6 +18,7 @@ import { syncLatestFXPrices } from '../modules/token/latest-fx-price'; import { AllNetworkConfigs } from '../modules/network/network-config'; import { sftmxService } from '../modules/sftmx/sftmx.service'; import { JobsController } from '../modules/controllers/jobs-controller'; +import { chainIdToChain } from '../modules/network/chain-id-to-chain'; const runningJobs: Set = new Set(); @@ -135,7 +136,13 @@ export function configureWorkerRoutes(app: Express) { ); break; case 'update-token-prices': - await runIfNotAlreadyRunning(job.name, chainId, () => tokenService.updateTokenPrices(), res, next); + await runIfNotAlreadyRunning( + job.name, + chainId, + () => tokenService.updateTokenPrices([chainId]), + res, + next, + ); break; case 'update-liquidity-for-active-pools': await runIfNotAlreadyRunning( @@ -156,7 +163,16 @@ export function configureWorkerRoutes(app: Express) { ); break; case 'update-pool-apr': - await runIfNotAlreadyRunning(job.name, chainId, () => poolService.updatePoolAprs(), res, next); + await runIfNotAlreadyRunning( + job.name, + chainId, + () => { + const chain = chainIdToChain[chainId]; + return poolService.updatePoolAprs(chain); + }, + res, + next, + ); break; case 'load-on-chain-data-for-pools-with-active-updates': await runIfNotAlreadyRunning( @@ -227,15 +243,6 @@ export function configureWorkerRoutes(app: Express) { next, ); break; - case 'sync-global-coingecko-prices': - await runIfNotAlreadyRunning( - job.name, - chainId, - () => tokenService.syncCoingeckoPricesForAllChains(), - res, - next, - ); - break; case 'sync-staking-for-pools': await runIfNotAlreadyRunning(job.name, chainId, () => poolService.syncStakingForPools(), res, next); break; @@ -285,7 +292,16 @@ export function configureWorkerRoutes(app: Express) { ); break; case 'feed-data-to-datastudio': - await runIfNotAlreadyRunning(job.name, chainId, () => datastudioService.feedPoolData(), res, next); + await runIfNotAlreadyRunning( + job.name, + chainId, + () => { + const chain = chainIdToChain[chainId]; + return datastudioService.feedPoolData(chain); + }, + res, + next, + ); break; case 'sync-latest-reliquary-snapshots': await runIfNotAlreadyRunning(