@@ -21,7 +21,9 @@ export type TokenPriceChartQueryVariables = {
2121 multichain : boolean
2222}
2323
24- function fallbackToPriceChartData ( priceHistoryEntry : GraphQLApi . PriceHistoryFallbackFragment ) : PriceChartData {
24+ type PriceHistoryEntry = Pick < GraphQLApi . PriceHistoryFallbackFragment , 'timestamp' | 'value' >
25+
26+ function fallbackToPriceChartData ( priceHistoryEntry : PriceHistoryEntry ) : PriceChartData {
2527 const { value, timestamp } = priceHistoryEntry
2628 const time = timestamp as UTCTimestamp
2729 return { time, value, open : value , high : value , low : value , close : value }
@@ -40,13 +42,17 @@ export function useTokenPriceChartData({
4042 skip,
4143 priceChartType,
4244 currentPriceOverride,
45+ preferProjectMarketData = false ,
4346} : {
4447 variables : TokenPriceChartQueryVariables
4548 skip : boolean
4649 priceChartType : PriceChartType
4750 currentPriceOverride ?: number
51+ preferProjectMarketData ?: boolean
4852} ) : ChartQueryResult < PriceChartData , ChartType . PRICE > & { disableCandlestickUI : boolean } {
4953 const [ fallback , enablePriceHistoryFallback ] = useReducer ( ( ) => true , false )
54+ // Project markets do not provide OHLC, so RWA charts always render as line charts even if stale UI state says candle.
55+ const effectivePriceChartType = preferProjectMarketData ? PriceChartType . LINE : priceChartType
5056
5157 // For candlestick charts, use subgraph OHLC data (required, not available in CoinGecko)
5258 // For line charts when fallback is needed, fetch both CoinGecko and subgraph data
@@ -65,14 +71,17 @@ export function useTokenPriceChartData({
6571 return chainId ? buildCurrencyId ( chainId , variables . address ) : undefined
6672 } , [ variables . chain , variables . address ] )
6773
74+ const shouldFetchCoinGeckoHistory =
75+ effectivePriceChartType === PriceChartType . LINE && ( ! variables . multichain || preferProjectMarketData )
76+
6877 const { data : coinGeckoData , loading : coinGeckoLoading } = GraphQLApi . useTokenPriceHistoryQuery ( {
6978 variables : {
7079 contract : currencyIdValue
7180 ? currencyIdToContractInput ( currencyIdValue )
7281 : { address : undefined , chain : variables . chain } ,
7382 duration : variables . duration ,
7483 } ,
75- skip : skip || ! currencyIdValue || priceChartType === PriceChartType . CANDLESTICK || variables . multichain ,
84+ skip : skip || ! currencyIdValue || ! shouldFetchCoinGeckoHistory ,
7685 // IMPORTANT: Must use no-cache to prevent infinite query loop.
7786 //
7887 // TokenPriceHistory returns Token objects (with chain/address) nested inside tokenProjects.
@@ -82,44 +91,64 @@ export function useTokenPriceChartData({
8291 fetchPolicy : 'no-cache' ,
8392 } )
8493
85- const loading =
86- subgraphLoading || ( priceChartType === PriceChartType . LINE && ! variables . multichain && coinGeckoLoading )
94+ const loading = subgraphLoading || ( shouldFetchCoinGeckoHistory && coinGeckoLoading )
8795
8896 // oxlint-disable-next-line complexity
8997 return useMemo ( ( ) => {
9098 const subgraphMarket = subgraphData ?. token ?. market
9199 const { ohlc, priceHistory : subgraphPriceHistory , price : subgraphPrice } = subgraphMarket ?? { }
92100
93- // Data source strategy: prefer CoinGecko for line charts, use subgraph for candlesticks
94- // Prefer per-chain CoinGecko history when available so multi-chain tokens render correctly
101+ // CoinGecko exposes both project-level market data and per-contract token market data.
102+ // Default token pages prefer per-contract CoinGecko history so multichain tokens stay chain-specific.
103+ // RWA pages use project-level history because the useful chart is the underlying security, not wrapper liquidity.
95104 const coinGeckoProject = coinGeckoData ?. tokenProjects ?. [ 0 ]
96105 const coinGeckoMarket = coinGeckoProject ?. markets ?. [ 0 ]
97106 const coinGeckoTokenMarket = coinGeckoProject ?. tokens . find ( ( token ) => token . chain === variables . chain ) ?. market
98- const coinGeckoPriceHistory = coinGeckoTokenMarket ?. priceHistory ?? coinGeckoMarket ?. priceHistory
99- const coinGeckoAggregatedPrice = coinGeckoTokenMarket ?. price ?. value ?? coinGeckoMarket ?. price ?. value
107+ let coinGeckoPriceHistory : ( PriceHistoryEntry | undefined ) [ ] | undefined =
108+ coinGeckoTokenMarket ?. priceHistory ?? coinGeckoMarket ?. priceHistory
109+ let coinGeckoCurrentPrice = coinGeckoTokenMarket ?. price ?. value ?? coinGeckoMarket ?. price ?. value
110+ if ( preferProjectMarketData ) {
111+ coinGeckoPriceHistory = coinGeckoMarket ?. priceHistory
112+ coinGeckoCurrentPrice = coinGeckoMarket ?. price ?. value
113+ }
100114
101- // For line charts, prefer CoinGecko priceHistory but use PER-CHAIN current price
102- // For candlestick charts, always use subgraph OHLC (only source)
103- const useCoinGeckoHistory =
104- priceChartType === PriceChartType . LINE && coinGeckoPriceHistory ?. length && ! variables . multichain
105- const priceHistory = useCoinGeckoHistory ? coinGeckoPriceHistory : subgraphPriceHistory
115+ // Candlestick charts always use subgraph OHLC. Line charts use CoinGecko history when available.
116+ const isWaitingForProjectMarketHistory =
117+ preferProjectMarketData && effectivePriceChartType === PriceChartType . LINE && coinGeckoLoading
118+ const shouldUseCoinGeckoHistory =
119+ effectivePriceChartType === PriceChartType . LINE &&
120+ Boolean ( coinGeckoPriceHistory ?. length ) &&
121+ ( ! variables . multichain || preferProjectMarketData )
106122
107- // CRITICAL: Always use per-chain price from subgraph for multi-chain tokens
108- // This ensures USDC on Ethereum shows Ethereum price, not aggregated price
123+ let priceHistory : ( PriceHistoryEntry | undefined ) [ ] | undefined = subgraphPriceHistory
124+ let ohlcPriceHistory = ohlc
125+ if ( isWaitingForProjectMarketHistory ) {
126+ priceHistory = undefined
127+ ohlcPriceHistory = undefined
128+ } else if ( shouldUseCoinGeckoHistory ) {
129+ priceHistory = coinGeckoPriceHistory
130+ ohlcPriceHistory = undefined
131+ }
132+
133+ // CRITICAL: By default, multi-chain tokens use per-chain subgraph price.
134+ // This ensures USDC on Ethereum shows Ethereum price, not aggregated price.
135+ // Tokenized securities opt into project-level price because the underlying security is the useful quote.
109136 // When centralized prices are enabled, the override provides live WebSocket prices
110- const currentPrice = currentPriceOverride ?? subgraphPrice ?. value ?? coinGeckoAggregatedPrice
137+ let resolvedMarketPrice = subgraphPrice ?. value ?? coinGeckoCurrentPrice
138+ if ( preferProjectMarketData ) {
139+ resolvedMarketPrice = coinGeckoCurrentPrice ?? subgraphPrice ?. value
140+ }
141+ const currentPrice = currentPriceOverride ?? resolvedMarketPrice
111142
112143 let entries =
113- ( ohlc
114- ? ohlc . filter ( ( v ) : v is GraphQLApi . CandlestickOhlcFragment => v !== undefined ) . map ( toPriceChartData )
115- : priceHistory
116- ?. filter ( ( v ) : v is GraphQLApi . PriceHistoryFallbackFragment => v !== undefined )
117- . map ( fallbackToPriceChartData ) ) ?? [ ]
144+ ( ohlcPriceHistory
145+ ? ohlcPriceHistory . filter ( ( v ) : v is GraphQLApi . CandlestickOhlcFragment => v !== undefined ) . map ( toPriceChartData )
146+ : priceHistory ?. filter ( ( v ) : v is PriceHistoryEntry => v !== undefined ) . map ( fallbackToPriceChartData ) ) ?? [ ]
118147
119- if ( ohlc ) {
148+ if ( ohlcPriceHistory ) {
120149 // Special case: backend returns invalid OHLC data on some chains. If we detect long series of 0's, return an empty array to trigger fallback.
121150 const zeroCount = entries . filter ( ( x ) => x . value === 0 ) . length
122- if ( ! ohlc . length || zeroCount / entries . length > CANDLESTICK_FALLBACK_THRESHOLD ) {
151+ if ( ! ohlcPriceHistory . length || zeroCount / entries . length > CANDLESTICK_FALLBACK_THRESHOLD ) {
123152 enablePriceHistoryFallback ( ) // triggers a re-fetch that uses priceHistory instead of OHLC
124153 return {
125154 chartType : ChartType . PRICE ,
@@ -132,7 +161,7 @@ export function useTokenPriceChartData({
132161
133162 // For line charts made using ohlc data, the min and max entries should point to their low/high, rather than close,
134163 // to ensure the chart line makes contact with the min/max lines.
135- if ( priceChartType === PriceChartType . LINE ) {
164+ if ( effectivePriceChartType === PriceChartType . LINE ) {
136165 let min = entries [ 0 ] . low
137166 let minIndex = 0
138167 let max = entries [ 0 ] . high
@@ -158,7 +187,7 @@ export function useTokenPriceChartData({
158187 }
159188 // Special case: backend data for OHLC data is currently too granular, so points should be combined, halving the data
160189 // oxlint-disable-next-line typescript/no-unnecessary-condition
161- else if ( priceChartType === PriceChartType . CANDLESTICK ) {
190+ else if ( effectivePriceChartType === PriceChartType . CANDLESTICK ) {
162191 const combinedEntries = [ ]
163192
164193 const startIndex = entries . length % 2 // If the length is odd, start at the second entry
@@ -205,16 +234,24 @@ export function useTokenPriceChartData({
205234 }
206235
207236 const dataQuality = checkDataQuality ( { data : entries , chartType : ChartType . PRICE , duration : variables . duration } )
208- return { chartType : ChartType . PRICE , entries, loading, dataQuality, disableCandlestickUI : fallback }
237+ return {
238+ chartType : ChartType . PRICE ,
239+ entries,
240+ loading,
241+ dataQuality,
242+ disableCandlestickUI : preferProjectMarketData || fallback ,
243+ }
209244 // oxlint-disable-next-line react-hooks/exhaustive-deps -- coinGeckoData.tokenProjects is intentionally accessed via optional chaining
210245 } , [
211246 currentPriceOverride ,
212247 subgraphData ?. token ?. market ,
213248 // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here
214249 coinGeckoData ?. tokenProjects ?. [ 0 ] ,
250+ coinGeckoLoading ,
251+ effectivePriceChartType ,
215252 fallback ,
216253 loading ,
217- priceChartType ,
254+ preferProjectMarketData ,
218255 variables . duration ,
219256 variables . chain ,
220257 variables . multichain ,
0 commit comments