From b995e0ebaf5460315c985f3e1d5674261228c1d6 Mon Sep 17 00:00:00 2001 From: An Nguyen Date: Wed, 26 Nov 2025 11:50:59 -0600 Subject: [PATCH] remove watchlist/price usage --- .../core-mobile/app/consts/reactQueryKeys.ts | 1 - .../networks/useNativeTokenPriceForNetwork.ts | 40 ------- .../useAvaxTokenPriceInSelectedCurrency.ts | 23 ---- .../core-mobile/app/hooks/useSimplePrices.ts | 14 ++- .../app/hooks/watchlist/useGetPrices.ts | 30 ----- .../app/hooks/watchlist/useTopTokens.ts | 7 +- .../app/hooks/watchlist/useWatchlist.ts | 37 +----- .../hooks/watchlist/useWatchlistListener.ts | 5 +- .../app/new/common/hooks/useMarketToken.ts | 4 +- .../approval/hooks/useNetworkFeeSelector.ts | 8 +- .../stake/components/StakeTokenUnitValue.tsx | 4 +- .../stake/screens/ClaimStakeRewardScreen.tsx | 4 +- .../features/track/components/ShareChart.tsx | 21 +--- .../track/screens/TrackTokenDetailScreen.tsx | 20 +--- .../(signedIn)/(modals)/addStake/amount.tsx | 4 +- .../(signedIn)/(modals)/addStake/duration.tsx | 4 +- .../app/services/token/TokenService.test.ts | 70 ------------ .../app/services/token/TokenService.ts | 108 +++--------------- .../__mocks__/additionalWatchlistPrice.json | 44 ------- .../core-mobile/app/services/token/types.ts | 7 -- .../watchlist/WatchlistService.test.ts | 69 ++++------- .../services/watchlist/WatchlistService.ts | 95 +++------------ .../watchlist/watchListCacheClient.ts | 20 +--- .../core-mobile/app/store/watchlist/types.ts | 5 - 24 files changed, 94 insertions(+), 550 deletions(-) delete mode 100644 packages/core-mobile/app/hooks/networks/useNativeTokenPriceForNetwork.ts delete mode 100644 packages/core-mobile/app/hooks/useAvaxTokenPriceInSelectedCurrency.ts delete mode 100644 packages/core-mobile/app/hooks/watchlist/useGetPrices.ts delete mode 100644 packages/core-mobile/app/services/token/__mocks__/additionalWatchlistPrice.json diff --git a/packages/core-mobile/app/consts/reactQueryKeys.ts b/packages/core-mobile/app/consts/reactQueryKeys.ts index a30e7d0946..355f79523c 100644 --- a/packages/core-mobile/app/consts/reactQueryKeys.ts +++ b/packages/core-mobile/app/consts/reactQueryKeys.ts @@ -16,7 +16,6 @@ export enum ReactQueryKeys { NETWORK_CONTRACT_TOKENS = 'networkContractTokens', WATCHLIST_TOP_TOKENS = 'watchlistTopTokens', WATCHLIST_TRENDING_TOKENS = 'watchlistTrendingTokens', - WATCHLIST_PRICES = 'watchlistPrices', WATCHLIST_TOKEN_SEARCH = 'watchlistTokenSearch', LAST_TRANSACTED_ERC20_NETWORKS = 'lastTransactedErc20Networks', diff --git a/packages/core-mobile/app/hooks/networks/useNativeTokenPriceForNetwork.ts b/packages/core-mobile/app/hooks/networks/useNativeTokenPriceForNetwork.ts deleted file mode 100644 index 5a2c36f4f4..0000000000 --- a/packages/core-mobile/app/hooks/networks/useNativeTokenPriceForNetwork.ts +++ /dev/null @@ -1,40 +0,0 @@ -import TokenService from 'services/token/TokenService' -import { VsCurrencyType } from '@avalabs/core-coingecko-sdk' -import { useSelector } from 'react-redux' -import { selectSelectedCurrency } from 'store/settings/currency' -import { Network } from '@avalabs/core-chains-sdk' -import { useQuery } from '@tanstack/react-query' - -const REFETCH_INTERVAL = 10000 // 10 seconds - -/** - * - * @param network - * @param customCurrency - * @return nativeTokenPrice in AVAX - */ -export const useNativeTokenPriceForNetwork = ( - network: Network | undefined, - customCurrency?: VsCurrencyType -): { nativeTokenPrice: number } => { - const selectedCurrency = useSelector( - selectSelectedCurrency - ).toLowerCase() as VsCurrencyType - const currency = customCurrency ?? (selectedCurrency as VsCurrencyType) - - const nativeTokenId = network?.pricingProviders?.coingecko.nativeTokenId ?? '' - - const { data } = useQuery({ - refetchInterval: REFETCH_INTERVAL, - enabled: Boolean(nativeTokenId), - queryKey: ['nativeTokenPrice', nativeTokenId, currency], - queryFn: async () => { - return TokenService.getPriceWithMarketDataByCoinId( - nativeTokenId, - currency - ) - } - }) - - return { nativeTokenPrice: data?.price ?? 0 } -} diff --git a/packages/core-mobile/app/hooks/useAvaxTokenPriceInSelectedCurrency.ts b/packages/core-mobile/app/hooks/useAvaxTokenPriceInSelectedCurrency.ts deleted file mode 100644 index 95fd18ec18..0000000000 --- a/packages/core-mobile/app/hooks/useAvaxTokenPriceInSelectedCurrency.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useNativeTokenPriceForNetwork } from 'hooks/networks/useNativeTokenPriceForNetwork' -import { useSelector } from 'react-redux' -import { selectSelectedCurrency } from 'store/settings/currency' -import { VsCurrencyType } from '@avalabs/core-coingecko-sdk' -import { selectIsDeveloperMode } from 'store/settings/advanced' -import { ChainId } from '@avalabs/core-chains-sdk' -import { useNetworks } from './networks/useNetworks' - -export function useAvaxTokenPriceInSelectedCurrency(): number { - const { getNetwork } = useNetworks() - const selectedCurrency = useSelector(selectSelectedCurrency) - const isDeveloperMode = useSelector(selectIsDeveloperMode) - const chainId = isDeveloperMode - ? ChainId.AVALANCHE_TESTNET_ID - : ChainId.AVALANCHE_MAINNET_ID - const avaxNetwork = getNetwork(chainId) - - const { nativeTokenPrice: avaxPrice } = useNativeTokenPriceForNetwork( - avaxNetwork, - selectedCurrency.toLowerCase() as VsCurrencyType - ) - return avaxPrice -} diff --git a/packages/core-mobile/app/hooks/useSimplePrices.ts b/packages/core-mobile/app/hooks/useSimplePrices.ts index c332c34ed8..9681458a23 100644 --- a/packages/core-mobile/app/hooks/useSimplePrices.ts +++ b/packages/core-mobile/app/hooks/useSimplePrices.ts @@ -4,17 +4,21 @@ import { Prices } from 'features/bridge/hooks/useBridge' import { useQuery, UseQueryResult } from '@tanstack/react-query' import { ReactQueryKeys } from 'consts/reactQueryKeys' +const REFETCH_INTERVAL = 10000 // 10 seconds + export const useSimplePrices = ( - coinIds: string[], + coingeckoIds: string[], currency: VsCurrencyType ): UseQueryResult => { return useQuery({ - enabled: coinIds.length > 0, - queryKey: [ReactQueryKeys.SIMPLE_PRICES, coinIds, currency], + enabled: coingeckoIds.length > 0, + refetchInterval: REFETCH_INTERVAL, + queryKey: [ReactQueryKeys.SIMPLE_PRICES, coingeckoIds, currency], queryFn: async () => TokenService.getSimplePrice({ - coinIds, - currency: currency.toLowerCase() as VsCurrencyType + coinIds: coingeckoIds, + currency: currency.toLowerCase() as VsCurrencyType, + includeMarketData: false }), select: data => { if (data === undefined) { diff --git a/packages/core-mobile/app/hooks/watchlist/useGetPrices.ts b/packages/core-mobile/app/hooks/watchlist/useGetPrices.ts deleted file mode 100644 index 2b1055dc58..0000000000 --- a/packages/core-mobile/app/hooks/watchlist/useGetPrices.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { UseQueryResult, useQuery } from '@tanstack/react-query' -import { ReactQueryKeys } from 'consts/reactQueryKeys' -import { useSelector } from 'react-redux' -import WatchlistService from 'services/watchlist/WatchlistService' -import { selectSelectedCurrency } from 'store/settings/currency' -import { Prices } from 'store/watchlist' -import { runAfterInteractions } from 'utils/runAfterInteractions' - -export const useGetPrices = ({ - coingeckoIds, - enabled -}: { - coingeckoIds: string[] - enabled: boolean -}): UseQueryResult => { - const currency = useSelector(selectSelectedCurrency).toLowerCase() - - return useQuery({ - enabled, - queryKey: [ReactQueryKeys.WATCHLIST_PRICES, currency, coingeckoIds], - queryFn: async () => { - const prices = await runAfterInteractions(async () => { - return WatchlistService.getPrices(coingeckoIds, currency) - }) - - return prices ?? {} - }, - refetchInterval: 30000 // 30 seconds - }) -} diff --git a/packages/core-mobile/app/hooks/watchlist/useTopTokens.ts b/packages/core-mobile/app/hooks/watchlist/useTopTokens.ts index e34cca6b30..972daad014 100644 --- a/packages/core-mobile/app/hooks/watchlist/useTopTokens.ts +++ b/packages/core-mobile/app/hooks/watchlist/useTopTokens.ts @@ -4,10 +4,13 @@ import { ReactQueryKeys } from 'consts/reactQueryKeys' import { useSelector } from 'react-redux' import WatchlistService from 'services/watchlist/WatchlistService' import { selectSelectedCurrency } from 'store/settings/currency' -import { TokensAndCharts } from 'store/watchlist' +import { Charts, MarketToken, Prices } from 'store/watchlist' import { runAfterInteractions } from 'utils/runAfterInteractions' -export const useTopTokens = (): UseQueryResult => { +export const useTopTokens = (): UseQueryResult< + { tokens: Record; charts: Charts; prices: Prices }, + Error +> => { const currency = useSelector(selectSelectedCurrency) const isFocused = useIsFocused() diff --git a/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts b/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts index 1ce493aedc..37b84ae8b1 100644 --- a/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts +++ b/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts @@ -11,11 +11,9 @@ import { import { useSelector } from 'react-redux' import { ChartData } from 'services/token/types' import { transformTrendingTokens } from 'services/watchlist/utils/transform' -import { useIsFocused } from '@react-navigation/native' import { LocalTokenWithBalance } from 'store/balance/types' import { getCaip2ChainIdForToken } from 'utils/caip2ChainIds' import { isNetworkContractToken } from 'utils/isNetworkContractToken' -import { useGetPrices } from './useGetPrices' import { useTopTokens } from './useTopTokens' import { useGetTrendingTokens } from './useGetTrendingTokens' @@ -61,37 +59,6 @@ export const useWatchlist = (): UseWatchListReturnType => { [trendingTokensResponse] ) - const topTokensCoingeckoIds = useMemo(() => { - return Object.values(topTokensResponse?.tokens ?? {}) - .map(token => token.coingeckoId) - .filter((id): id is string => typeof id === 'string') - }, [topTokensResponse?.tokens]) - - const isFocused = useIsFocused() - - const { data: topTokenPrices } = useGetPrices({ - coingeckoIds: topTokensCoingeckoIds, - enabled: isFocused && topTokensCoingeckoIds.length > 0 - }) - - // Map prices from coingeckoId back to internalId for consistent access - const topTokenPricesById = useMemo(() => { - if (!topTokenPrices || !topTokensResponse?.tokens) { - return {} - } - - const pricesById: Prices = {} - Object.values(topTokensResponse.tokens).forEach(token => { - const price = token.coingeckoId - ? topTokenPrices[token.coingeckoId] - : undefined - if (price) { - pricesById[token.id] = price - } - }) - return pricesById - }, [topTokenPrices, topTokensResponse?.tokens]) - const isLoadingFavorites = favoriteIds.length > 0 && isLoading const favorites = useMemo(() => { @@ -150,9 +117,9 @@ export const useWatchlist = (): UseWatchListReturnType => { const prices = useMemo(() => { return { ...transformedTrendingTokens?.prices, - ...topTokenPricesById + ...topTokensResponse?.prices } - }, [topTokenPricesById, transformedTrendingTokens?.prices]) + }, [topTokensResponse?.prices, transformedTrendingTokens?.prices]) const getWatchlistPrice = useCallback( (id: string): PriceData | undefined => { diff --git a/packages/core-mobile/app/hooks/watchlist/useWatchlistListener.ts b/packages/core-mobile/app/hooks/watchlist/useWatchlistListener.ts index 5de546c3c0..28031257a8 100644 --- a/packages/core-mobile/app/hooks/watchlist/useWatchlistListener.ts +++ b/packages/core-mobile/app/hooks/watchlist/useWatchlistListener.ts @@ -16,10 +16,7 @@ export const useWatchlistListener = (): void => { matcher: isAnyOf(toggleDeveloperMode, fetchWatchlist), effect: async () => { await queryClient.invalidateQueries({ - queryKey: [ - ReactQueryKeys.WATCHLIST_TOP_TOKENS, - ReactQueryKeys.WATCHLIST_PRICES - ] + queryKey: [ReactQueryKeys.WATCHLIST_TOP_TOKENS] }) } }) diff --git a/packages/core-mobile/app/new/common/hooks/useMarketToken.ts b/packages/core-mobile/app/new/common/hooks/useMarketToken.ts index 045d2ead7b..bd8e55637c 100644 --- a/packages/core-mobile/app/new/common/hooks/useMarketToken.ts +++ b/packages/core-mobile/app/new/common/hooks/useMarketToken.ts @@ -18,7 +18,7 @@ export const useMarketToken = ({ const resolvedMarketToken = resolveMarketToken(token) if (errorContext && !resolvedMarketToken) { - Logger.error(`[${errorContext}] Market token not found`, { + Logger.warn(`[${errorContext}] Market token not found`, { symbol: token.symbol }) } @@ -26,7 +26,7 @@ export const useMarketToken = ({ errorContext && resolvedMarketToken?.priceChangePercentage24h === undefined ) { - Logger.error( + Logger.warn( `[${errorContext}] Market token priceChangePercentage24h is undefined`, { symbol: token.symbol diff --git a/packages/core-mobile/app/new/features/approval/hooks/useNetworkFeeSelector.ts b/packages/core-mobile/app/new/features/approval/hooks/useNetworkFeeSelector.ts index d23535b3c7..cea74433bd 100644 --- a/packages/core-mobile/app/new/features/approval/hooks/useNetworkFeeSelector.ts +++ b/packages/core-mobile/app/new/features/approval/hooks/useNetworkFeeSelector.ts @@ -6,7 +6,6 @@ import { showAlertWithTextInput } from 'common/utils/alertWithTextInput' import { GAS_LIMIT_FOR_X_CHAIN } from 'consts/fees' -import { useNativeTokenPriceForNetwork } from 'hooks/networks/useNativeTokenPriceForNetwork' import { useNetworks } from 'hooks/networks/useNetworks' import { useNetworkFee } from 'hooks/useNetworkFee' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -18,6 +17,7 @@ import { isAvmNetwork } from 'utils/network/isAvalancheNetwork' import { sanitizeDecimalInput } from 'utils/units/sanitize' import { calculateGasAndFees, Eip1559Fees, GasAndFees } from 'utils/Utils' import { normalizeNumericTextInput } from '@avalabs/k2-alpine/src/utils/tokenUnitInput' +import { useSimplePrices } from 'hooks/useSimplePrices' import { DEFAULT_NETWORK_TOKEN_DECIMALS, DEFAULT_NETWORK_TOKEN_SYMBOL @@ -70,10 +70,12 @@ export const useNetworkFeeSelector = ({ const feeDecimals = networkFee?.displayDecimals const isBaseUnitRate = feeDecimals === undefined - const { nativeTokenPrice } = useNativeTokenPriceForNetwork( - network, + const { data } = useSimplePrices( + [network?.pricingProviders?.coingecko.nativeTokenId ?? ''], selectedCurrency.toLowerCase() as VsCurrencyType ) + const nativeTokenPrice = + data?.[network?.pricingProviders?.coingecko.nativeTokenId ?? ''] ?? 0 const isAvalancheCChain = network ? isAvalancheCChainId(network.chainId) diff --git a/packages/core-mobile/app/new/features/stake/components/StakeTokenUnitValue.tsx b/packages/core-mobile/app/new/features/stake/components/StakeTokenUnitValue.tsx index 8e2d8a3f64..c377196a5e 100644 --- a/packages/core-mobile/app/new/features/stake/components/StakeTokenUnitValue.tsx +++ b/packages/core-mobile/app/new/features/stake/components/StakeTokenUnitValue.tsx @@ -2,7 +2,7 @@ import { TokenUnit } from '@avalabs/core-utils-sdk' import { SxProp, Text, View } from '@avalabs/k2-alpine' import { useFormatCurrency } from 'common/hooks/useFormatCurrency' import { UNKNOWN_AMOUNT } from 'consts/amount' -import { useAvaxTokenPriceInSelectedCurrency } from 'hooks/useAvaxTokenPriceInSelectedCurrency' +import { useAvaxPrice } from 'features/portfolio/hooks/useAvaxPrice' import React from 'react' export const StakeTokenUnitValue = ({ @@ -14,7 +14,7 @@ export const StakeTokenUnitValue = ({ isReward?: boolean textSx?: SxProp }): JSX.Element => { - const avaxPrice = useAvaxTokenPriceInSelectedCurrency() + const avaxPrice = useAvaxPrice() const { formatCurrency } = useFormatCurrency() const valueInCurrency = value?.mul(avaxPrice) const valueInCurrencyDisplay = valueInCurrency diff --git a/packages/core-mobile/app/new/features/stake/screens/ClaimStakeRewardScreen.tsx b/packages/core-mobile/app/new/features/stake/screens/ClaimStakeRewardScreen.tsx index 5e3c4844e0..1612230551 100644 --- a/packages/core-mobile/app/new/features/stake/screens/ClaimStakeRewardScreen.tsx +++ b/packages/core-mobile/app/new/features/stake/screens/ClaimStakeRewardScreen.tsx @@ -20,13 +20,13 @@ import { useRouter } from 'expo-router' import { StakeTokenUnitValue } from 'features/stake/components/StakeTokenUnitValue' import { useClaimRewards } from 'hooks/earn/useClaimRewards' import { usePChainBalance } from 'hooks/earn/usePChainBalance' -import { useAvaxTokenPriceInSelectedCurrency } from 'hooks/useAvaxTokenPriceInSelectedCurrency' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useSelector } from 'react-redux' import AnalyticsService from 'services/analytics/AnalyticsService' import NetworkService from 'services/network/NetworkService' import { selectIsDeveloperMode } from 'store/settings/advanced' import { useRefreshStakingBalances } from 'hooks/earn/useRefreshStakingBalances' +import { useAvaxPrice } from 'features/portfolio/hooks/useAvaxPrice' export const ClaimStakeRewardScreen = (): JSX.Element => { const { navigate, back } = useRouter() @@ -37,7 +37,7 @@ export const ClaimStakeRewardScreen = (): JSX.Element => { useState() const isDeveloperMode = useSelector(selectIsDeveloperMode) const pNetwork = NetworkService.getAvalancheNetworkP(isDeveloperMode) - const avaxPrice = useAvaxTokenPriceInSelectedCurrency() + const avaxPrice = useAvaxPrice() const refreshStakingBalances = useRefreshStakingBalances() const onClaimSuccess = (): void => { refreshStakingBalances({ shouldRefreshStakes: false }) diff --git a/packages/core-mobile/app/new/features/track/components/ShareChart.tsx b/packages/core-mobile/app/new/features/track/components/ShareChart.tsx index 06f5ddbf74..da0525b1a1 100644 --- a/packages/core-mobile/app/new/features/track/components/ShareChart.tsx +++ b/packages/core-mobile/app/new/features/track/components/ShareChart.tsx @@ -9,13 +9,11 @@ import { useTheme, View } from '@avalabs/k2-alpine' -import { useIsFocused } from '@react-navigation/native' import { TokenLogo } from 'common/components/TokenLogo' import { useFormatCurrency } from 'common/hooks/useFormatCurrency' import { useTokenDetails } from 'common/hooks/useTokenDetails' import { UNKNOWN_AMOUNT } from 'consts/amount' import SparklineChart from 'features/track/components/SparklineChart' -import { useGetPrices } from 'hooks/watchlist/useGetPrices' import React, { useMemo } from 'react' import { MarketType } from 'store/watchlist' import { formatLargeCurrency } from 'utils/Utils' @@ -30,27 +28,14 @@ export const ShareChart = ({ }): JSX.Element => { const { theme } = useTheme() const { theme: inversedTheme } = useInversedTheme({ isDark: theme.isDark }) - const { chartData, ranges, coingeckoId, tokenInfo, token } = useTokenDetails({ + const { chartData, ranges, tokenInfo, token } = useTokenDetails({ tokenId, marketType }) - const isFocused = useIsFocused() - - const { data: prices } = useGetPrices({ - coingeckoIds: [coingeckoId], - enabled: - isFocused && - tokenInfo?.currentPrice === undefined && - coingeckoId.length > 0 - }) const currentPrice = useMemo(() => { - return ( - tokenInfo?.currentPrice ?? - prices?.[coingeckoId]?.priceInCurrency ?? - token?.currentPrice - ) - }, [tokenInfo?.currentPrice, prices, coingeckoId, token?.currentPrice]) + return tokenInfo?.currentPrice ?? token?.currentPrice + }, [tokenInfo?.currentPrice, token?.currentPrice]) const range = useMemo(() => { return { diff --git a/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx b/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx index 7fe9d911a2..d76cdd03f9 100644 --- a/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx +++ b/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx @@ -20,7 +20,6 @@ import { useLocalSearchParams, useRouter } from 'expo-router' * TODO: Adjust import back to expo-router once the bug is resolved. */ import { truncateAddress } from '@avalabs/core-utils-sdk' -import { useIsFocused } from '@react-navigation/native' import { FavoriteBarButton } from 'common/components/FavoriteBarButton' import { ScrollScreen } from 'common/components/ScrollScreen' import { ShareBarButton } from 'common/components/ShareBarButton' @@ -36,7 +35,6 @@ import { useNavigateToSwap } from 'features/swap/hooks/useNavigateToSwap' import { SelectedChartDataIndicator } from 'features/track/components/SelectedChartDataIndicator' import { TokenDetailChart } from 'features/track/components/TokenDetailChart' import { TokenHeader } from 'features/track/components/TokenHeader' -import { useGetPrices } from 'hooks/watchlist/useGetPrices' import React, { useCallback, useEffect, useMemo, useState } from 'react' import { StyleSheet } from 'react-native' import Animated, { @@ -93,18 +91,8 @@ const TrackTokenDetailScreen = (): JSX.Element => { token } = useTokenDetails({ tokenId, marketType }) - const isFocused = useIsFocused() const { navigateToBuy } = useBuy() - const { data: prices } = useGetPrices({ - coingeckoIds: [coingeckoId], - enabled: - isFocused && - tokenInfo !== undefined && - tokenInfo.currentPrice === undefined && - coingeckoId.length > 0 - }) - const selectedSegmentIndex = useSharedValue(0) const lastUpdatedDate = chartData?.[chartData.length - 1]?.date @@ -372,12 +360,8 @@ const TrackTokenDetailScreen = (): JSX.Element => { }, [actions]) const currentPrice = useMemo(() => { - return ( - tokenInfo?.currentPrice ?? - prices?.[coingeckoId]?.priceInCurrency ?? - token?.currentPrice - ) - }, [tokenInfo?.currentPrice, prices, coingeckoId, token?.currentPrice]) + return tokenInfo?.currentPrice ?? token?.currentPrice + }, [tokenInfo?.currentPrice, token?.currentPrice]) const range = useMemo(() => { return { diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/amount.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/amount.tsx index 04eb4bc32a..800a33efe3 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/amount.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/amount.tsx @@ -12,11 +12,11 @@ import { ScrollScreen } from 'common/components/ScrollScreen' import { useFormatCurrency } from 'common/hooks/useFormatCurrency' import { useDelegationContext } from 'contexts/DelegationContext' import { useRouter } from 'expo-router' +import { useAvaxPrice } from 'features/portfolio/hooks/useAvaxPrice' import { useCChainBalance } from 'hooks/earn/useCChainBalance' import { useGetClaimableBalance } from 'hooks/earn/useGetClaimableBalance' import { useGetStuckBalance } from 'hooks/earn/useGetStuckBalance' import useStakingParams from 'hooks/earn/useStakingParams' -import { useAvaxTokenPriceInSelectedCurrency } from 'hooks/useAvaxTokenPriceInSelectedCurrency' import React, { useCallback, useEffect, useMemo, useState } from 'react' import AnalyticsService from 'services/analytics/AnalyticsService' import { cChainToken, xpChainToken } from 'utils/units/knownTokens' @@ -55,7 +55,7 @@ const StakeAmountScreen = (): JSX.Element => { const notEnoughBalance = cumulativeBalance?.lt(stakeAmount) ?? true const inputValid = !amountNotEnough && !notEnoughBalance && !stakeAmount.isZero() - const avaxPrice = useAvaxTokenPriceInSelectedCurrency() + const avaxPrice = useAvaxPrice() const { formatCurrency } = useFormatCurrency() const handleAmountChange = useCallback( diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/duration.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/duration.tsx index b856fc33e4..0c5d5928cc 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/duration.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/addStake/duration.tsx @@ -13,6 +13,7 @@ import { useFormatCurrency } from 'common/hooks/useFormatCurrency' import { useDelegationContext } from 'contexts/DelegationContext' import { differenceInDays, getUnixTime, millisecondsToSeconds } from 'date-fns' import { useRouter } from 'expo-router' +import { useAvaxPrice } from 'features/portfolio/hooks/useAvaxPrice' import { DurationOptions, getCustomDurationIndex, @@ -24,7 +25,6 @@ import { useStakeEstimatedReward } from 'features/stake/hooks/useStakeEstimatedR import { useStakeEstimatedRewards } from 'features/stake/hooks/useStakeEstimatedRewards' import { convertToDurationInSeconds } from 'features/stake/utils' import { useNow } from 'hooks/time/useNow' -import { useAvaxTokenPriceInSelectedCurrency } from 'hooks/useAvaxTokenPriceInSelectedCurrency' import { useDebounce } from 'hooks/useDebounce' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { @@ -48,7 +48,7 @@ const StakeDurationScreen = (): JSX.Element => { const { stakeAmount } = useDelegationContext() const isDeveloperMode = useSelector(selectIsDeveloperMode) - const avaxPrice = useAvaxTokenPriceInSelectedCurrency() + const avaxPrice = useAvaxPrice() const { formatCurrency } = useFormatCurrency() const now = useNow() const rewardChartRef = useRef(null) diff --git a/packages/core-mobile/app/services/token/TokenService.test.ts b/packages/core-mobile/app/services/token/TokenService.test.ts index 67f97d3b56..6e983404e8 100644 --- a/packages/core-mobile/app/services/token/TokenService.test.ts +++ b/packages/core-mobile/app/services/token/TokenService.test.ts @@ -1,5 +1,4 @@ import * as sdk from '@avalabs/core-coingecko-sdk' -import { watchListCacheClient } from 'services/watchlist/watchListCacheClient' import * as inMemoryCache from 'utils/InMemoryCache' import TokenService from './TokenService' import { coingeckoProxyClient as proxy } from './coingeckoProxyClient' @@ -89,75 +88,6 @@ describe('getSimplePrice', () => { expect(proxyMock).toHaveBeenCalledTimes(1) expect(result?.test?.usd?.price).toEqual(1) }) - it('should return simple price data from cache', async () => { - inMemoryCacheMock.mockImplementation(() => WATCHLIST_PRICE) - const result = await TokenService.getSimplePrice({ - coinIds: ['test'], - currency: sdk.VsCurrencyType.USD - }) - expect(simplePrice).not.toHaveBeenCalled() - expect(proxyMock).not.toHaveBeenCalled() - expect(result?.test?.usd?.price).toEqual(1) - }) -}) - -describe('getPriceWithMarketDataByCoinId', () => { - const watchlistSimplePriceMock = jest.spyOn( - watchListCacheClient, - 'simplePrice' - ) - watchlistSimplePriceMock.mockImplementationOnce(async () => { - return WATCHLIST_PRICE - }) - it('should return data for token test in currency USD', async () => { - const result = await TokenService.getPriceWithMarketDataByCoinId('test') - expect(result.price).toEqual(1) - }) - it('should return data for token aave in currency AUD', async () => { - const result = await TokenService.getPriceWithMarketDataByCoinId( - 'test-aave', - sdk.VsCurrencyType.AUD - ) - expect(result.price).toEqual(10) - }) - it('should return 0 for unknown token', async () => { - const result = await TokenService.getPriceWithMarketDataByCoinId('unknown') - expect(result.price).toEqual(0) - }) -}) - -describe('getPriceWithMarketDataByCoinIds', () => { - const watchlistSimplePriceMock = jest.spyOn( - watchListCacheClient, - 'simplePrice' - ) - watchlistSimplePriceMock.mockImplementationOnce(async () => { - return WATCHLIST_PRICE - }) - it('should return data for list of tokens in currency USD', async () => { - const result = await TokenService.getPriceWithMarketDataByCoinIds([ - 'test', - 'test-aave' - ]) - // @ts-ignore - expect(Object.keys(result).length).toEqual(2) - }) - it('should return empty object for tokens in currency USD', async () => { - const result = await TokenService.getPriceWithMarketDataByCoinIds([ - 'unknown1', - 'unknonw2' - ]) - // @ts-ignore - expect(Object.keys(result).length).toEqual(0) - }) - it('should return data for matching token in currency USD', async () => { - const result = await TokenService.getPriceWithMarketDataByCoinIds([ - 'unknown1', - 'test' - ]) - // @ts-ignore - expect(Object.keys(result).length).toEqual(1) - }) }) describe('getChartDataForCoinId', () => { diff --git a/packages/core-mobile/app/services/token/TokenService.ts b/packages/core-mobile/app/services/token/TokenService.ts index 511762e073..8f85ee2d69 100644 --- a/packages/core-mobile/app/services/token/TokenService.ts +++ b/packages/core-mobile/app/services/token/TokenService.ts @@ -27,7 +27,6 @@ import { CoinMarket, Error as CoingeckoError, GetMarketsParams, - PriceWithMarketData, SimplePriceResponse, CoinsSearchResponse, ContractMarketChartResponse, @@ -122,53 +121,6 @@ export class TokenService { } } - /** - * Get token price with market data for a coin - * @param coinId the coin id ie avalanche-2 for avax - * @param currency the currency to be used - * @returns the token price with market data - */ - async getPriceWithMarketDataByCoinId( - coinId: string, - currency: VsCurrencyType = VsCurrencyType.USD - ): Promise { - const allPriceData = await this.fetchPriceWithMarketData() - const data = allPriceData?.[coinId]?.[currency] - return { - price: data?.price ?? 0, - change24: data?.change24 ?? 0, - marketCap: data?.marketCap ?? 0, - vol24: data?.vol24 ?? 0 - } - } - - /** - * Get token price with market data for a list of coins - * @param coinIds the coin ids - * @param currency the currency to be used - * @returns a list of token price with market data - */ - async getPriceWithMarketDataByCoinIds( - coinIds: string[], - currency: VsCurrencyType = VsCurrencyType.USD - ): Promise { - const allPriceData = await this.fetchPriceWithMarketData() - return coinIds.reduce((acc, coinId) => { - const priceData = allPriceData?.[coinId]?.[currency] - if (priceData) { - acc[coinId] = { - [currency]: { - price: priceData?.price, - change24: priceData?.change24, - marketCap: priceData?.marketCap, - vol24: priceData?.vol24 - } - } - } - return acc - }, {} as SimplePriceResponse) - } - /** * Get chart data for a coin * @param coingeckoId the coin id @@ -254,27 +206,6 @@ export class TokenService { return data } - /** - * Get token price with market data from cached watchlist - * @returns token price with market data - */ - async fetchPriceWithMarketData(): Promise { - try { - let data: SimplePriceResponse | undefined - const cacheId = `fetchPriceWithMarketData` - - data = getCache(cacheId) - - if (data === undefined) { - data = await watchListCacheClient.simplePrice() - setCache(cacheId, data) - } - return data - } catch (e) { - return Promise.resolve(undefined) - } - } - /** * Get markets first on coingecko (free tier) directly, * if we get 429 error, retry it on coingecko proxy (paid service) @@ -325,35 +256,28 @@ export class TokenService { */ async getSimplePrice({ coinIds = [], - currency = VsCurrencyType.USD + currency = VsCurrencyType.USD, + includeMarketData = false }: { coinIds: string[] currency: VsCurrencyType + includeMarketData?: boolean }): Promise { let data: SimplePriceResponse | undefined - const key = coinIds ? `${arrayHash(coinIds)}-${currency}` : `${currency}` - - const cacheId = `getSimplePrice-${key}` - - data = getCache(cacheId) - - if (data === undefined) { - try { - data = await coingeckoRetry(useCoingeckoProxy => - this.simplePrice({ - coinIds, - currencies: [currency], - marketCap: true, - vol24: true, - change24: true, - useCoingeckoProxy - }) - ) - } catch { - data = undefined - } - setCache(cacheId, data) + try { + data = await coingeckoRetry(useCoingeckoProxy => + this.simplePrice({ + coinIds, + currencies: [currency], + marketCap: includeMarketData, + vol24: includeMarketData, + change24: includeMarketData, + useCoingeckoProxy + }) + ) + } catch { + data = undefined } return data diff --git a/packages/core-mobile/app/services/token/__mocks__/additionalWatchlistPrice.json b/packages/core-mobile/app/services/token/__mocks__/additionalWatchlistPrice.json deleted file mode 100644 index f91b1da20b..0000000000 --- a/packages/core-mobile/app/services/token/__mocks__/additionalWatchlistPrice.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "additional-watchlist-price-1": { - "aud": { - "change24": -6.65808862188606, - "marketCap": 1940363515.739892, - "price": 10, - "vol24": 213824387.2699328 - }, - "usd": { - "change24": -6.65808862188606, - "marketCap": 1940363515.739892, - "price": 131.81, - "vol24": 213824387.2699328 - } - }, - "additional-watchlist-price-2": { - "aud": { - "change24": -6.65808862188606, - "marketCap": 1940363515.739892, - "price": 3, - "vol24": 213824387.2699328 - }, - "usd": { - "change24": -6.65808862188606, - "marketCap": 1940363515.739892, - "price": 1, - "vol24": 213824387.2699328 - } - }, - "additional-watchlist-price-3": { - "aud": { - "change24": -6.65808862188606, - "marketCap": 1940363515.739892, - "price": 30, - "vol24": 213824387.2699328 - }, - "usd": { - "change24": -6.65808862188606, - "marketCap": 1940363515.739892, - "price": 10, - "vol24": 213824387.2699328 - } - } -} diff --git a/packages/core-mobile/app/services/token/types.ts b/packages/core-mobile/app/services/token/types.ts index 7c8f0a9cf5..ccda85e967 100644 --- a/packages/core-mobile/app/services/token/types.ts +++ b/packages/core-mobile/app/services/token/types.ts @@ -36,13 +36,6 @@ export const ChartDataSchema = object({ export type ChartData = z.infer -export type PriceWithMarketData = { - price: number - change24: number - marketCap: number - vol24: number -} - export type GetMarketsParams = { currency?: VsCurrencyType sparkline?: boolean diff --git a/packages/core-mobile/app/services/watchlist/WatchlistService.test.ts b/packages/core-mobile/app/services/watchlist/WatchlistService.test.ts index 57a6612133..3df4238289 100644 --- a/packages/core-mobile/app/services/watchlist/WatchlistService.test.ts +++ b/packages/core-mobile/app/services/watchlist/WatchlistService.test.ts @@ -2,8 +2,6 @@ import TokenService from 'services/token/TokenService' import { transformSparklineData } from 'services/token/utils' import { tokenAggregatorApi } from 'utils/network/tokenAggregator' import { MarketType } from 'store/watchlist' -import WATCHLIST_PRICE from '../token/__mocks__/watchlistPrice.json' -import ADDITIONAL_WATCHLIST_PRICE from '../token/__mocks__/additionalWatchlistPrice.json' import WatchlistService from './WatchlistService' jest.mock('utils/network/tokenAggregator', () => ({ @@ -54,7 +52,6 @@ describe('getTopMarkets', () => { price_change_percentage_24h: -3.4 } ]) - const result = await WatchlistService.getTopTokens('USD') const m = result.tokens @@ -110,6 +107,28 @@ describe('getTopMarkets', () => { expect(result.charts).toEqual({}) }) + it('should return correctly mapped prices', async () => { + apiMock.mockResolvedValue([ + { + internalId: 'avax', + current_price: 35, + price_change_24h: 1.2, + price_change_percentage_24h: -3.4, + market_cap: 2000000000, + total_volume: 1000000000 + } + ]) + + const result = await WatchlistService.getTopTokens('USD') + + expect(result.prices.avax).toEqual({ + priceInCurrency: 35, + change24: 1.2, + marketCap: 2000000000, + vol24: 1000000000 + }) + }) + it('should handle missing optional fields gracefully', async () => { apiMock.mockResolvedValue([ { @@ -137,50 +156,6 @@ describe('getTopMarkets', () => { }) }) -describe('getPrices', () => { - const fetchPriceWithMarketDataMock = - TokenService.fetchPriceWithMarketData as jest.Mock - const getSimplePriceMock = TokenService.getSimplePrice as jest.Mock - - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should return all merged price data from cache + simple price API', async () => { - fetchPriceWithMarketDataMock.mockResolvedValue(WATCHLIST_PRICE) - getSimplePriceMock.mockResolvedValue(ADDITIONAL_WATCHLIST_PRICE) - - const result = await WatchlistService.getPrices( - [ - 'test-aave', - 'test', - 'additional-watchlist-price-1', - 'additional-watchlist-price-2', - 'additional-watchlist-price-3' - ], - 'usd' - ) - - expect(fetchPriceWithMarketDataMock).toHaveBeenCalledTimes(1) - expect(getSimplePriceMock).toHaveBeenCalledTimes(1) - expect(Object.keys(result)).toHaveLength(5) - }) - - it('should return only cached price data when all IDs exist in cache', async () => { - fetchPriceWithMarketDataMock.mockResolvedValue(WATCHLIST_PRICE) - getSimplePriceMock.mockResolvedValue(ADDITIONAL_WATCHLIST_PRICE) - - const result = await WatchlistService.getPrices( - ['test-aave', 'test'], - 'usd' - ) - - expect(fetchPriceWithMarketDataMock).toHaveBeenCalledTimes(1) - expect(getSimplePriceMock).not.toHaveBeenCalled() - expect(Object.keys(result)).toHaveLength(2) - }) -}) - describe('tokenSearch', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/packages/core-mobile/app/services/watchlist/WatchlistService.ts b/packages/core-mobile/app/services/watchlist/WatchlistService.ts index 1584068561..6fdd46d783 100644 --- a/packages/core-mobile/app/services/watchlist/WatchlistService.ts +++ b/packages/core-mobile/app/services/watchlist/WatchlistService.ts @@ -3,56 +3,38 @@ import TokenService from 'services/token/TokenService' import { SimplePriceResponse, CoinMarket, - SimplePriceInCurrencyResponse, TrendingToken } from 'services/token/types' import { transformSparklineData } from 'services/token/utils' -import { - Charts, - MarketToken, - MarketType, - PriceData, - Prices -} from 'store/watchlist/types' +import { Charts, MarketToken, MarketType, Prices } from 'store/watchlist/types' import Logger from 'utils/Logger' import { tokenAggregatorApi } from 'utils/network/tokenAggregator' -import { WatchlistMarketsResponse } from './types' - -const fetchTopMarkets = async ({ - currency -}: { - currency: string -}): Promise => { - return tokenAggregatorApi.getV1watchlistmarkets({ - queries: { - currency: currency, - topMarkets: true - } - }) -} /* WatchlistService handles the following 3 API calls: 1. getTopTokens: get top tokens - 2. getPrices - - get price data from cache - - get price data from network (tokens not in cache) - 3. tokenSearch + 2. tokenSearch - get token Id from network - get price and market data from cached - get price and market data from network (tokens not in cache) + 3. getTrendingTokens: get trending tokens */ class WatchlistService { async getTopTokens(currency: string): Promise<{ tokens: Record charts: Charts + prices: Prices }> { - const topMarkets = await fetchTopMarkets({ - currency: currency.toLowerCase() + const topMarkets = await tokenAggregatorApi.getV1watchlistmarkets({ + queries: { + currency: currency.toLowerCase(), + topMarkets: true + } }) const tokens: Record = {} const charts: Charts = {} + const prices: Prices = {} topMarkets.forEach(token => { const id = token.internalId @@ -75,45 +57,16 @@ class WatchlistService { if (token.sparkline_in_7d?.price) { charts[id] = transformSparklineData(token.sparkline_in_7d.price) } - }) - - return { tokens, charts } - } - - async getPrices(coingeckoIds: string[], currency: string): Promise { - const allPriceData = await TokenService.fetchPriceWithMarketData() - const prices: Prices = {} - const otherIds: string[] = [] - - coingeckoIds.forEach(tokenId => { - const pricesInCurrency = allPriceData?.[tokenId] - if (!pricesInCurrency) { - otherIds.push(tokenId) - return + prices[id] = { + priceInCurrency: token.current_price ?? 0, + change24: token.price_change_24h ?? 0, + marketCap: token.market_cap ?? 0, + vol24: token.total_volume ?? 0 } - prices[tokenId] = this.getPriceInCurrency(pricesInCurrency, currency) }) - if (otherIds.length !== 0) { - const otherPriceData = await TokenService.getSimplePrice({ - coinIds: otherIds, - currency: currency as VsCurrencyType - }) - - for (const tokenId in otherPriceData) { - const otherPriceInCurrency = otherPriceData?.[tokenId] - if (!otherPriceInCurrency) { - continue - } - prices[tokenId] = this.getPriceInCurrency( - otherPriceInCurrency, - currency - ) - } - } - - return prices + return { tokens, charts, prices } } // eslint-disable-next-line sonarjs/cognitive-complexity @@ -202,19 +155,6 @@ class WatchlistService { return TokenService.getTrendingTokens(exchangeRate) } - private getPriceInCurrency( - priceData: SimplePriceInCurrencyResponse, - currency: string - ): PriceData { - const price = priceData[currency as VsCurrencyType] - return { - priceInCurrency: price?.price ?? 0, - change24: price?.change24 ?? 0, - marketCap: price?.marketCap ?? 0, - vol24: price?.vol24 ?? 0 - } - } - private async getPriceWithMarketDataByCoinIds( coinIds: string[], currency: string @@ -222,7 +162,8 @@ class WatchlistService { try { return TokenService.getSimplePrice({ coinIds, - currency: currency as VsCurrencyType + currency: currency as VsCurrencyType, + includeMarketData: true }) } catch (error) { Logger.error('Failed to fetch price data', { error }) diff --git a/packages/core-mobile/app/services/watchlist/watchListCacheClient.ts b/packages/core-mobile/app/services/watchlist/watchListCacheClient.ts index d0b3a272e2..1bdc67bc62 100644 --- a/packages/core-mobile/app/services/watchlist/watchListCacheClient.ts +++ b/packages/core-mobile/app/services/watchlist/watchListCacheClient.ts @@ -3,7 +3,7 @@ import { Zodios } from '@zodios/core' import Config from 'react-native-config' import { array, z } from 'zod' import Logger from 'utils/Logger' -import { SimplePriceResponseSchema, TrendingTokenSchema } from '../token/types' +import { TrendingTokenSchema } from '../token/types' if (!Config.PROXY_URL) Logger.warn('PROXY_URL is missing in env file. Watchlist is disabled.') @@ -11,19 +11,12 @@ if (!Config.PROXY_URL) const baseUrl = `${Config.PROXY_URL}/watchlist` // Infer types from schemas for typings -export type SimplePriceResponse = z.infer export type TrendingToken = z.infer // Dev (validated) and Prod (raw) clients const devClient = new Zodios( baseUrl, [ - { - method: 'get', - path: '/price', - alias: 'simplePrice', - response: SimplePriceResponseSchema - }, { method: 'get', path: '/trending', @@ -47,17 +40,6 @@ const prodClient = axios.create({ const useValidation = __DEV__ //in normal use export const watchListCacheClient = { - /** - * GET /price - */ - async simplePrice(): Promise { - if (useValidation) { - return devClient.simplePrice() - } - const { data } = await prodClient.get('/price') - return data - }, - /** * GET /trending */ diff --git a/packages/core-mobile/app/store/watchlist/types.ts b/packages/core-mobile/app/store/watchlist/types.ts index 44f8fda740..477e915d94 100644 --- a/packages/core-mobile/app/store/watchlist/types.ts +++ b/packages/core-mobile/app/store/watchlist/types.ts @@ -56,11 +56,6 @@ export type Charts = { [tokenId: string]: ChartData } export type Prices = { [tokenId: string]: PriceData } -export type TokensAndCharts = { - tokens: Record - charts: Charts -} - export type WatchListFavoriteState = { favorites: InternalId[] }