@naculus/connect-appkit-react 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/api-extractor.json +21 -0
  3. package/dist/client.cjs +2 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.js +2 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/index.cjs +2 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +39 -0
  12. package/src/chain-abstraction/index.ts +8 -0
  13. package/src/chain-abstraction/useCompareCosts.ts +137 -0
  14. package/src/chain-abstraction/useExecuteRoute.ts +129 -0
  15. package/src/chain-abstraction/useRouteQuote.ts +168 -0
  16. package/src/client.test.ts +242 -0
  17. package/src/client.ts +265 -0
  18. package/src/hooks/useAccount.test.ts +67 -0
  19. package/src/hooks/useAccount.ts +37 -0
  20. package/src/hooks/useBalance.test.ts +459 -0
  21. package/src/hooks/useBalance.ts +170 -0
  22. package/src/hooks/useChain.test.ts +160 -0
  23. package/src/hooks/useChain.ts +53 -0
  24. package/src/hooks/useConnect.test.ts +112 -0
  25. package/src/hooks/useConnect.ts +29 -0
  26. package/src/hooks/useDisconnect.test.ts +75 -0
  27. package/src/hooks/useDisconnect.ts +32 -0
  28. package/src/hooks/useERC20Allowance.ts +93 -0
  29. package/src/hooks/useERC20Approve.ts +220 -0
  30. package/src/hooks/useERC20Transfer.ts +134 -0
  31. package/src/hooks/useERC20TransferSimulation.ts +147 -0
  32. package/src/hooks/useEmbeddedWallet.test.ts +266 -0
  33. package/src/hooks/useEmbeddedWallet.ts +181 -0
  34. package/src/hooks/useLastTx.test.ts +117 -0
  35. package/src/hooks/useLastTx.ts +73 -0
  36. package/src/hooks/useLookupAddress.ts +131 -0
  37. package/src/hooks/useNotification.ts +308 -0
  38. package/src/hooks/useResolveName.ts +128 -0
  39. package/src/hooks/useSIWxLogin.test.ts +307 -0
  40. package/src/hooks/useSIWxLogin.ts +163 -0
  41. package/src/hooks/useSIWxSession.test.ts +481 -0
  42. package/src/hooks/useSIWxSession.ts +290 -0
  43. package/src/hooks/useSendCalls.ts +103 -0
  44. package/src/hooks/useSendTransaction.test.ts +175 -0
  45. package/src/hooks/useSendTransaction.ts +80 -0
  46. package/src/hooks/useSendUserOperation.ts +313 -0
  47. package/src/hooks/useSession.ts +96 -0
  48. package/src/hooks/useSessionKeys.ts +356 -0
  49. package/src/hooks/useSignInWithEthereum.test.ts +224 -0
  50. package/src/hooks/useSignInWithEthereum.ts +46 -0
  51. package/src/hooks/useSignInWithX.test.ts +210 -0
  52. package/src/hooks/useSignInWithX.ts +141 -0
  53. package/src/hooks/useSignMessage.test.ts +151 -0
  54. package/src/hooks/useSignMessage.ts +65 -0
  55. package/src/hooks/useSimulateTransfer.ts +165 -0
  56. package/src/hooks/useSiwxAuthSession.test.ts +456 -0
  57. package/src/hooks/useSiwxAuthSession.ts +163 -0
  58. package/src/hooks/useSmartAccount.ts +194 -0
  59. package/src/hooks/useSwitchChain.ts +68 -0
  60. package/src/hooks/useTokenBalance.test.ts +337 -0
  61. package/src/hooks/useTokenBalance.ts +210 -0
  62. package/src/hooks/useTransactionSimulation.ts +401 -0
  63. package/src/hooks/useTxHistory.test.ts +108 -0
  64. package/src/hooks/useTxHistory.ts +83 -0
  65. package/src/hooks/useTxMonitor.test.ts +127 -0
  66. package/src/hooks/useTxMonitor.ts +178 -0
  67. package/src/hooks/useUserOpStatus.ts +245 -0
  68. package/src/hooks/useValidateDestination.test.ts +96 -0
  69. package/src/hooks/useValidateDestination.ts +72 -0
  70. package/src/hooks/useViemClient.test.ts +155 -0
  71. package/src/hooks/useViemClient.ts +65 -0
  72. package/src/hooks/useWallet.test.ts +133 -0
  73. package/src/hooks/useWallet.ts +39 -0
  74. package/src/hooks/useWeb3ErrorHandler.ts +102 -0
  75. package/src/index.ts +186 -0
  76. package/src/provider/Web3ConnectProvider.test.tsx +178 -0
  77. package/src/provider/Web3ConnectProvider.tsx +530 -0
  78. package/src/token-list/TokenSelector.tsx +351 -0
  79. package/src/token-list/defaults.ts +39 -0
  80. package/src/token-list/index.ts +14 -0
  81. package/src/token-list/useTokenList.ts +119 -0
  82. package/src/token-list/useTokenSearch.ts +99 -0
  83. package/src/types/index.ts +99 -0
  84. package/src/utils/chains.ts +115 -0
  85. package/src/utils/errorMessages.ts +142 -0
  86. package/src/utils/i18n.ts +90 -0
  87. package/src/wc-generated/components.ts +377 -0
  88. package/test/setup.ts +17 -0
  89. package/test/test-utils.tsx +78 -0
  90. package/tsup.config.ts +18 -0
@@ -0,0 +1,337 @@
1
+ /// <reference types="vitest" />
2
+ /// @vitest-environment jsdom
3
+
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import { renderHook, act, waitFor } from "@testing-library/react";
6
+ import type { Address } from "viem";
7
+
8
+ const mockUseWeb3 = vi.fn();
9
+ vi.mock("../provider/Web3ConnectProvider", () => ({
10
+ useWeb3: () => mockUseWeb3(),
11
+ }));
12
+
13
+ const mockUseAccount = vi.fn();
14
+ vi.mock("./useAccount", () => ({
15
+ useAccount: () => mockUseAccount(),
16
+ }));
17
+
18
+ const mockReadContract = vi.fn();
19
+ const mockCreatePublicClient = vi.fn();
20
+ const mockFormatUnits = vi.fn((value: bigint, decimals: number) => {
21
+ const str = value.toString();
22
+ const padded = str.padStart(decimals + 1, "0");
23
+ const intPart = padded.slice(0, padded.length - decimals) || "0";
24
+ const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
25
+ return fracPart ? intPart + "." + fracPart : intPart;
26
+ });
27
+
28
+ vi.mock("viem", () => ({
29
+ createPublicClient: (...args: unknown[]) => mockCreatePublicClient(...args),
30
+ http: (url: string) => ({ transport: "http", url }),
31
+ formatUnits: (...args: [bigint, number]) => mockFormatUnits(...args),
32
+ }));
33
+
34
+ import { useTokenBalance } from "./useTokenBalance";
35
+ import type { TokenInfo } from "./useTokenBalance";
36
+
37
+ const USDC: TokenInfo = {
38
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as Address,
39
+ symbol: "USDC",
40
+ decimals: 6,
41
+ name: "USD Coin",
42
+ };
43
+
44
+ const DAI: TokenInfo = {
45
+ address: "0x6B175474E89094C44Da98b954EedeAC495271d0F" as Address,
46
+ symbol: "DAI",
47
+ decimals: 18,
48
+ name: "Dai Stablecoin",
49
+ };
50
+
51
+ describe("useTokenBalance", () => {
52
+ beforeEach(() => {
53
+ vi.clearAllMocks();
54
+ mockCreatePublicClient.mockImplementation(({ chain }) => ({
55
+ chain,
56
+ readContract: mockReadContract,
57
+ }));
58
+ });
59
+
60
+ it("should fetch a single token balance", async () => {
61
+ mockReadContract.mockResolvedValue(BigInt("1000000"));
62
+ mockUseWeb3.mockReturnValue({
63
+ chainId: "eip155:1",
64
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
65
+ });
66
+ mockUseAccount.mockReturnValue({
67
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
68
+ isConnected: true,
69
+ });
70
+
71
+ const { result } = renderHook(() => useTokenBalance({ tokens: [USDC] }));
72
+
73
+ await waitFor(() => {
74
+ expect(mockReadContract).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ await waitFor(() => {
78
+ expect(result.current.tokenBalances).toHaveLength(1);
79
+ });
80
+ expect(result.current.tokenBalances[0].balance).toBe("1000000");
81
+ expect(result.current.tokenBalances[0].formatted).toBe("1");
82
+ expect(result.current.tokenBalances[0].symbol).toBe("USDC");
83
+ expect(result.current.tokenBalances[0].address).toBe(USDC.address);
84
+ });
85
+
86
+ it("should fetch multiple token balances", async () => {
87
+ mockReadContract
88
+ .mockResolvedValueOnce(BigInt("1000000"))
89
+ .mockResolvedValueOnce(BigInt("5000000000000000000"));
90
+ mockUseWeb3.mockReturnValue({
91
+ chainId: "eip155:1",
92
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
93
+ });
94
+ mockUseAccount.mockReturnValue({
95
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
96
+ isConnected: true,
97
+ });
98
+
99
+ const { result } = renderHook(() => useTokenBalance({ tokens: [USDC, DAI] }));
100
+
101
+ await waitFor(() => {
102
+ expect(mockReadContract).toHaveBeenCalledTimes(2);
103
+ });
104
+
105
+ await waitFor(() => {
106
+ expect(result.current.tokenBalances).toHaveLength(2);
107
+ });
108
+
109
+ expect(result.current.tokenBalances[0].symbol).toBe("USDC");
110
+ expect(result.current.tokenBalances[0].balance).toBe("1000000");
111
+ expect(result.current.tokenBalances[0].formatted).toBe("1");
112
+ expect(result.current.tokenBalances[1].symbol).toBe("DAI");
113
+ expect(result.current.tokenBalances[1].balance).toBe("5000000000000000000");
114
+ expect(result.current.tokenBalances[1].formatted).toBe("5");
115
+ });
116
+
117
+ it("should handle readContract error for a single token", async () => {
118
+ mockReadContract.mockRejectedValue(new Error("RPC error"));
119
+ mockUseWeb3.mockReturnValue({
120
+ chainId: "eip155:1",
121
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
122
+ });
123
+ mockUseAccount.mockReturnValue({
124
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
125
+ isConnected: true,
126
+ });
127
+
128
+ const { result } = renderHook(() => useTokenBalance({ tokens: [USDC] }));
129
+
130
+ await waitFor(() => {
131
+ expect(result.current.tokenBalances[0]).toBeDefined();
132
+ });
133
+
134
+ expect(result.current.tokenBalances[0].balance).toBeNull();
135
+ expect(result.current.tokenBalances[0].formatted).toBeNull();
136
+ expect(result.current.tokenBalances[0].symbol).toBe("USDC");
137
+ });
138
+
139
+ it("should handle partial failure with multiple tokens", async () => {
140
+ mockReadContract
141
+ .mockResolvedValueOnce(BigInt("1000000"))
142
+ .mockRejectedValueOnce(new Error("DAI error"));
143
+ mockUseWeb3.mockReturnValue({
144
+ chainId: "eip155:1",
145
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
146
+ });
147
+ mockUseAccount.mockReturnValue({
148
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
149
+ isConnected: true,
150
+ });
151
+
152
+ const { result } = renderHook(() => useTokenBalance({ tokens: [USDC, DAI] }));
153
+
154
+ await waitFor(() => {
155
+ expect(result.current.tokenBalances).toHaveLength(2);
156
+ });
157
+
158
+ expect(result.current.tokenBalances[0].symbol).toBe("USDC");
159
+ expect(result.current.tokenBalances[0].balance).toBe("1000000");
160
+ expect(result.current.tokenBalances[1].symbol).toBe("DAI");
161
+ expect(result.current.tokenBalances[1].balance).toBeNull();
162
+ expect(result.current.tokenBalances[1].formatted).toBeNull();
163
+ });
164
+
165
+ it("should return empty array when no tokens specified", async () => {
166
+ mockUseWeb3.mockReturnValue({
167
+ chainId: "eip155:1",
168
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
169
+ });
170
+ mockUseAccount.mockReturnValue({
171
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
172
+ isConnected: true,
173
+ });
174
+
175
+ const { result } = renderHook(() => useTokenBalance({ tokens: [] }));
176
+
177
+ await act(async () => {
178
+ await new Promise((resolve) => setTimeout(resolve, 100));
179
+ });
180
+
181
+ expect(mockReadContract).not.toHaveBeenCalled();
182
+ expect(result.current.tokenBalances).toEqual([]);
183
+ expect(result.current.isLoading).toBe(false);
184
+ });
185
+
186
+ it("should return empty array when not connected", async () => {
187
+ mockUseWeb3.mockReturnValue({
188
+ chainId: null,
189
+ chains: [],
190
+ });
191
+ mockUseAccount.mockReturnValue({
192
+ evmAccount: null,
193
+ isConnected: false,
194
+ });
195
+
196
+ const { result } = renderHook(() => useTokenBalance({ tokens: [USDC] }));
197
+
198
+ await act(async () => {
199
+ await new Promise((resolve) => setTimeout(resolve, 100));
200
+ });
201
+
202
+ expect(mockReadContract).not.toHaveBeenCalled();
203
+ expect(result.current.tokenBalances).toEqual([]);
204
+ });
205
+
206
+ it("should expose getTokenBalance helper", async () => {
207
+ mockReadContract.mockResolvedValue(BigInt("1000000"));
208
+ mockUseWeb3.mockReturnValue({
209
+ chainId: "eip155:1",
210
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
211
+ });
212
+ mockUseAccount.mockReturnValue({
213
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
214
+ isConnected: true,
215
+ });
216
+
217
+ const { result } = renderHook(() => useTokenBalance({ tokens: [USDC, DAI] }));
218
+
219
+ await waitFor(() => {
220
+ expect(mockReadContract).toHaveBeenCalledTimes(2);
221
+ });
222
+
223
+ await waitFor(() => {
224
+ expect(result.current.tokenBalances).toHaveLength(2);
225
+ });
226
+
227
+ const usdcResult = result.current.getTokenBalance(USDC.address);
228
+ expect(usdcResult).toBeDefined();
229
+ expect(usdcResult!.symbol).toBe("USDC");
230
+ expect(usdcResult!.balance).toBe("1000000");
231
+
232
+ const unknown = result.current.getTokenBalance("0x0000000000000000000000000000000000000000" as Address);
233
+ expect(unknown).toBeUndefined();
234
+ });
235
+
236
+ it("should auto-refresh at specified interval", async () => {
237
+ vi.useFakeTimers();
238
+ mockReadContract.mockResolvedValue(BigInt("1000000"));
239
+ mockUseWeb3.mockReturnValue({
240
+ chainId: "eip155:1",
241
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
242
+ });
243
+ mockUseAccount.mockReturnValue({
244
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
245
+ isConnected: true,
246
+ });
247
+
248
+ renderHook(() => useTokenBalance({ tokens: [USDC], refreshInterval: 10000 }));
249
+
250
+ await act(async () => {
251
+ vi.advanceTimersByTime(0);
252
+ });
253
+ expect(mockReadContract).toHaveBeenCalledTimes(1);
254
+
255
+ mockReadContract.mockResolvedValue(BigInt("2000000"));
256
+ await act(async () => {
257
+ vi.advanceTimersByTime(10000);
258
+ });
259
+ expect(mockReadContract).toHaveBeenCalledTimes(2);
260
+
261
+ await act(async () => {
262
+ vi.advanceTimersByTime(10000);
263
+ });
264
+ expect(mockReadContract).toHaveBeenCalledTimes(3);
265
+
266
+ vi.useRealTimers();
267
+ });
268
+
269
+ it("should clear interval on unmount", async () => {
270
+ vi.useFakeTimers();
271
+ mockReadContract.mockResolvedValue(BigInt("1000000"));
272
+ mockUseWeb3.mockReturnValue({
273
+ chainId: "eip155:1",
274
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
275
+ });
276
+ mockUseAccount.mockReturnValue({
277
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
278
+ isConnected: true,
279
+ });
280
+
281
+ const { unmount } = renderHook(() =>
282
+ useTokenBalance({ tokens: [USDC], refreshInterval: 5000 })
283
+ );
284
+
285
+ await act(async () => {
286
+ vi.advanceTimersByTime(0);
287
+ });
288
+ const callsAfterMount = mockReadContract.mock.calls.length;
289
+
290
+ unmount();
291
+
292
+ vi.advanceTimersByTime(10000);
293
+ expect(mockReadContract).toHaveBeenCalledTimes(callsAfterMount);
294
+
295
+ vi.useRealTimers();
296
+ });
297
+
298
+ it("should not auto-refresh when refreshInterval is 0", async () => {
299
+ vi.useFakeTimers();
300
+ mockReadContract.mockResolvedValue(BigInt("1000000"));
301
+ mockUseWeb3.mockReturnValue({
302
+ chainId: "eip155:1",
303
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
304
+ });
305
+ mockUseAccount.mockReturnValue({
306
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
307
+ isConnected: true,
308
+ });
309
+
310
+ renderHook(() => useTokenBalance({ tokens: [USDC], refreshInterval: 0 }));
311
+
312
+ await act(async () => {
313
+ vi.advanceTimersByTime(0);
314
+ });
315
+ const callsAfterInit = mockReadContract.mock.calls.length;
316
+
317
+ vi.advanceTimersByTime(30000);
318
+ expect(mockReadContract).toHaveBeenCalledTimes(callsAfterInit);
319
+
320
+ vi.useRealTimers();
321
+ });
322
+
323
+ it("should use chain token symbol as default when no tokens match", () => {
324
+ mockUseWeb3.mockReturnValue({
325
+ chainId: "eip155:1",
326
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
327
+ });
328
+ mockUseAccount.mockReturnValue({
329
+ evmAccount: null,
330
+ isConnected: false,
331
+ });
332
+
333
+ const { result } = renderHook(() => useTokenBalance({ tokens: [] }));
334
+ expect(result.current.chain).toBeDefined();
335
+ expect(result.current.chain!.name).toBe("Ethereum");
336
+ });
337
+ });
@@ -0,0 +1,210 @@
1
+ import { useState, useEffect, useCallback, useMemo, useRef } from "react";
2
+ import { useWeb3 } from "../provider/Web3ConnectProvider";
3
+ import { useAccount } from "./useAccount";
4
+ import {
5
+ createPublicClient,
6
+ http,
7
+ formatUnits,
8
+ type PublicClient,
9
+ type Address,
10
+ } from "viem";
11
+
12
+ export interface TokenInfo {
13
+ /** ERC-20 token contract address */
14
+ address: Address;
15
+ /** Token symbol (e.g. "USDC", "LINK") */
16
+ symbol: string;
17
+ /** Token decimals (e.g. 6 for USDC, 18 for most) */
18
+ decimals: number;
19
+ /** Optional display name */
20
+ name?: string;
21
+ }
22
+
23
+ export interface UseTokenBalanceOptions {
24
+ /** ERC-20 token(s) to query */
25
+ tokens: TokenInfo[];
26
+ /** Auto-refresh interval in milliseconds. Default: undefined (no auto-refresh). */
27
+ refreshInterval?: number;
28
+ }
29
+
30
+ export interface TokenBalanceResult {
31
+ address: Address;
32
+ symbol: string;
33
+ decimals: number;
34
+ name?: string;
35
+ /** Raw balance as string (in smallest unit), or null if not loaded */
36
+ balance: string | null;
37
+ /** Human-readable formatted balance, or null if not loaded */
38
+ formatted: string | null;
39
+ }
40
+
41
+ export function useTokenBalance(options: UseTokenBalanceOptions) {
42
+ const { evmAccount, isConnected } = useAccount();
43
+ const { chainId, chains } = useWeb3();
44
+ const [tokenBalances, setTokenBalances] = useState<TokenBalanceResult[]>([]);
45
+ const [isLoading, setIsLoading] = useState(false);
46
+ const [error, setError] = useState<Error | null>(null);
47
+ const { tokens, refreshInterval } = options;
48
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
49
+ // Stable key for tokens to avoid infinite loops when array reference changes
50
+ const tokensKey = useMemo(
51
+ () => tokens.map(t => t.address.toLowerCase() + "|" + t.decimals + "|" + t.symbol).join(","),
52
+ [tokens]
53
+ );
54
+
55
+ const currentChain = useMemo(() => {
56
+ if (!chainId || !chains.length) return null;
57
+ const namespace = chainId.startsWith("eip155:") ? "eip155" : null;
58
+ if (!namespace) return null;
59
+ const chainNum = parseInt(chainId.split(":")[1], 10);
60
+ return chains.find((c) => c.namespace === namespace && c.id === chainNum) ?? null;
61
+ }, [chainId, chains]);
62
+
63
+ const [client, setClient] = useState<PublicClient | null>(null);
64
+
65
+ useEffect(() => {
66
+ if (!currentChain?.rpcUrl) {
67
+ setClient(null);
68
+ return;
69
+ }
70
+
71
+ const publicClient = createPublicClient({
72
+ transport: http(currentChain.rpcUrl),
73
+ chain: {
74
+ id: currentChain.id,
75
+ name: currentChain.name,
76
+ nativeCurrency: {
77
+ name: currentChain.token ?? "ETH",
78
+ symbol: currentChain.token ?? "ETH",
79
+ decimals: 18,
80
+ },
81
+ rpcUrls: {
82
+ default: { http: [currentChain.rpcUrl] },
83
+ public: { http: [currentChain.rpcUrl] },
84
+ },
85
+ },
86
+ });
87
+
88
+ setClient(publicClient);
89
+ }, [currentChain]);
90
+
91
+ // ERC-20 ABI fragment for balanceOf
92
+ const erc20Abi = [
93
+ {
94
+ name: "balanceOf",
95
+ type: "function",
96
+ stateMutability: "view",
97
+ inputs: [{ name: "owner", type: "address" }],
98
+ outputs: [{ name: "balance", type: "uint256" }],
99
+ },
100
+ ] as const;
101
+
102
+ const fetchTokenBalances = useCallback(async () => {
103
+ if (!evmAccount || !client || tokens.length === 0) {
104
+ setTokenBalances([]);
105
+ return;
106
+ }
107
+
108
+ setIsLoading(true);
109
+ setError(null);
110
+
111
+ try {
112
+ const address = evmAccount.includes(":")
113
+ ? (evmAccount.split(":").pop() as Address)
114
+ : (evmAccount as Address);
115
+
116
+ // Query all token balances with individual error handling
117
+ const results = await Promise.allSettled(
118
+ tokens.map(async (token) => {
119
+ try {
120
+ const balance = await client.readContract({
121
+ address: token.address,
122
+ abi: erc20Abi,
123
+ functionName: "balanceOf",
124
+ args: [address],
125
+ });
126
+ return {
127
+ address: token.address,
128
+ symbol: token.symbol,
129
+ decimals: token.decimals,
130
+ name: token.name,
131
+ balance: balance.toString(),
132
+ formatted: formatUnits(balance, token.decimals),
133
+ } as TokenBalanceResult;
134
+ } catch {
135
+ return {
136
+ address: token.address,
137
+ symbol: token.symbol,
138
+ decimals: token.decimals,
139
+ name: token.name,
140
+ balance: null,
141
+ formatted: null,
142
+ } as TokenBalanceResult;
143
+ }
144
+ })
145
+ );
146
+
147
+ const balances: TokenBalanceResult[] = results.map((r) => {
148
+ if (r.status === "fulfilled") {
149
+ return r.value;
150
+ }
151
+ // Fallback for rejected promises (shouldn't happen with inner catch)
152
+ return {
153
+ address: "" as Address,
154
+ symbol: "?",
155
+ decimals: 0,
156
+ balance: null,
157
+ formatted: null,
158
+ };
159
+ });
160
+
161
+ setTokenBalances(balances);
162
+ } catch (err) {
163
+ setError(err instanceof Error ? err : new Error("Failed to fetch token balances"));
164
+ } finally {
165
+ setIsLoading(false);
166
+ }
167
+ }, [evmAccount, client, tokensKey]);
168
+
169
+ // Initial fetch
170
+ useEffect(() => {
171
+ if (isConnected && evmAccount && tokens.length > 0) {
172
+ fetchTokenBalances();
173
+ }
174
+ }, [isConnected, evmAccount, tokensKey, fetchTokenBalances]);
175
+
176
+ // Auto-refresh interval
177
+ useEffect(() => {
178
+ if (refreshInterval && refreshInterval > 0 && isConnected && evmAccount && tokens.length > 0) {
179
+ intervalRef.current = setInterval(fetchTokenBalances, refreshInterval);
180
+ }
181
+ return () => {
182
+ if (intervalRef.current) {
183
+ clearInterval(intervalRef.current);
184
+ intervalRef.current = null;
185
+ }
186
+ };
187
+ }, [refreshInterval, isConnected, evmAccount, tokensKey, fetchTokenBalances]);
188
+
189
+ // Helper: get a single token balance by address
190
+ const getTokenBalance = useCallback(
191
+ (tokenAddress: Address): TokenBalanceResult | undefined => {
192
+ return tokenBalances.find(
193
+ (tb) => tb.address.toLowerCase() === tokenAddress.toLowerCase()
194
+ );
195
+ },
196
+ [tokenBalances]
197
+ );
198
+
199
+ return {
200
+ /** Array of token balance results */
201
+ tokenBalances,
202
+ isLoading,
203
+ error,
204
+ /** Re-fetch all token balances */
205
+ refetch: fetchTokenBalances,
206
+ /** Get a specific token balance by contract address */
207
+ getTokenBalance,
208
+ chain: currentChain,
209
+ };
210
+ }