88 UTCTimestamp ,
99} from "lightweight-charts" ;
1010import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
11+ import { z } from "zod" ;
1112import { useListenMetadata } from "../hooks/useListenMetadata" ;
1213import { CandlestickData , CandlestickDataSchema } from "../lib/types" ;
1314import { useTokenStore } from "../store/tokenStore" ;
@@ -31,6 +32,34 @@ export interface ChartProps {
3132// Available time intervals for the chart
3233const INTERVALS = [ "30s" , "1m" , "5m" , "15m" , "30m" , "1h" , "4h" , "1d" ] as const ;
3334
35+ // GeckoTerminal API schemas
36+ const GeckoTerminalPoolSchema = z . object ( {
37+ data : z . array (
38+ z . object ( {
39+ attributes : z . object ( {
40+ address : z . string ( ) ,
41+ volume_usd : z . object ( {
42+ h24 : z . string ( ) ,
43+ } ) ,
44+ } ) ,
45+ } )
46+ ) ,
47+ } ) ;
48+
49+ const GeckoTerminalOHLCVSchema = z . object ( {
50+ data : z . object ( {
51+ attributes : z . object ( {
52+ ohlcv_list : z . array (
53+ z . array ( z . union ( [ z . number ( ) , z . string ( ) ] ) )
54+ ) ,
55+ } ) ,
56+ } ) ,
57+ } ) ;
58+
59+ // Cache for pool addresses to prevent repeated API calls
60+ const poolAddressCache : Record < string , { address : string ; timestamp : number } > = { } ;
61+ const CACHE_DURATION = 5 * 60 * 1000 ; // 5 minutes
62+
3463// Add TradingView color constants
3564const TV_COLORS = {
3665 GREEN : "#26a69a" ,
@@ -39,6 +68,134 @@ const TV_COLORS = {
3968 RED_TRANSPARENT : "rgba(239, 83, 80, 0.3)" ,
4069} as const ;
4170
71+ // Find the most liquid pool for a token on Solana
72+ async function findSolanaPoolAddress ( tokenAddress : string ) : Promise < string | null > {
73+ const cacheKey = `solana:${ tokenAddress } ` ;
74+ const cached = poolAddressCache [ cacheKey ] ;
75+
76+ if ( cached && Date . now ( ) - cached . timestamp < CACHE_DURATION ) {
77+ return cached . address ;
78+ }
79+
80+ try {
81+ const response = await fetch (
82+ `https://api.geckoterminal.com/api/v2/networks/solana/tokens/${ tokenAddress } /pools` ,
83+ {
84+ headers : {
85+ Accept : "application/json;version=20230302" ,
86+ } ,
87+ }
88+ ) ;
89+
90+ if ( ! response . ok ) {
91+ console . error ( `Failed to fetch pools for token ${ tokenAddress } ` ) ;
92+ return null ;
93+ }
94+
95+ const json = await response . json ( ) ;
96+ const result = GeckoTerminalPoolSchema . safeParse ( json ) ;
97+
98+ if ( ! result . success ) {
99+ console . error ( "Failed to parse pool response:" , result . error ) ;
100+ return null ;
101+ }
102+
103+ // Sort pools by 24h volume to get the most active pool
104+ const sortedPools = result . data . data . sort (
105+ ( a , b ) =>
106+ parseFloat ( b . attributes . volume_usd . h24 ) -
107+ parseFloat ( a . attributes . volume_usd . h24 )
108+ ) ;
109+
110+ if ( sortedPools . length > 0 ) {
111+ const address = sortedPools [ 0 ] . attributes . address ;
112+ poolAddressCache [ cacheKey ] = {
113+ address,
114+ timestamp : Date . now ( ) ,
115+ } ;
116+ return address ;
117+ }
118+
119+ return null ;
120+ } catch ( error ) {
121+ console . error ( "Failed to fetch pool address:" , error ) ;
122+ return null ;
123+ }
124+ }
125+
126+ // Map chart intervals to GeckoTerminal timeframes
127+ function mapIntervalToGeckoTimeframe ( interval : string ) : { timeframe : string ; aggregate : number } {
128+ switch ( interval ) {
129+ case "30s" :
130+ case "1m" :
131+ return { timeframe : "minute" , aggregate : 1 } ;
132+ case "5m" :
133+ return { timeframe : "minute" , aggregate : 5 } ;
134+ case "15m" :
135+ return { timeframe : "minute" , aggregate : 15 } ;
136+ case "30m" :
137+ return { timeframe : "hour" , aggregate : 1 } ; // Use 1h as closest available
138+ case "1h" :
139+ return { timeframe : "hour" , aggregate : 1 } ;
140+ case "4h" :
141+ return { timeframe : "hour" , aggregate : 4 } ;
142+ case "1d" :
143+ return { timeframe : "day" , aggregate : 1 } ;
144+ default :
145+ return { timeframe : "minute" , aggregate : 1 } ;
146+ }
147+ }
148+
149+ // Fetch OHLC data from GeckoTerminal
150+ async function fetchGeckoTerminalOHLC (
151+ poolAddress : string ,
152+ interval : string
153+ ) : Promise < CandlestickData | null > {
154+ try {
155+ const { timeframe, aggregate } = mapIntervalToGeckoTimeframe ( interval ) ;
156+
157+ const response = await fetch (
158+ `https://api.geckoterminal.com/api/v2/networks/solana/pools/${ poolAddress } /ohlcv/${ timeframe } ?aggregate=${ aggregate } &limit=1000` ,
159+ {
160+ headers : {
161+ Accept : "application/json;version=20230302" ,
162+ } ,
163+ }
164+ ) ;
165+
166+ if ( ! response . ok ) {
167+ console . error ( `Failed to fetch OHLC data for pool ${ poolAddress } ` ) ;
168+ return null ;
169+ }
170+
171+ const json = await response . json ( ) ;
172+ const result = GeckoTerminalOHLCVSchema . safeParse ( json ) ;
173+
174+ if ( ! result . success ) {
175+ console . error ( "Failed to parse OHLC response:" , result . error ) ;
176+ return null ;
177+ }
178+
179+ // Convert GeckoTerminal OHLCV format to our Candlestick format
180+ const candlesticks : CandlestickData = result . data . data . attributes . ohlcv_list . map ( ( item ) => {
181+ const [ timestamp , open , high , low , close , volume ] = item ;
182+ return {
183+ timestamp : typeof timestamp === "number" ? timestamp : parseInt ( timestamp as string ) ,
184+ open : typeof open === "number" ? open : parseFloat ( open as string ) ,
185+ high : typeof high === "number" ? high : parseFloat ( high as string ) ,
186+ low : typeof low === "number" ? low : parseFloat ( low as string ) ,
187+ close : typeof close === "number" ? close : parseFloat ( close as string ) ,
188+ volume : typeof volume === "number" ? volume : parseFloat ( volume as string ) ,
189+ } ;
190+ } ) ;
191+
192+ return candlesticks ;
193+ } catch ( error ) {
194+ console . error ( "Failed to fetch GeckoTerminal OHLC data:" , error ) ;
195+ return null ;
196+ }
197+ }
198+
42199export function InnerChart ( { data, displayOhlc } : InnerChartProps ) {
43200 const chartContainerRef = useRef < HTMLDivElement > ( null ) ;
44201 const chartRef = useRef < ReturnType < typeof createChart > > ( null ) ;
@@ -446,14 +603,27 @@ export function Chart({ mint, interval: defaultInterval = "30s" }: ChartProps) {
446603 if ( isDisposed . current ) return ;
447604
448605 try {
449- const response = await fetch (
450- // use prod for charts always
451- `https://api.listen-rs.com/v1/adapter/candlesticks?mint=${ mint } &interval=${ selectedInterval } `
452- ) ;
453- const responseData = CandlestickDataSchema . parse ( await response . json ( ) ) ;
606+ // First try to get data from GeckoTerminal
607+ const poolAddress = await findSolanaPoolAddress ( mint ) ;
608+ let responseData : CandlestickData | null = null ;
609+
610+ if ( poolAddress ) {
611+ console . log ( `Found pool address ${ poolAddress } for token ${ mint } , fetching from GeckoTerminal` ) ;
612+ responseData = await fetchGeckoTerminalOHLC ( poolAddress , selectedInterval ) ;
613+ }
614+
615+ // If GeckoTerminal fails or no pool found, fall back to custom API
616+ if ( ! responseData || responseData . length === 0 ) {
617+ console . log ( `Falling back to custom API for token ${ mint } ` ) ;
618+ const response = await fetch (
619+ // use prod for charts always
620+ `https://api.listen-rs.com/v1/adapter/candlesticks?mint=${ mint } &interval=${ selectedInterval } `
621+ ) ;
622+ responseData = CandlestickDataSchema . parse ( await response . json ( ) ) ;
623+ }
454624
455625 if ( ! isDisposed . current ) {
456- setData ( responseData ) ;
626+ setData ( responseData || [ ] ) ;
457627 setIsLoading ( false ) ;
458628 }
459629 } catch ( error ) {
0 commit comments