@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.
- package/README.md +272 -0
- package/dist/chunk-6JW37N76.mjs +211 -0
- package/dist/chunk-GJBJYQCU.mjs +218 -0
- package/dist/chunk-JHG7XCWW.mjs +218 -0
- package/dist/index.d.mts +765 -0
- package/dist/index.d.ts +765 -0
- package/dist/index.js +2356 -0
- package/dist/index.mjs +2295 -0
- package/dist/useBridge-LDEXWLEC.mjs +10 -0
- package/dist/useBridge-VGN5DMO6.mjs +10 -0
- package/dist/useBridge-WJA4XLLR.mjs +10 -0
- package/package.json +63 -0
- package/src/BridgeWidget.tsx +1133 -0
- package/src/__tests__/BridgeWidget.test.tsx +310 -0
- package/src/__tests__/chains.test.ts +131 -0
- package/src/__tests__/constants.test.ts +77 -0
- package/src/__tests__/hooks.test.ts +127 -0
- package/src/__tests__/icons.test.tsx +159 -0
- package/src/__tests__/setup.ts +8 -0
- package/src/__tests__/theme.test.ts +148 -0
- package/src/__tests__/useBridge.test.ts +133 -0
- package/src/__tests__/utils.test.ts +255 -0
- package/src/chains.ts +209 -0
- package/src/constants.ts +97 -0
- package/src/hooks.ts +349 -0
- package/src/icons.tsx +228 -0
- package/src/index.tsx +111 -0
- package/src/theme.ts +131 -0
- package/src/types.ts +160 -0
- package/src/useBridge.ts +424 -0
- package/src/utils.ts +239 -0
|
@@ -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
|
+
});
|