Skip to content

Commit 1284450

Browse files
ci(release): publish latest release
1 parent c9e3e71 commit 1284450

47 files changed

Lines changed: 935 additions & 174 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

RELEASE

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
IPFS hash of the deployment:
2-
- CIDv0: `QmX6xgfkLeWhGQvAsGjuBaTgXEf9cL6oXjfnMZpTLV1pwk`
3-
- CIDv1: `bafybeiecg5qcuu7gbjhkpcoxhh7cg3fq4cuzygtzw4qigunxyysp4qk2um`
2+
- CIDv0: `QmcSvSrURT9nXdKL4981wSjoVEtAmvpA9vmwXKY4rgfvqS`
3+
- CIDv1: `bafybeigrt3su45yk54xor7ogqlu3d7ck4rjpknuq6kfnfiyy65at5coevu`
44

55
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
66

@@ -10,5 +10,5 @@ You can also access the Uniswap Interface from an IPFS gateway.
1010
Your Uniswap settings are never remembered across different URLs.
1111

1212
IPFS gateways:
13-
- https://bafybeiecg5qcuu7gbjhkpcoxhh7cg3fq4cuzygtzw4qigunxyysp4qk2um.ipfs.dweb.link/
14-
- [ipfs://QmX6xgfkLeWhGQvAsGjuBaTgXEf9cL6oXjfnMZpTLV1pwk/](ipfs://QmX6xgfkLeWhGQvAsGjuBaTgXEf9cL6oXjfnMZpTLV1pwk/)
13+
- https://bafybeigrt3su45yk54xor7ogqlu3d7ck4rjpknuq6kfnfiyy65at5coevu.ipfs.dweb.link/
14+
- [ipfs://QmcSvSrURT9nXdKL4981wSjoVEtAmvpA9vmwXKY4rgfvqS/](ipfs://QmcSvSrURT9nXdKL4981wSjoVEtAmvpA9vmwXKY4rgfvqS/)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web/5.148.1
1+
web/5.148.2

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
"@tanstack/react-query": "5.90.20",
123123
"@uniswap/analytics": "1.7.2",
124124
"@uniswap/analytics-events": "2.43.0",
125-
"@uniswap/client-data-api": "0.0.85",
125+
"@uniswap/client-data-api": "0.0.89",
126126
"@uniswap/client-explore": "0.0.18",
127127
"@uniswap/client-notification-service": "0.0.11",
128128
"@uniswap/ethers-rs-mobile": "0.0.5",

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@
202202
"@types/react-scroll-sync": "0.9.0",
203203
"@uniswap/analytics": "1.7.2",
204204
"@uniswap/analytics-events": "2.43.0",
205-
"@uniswap/client-data-api": "0.0.85",
205+
"@uniswap/client-data-api": "0.0.89",
206206
"@uniswap/client-explore": "0.0.18",
207207
"@uniswap/client-liquidity": "0.3.5",
208208
"@uniswap/client-notification-service": "0.0.11",

apps/web/src/components/FeatureFlagModal/flagGroups.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ export function buildFlagGroups(extras: {
164164
name: 'Prices',
165165
flags: [{ flag: FeatureFlags.CentralizedPrices, label: 'Enable Centralized Prices' }],
166166
},
167+
{
168+
name: 'RWA',
169+
flags: [{ flag: FeatureFlags.RWACoinGeckoData, label: 'Enable RWA CoinGecko Data' }],
170+
},
167171
{ name: 'Experiments', flags: [] },
168172
{
169173
name: 'Layers',

apps/web/src/hooks/useTokenPriceChartData.test.ts

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,60 @@ const SUBGRAPH_PRICE_HISTORY = [priceHistoryEntry(1000, 10), priceHistoryEntry(2
2929

3030
const COINGECKO_PRICE_HISTORY = [priceHistoryEntry(1000, 20), priceHistoryEntry(2000, 21), priceHistoryEntry(3000, 22)]
3131

32+
const COINGECKO_PROJECT_PRICE_HISTORY = [
33+
priceHistoryEntry(1000, 30),
34+
priceHistoryEntry(2000, 31),
35+
priceHistoryEntry(3000, 32),
36+
]
37+
38+
const SUBGRAPH_OHLC = [
39+
{
40+
timestamp: 1000,
41+
open: { value: 10 },
42+
high: { value: 11 },
43+
low: { value: 9 },
44+
close: { value: 10 },
45+
},
46+
{
47+
timestamp: 2000,
48+
open: { value: 10 },
49+
high: { value: 12 },
50+
low: { value: 10 },
51+
close: { value: 11 },
52+
},
53+
{
54+
timestamp: 3000,
55+
open: { value: 11 },
56+
high: { value: 13 },
57+
low: { value: 11 },
58+
close: { value: 12 },
59+
},
60+
]
61+
3262
const BASE_VARIABLES = {
3363
chain: GraphQLApi.Chain.Ethereum,
3464
address: '0x68749665FF8D2d112Fa859AA293F07A622782F38',
3565
duration: GraphQLApi.HistoryDuration.Year,
3666
multichain: false,
3767
}
3868

39-
function makeSubgraphResult(priceHistory: typeof SUBGRAPH_PRICE_HISTORY) {
69+
function makeSubgraphResult(priceHistory: typeof SUBGRAPH_PRICE_HISTORY, ohlc: typeof SUBGRAPH_OHLC | null = null) {
4070
return {
41-
data: { token: { market: { priceHistory, ohlc: null, price: { value: 12 } } } },
71+
data: { token: { market: { priceHistory, ohlc, price: { value: 12 } } } },
4272
loading: false,
4373
}
4474
}
4575

46-
function makeCoinGeckoResult(priceHistory: typeof COINGECKO_PRICE_HISTORY | []) {
76+
function makeCoinGeckoResult(
77+
priceHistory: typeof COINGECKO_PRICE_HISTORY | [],
78+
projectPriceHistory: typeof COINGECKO_PROJECT_PRICE_HISTORY | [] = [],
79+
) {
4780
return {
4881
data: {
4982
tokenProjects: [
5083
{
5184
tokens: [{ chain: GraphQLApi.Chain.Ethereum, market: { priceHistory } }],
52-
markets: [],
85+
markets: projectPriceHistory.length ? [{ price: { value: 32 }, priceHistory: projectPriceHistory }] : [],
5386
},
5487
],
5588
},
@@ -123,4 +156,87 @@ describe('useTokenPriceChartData', () => {
123156
// Subgraph entries start at value 10
124157
expect(result.current.entries[0].value).toBe(10)
125158
})
159+
160+
it('uses project CoinGecko price history for multichain tokens when project market data is preferred', () => {
161+
mockUseTokenPriceHistoryQuery.mockReturnValue(
162+
makeCoinGeckoResult(COINGECKO_PRICE_HISTORY, COINGECKO_PROJECT_PRICE_HISTORY),
163+
)
164+
165+
const { result } = renderHook(() =>
166+
useTokenPriceChartData({
167+
variables: { ...BASE_VARIABLES, multichain: true },
168+
skip: false,
169+
priceChartType: PriceChartType.LINE,
170+
preferProjectMarketData: true,
171+
}),
172+
)
173+
174+
// Project CoinGecko entries start at value 30; token-level entries start at 20; subgraph starts at 10
175+
expect(result.current.entries[0].value).toBe(30)
176+
expect(mockUseTokenPriceHistoryQuery).toHaveBeenCalledWith(
177+
expect.objectContaining({
178+
skip: false,
179+
}),
180+
)
181+
})
182+
183+
it('disables candlesticks and uses project CoinGecko line history when project market data is preferred', () => {
184+
mockUseTokenPriceHistoryQuery.mockReturnValue(
185+
makeCoinGeckoResult(COINGECKO_PRICE_HISTORY, COINGECKO_PROJECT_PRICE_HISTORY),
186+
)
187+
mockUseTokenPriceQuery.mockReturnValue(makeSubgraphResult(SUBGRAPH_PRICE_HISTORY, SUBGRAPH_OHLC))
188+
189+
const { result } = renderHook(() =>
190+
useTokenPriceChartData({
191+
variables: { ...BASE_VARIABLES, multichain: true },
192+
skip: false,
193+
priceChartType: PriceChartType.CANDLESTICK,
194+
preferProjectMarketData: true,
195+
}),
196+
)
197+
198+
expect(result.current.disableCandlestickUI).toBe(true)
199+
expect(result.current.entries[0].value).toBe(30)
200+
expect(mockUseTokenPriceHistoryQuery).toHaveBeenCalledWith(
201+
expect.objectContaining({
202+
skip: false,
203+
}),
204+
)
205+
})
206+
207+
it('does not fall back to subgraph data while preferred project market history is loading', () => {
208+
mockUseTokenPriceHistoryQuery.mockReturnValue({ data: undefined, loading: true })
209+
mockUseTokenPriceQuery.mockReturnValue(makeSubgraphResult(SUBGRAPH_PRICE_HISTORY, SUBGRAPH_OHLC))
210+
211+
const { result } = renderHook(() =>
212+
useTokenPriceChartData({
213+
variables: { ...BASE_VARIABLES, multichain: true },
214+
skip: false,
215+
priceChartType: PriceChartType.LINE,
216+
preferProjectMarketData: true,
217+
}),
218+
)
219+
220+
expect(result.current.loading).toBe(true)
221+
expect(result.current.dataQuality).toBe(DataQuality.INVALID)
222+
expect(result.current.entries).toHaveLength(0)
223+
})
224+
225+
it('falls back to subgraph data when preferred project market history is missing after loading', () => {
226+
mockUseTokenPriceHistoryQuery.mockReturnValue(makeCoinGeckoResult([], []))
227+
mockUseTokenPriceQuery.mockReturnValue(makeSubgraphResult(SUBGRAPH_PRICE_HISTORY, SUBGRAPH_OHLC))
228+
229+
const { result } = renderHook(() =>
230+
useTokenPriceChartData({
231+
variables: { ...BASE_VARIABLES, multichain: true },
232+
skip: false,
233+
priceChartType: PriceChartType.LINE,
234+
preferProjectMarketData: true,
235+
}),
236+
)
237+
238+
expect(result.current.loading).toBe(false)
239+
expect(result.current.dataQuality).toBe(DataQuality.VALID)
240+
expect(result.current.entries[0].value).toBe(9)
241+
})
126242
})

apps/web/src/hooks/useTokenPriceChartData.ts

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)