@hongming-wang/usdc-bridge-widget 0.1.0

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.
@@ -0,0 +1,310 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import React from "react";
4
+ import { BridgeWidget } from "../BridgeWidget";
5
+
6
+ // Create mock functions that can be reconfigured
7
+ const mockUseAccount = vi.fn();
8
+ const mockUseChainId = vi.fn();
9
+ const mockUseSwitchChain = vi.fn();
10
+ const mockUseConnect = vi.fn();
11
+ const mockUseWaitForTransactionReceipt = vi.fn();
12
+ const mockUseUSDCBalance = vi.fn();
13
+ const mockUseAllUSDCBalances = vi.fn();
14
+ const mockUseUSDCAllowance = vi.fn();
15
+ const mockUseBridge = vi.fn();
16
+
17
+ // Mock wagmi hooks
18
+ vi.mock("wagmi", () => ({
19
+ useAccount: () => mockUseAccount(),
20
+ useChainId: () => mockUseChainId(),
21
+ useSwitchChain: () => mockUseSwitchChain(),
22
+ useWaitForTransactionReceipt: () => mockUseWaitForTransactionReceipt(),
23
+ useConnect: () => mockUseConnect(),
24
+ }));
25
+
26
+ // Mock custom hooks
27
+ vi.mock("../hooks", () => ({
28
+ useUSDCBalance: () => mockUseUSDCBalance(),
29
+ useAllUSDCBalances: () => mockUseAllUSDCBalances(),
30
+ useUSDCAllowance: () => mockUseUSDCAllowance(),
31
+ }));
32
+
33
+ // Mock useBridge hook
34
+ vi.mock("../useBridge", () => ({
35
+ useBridge: () => mockUseBridge(),
36
+ }));
37
+
38
+ // Mock chains
39
+ vi.mock("../chains", () => ({
40
+ DEFAULT_CHAIN_CONFIGS: [
41
+ {
42
+ chain: { id: 1, name: "Ethereum" },
43
+ usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
44
+ tokenMessengerAddress: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
45
+ iconUrl: "https://example.com/eth.png",
46
+ },
47
+ {
48
+ chain: { id: 8453, name: "Base" },
49
+ usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
50
+ tokenMessengerAddress: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
51
+ iconUrl: "https://example.com/base.png",
52
+ },
53
+ ],
54
+ }));
55
+
56
+ // Mock utils
57
+ vi.mock("../utils", () => ({
58
+ formatNumber: vi.fn((value: string | number) => {
59
+ const num = typeof value === "string" ? parseFloat(value) : value;
60
+ return isNaN(num)
61
+ ? "0"
62
+ : num.toLocaleString("en-US", {
63
+ minimumFractionDigits: 2,
64
+ maximumFractionDigits: 2,
65
+ });
66
+ }),
67
+ getErrorMessage: vi.fn((error: unknown) => {
68
+ if (error instanceof Error) return error.message;
69
+ return "Unknown error";
70
+ }),
71
+ validateAmountInput: vi.fn((value: string) => {
72
+ // Simple validation mock
73
+ if (value === "" || /^[0-9]*\.?[0-9]*$/.test(value)) {
74
+ return { isValid: true, sanitized: value };
75
+ }
76
+ return { isValid: false, sanitized: "" };
77
+ }),
78
+ validateChainConfigs: vi.fn(() => ({ isValid: true, errors: [] })),
79
+ }));
80
+
81
+ // Mock constants
82
+ vi.mock("../constants", () => ({
83
+ USDC_BRAND_COLOR: "#2775ca",
84
+ }));
85
+
86
+ // Default mock values
87
+ function setupDefaultMocks() {
88
+ mockUseAccount.mockReturnValue({
89
+ address: "0x1234567890123456789012345678901234567890",
90
+ isConnected: true,
91
+ });
92
+ mockUseChainId.mockReturnValue(1);
93
+ mockUseSwitchChain.mockReturnValue({
94
+ switchChainAsync: vi.fn(),
95
+ isPending: false,
96
+ });
97
+ mockUseConnect.mockReturnValue({
98
+ connect: vi.fn(),
99
+ connectors: [],
100
+ });
101
+ mockUseWaitForTransactionReceipt.mockReturnValue({
102
+ isLoading: false,
103
+ isSuccess: false,
104
+ });
105
+ mockUseUSDCBalance.mockReturnValue({
106
+ balance: 1000000000n,
107
+ balanceFormatted: "1000.00",
108
+ isLoading: false,
109
+ refetch: vi.fn(),
110
+ });
111
+ mockUseAllUSDCBalances.mockReturnValue({
112
+ balances: {
113
+ 1: { balance: 1000000000n, formatted: "1000.00" },
114
+ 8453: { balance: 500000000n, formatted: "500.00" },
115
+ },
116
+ isLoading: false,
117
+ refetch: vi.fn(),
118
+ });
119
+ mockUseUSDCAllowance.mockReturnValue({
120
+ allowance: 0n,
121
+ allowanceFormatted: "0",
122
+ isLoading: false,
123
+ isApproving: false,
124
+ approve: vi.fn(),
125
+ needsApproval: vi.fn(() => true),
126
+ refetch: vi.fn(),
127
+ approvalError: null,
128
+ });
129
+ mockUseBridge.mockReturnValue({
130
+ bridge: vi.fn(),
131
+ state: { status: "idle", events: [] },
132
+ reset: vi.fn(),
133
+ });
134
+ }
135
+
136
+ describe("BridgeWidget", () => {
137
+ beforeEach(() => {
138
+ vi.clearAllMocks();
139
+ setupDefaultMocks();
140
+ });
141
+
142
+ it("renders the widget container", () => {
143
+ render(<BridgeWidget />);
144
+ const widget = screen.getByRole("region", { name: "USDC Bridge Widget" });
145
+ expect(widget).toBeDefined();
146
+ });
147
+
148
+ it("renders source and destination chain selectors", () => {
149
+ render(<BridgeWidget />);
150
+ expect(screen.getByText("From")).toBeDefined();
151
+ expect(screen.getByText("To")).toBeDefined();
152
+ });
153
+
154
+ it("renders the amount input", () => {
155
+ render(<BridgeWidget />);
156
+ expect(screen.getByText("Amount")).toBeDefined();
157
+ expect(screen.getByPlaceholderText("0.00")).toBeDefined();
158
+ });
159
+
160
+ it("renders balance display", () => {
161
+ render(<BridgeWidget />);
162
+ expect(screen.getByText(/Balance:/)).toBeDefined();
163
+ });
164
+
165
+ it("renders MAX button", () => {
166
+ render(<BridgeWidget />);
167
+ const maxButton = screen.getByRole("button", {
168
+ name: "Set maximum amount",
169
+ });
170
+ expect(maxButton).toBeDefined();
171
+ });
172
+
173
+ it("renders swap button", () => {
174
+ render(<BridgeWidget />);
175
+ const swapButton = screen.getByRole("button", {
176
+ name: "Swap source and destination chains",
177
+ });
178
+ expect(swapButton).toBeDefined();
179
+ });
180
+
181
+ it("shows Approve & Bridge button when approval needed", () => {
182
+ render(<BridgeWidget />);
183
+ // Enter an amount first
184
+ const input = screen.getByPlaceholderText("0.00");
185
+ fireEvent.change(input, { target: { value: "100" } });
186
+
187
+ expect(screen.getByText("Approve & Bridge USDC")).toBeDefined();
188
+ });
189
+
190
+ it("shows Enter Amount when no amount is entered", () => {
191
+ render(<BridgeWidget />);
192
+ expect(screen.getByText("Enter Amount")).toBeDefined();
193
+ });
194
+
195
+ it("rejects scientific notation in amount input", () => {
196
+ render(<BridgeWidget />);
197
+ const input = screen.getByPlaceholderText("0.00") as HTMLInputElement;
198
+
199
+ // Try to enter scientific notation
200
+ fireEvent.change(input, { target: { value: "1e6" } });
201
+ expect(input.value).toBe(""); // Should be rejected
202
+
203
+ // Valid decimal should work
204
+ fireEvent.change(input, { target: { value: "100.50" } });
205
+ expect(input.value).toBe("100.50");
206
+ });
207
+
208
+ it("applies custom className", () => {
209
+ render(<BridgeWidget className="custom-class" />);
210
+ const widget = screen.getByRole("region", { name: "USDC Bridge Widget" });
211
+ expect(widget.className).toBe("custom-class");
212
+ });
213
+
214
+ it("applies custom style", () => {
215
+ render(<BridgeWidget style={{ backgroundColor: "red" }} />);
216
+ const widget = screen.getByRole("region", { name: "USDC Bridge Widget" });
217
+ expect(widget.style.backgroundColor).toBe("red");
218
+ });
219
+
220
+ it("calls onBridgeStart when bridge is initiated", async () => {
221
+ const onBridgeStart = vi.fn();
222
+ render(<BridgeWidget onBridgeStart={onBridgeStart} />);
223
+
224
+ // Enter an amount
225
+ const input = screen.getByPlaceholderText("0.00");
226
+ fireEvent.change(input, { target: { value: "100" } });
227
+
228
+ // Click the bridge button
229
+ const bridgeButton = screen.getByText("Approve & Bridge USDC");
230
+ fireEvent.click(bridgeButton);
231
+
232
+ // onBridgeStart should be called with the default chains
233
+ expect(onBridgeStart).toHaveBeenCalledWith(
234
+ expect.objectContaining({
235
+ sourceChainId: 1,
236
+ amount: "100",
237
+ })
238
+ );
239
+ });
240
+ });
241
+
242
+ describe("BridgeWidget - Disconnected State", () => {
243
+ beforeEach(() => {
244
+ vi.clearAllMocks();
245
+ setupDefaultMocks();
246
+ // Override to disconnected state
247
+ mockUseAccount.mockReturnValue({
248
+ address: undefined,
249
+ isConnected: false,
250
+ });
251
+ });
252
+
253
+ it("shows Connect Wallet button when disconnected", () => {
254
+ render(<BridgeWidget />);
255
+ expect(screen.getByText("Connect Wallet")).toBeDefined();
256
+ });
257
+
258
+ it("calls onConnectWallet when Connect Wallet is clicked", () => {
259
+ const onConnectWallet = vi.fn();
260
+ render(<BridgeWidget onConnectWallet={onConnectWallet} />);
261
+
262
+ const connectButton = screen.getByText("Connect Wallet");
263
+ fireEvent.click(connectButton);
264
+
265
+ expect(onConnectWallet).toHaveBeenCalled();
266
+ });
267
+ });
268
+
269
+ describe("BridgeWidget - Chain Switch Required", () => {
270
+ beforeEach(() => {
271
+ vi.clearAllMocks();
272
+ setupDefaultMocks();
273
+ // Connected to Base (8453) but source is Ethereum (1)
274
+ mockUseChainId.mockReturnValue(8453);
275
+ });
276
+
277
+ it("shows Switch Chain button when on wrong network", () => {
278
+ render(<BridgeWidget />);
279
+ expect(screen.getByText(/Switch to/)).toBeDefined();
280
+ });
281
+ });
282
+
283
+ describe("BridgeWidget - Accessibility", () => {
284
+ beforeEach(() => {
285
+ vi.clearAllMocks();
286
+ setupDefaultMocks();
287
+ });
288
+
289
+ it("has accessible labels for chain selectors", () => {
290
+ render(<BridgeWidget />);
291
+
292
+ // Chain selector buttons should have proper aria attributes
293
+ const buttons = screen.getAllByRole("button");
294
+ const chainButtons = buttons.filter(
295
+ (btn) => btn.getAttribute("aria-haspopup") === "listbox"
296
+ );
297
+
298
+ expect(chainButtons.length).toBe(2);
299
+ chainButtons.forEach((btn) => {
300
+ expect(btn.getAttribute("aria-expanded")).toBeDefined();
301
+ });
302
+ });
303
+
304
+ it("has accessible amount input", () => {
305
+ render(<BridgeWidget />);
306
+ const input = screen.getByPlaceholderText("0.00");
307
+ expect(input.getAttribute("aria-labelledby")).toBeDefined();
308
+ expect(input.getAttribute("aria-describedby")).toBeDefined();
309
+ });
310
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ createChainConfig,
4
+ DEFAULT_CHAIN_CONFIGS,
5
+ unichain,
6
+ hyperEvm,
7
+ plume,
8
+ monad,
9
+ codex,
10
+ mainnet,
11
+ } from "../chains";
12
+ import { USDC_ADDRESSES, TOKEN_MESSENGER_V2_ADDRESS } from "../constants";
13
+
14
+ describe("Custom Chain Definitions", () => {
15
+ it("unichain has correct chain ID", () => {
16
+ expect(unichain.id).toBe(130);
17
+ expect(unichain.name).toBe("Unichain");
18
+ });
19
+
20
+ it("hyperEvm has correct chain ID", () => {
21
+ expect(hyperEvm.id).toBe(999);
22
+ expect(hyperEvm.name).toBe("HyperEVM");
23
+ });
24
+
25
+ it("plume has correct chain ID", () => {
26
+ expect(plume.id).toBe(98866);
27
+ expect(plume.name).toBe("Plume");
28
+ });
29
+
30
+ it("monad has correct chain ID", () => {
31
+ expect(monad.id).toBe(10200);
32
+ expect(monad.name).toBe("Monad");
33
+ });
34
+
35
+ it("codex has correct chain ID", () => {
36
+ expect(codex.id).toBe(81224);
37
+ expect(codex.name).toBe("Codex");
38
+ });
39
+
40
+ it("all custom chains have valid RPC URLs", () => {
41
+ [unichain, hyperEvm, plume, monad, codex].forEach((chain) => {
42
+ expect(chain.rpcUrls.default.http).toBeDefined();
43
+ expect(chain.rpcUrls.default.http.length).toBeGreaterThan(0);
44
+ });
45
+ });
46
+ });
47
+
48
+ describe("createChainConfig", () => {
49
+ it("creates config with default addresses from constants", () => {
50
+ const config = createChainConfig(mainnet);
51
+ expect(config.chain).toBe(mainnet);
52
+ expect(config.usdcAddress).toBe(USDC_ADDRESSES[1]);
53
+ expect(config.tokenMessengerAddress).toBe(TOKEN_MESSENGER_V2_ADDRESS);
54
+ });
55
+
56
+ it("allows overriding USDC address", () => {
57
+ const customAddress = "0x1234567890123456789012345678901234567890" as const;
58
+ const config = createChainConfig(mainnet, { usdcAddress: customAddress });
59
+ expect(config.usdcAddress).toBe(customAddress);
60
+ });
61
+
62
+ it("allows overriding token messenger address", () => {
63
+ const customAddress = "0x1234567890123456789012345678901234567890" as const;
64
+ const config = createChainConfig(mainnet, {
65
+ tokenMessengerAddress: customAddress,
66
+ });
67
+ expect(config.tokenMessengerAddress).toBe(customAddress);
68
+ });
69
+
70
+ it("allows overriding icon URL", () => {
71
+ const customIcon = "https://example.com/icon.png";
72
+ const config = createChainConfig(mainnet, { iconUrl: customIcon });
73
+ expect(config.iconUrl).toBe(customIcon);
74
+ });
75
+ });
76
+
77
+ describe("DEFAULT_CHAIN_CONFIGS", () => {
78
+ it("has multiple chain configurations", () => {
79
+ expect(DEFAULT_CHAIN_CONFIGS.length).toBeGreaterThan(10);
80
+ });
81
+
82
+ it("all configs have valid chain objects", () => {
83
+ DEFAULT_CHAIN_CONFIGS.forEach((config) => {
84
+ expect(config.chain).toBeDefined();
85
+ expect(config.chain.id).toBeTypeOf("number");
86
+ expect(config.chain.name).toBeTypeOf("string");
87
+ });
88
+ });
89
+
90
+ it("all configs have valid USDC addresses", () => {
91
+ DEFAULT_CHAIN_CONFIGS.forEach((config) => {
92
+ expect(config.usdcAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
93
+ });
94
+ });
95
+
96
+ it("all configs have token messenger addresses", () => {
97
+ DEFAULT_CHAIN_CONFIGS.forEach((config) => {
98
+ expect(config.tokenMessengerAddress).toBeDefined();
99
+ expect(config.tokenMessengerAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
100
+ });
101
+ });
102
+
103
+ it("includes Ethereum mainnet", () => {
104
+ const ethereumConfig = DEFAULT_CHAIN_CONFIGS.find(
105
+ (c) => c.chain.id === 1
106
+ );
107
+ expect(ethereumConfig).toBeDefined();
108
+ expect(ethereumConfig?.chain.name).toBe("Ethereum");
109
+ });
110
+
111
+ it("includes all supported custom chains", () => {
112
+ // Note: Monad (10200) is not included as it's not yet supported by Circle Bridge Kit
113
+ const supportedCustomChainIds = [130, 999, 98866, 81224]; // unichain, hyperEvm, plume, codex
114
+ supportedCustomChainIds.forEach((id) => {
115
+ const config = DEFAULT_CHAIN_CONFIGS.find((c) => c.chain.id === id);
116
+ expect(config).toBeDefined();
117
+ });
118
+ });
119
+
120
+ it("monad chain is exported but not in defaults (not yet supported)", () => {
121
+ // Monad is defined for future use but not in DEFAULT_CHAIN_CONFIGS
122
+ const monadConfig = DEFAULT_CHAIN_CONFIGS.find((c) => c.chain.id === 10200);
123
+ expect(monadConfig).toBeUndefined();
124
+ });
125
+
126
+ it("has no duplicate chain IDs", () => {
127
+ const chainIds = DEFAULT_CHAIN_CONFIGS.map((c) => c.chain.id);
128
+ const uniqueIds = new Set(chainIds);
129
+ expect(uniqueIds.size).toBe(chainIds.length);
130
+ });
131
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ USDC_DECIMALS,
4
+ USDC_ADDRESSES,
5
+ TOKEN_MESSENGER_V2_ADDRESS,
6
+ TOKEN_MESSENGER_ADDRESSES,
7
+ CHAIN_ICONS,
8
+ MAX_USDC_AMOUNT,
9
+ MIN_USDC_AMOUNT,
10
+ DEFAULT_LOCALE,
11
+ } from "../constants";
12
+
13
+ describe("USDC Constants", () => {
14
+ it("has correct USDC decimals", () => {
15
+ expect(USDC_DECIMALS).toBe(6);
16
+ });
17
+
18
+ it("has USDC address for Ethereum mainnet", () => {
19
+ expect(USDC_ADDRESSES[1]).toBe(
20
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
21
+ );
22
+ });
23
+
24
+ it("all USDC addresses are valid hex strings", () => {
25
+ Object.values(USDC_ADDRESSES).forEach((address) => {
26
+ expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
27
+ });
28
+ });
29
+ });
30
+
31
+ describe("Token Messenger Constants", () => {
32
+ it("has valid V2 address", () => {
33
+ expect(TOKEN_MESSENGER_V2_ADDRESS).toMatch(/^0x[a-fA-F0-9]{40}$/);
34
+ });
35
+
36
+ it("all chain messenger addresses use V2 address", () => {
37
+ Object.values(TOKEN_MESSENGER_ADDRESSES).forEach((address) => {
38
+ expect(address).toBe(TOKEN_MESSENGER_V2_ADDRESS);
39
+ });
40
+ });
41
+
42
+ it("has messenger addresses for all USDC chains", () => {
43
+ Object.keys(USDC_ADDRESSES).forEach((chainId) => {
44
+ expect(TOKEN_MESSENGER_ADDRESSES[Number(chainId)]).toBeDefined();
45
+ });
46
+ });
47
+ });
48
+
49
+ describe("Chain Icons", () => {
50
+ it("has icon URLs for major chains", () => {
51
+ expect(CHAIN_ICONS[1]).toBeDefined(); // Ethereum
52
+ expect(CHAIN_ICONS[42161]).toBeDefined(); // Arbitrum
53
+ expect(CHAIN_ICONS[8453]).toBeDefined(); // Base
54
+ });
55
+
56
+ it("all icon URLs are valid URLs", () => {
57
+ Object.values(CHAIN_ICONS).forEach((url) => {
58
+ expect(url).toMatch(/^https?:\/\/.+/);
59
+ });
60
+ });
61
+ });
62
+
63
+ describe("Amount Constants", () => {
64
+ it("MAX_USDC_AMOUNT is a reasonable value (100 billion)", () => {
65
+ expect(MAX_USDC_AMOUNT).toBe("100000000000");
66
+ expect(parseFloat(MAX_USDC_AMOUNT)).toBe(100_000_000_000);
67
+ });
68
+
69
+ it("MIN_USDC_AMOUNT is smallest USDC unit", () => {
70
+ expect(MIN_USDC_AMOUNT).toBe("0.000001");
71
+ expect(parseFloat(MIN_USDC_AMOUNT)).toBe(0.000001);
72
+ });
73
+
74
+ it("DEFAULT_LOCALE is en-US for consistent financial display", () => {
75
+ expect(DEFAULT_LOCALE).toBe("en-US");
76
+ });
77
+ });
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useFormatNumber, useAllUSDCBalances } from "../hooks";
4
+ import type { BridgeChainConfig } from "../types";
5
+
6
+ // Create mock functions
7
+ const mockUseReadContracts = vi.fn();
8
+
9
+ // Mock wagmi hooks
10
+ vi.mock("wagmi", () => ({
11
+ useAccount: vi.fn(() => ({ address: "0x1234567890123456789012345678901234567890" })),
12
+ useReadContract: vi.fn(() => ({
13
+ data: 1000000000n,
14
+ isLoading: false,
15
+ refetch: vi.fn(),
16
+ })),
17
+ useReadContracts: () => mockUseReadContracts(),
18
+ useWriteContract: vi.fn(() => ({
19
+ writeContractAsync: vi.fn(),
20
+ isPending: false,
21
+ })),
22
+ useWaitForTransactionReceipt: vi.fn(() => ({
23
+ isLoading: false,
24
+ isSuccess: false,
25
+ })),
26
+ }));
27
+
28
+ describe("useFormatNumber", () => {
29
+ it("returns a formatting function", () => {
30
+ const { result } = renderHook(() => useFormatNumber());
31
+ expect(typeof result.current).toBe("function");
32
+ });
33
+
34
+ it("formats numbers correctly", () => {
35
+ const { result } = renderHook(() => useFormatNumber());
36
+ expect(result.current(1000, 2)).toBe("1,000.00");
37
+ });
38
+
39
+ it("returns stable function reference", () => {
40
+ const { result, rerender } = renderHook(() => useFormatNumber());
41
+ const firstRef = result.current;
42
+ rerender();
43
+ expect(result.current).toBe(firstRef);
44
+ });
45
+ });
46
+
47
+ describe("useAllUSDCBalances", () => {
48
+ const mockChainConfigs: BridgeChainConfig[] = [
49
+ {
50
+ chain: { id: 1, name: "Ethereum" } as BridgeChainConfig["chain"],
51
+ usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
52
+ },
53
+ {
54
+ chain: { id: 8453, name: "Base" } as BridgeChainConfig["chain"],
55
+ usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
56
+ },
57
+ ];
58
+
59
+ beforeEach(() => {
60
+ vi.clearAllMocks();
61
+ mockUseReadContracts.mockReturnValue({
62
+ data: [
63
+ { status: "success", result: 1000000000n },
64
+ { status: "success", result: 500000000n },
65
+ ],
66
+ isLoading: false,
67
+ refetch: vi.fn(),
68
+ });
69
+ });
70
+
71
+ it("returns balances for all chains", () => {
72
+ const { result } = renderHook(() => useAllUSDCBalances(mockChainConfigs));
73
+
74
+ expect(result.current.balances[1]).toBeDefined();
75
+ expect(result.current.balances[8453]).toBeDefined();
76
+ expect(result.current.balances[1].balance).toBe(1000000000n);
77
+ expect(result.current.balances[8453].balance).toBe(500000000n);
78
+ });
79
+
80
+ it("formats balances correctly", () => {
81
+ const { result } = renderHook(() => useAllUSDCBalances(mockChainConfigs));
82
+
83
+ expect(result.current.balances[1].formatted).toBe("1000");
84
+ expect(result.current.balances[8453].formatted).toBe("500");
85
+ });
86
+
87
+ it("returns loading state", () => {
88
+ mockUseReadContracts.mockReturnValue({
89
+ data: undefined,
90
+ isLoading: true,
91
+ refetch: vi.fn(),
92
+ });
93
+
94
+ const { result } = renderHook(() => useAllUSDCBalances(mockChainConfigs));
95
+ expect(result.current.isLoading).toBe(true);
96
+ });
97
+
98
+ it("handles empty results", () => {
99
+ mockUseReadContracts.mockReturnValue({
100
+ data: undefined,
101
+ isLoading: false,
102
+ refetch: vi.fn(),
103
+ });
104
+
105
+ const { result } = renderHook(() => useAllUSDCBalances(mockChainConfigs));
106
+ expect(result.current.balances).toEqual({});
107
+ });
108
+
109
+ it("handles failed contract calls", () => {
110
+ mockUseReadContracts.mockReturnValue({
111
+ data: [
112
+ { status: "failure", error: new Error("Failed") },
113
+ { status: "success", result: 500000000n },
114
+ ],
115
+ isLoading: false,
116
+ refetch: vi.fn(),
117
+ });
118
+
119
+ const { result } = renderHook(() => useAllUSDCBalances(mockChainConfigs));
120
+
121
+ // Failed call should return 0
122
+ expect(result.current.balances[1].balance).toBe(0n);
123
+ expect(result.current.balances[1].formatted).toBe("0");
124
+ // Successful call should work
125
+ expect(result.current.balances[8453].balance).toBe(500000000n);
126
+ });
127
+ });