Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions apps/web/src/hooks/useLightLinkTokenQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { useEffect, useMemo, useState } from 'react'
import { Token } from '@uniswap/sdk-core'
import { GraphQLApi } from '@universe/api'
import { UniverseChainId } from 'uniswap/src/features/chains/types'

const LIGHTLINK_SUBGRAPH_URL =
'https://graph.phoenix.lightlink.io/query/subgraphs/name/uniswap-v3-lightlink'

// Known LightLink token logos
const LIGHTLINK_TOKEN_LOGOS: Record<string, string> = {
'0x7ebef2a4b1b09381ec5b9df8c5c6f2dbeca59c73':
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png',
'0xbcf8c1b03bbdda88d579330bdf236b58f8bb2cfd':
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
}

export function getLightLinkTokenLogoUrl(address: string): string | undefined {
return LIGHTLINK_TOKEN_LOGOS[address.toLowerCase()]
}

interface SubgraphToken {
id: string
symbol: string
name: string
decimals: string
volumeUSD: string
totalValueLockedUSD: string
derivedETH: string
tokenDayData: Array<{
date: number
priceUSD: string
volumeUSD: string
}>
}

interface LightLinkTokenQueryResult {
data: { token: GraphQLApi.TokenWebQuery['token'] } | undefined
loading: boolean
error: Error | undefined
currency: Token | undefined
}

export function useLightLinkTokenQuery(address: string | undefined): LightLinkTokenQueryResult {
const [subgraphToken, setSubgraphToken] = useState<SubgraphToken | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | undefined>()

useEffect(() => {
if (!address) {
setLoading(false)
return
}

let cancelled = false
setLoading(true)

const query = `{
token(id: "${address.toLowerCase()}") {
id
symbol
name
decimals
volumeUSD
totalValueLockedUSD
derivedETH
tokenDayData(first: 365, orderBy: date, orderDirection: desc) {
date
priceUSD
volumeUSD
}
}
}`

fetch(LIGHTLINK_SUBGRAPH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
})
.then((res) => res.json())
.then((json) => {
if (!cancelled) {
setSubgraphToken(json.data?.token ?? null)
setLoading(false)
}
})
.catch((err) => {
if (!cancelled) {
setError(err)
setLoading(false)
}
})

return () => {
cancelled = true
}
}, [address])

const currency = useMemo(() => {
if (!subgraphToken || !address) return undefined
return new Token(
UniverseChainId.LightLink,
address,
parseInt(subgraphToken.decimals, 10),
subgraphToken.symbol,
subgraphToken.name,
)
}, [subgraphToken, address])

// Shape the data to match TokenWebQuery['token'] as closely as possible
const data = useMemo(() => {
if (!subgraphToken) return undefined

const logoUrl = getLightLinkTokenLogoUrl(subgraphToken.id)
const volume24H = parseFloat(subgraphToken.volumeUSD)
const tvl = parseFloat(subgraphToken.totalValueLockedUSD)

const token: GraphQLApi.TokenWebQuery['token'] = {
__typename: 'Token' as const,
id: `lightlink-${subgraphToken.id}`,
name: subgraphToken.name,
symbol: subgraphToken.symbol,
decimals: parseInt(subgraphToken.decimals, 10),
address: subgraphToken.id,
chain: GraphQLApi.Chain.UnknownChain,
standard: GraphQLApi.TokenStandard.Erc20,
feeData: null,
market: {
__typename: 'TokenMarket' as const,
id: `lightlink-market-${subgraphToken.id}`,
volume24H: { __typename: 'Amount' as const, id: `vol-${subgraphToken.id}`, value: volume24H, currency: GraphQLApi.Currency.Usd },
totalValueLocked: { __typename: 'Amount' as const, id: `tvl-${subgraphToken.id}`, value: tvl, currency: GraphQLApi.Currency.Usd },
price: subgraphToken.tokenDayData.length > 0
? {
__typename: 'Amount' as const,
id: `price-${subgraphToken.id}`,
value: parseFloat(subgraphToken.tokenDayData[0].priceUSD),
currency: GraphQLApi.Currency.Usd,
}
: null,
pricePercentChange24h: null,
priceHigh52W: null,
priceLow52W: null,
},
project: {
__typename: 'TokenProject' as const,
id: `lightlink-project-${subgraphToken.id}`,
name: subgraphToken.name,
description: null,
homepageUrl: null,
twitterName: null,
logoUrl: logoUrl ?? null,
tokens: [
{
__typename: 'Token' as const,
id: `lightlink-${subgraphToken.id}`,
chain: GraphQLApi.Chain.UnknownChain,
address: subgraphToken.id,
},
],
},
}

return { token }
}, [subgraphToken])

return { data, loading, error, currency }
}
31 changes: 27 additions & 4 deletions apps/web/src/pages/TokenDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleto
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { useActiveAddresses } from 'features/accounts/store/hooks'
import { useSrcColor } from 'hooks/useColor'
import { useLightLinkTokenQuery } from 'hooks/useLightLinkTokenQuery'
import { ExploreTab } from 'pages/Explore/constants'
import { useDynamicMetatags } from 'pages/metatags'
import { LoadedTDPContext, MultiChainMap, PendingTDPContext, TDPProvider } from 'pages/TokenDetails/TDPContext'
Expand Down Expand Up @@ -79,24 +80,46 @@ function useCreateTDPContext(): PendingTDPContext | LoadedTDPContext {
}

const currencyChainInfo = getChainInfo(useChainIdFromUrlParam() ?? UniverseChainId.Mainnet)
const isLightLink = currencyChainInfo.id === UniverseChainId.LightLink

const isNative = tokenAddress === NATIVE_CHAIN_ID

const tokenDBAddress = isNative ? getNativeTokenDBAddress(currencyChainInfo.backendChain.chain) : tokenAddress

// Standard GraphQL query (skipped for LightLink)
const tokenQuery = GraphQLApi.useTokenWebQuery({
variables: { address: tokenDBAddress, chain: currencyChainInfo.backendChain.chain },
errorPolicy: 'all',
skip: isLightLink,
})

// LightLink subgraph fallback
const lightLinkResult = useLightLinkTokenQuery(isLightLink ? tokenAddress : undefined)

// Merge: for LightLink, overlay subgraph data onto tokenQuery shape
const effectiveTokenQuery = useMemo(() => {
if (!isLightLink) return tokenQuery
// Create a compatible object that looks like an Apollo QueryResult
return {
...tokenQuery,
data: lightLinkResult.data as GraphQLApi.TokenWebQuery | undefined,
loading: lightLinkResult.loading,
error: lightLinkResult.error as any,
}
}, [isLightLink, tokenQuery, lightLinkResult.data, lightLinkResult.loading, lightLinkResult.error])

const currency = useMemo(() => {
if (isNative) {
return nativeOnChain(currencyChainInfo.id)
}
if (isLightLink) {
return lightLinkResult.currency
}
if (tokenQuery.data?.token) {
return gqlToCurrency(tokenQuery.data.token)
}
return undefined
}, [tokenQuery.data?.token, isNative, currencyChainInfo.id])
}, [tokenQuery.data?.token, isNative, currencyChainInfo.id, isLightLink, lightLinkResult.currency])

const chartState = useCreateTDPChartState(tokenDBAddress, currencyChainInfo.backendChain.chain)

Expand All @@ -106,7 +129,7 @@ function useCreateTDPContext(): PendingTDPContext | LoadedTDPContext {
const colors = useSporeColors()
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const { preloadedLogoSrc } = (useLocation().state as { preloadedLogoSrc?: string }) ?? {}
const extractedColorSrc = tokenQuery.data?.token?.project?.logoUrl ?? preloadedLogoSrc
const extractedColorSrc = effectiveTokenQuery.data?.token?.project?.logoUrl ?? preloadedLogoSrc
const tokenColor =
useSrcColor({
src: extractedColorSrc,
Expand All @@ -121,7 +144,7 @@ function useCreateTDPContext(): PendingTDPContext | LoadedTDPContext {
currencyChainId: currencyChainInfo.id,
// `currency.address` is checksummed, whereas the `tokenAddress` url param may not be
address: (currency?.isNative ? NATIVE_CHAIN_ID : currency?.address) ?? tokenAddress,
tokenQuery,
tokenQuery: effectiveTokenQuery as any,
chartState,
multiChainMap,
tokenColor,
Expand All @@ -131,7 +154,7 @@ function useCreateTDPContext(): PendingTDPContext | LoadedTDPContext {
currencyChainInfo.backendChain.chain,
currencyChainInfo.id,
tokenAddress,
tokenQuery,
effectiveTokenQuery,
chartState,
multiChainMap,
tokenColor,
Expand Down