@@ -16,7 +16,6 @@ import UnwrapNativeToken from "./wrappedNativeToken/UnwrapNativeToken";
1616import DisplayNativeBalance from "./wrappedNativeToken/DisplayNativeBalance" ;
1717import DisplayWrappedBalance from "./wrappedNativeToken/DisplayWrappedBalance" ;
1818import { BaseConsoleToolProps , ConsoleToolMetadata , withConsoleToolMetadata } from "../../../components/WithConsoleToolMetadata" ;
19- import { useConnectedWallet } from "@/components/toolbox/contexts/ConnectedWalletContext" ;
2019import useConsoleNotifications from "@/hooks/useConsoleNotifications" ;
2120import { generateConsoleToolGitHubUrl } from "@/components/toolbox/utils/github-url" ;
2221
@@ -35,23 +34,25 @@ const metadata: ConsoleToolMetadata = {
3534
3635function DeployWrappedNative ( { onSuccess } : BaseConsoleToolProps ) {
3736 const [ criticalError , setCriticalError ] = useState < Error | null > ( null ) ;
37+ const [ isMounted , setIsMounted ] = useState ( false ) ;
3838
3939 const setWrappedNativeToken = useSetWrappedNativeToken ( ) ;
4040 const selectedL1 = useSelectedL1 ( ) ( ) ;
41- const [ wrappedNativeTokenAddress , setLocalWrappedNativeTokenAddress ] = useState < string > ( '' ) ;
42- const [ hasPredeployedToken , setHasPredeployedToken ] = useState ( false ) ;
43- const [ isCheckingToken , setIsCheckingToken ] = useState ( false ) ;
44- const { coreWalletClient } = useConnectedWallet ( ) ;
45- const { walletEVMAddress, walletChainId } = useWalletStore ( ) ;
46- const setNativeCurrencyInfo = useSetNativeCurrencyInfo ( ) ;
47- const { notify } = useConsoleNotifications ( ) ;
48- const viemChain = useViemChainStore ( ) ;
49- const [ isDeploying , setIsDeploying ] = useState ( false ) ;
5041
5142 // Get cached values from wallet store
5243 const cachedWrappedToken = useWrappedNativeToken ( ) ;
5344 const cachedNativeCurrency = useNativeCurrencyInfo ( ) ;
5445
46+ // Initialize with cached value to prevent flickering
47+ const [ wrappedNativeTokenAddress , setLocalWrappedNativeTokenAddress ] = useState < string > ( cachedWrappedToken || '' ) ;
48+ const [ hasPredeployedToken , setHasPredeployedToken ] = useState ( ! ! cachedWrappedToken ) ;
49+ const [ isCheckingToken , setIsCheckingToken ] = useState ( ! cachedWrappedToken ) ;
50+ const { coreWalletClient, walletEVMAddress, walletChainId } = useWalletStore ( ) ;
51+ const setNativeCurrencyInfo = useSetNativeCurrencyInfo ( ) ;
52+ const { notify } = useConsoleNotifications ( ) ;
53+ const viemChain = useViemChainStore ( ) ;
54+ const [ isDeploying , setIsDeploying ] = useState ( false ) ;
55+
5556 // Get native token symbol (use cached value if available)
5657 const nativeTokenSymbol = cachedNativeCurrency ?. symbol || viemChain ?. nativeCurrency ?. symbol || selectedL1 ?. coinName || 'COIN' ;
5758 const wrappedTokenSymbol = `W${ nativeTokenSymbol } ` ;
@@ -61,10 +62,57 @@ function DeployWrappedNative({ onSuccess }: BaseConsoleToolProps) {
6162 throw criticalError ;
6263 }
6364
65+ // Handle mounting to avoid hydration errors
66+ useEffect ( ( ) => {
67+ setIsMounted ( true ) ;
68+ } , [ ] ) ;
69+
70+ // Sync cached wrapped token with local state immediately
71+ useEffect ( ( ) => {
72+ if ( cachedWrappedToken && wrappedNativeTokenAddress !== cachedWrappedToken ) {
73+ setLocalWrappedNativeTokenAddress ( cachedWrappedToken ) ;
74+ setHasPredeployedToken ( true ) ;
75+ setIsCheckingToken ( false ) ;
76+ }
77+ } , [ cachedWrappedToken , wrappedNativeTokenAddress ] ) ;
78+
79+ // Validate that an address is a valid wrapped native token contract
80+ async function validateWrappedTokenContract ( address : string , publicClient : any ) : Promise < boolean > {
81+ try {
82+ // Check if contract has bytecode
83+ const code = await publicClient . getBytecode ( { address : address as `0x${string } ` } ) ;
84+ if ( ! code || code === '0x' ) {
85+ return false ;
86+ }
87+
88+ // Try to call balanceOf to verify it's a valid ERC20-like contract
89+ // We use a test address to avoid issues with undefined walletEVMAddress
90+ await publicClient . readContract ( {
91+ address : address as `0x${string } `,
92+ abi : WrappedNativeToken . abi ,
93+ functionName : 'balanceOf' ,
94+ args : [ '0x0000000000000000000000000000000000000000' ]
95+ } ) ;
96+
97+ return true ;
98+ } catch ( error ) {
99+ console . error ( 'Contract validation failed:' , error ) ;
100+ return false ;
101+ }
102+ }
103+
64104 // Check for pre-deployed wrapped native token
65105 useEffect ( ( ) => {
66106 async function checkToken ( ) {
67- if ( ! viemChain || ! walletEVMAddress ) return ;
107+ if ( ! isMounted || ! viemChain || ! walletEVMAddress ) {
108+ return ;
109+ }
110+
111+ // If we have a cached token and it's already set locally, no need to check again
112+ if ( cachedWrappedToken && wrappedNativeTokenAddress === cachedWrappedToken ) {
113+ setIsCheckingToken ( false ) ;
114+ return ;
115+ }
68116
69117 setIsCheckingToken ( true ) ;
70118 try {
@@ -75,39 +123,51 @@ function DeployWrappedNative({ onSuccess }: BaseConsoleToolProps) {
75123 setNativeCurrencyInfo ( walletChainId , viemChain . nativeCurrency ) ;
76124 }
77125
126+ const publicClient = createPublicClient ( {
127+ transport : http ( viemChain . rpcUrls . default . http [ 0 ] || "" )
128+ } ) ;
129+
78130 // Check cache first for wrapped token
79131 let tokenAddress = cachedWrappedToken || '' ;
80132
81- // If not in cache, check other sources
133+ // Validate cached address if it exists
134+ if ( tokenAddress ) {
135+ const isValid = await validateWrappedTokenContract ( tokenAddress , publicClient ) ;
136+ if ( ! isValid ) {
137+ console . warn ( `Cached wrapped token address ${ tokenAddress } is invalid, clearing it` ) ;
138+ tokenAddress = '' ;
139+ setWrappedNativeToken ( '' ) ; // Clear invalid address from store
140+ } else {
141+ setHasPredeployedToken ( true ) ;
142+ }
143+ }
144+
145+ // If not in cache or invalid, check other sources
82146 if ( ! tokenAddress ) {
83147 if ( selectedL1 ?. wrappedTokenAddress ) {
84- tokenAddress = selectedL1 . wrappedTokenAddress ;
85- } else {
86- // Check if pre-deployed wrapped native token exists
87- const publicClient = createPublicClient ( {
88- transport : http ( viemChain . rpcUrls . default . http [ 0 ] || "" )
89- } ) ;
90-
91- const code = await publicClient . getBytecode ( { address : PREDEPLOYED_WRAPPED_NATIVE_ADDRESS as `0x${string } ` } ) ;
92- const hasPredeployed = code !== undefined && code !== '0x' ;
93- setHasPredeployedToken ( hasPredeployed ) ;
148+ const isValid = await validateWrappedTokenContract ( selectedL1 . wrappedTokenAddress , publicClient ) ;
149+ if ( isValid ) {
150+ tokenAddress = selectedL1 . wrappedTokenAddress ;
151+ setHasPredeployedToken ( true ) ;
152+ }
153+ }
154+
155+ // If still no valid token, check pre-deployed address
156+ if ( ! tokenAddress ) {
157+ const isValid = await validateWrappedTokenContract ( PREDEPLOYED_WRAPPED_NATIVE_ADDRESS , publicClient ) ;
158+ setHasPredeployedToken ( isValid ) ;
94159
95- if ( hasPredeployed ) {
160+ if ( isValid ) {
96161 tokenAddress = PREDEPLOYED_WRAPPED_NATIVE_ADDRESS ;
97162 }
98163 }
99-
100- // No need to cache here since we're using toolboxStore
101- } else {
102- // If we got from cache, we assume it exists
103- setHasPredeployedToken ( true ) ;
104164 }
105165
106166 setLocalWrappedNativeTokenAddress ( tokenAddress ) ;
107167
108- // If we detected pre-deployed token and nothing in store, save it
109- if ( tokenAddress === PREDEPLOYED_WRAPPED_NATIVE_ADDRESS && ! selectedL1 ?. wrappedTokenAddress ) {
110- setWrappedNativeToken ( PREDEPLOYED_WRAPPED_NATIVE_ADDRESS ) ;
168+ // If we detected a valid token and nothing in store, save it
169+ if ( tokenAddress && ! cachedWrappedToken ) {
170+ setWrappedNativeToken ( tokenAddress ) ;
111171 }
112172 } catch ( error ) {
113173 console . error ( 'Error checking token:' , error ) ;
@@ -117,9 +177,14 @@ function DeployWrappedNative({ onSuccess }: BaseConsoleToolProps) {
117177 }
118178
119179 checkToken ( ) ;
120- } , [ viemChain , walletEVMAddress , selectedL1 , walletChainId , cachedWrappedToken , cachedNativeCurrency , setNativeCurrencyInfo ] ) ;
121-
180+ } , [ isMounted , viemChain , walletEVMAddress , selectedL1 , walletChainId , cachedWrappedToken , cachedNativeCurrency , wrappedNativeTokenAddress ] ) ;
181+
122182 async function handleDeploy ( ) {
183+ if ( ! coreWalletClient ) {
184+ setCriticalError ( new Error ( "Core wallet not found" ) ) ;
185+ return ;
186+ }
187+
123188 setIsDeploying ( true ) ;
124189 try {
125190 if ( ! viemChain ) throw new Error ( "No chain selected" ) ;
@@ -135,6 +200,7 @@ function DeployWrappedNative({ onSuccess }: BaseConsoleToolProps) {
135200 chain : viemChain ,
136201 account : walletEVMAddress as `0x${string } `
137202 } ) ;
203+
138204 notify ( {
139205 type : 'deploy' ,
140206 name : 'WrappedNativeToken'
@@ -156,81 +222,82 @@ function DeployWrappedNative({ onSuccess }: BaseConsoleToolProps) {
156222 }
157223
158224
225+ // Don't render anything until we've finished checking (or during SSR/initial mount)
226+ if ( ! isMounted || isCheckingToken ) {
227+ return (
228+ < div className = "text-center py-8 text-zinc-500" >
229+ Checking for wrapped native token...
230+ </ div >
231+ ) ;
232+ }
233+
159234 return (
160235 < div className = "space-y-6" >
161- { isCheckingToken ? (
162- < div className = "text-center py-8 text-zinc-500" >
163- Checking for wrapped native token...
236+ { /* Token Address Display */ }
237+ { wrappedNativeTokenAddress && (
238+ < Success
239+ label = { `Wrapped Native Token Address (${ wrappedTokenSymbol } )` }
240+ value = { wrappedNativeTokenAddress }
241+ />
242+ ) }
243+
244+ { /* Deploy Section - Only show if no wrapped token exists */ }
245+ { ! wrappedNativeTokenAddress && (
246+ < div className = "space-y-4" >
247+ < div >
248+ { hasPredeployedToken ? (
249+ < div className = "space-y-2" >
250+ < p className = "text-sm text-green-600 dark:text-green-400" >
251+ ✓ Pre-deployed wrapped native token detected at { PREDEPLOYED_WRAPPED_NATIVE_ADDRESS }
252+ </ p >
253+ < p className = "text-xs text-zinc-500 dark:text-zinc-400" >
254+ This token wraps your L1's native token ({ nativeTokenSymbol } → { wrappedTokenSymbol } )
255+ </ p >
256+ </ div >
257+ ) : (
258+ < p className = "text-sm text-zinc-600 dark:text-zinc-400" >
259+ No wrapped native token found. Deploy one to enable wrapping functionality.
260+ </ p >
261+ ) }
262+ </ div >
263+
264+ < Button
265+ variant = "primary"
266+ onClick = { handleDeploy }
267+ loading = { isDeploying }
268+ disabled = { isDeploying }
269+ >
270+ Deploy Wrapped Native Token
271+ </ Button >
164272 </ div >
165- ) : (
166- < >
167- { /* Token Address Display */ }
168- { wrappedNativeTokenAddress && (
169- < Success
170- label = { `Wrapped Native Token Address (${ wrappedTokenSymbol } )` }
171- value = { wrappedNativeTokenAddress }
273+ ) }
274+
275+ { /* Independent Tools Section - Only show if wrapped token exists */ }
276+ { wrappedNativeTokenAddress && (
277+ < div className = "space-y-6" >
278+ { /* Balance Display Row */ }
279+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
280+ < DisplayNativeBalance
281+ onError = { setCriticalError }
172282 />
173- ) }
174-
175- { /* Deploy Section - Only show if no wrapped token exists */ }
176- { ! wrappedNativeTokenAddress && (
177- < div className = "space-y-4" >
178- < div >
179- { hasPredeployedToken ? (
180- < div className = "space-y-2" >
181- < p className = "text-sm text-green-600 dark:text-green-400" >
182- ✓ Pre-deployed wrapped native token detected at { PREDEPLOYED_WRAPPED_NATIVE_ADDRESS }
183- </ p >
184- < p className = "text-xs text-zinc-500 dark:text-zinc-400" >
185- This token wraps your L1's native token ({ nativeTokenSymbol } → { wrappedTokenSymbol } )
186- </ p >
187- </ div >
188- ) : (
189- < p className = "text-sm text-zinc-600 dark:text-zinc-400" >
190- No wrapped native token found. Deploy one to enable wrapping functionality.
191- </ p >
192- ) }
193- </ div >
194-
195- < Button
196- variant = "primary"
197- onClick = { handleDeploy }
198- loading = { isDeploying }
199- disabled = { isDeploying }
200- >
201- Deploy Wrapped Native Token
202- </ Button >
203- </ div >
204- ) }
205-
206- { /* Independent Tools Section - Only show if wrapped token exists */ }
207- { wrappedNativeTokenAddress && (
208- < div className = "space-y-6" >
209- { /* Balance Display Row */ }
210- < div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
211- < DisplayNativeBalance
212- onError = { setCriticalError }
213- />
214- < DisplayWrappedBalance
215- wrappedNativeTokenAddress = { wrappedNativeTokenAddress }
216- onError = { setCriticalError }
217- />
218- </ div >
219-
220- { /* Wrap/Unwrap Tools Row */ }
221- < div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
222- < WrapNativeToken
223- wrappedNativeTokenAddress = { wrappedNativeTokenAddress }
224- onError = { setCriticalError }
225- />
226- < UnwrapNativeToken
227- wrappedNativeTokenAddress = { wrappedNativeTokenAddress }
228- onError = { setCriticalError }
229- />
230- </ div >
231- </ div >
232- ) }
233- </ >
283+ < DisplayWrappedBalance
284+ wrappedNativeTokenAddress = { wrappedNativeTokenAddress }
285+ onError = { setCriticalError }
286+ />
287+ </ div >
288+
289+ { /* Wrap/Unwrap Tools Row */ }
290+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
291+ < WrapNativeToken
292+ wrappedNativeTokenAddress = { wrappedNativeTokenAddress }
293+ onError = { setCriticalError }
294+ />
295+ < UnwrapNativeToken
296+ wrappedNativeTokenAddress = { wrappedNativeTokenAddress }
297+ onError = { setCriticalError }
298+ />
299+ </ div >
300+ </ div >
234301 ) }
235302 </ div >
236303 ) ;
0 commit comments