11"use client" ;
22
33import { useState , useEffect , useCallback , useRef } from "react" ;
4- import { SILK_LCD } from "@/lib/chain-config" ;
4+ import { SILK_LCD , fetchWithTimeout } from "@/lib/chain-config" ;
55
66export interface SmartToken {
77 denom : string ;
@@ -39,6 +39,9 @@ export interface RWAData {
3939// Features that indicate RWA compliance
4040const RWA_FEATURES = [ "whitelisting" , "freezing" , "clawback" , "burning" , "minting" ] ;
4141
42+ // Max tokens to process (prevents unbounded fetching)
43+ const MAX_TOKENS = 500 ;
44+
4245function parseSupply ( amount : string , precision : number ) : number {
4346 return parseInt ( amount ) / Math . pow ( 10 , precision ) ;
4447}
@@ -48,6 +51,7 @@ export function useRWATokens(): RWAData & { refresh: () => void } {
4851 const [ loading , setLoading ] = useState ( true ) ;
4952 const [ error , setError ] = useState < string | null > ( null ) ;
5053 const fetchedRef = useRef ( false ) ;
54+ const mountedRef = useRef ( true ) ;
5155
5256 const fetchTokens = useCallback ( async ( ) => {
5357 try {
@@ -61,51 +65,59 @@ export function useRWATokens(): RWAData & { refresh: () => void } {
6165 do {
6266 const paginationParam = nextKey ? `&pagination.key=${ encodeURIComponent ( nextKey ) } ` : "" ;
6367 const url : string = `${ SILK_LCD } /cosmos/bank/v1beta1/supply?pagination.limit=500${ paginationParam } ` ;
64- const res = await fetch ( url ) ;
65- if ( ! res . ok ) throw new Error ( `Supply fetch failed: ${ res . status } ` ) ;
68+ const res = await fetchWithTimeout ( url ) ;
6669 const data = await res . json ( ) ;
6770 allDenoms . push ( ...( data . supply || [ ] ) ) ;
6871 nextKey = data . pagination ?. next_key || null ;
6972 } while ( nextKey ) ;
7073
74+ if ( ! mountedRef . current ) return ;
75+
7176 // Step 2: Filter for Smart Token denoms (pattern: subunit-core1...)
72- const smartDenoms = allDenoms . filter (
73- ( d ) => d . denom . match ( / ^ [ a - z A - Z 0 - 9 ] + - c o r e 1 [ a - z 0 - 9 ] + $ / ) && d . denom !== "ucore"
74- ) ;
77+ const smartDenoms = allDenoms
78+ . filter ( ( d ) => d . denom . match ( / ^ [ a - z A - Z 0 - 9 ] + - c o r e 1 [ a - z 0 - 9 ] + $ / ) && d . denom !== "ucore" )
79+ . slice ( 0 , MAX_TOKENS ) ; // Cap at MAX_TOKENS
7580
7681 // Step 3: Fetch token details for each (batch with concurrency limit)
7782 const BATCH_SIZE = 15 ;
7883 const smartTokens : SmartToken [ ] = [ ] ;
7984
8085 for ( let i = 0 ; i < smartDenoms . length ; i += BATCH_SIZE ) {
86+ if ( ! mountedRef . current ) return ; // Abort if unmounted
87+
8188 const batch = smartDenoms . slice ( i , i + BATCH_SIZE ) ;
8289 const results = await Promise . allSettled (
8390 batch . map ( async ( d ) => {
84- const res = await fetch ( `${ SILK_LCD } /coreum/asset/ft/v1/tokens/${ d . denom } ` ) ;
85- if ( ! res . ok ) return null ;
86- const data = await res . json ( ) ;
87- const token = data . token ;
88- return {
89- denom : token . denom ,
90- issuer : token . issuer ,
91- symbol : token . symbol || token . subunit ?. toUpperCase ( ) || "???" ,
92- subunit : token . subunit ,
93- precision : token . precision || 0 ,
94- description : token . description || "" ,
95- globally_frozen : token . globally_frozen || false ,
96- features : token . features || [ ] ,
97- burn_rate : token . burn_rate || "0" ,
98- send_commission_rate : token . send_commission_rate || "0" ,
99- supply : parseSupply ( d . amount , token . precision || 0 ) ,
100- admin : token . admin || token . issuer ,
101- } as SmartToken ;
91+ try {
92+ const res = await fetchWithTimeout ( `${ SILK_LCD } /coreum/asset/ft/v1/tokens/${ d . denom } ` ) ;
93+ const data = await res . json ( ) ;
94+ const token = data . token ;
95+ return {
96+ denom : token . denom ,
97+ issuer : token . issuer ,
98+ symbol : token . symbol || token . subunit ?. toUpperCase ( ) || "???" ,
99+ subunit : token . subunit ,
100+ precision : token . precision || 0 ,
101+ description : token . description || "" ,
102+ globally_frozen : token . globally_frozen || false ,
103+ features : token . features || [ ] ,
104+ burn_rate : token . burn_rate || "0" ,
105+ send_commission_rate : token . send_commission_rate || "0" ,
106+ supply : parseSupply ( d . amount , token . precision || 0 ) ,
107+ admin : token . admin || token . issuer ,
108+ } as SmartToken ;
109+ } catch {
110+ return null ;
111+ }
102112 } )
103113 ) ;
104114 for ( const r of results ) {
105115 if ( r . status === "fulfilled" && r . value ) smartTokens . push ( r . value ) ;
106116 }
107117 }
108118
119+ if ( ! mountedRef . current ) return ;
120+
109121 // Sort by number of RWA features (most compliant first)
110122 smartTokens . sort ( ( a , b ) => {
111123 const aScore = a . features . filter ( ( f ) => RWA_FEATURES . includes ( f ) ) . length ;
@@ -115,17 +127,19 @@ export function useRWATokens(): RWAData & { refresh: () => void } {
115127
116128 setTokens ( smartTokens ) ;
117129 } catch ( err ) {
118- setError ( err instanceof Error ? err . message : "Failed to fetch tokens" ) ;
130+ if ( mountedRef . current ) setError ( err instanceof Error ? err . message : "Failed to fetch tokens" ) ;
119131 } finally {
120- setLoading ( false ) ;
132+ if ( mountedRef . current ) setLoading ( false ) ;
121133 }
122134 } , [ ] ) ;
123135
124136 useEffect ( ( ) => {
137+ mountedRef . current = true ;
125138 if ( ! fetchedRef . current ) {
126139 fetchedRef . current = true ;
127140 fetchTokens ( ) ;
128141 }
142+ return ( ) => { mountedRef . current = false ; } ;
129143 } , [ fetchTokens ] ) ;
130144
131145 // Compute stats
0 commit comments