Skip to content

Commit e4bedd9

Browse files
authored
Merge pull request #3260 from ava-labs/fix-wrapped-native-token
fix flicker
2 parents e25eeec + 368d8e1 commit e4bedd9

File tree

5 files changed

+206
-138
lines changed

5 files changed

+206
-138
lines changed

components/toolbox/console/ictt/setup/DeployWrappedNative.tsx

Lines changed: 171 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import UnwrapNativeToken from "./wrappedNativeToken/UnwrapNativeToken";
1616
import DisplayNativeBalance from "./wrappedNativeToken/DisplayNativeBalance";
1717
import DisplayWrappedBalance from "./wrappedNativeToken/DisplayWrappedBalance";
1818
import { BaseConsoleToolProps, ConsoleToolMetadata, withConsoleToolMetadata } from "../../../components/WithConsoleToolMetadata";
19-
import { useConnectedWallet } from "@/components/toolbox/contexts/ConnectedWalletContext";
2019
import useConsoleNotifications from "@/hooks/useConsoleNotifications";
2120
import { generateConsoleToolGitHubUrl } from "@/components/toolbox/utils/github-url";
2221

@@ -35,23 +34,25 @@ const metadata: ConsoleToolMetadata = {
3534

3635
function 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
);

components/toolbox/console/ictt/setup/wrappedNativeToken/DisplayNativeBalance.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export default function DisplayNativeBalance({ onError }: DisplayNativeBalancePr
3030
if (!cachedNativeCurrency && viemChain?.nativeCurrency) {
3131
setNativeCurrencyInfo(walletChainId, viemChain.nativeCurrency);
3232
}
33-
}, [cachedNativeCurrency, viemChain?.nativeCurrency, walletChainId, setNativeCurrencyInfo]);
34-
33+
}, [cachedNativeCurrency, viemChain?.nativeCurrency, walletChainId]);
34+
3535
if (isLoading) {
3636
return (
3737
<div className="bg-zinc-50 dark:bg-zinc-900 p-4 rounded-lg">

0 commit comments

Comments
 (0)