@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,159 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
ChevronDownIcon,
|
|
6
|
+
SwapIcon,
|
|
7
|
+
SpinnerIcon,
|
|
8
|
+
CheckIcon,
|
|
9
|
+
ErrorIcon,
|
|
10
|
+
ExternalLinkIcon,
|
|
11
|
+
WalletIcon,
|
|
12
|
+
} from "../icons";
|
|
13
|
+
|
|
14
|
+
describe("ChevronDownIcon", () => {
|
|
15
|
+
it("renders an SVG element", () => {
|
|
16
|
+
const { container } = render(<ChevronDownIcon />);
|
|
17
|
+
const svg = container.querySelector("svg");
|
|
18
|
+
expect(svg).toBeDefined();
|
|
19
|
+
expect(svg?.getAttribute("aria-hidden")).toBe("true");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("applies default size", () => {
|
|
23
|
+
const { container } = render(<ChevronDownIcon />);
|
|
24
|
+
const svg = container.querySelector("svg");
|
|
25
|
+
expect(svg?.getAttribute("width")).toBe("16");
|
|
26
|
+
expect(svg?.getAttribute("height")).toBe("16");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("applies custom size", () => {
|
|
30
|
+
const { container } = render(<ChevronDownIcon size={24} />);
|
|
31
|
+
const svg = container.querySelector("svg");
|
|
32
|
+
expect(svg?.getAttribute("width")).toBe("24");
|
|
33
|
+
expect(svg?.getAttribute("height")).toBe("24");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("applies custom color", () => {
|
|
37
|
+
const { container } = render(<ChevronDownIcon color="#ff0000" />);
|
|
38
|
+
const svg = container.querySelector("svg");
|
|
39
|
+
expect(svg?.getAttribute("stroke")).toBe("#ff0000");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("applies custom style", () => {
|
|
43
|
+
const { container } = render(
|
|
44
|
+
<ChevronDownIcon style={{ opacity: 0.5 }} />
|
|
45
|
+
);
|
|
46
|
+
const svg = container.querySelector("svg");
|
|
47
|
+
expect(svg?.style.opacity).toBe("0.5");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("SwapIcon", () => {
|
|
52
|
+
it("renders an SVG element", () => {
|
|
53
|
+
const { container } = render(<SwapIcon />);
|
|
54
|
+
const svg = container.querySelector("svg");
|
|
55
|
+
expect(svg).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("has default size of 20", () => {
|
|
59
|
+
const { container } = render(<SwapIcon />);
|
|
60
|
+
const svg = container.querySelector("svg");
|
|
61
|
+
expect(svg?.getAttribute("width")).toBe("20");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("SpinnerIcon", () => {
|
|
66
|
+
it("renders an SVG element", () => {
|
|
67
|
+
const { container } = render(<SpinnerIcon />);
|
|
68
|
+
const svg = container.querySelector("svg");
|
|
69
|
+
expect(svg).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("has animation style", () => {
|
|
73
|
+
const { container } = render(<SpinnerIcon />);
|
|
74
|
+
const svg = container.querySelector("svg");
|
|
75
|
+
expect(svg?.style.animation).toContain("spin");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("CheckIcon", () => {
|
|
80
|
+
it("renders an SVG element", () => {
|
|
81
|
+
const { container } = render(<CheckIcon />);
|
|
82
|
+
const svg = container.querySelector("svg");
|
|
83
|
+
expect(svg).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("has check path", () => {
|
|
87
|
+
const { container } = render(<CheckIcon />);
|
|
88
|
+
const path = container.querySelector("path");
|
|
89
|
+
expect(path?.getAttribute("d")).toContain("5 13");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("ErrorIcon", () => {
|
|
94
|
+
it("renders an SVG element", () => {
|
|
95
|
+
const { container } = render(<ErrorIcon />);
|
|
96
|
+
const svg = container.querySelector("svg");
|
|
97
|
+
expect(svg).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("has X path", () => {
|
|
101
|
+
const { container } = render(<ErrorIcon />);
|
|
102
|
+
const path = container.querySelector("path");
|
|
103
|
+
expect(path?.getAttribute("d")).toContain("6 18");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("ExternalLinkIcon", () => {
|
|
108
|
+
it("renders an SVG element", () => {
|
|
109
|
+
const { container } = render(<ExternalLinkIcon />);
|
|
110
|
+
const svg = container.querySelector("svg");
|
|
111
|
+
expect(svg).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("has default size of 16", () => {
|
|
115
|
+
const { container } = render(<ExternalLinkIcon />);
|
|
116
|
+
const svg = container.querySelector("svg");
|
|
117
|
+
expect(svg?.getAttribute("width")).toBe("16");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("WalletIcon", () => {
|
|
122
|
+
it("renders an SVG element", () => {
|
|
123
|
+
const { container } = render(<WalletIcon />);
|
|
124
|
+
const svg = container.querySelector("svg");
|
|
125
|
+
expect(svg).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("has default size of 20", () => {
|
|
129
|
+
const { container } = render(<WalletIcon />);
|
|
130
|
+
const svg = container.querySelector("svg");
|
|
131
|
+
expect(svg?.getAttribute("width")).toBe("20");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("All icons", () => {
|
|
136
|
+
const icons = [
|
|
137
|
+
{ Component: ChevronDownIcon, name: "ChevronDownIcon" },
|
|
138
|
+
{ Component: SwapIcon, name: "SwapIcon" },
|
|
139
|
+
{ Component: SpinnerIcon, name: "SpinnerIcon" },
|
|
140
|
+
{ Component: CheckIcon, name: "CheckIcon" },
|
|
141
|
+
{ Component: ErrorIcon, name: "ErrorIcon" },
|
|
142
|
+
{ Component: ExternalLinkIcon, name: "ExternalLinkIcon" },
|
|
143
|
+
{ Component: WalletIcon, name: "WalletIcon" },
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
icons.forEach(({ Component, name }) => {
|
|
147
|
+
it(`${name} is accessible (aria-hidden)`, () => {
|
|
148
|
+
const { container } = render(<Component />);
|
|
149
|
+
const svg = container.querySelector("svg");
|
|
150
|
+
expect(svg?.getAttribute("aria-hidden")).toBe("true");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it(`${name} uses currentColor by default`, () => {
|
|
154
|
+
const { container } = render(<Component />);
|
|
155
|
+
const svg = container.querySelector("svg");
|
|
156
|
+
expect(svg?.getAttribute("stroke")).toBe("currentColor");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
defaultTheme,
|
|
4
|
+
mergeTheme,
|
|
5
|
+
themePresets,
|
|
6
|
+
THEME_COLORS,
|
|
7
|
+
THEME_SIZING,
|
|
8
|
+
THEME_FONTS,
|
|
9
|
+
} from "../theme";
|
|
10
|
+
|
|
11
|
+
describe("THEME_COLORS", () => {
|
|
12
|
+
it("has all required color keys", () => {
|
|
13
|
+
expect(THEME_COLORS.primary).toBeDefined();
|
|
14
|
+
expect(THEME_COLORS.secondary).toBeDefined();
|
|
15
|
+
expect(THEME_COLORS.success).toBeDefined();
|
|
16
|
+
expect(THEME_COLORS.error).toBeDefined();
|
|
17
|
+
expect(THEME_COLORS.text).toBeDefined();
|
|
18
|
+
expect(THEME_COLORS.mutedText).toBeDefined();
|
|
19
|
+
expect(THEME_COLORS.border).toBeDefined();
|
|
20
|
+
expect(THEME_COLORS.background).toBeDefined();
|
|
21
|
+
expect(THEME_COLORS.hover).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("has valid hex color for primary", () => {
|
|
25
|
+
expect(THEME_COLORS.primary).toMatch(/^#[0-9a-fA-F]{6}$/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("has valid rgba color for background", () => {
|
|
29
|
+
expect(THEME_COLORS.background).toContain("rgba");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("THEME_SIZING", () => {
|
|
34
|
+
it("has border radius as number", () => {
|
|
35
|
+
expect(typeof THEME_SIZING.borderRadius).toBe("number");
|
|
36
|
+
expect(THEME_SIZING.borderRadius).toBeGreaterThan(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("has valid max width", () => {
|
|
40
|
+
expect(THEME_SIZING.maxWidth).toBe("480px");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("THEME_FONTS", () => {
|
|
45
|
+
it("has font family string", () => {
|
|
46
|
+
expect(typeof THEME_FONTS.family).toBe("string");
|
|
47
|
+
expect(THEME_FONTS.family.length).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("has font sizes object", () => {
|
|
51
|
+
expect(THEME_FONTS.sizes.xs).toBeDefined();
|
|
52
|
+
expect(THEME_FONTS.sizes.sm).toBeDefined();
|
|
53
|
+
expect(THEME_FONTS.sizes.base).toBeDefined();
|
|
54
|
+
expect(THEME_FONTS.sizes.lg).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("has font weights object", () => {
|
|
58
|
+
expect(THEME_FONTS.weights.normal).toBe(400);
|
|
59
|
+
expect(THEME_FONTS.weights.medium).toBe(500);
|
|
60
|
+
expect(THEME_FONTS.weights.semibold).toBe(600);
|
|
61
|
+
expect(THEME_FONTS.weights.bold).toBe(700);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("defaultTheme", () => {
|
|
66
|
+
it("has all required properties", () => {
|
|
67
|
+
expect(defaultTheme.primaryColor).toBeDefined();
|
|
68
|
+
expect(defaultTheme.secondaryColor).toBeDefined();
|
|
69
|
+
expect(defaultTheme.backgroundColor).toBeDefined();
|
|
70
|
+
expect(defaultTheme.cardBackgroundColor).toBeDefined();
|
|
71
|
+
expect(defaultTheme.textColor).toBeDefined();
|
|
72
|
+
expect(defaultTheme.mutedTextColor).toBeDefined();
|
|
73
|
+
expect(defaultTheme.borderColor).toBeDefined();
|
|
74
|
+
expect(defaultTheme.successColor).toBeDefined();
|
|
75
|
+
expect(defaultTheme.errorColor).toBeDefined();
|
|
76
|
+
expect(defaultTheme.hoverColor).toBeDefined();
|
|
77
|
+
expect(defaultTheme.borderRadius).toBeDefined();
|
|
78
|
+
expect(defaultTheme.fontFamily).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("uses THEME_COLORS values", () => {
|
|
82
|
+
expect(defaultTheme.primaryColor).toBe(THEME_COLORS.primary);
|
|
83
|
+
expect(defaultTheme.successColor).toBe(THEME_COLORS.success);
|
|
84
|
+
expect(defaultTheme.errorColor).toBe(THEME_COLORS.error);
|
|
85
|
+
expect(defaultTheme.hoverColor).toBe(THEME_COLORS.hover);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("mergeTheme", () => {
|
|
90
|
+
it("returns default theme when no override provided", () => {
|
|
91
|
+
const result = mergeTheme();
|
|
92
|
+
expect(result).toEqual(defaultTheme);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns default theme when undefined provided", () => {
|
|
96
|
+
const result = mergeTheme(undefined);
|
|
97
|
+
expect(result).toEqual(defaultTheme);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("merges partial theme with defaults", () => {
|
|
101
|
+
const result = mergeTheme({ primaryColor: "#ff0000" });
|
|
102
|
+
expect(result.primaryColor).toBe("#ff0000");
|
|
103
|
+
expect(result.secondaryColor).toBe(defaultTheme.secondaryColor);
|
|
104
|
+
expect(result.backgroundColor).toBe(defaultTheme.backgroundColor);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("overrides multiple properties", () => {
|
|
108
|
+
const result = mergeTheme({
|
|
109
|
+
primaryColor: "#ff0000",
|
|
110
|
+
borderRadius: 20,
|
|
111
|
+
fontFamily: "Arial",
|
|
112
|
+
});
|
|
113
|
+
expect(result.primaryColor).toBe("#ff0000");
|
|
114
|
+
expect(result.borderRadius).toBe(20);
|
|
115
|
+
expect(result.fontFamily).toBe("Arial");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("themePresets", () => {
|
|
120
|
+
it("has dark preset matching default", () => {
|
|
121
|
+
expect(themePresets.dark).toEqual(defaultTheme);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("has light preset with inverted colors", () => {
|
|
125
|
+
expect(themePresets.light.textColor).not.toBe(defaultTheme.textColor);
|
|
126
|
+
expect(themePresets.light.backgroundColor).not.toBe(
|
|
127
|
+
defaultTheme.backgroundColor
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("has blue preset with blue primary color", () => {
|
|
132
|
+
expect(themePresets.blue.primaryColor).toBe("#3b82f6");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("has green preset with green primary color", () => {
|
|
136
|
+
expect(themePresets.green.primaryColor).toBe("#10b981");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("all presets have complete theme properties", () => {
|
|
140
|
+
Object.values(themePresets).forEach((preset) => {
|
|
141
|
+
expect(preset.primaryColor).toBeDefined();
|
|
142
|
+
expect(preset.secondaryColor).toBeDefined();
|
|
143
|
+
expect(preset.backgroundColor).toBeDefined();
|
|
144
|
+
expect(preset.hoverColor).toBeDefined();
|
|
145
|
+
expect(preset.borderRadius).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import { useBridge, useBridgeQuote, getBridgeChain, getChainName } from "../useBridge";
|
|
4
|
+
|
|
5
|
+
// Mock wagmi
|
|
6
|
+
vi.mock("wagmi", () => ({
|
|
7
|
+
useAccount: vi.fn(() => ({
|
|
8
|
+
connector: {
|
|
9
|
+
getProvider: vi.fn().mockResolvedValue({
|
|
10
|
+
request: vi.fn(),
|
|
11
|
+
}),
|
|
12
|
+
},
|
|
13
|
+
isConnected: true,
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock Circle Bridge Kit
|
|
18
|
+
vi.mock("@circle-fin/bridge-kit", () => ({
|
|
19
|
+
BridgeKit: vi.fn().mockImplementation(() => ({
|
|
20
|
+
on: vi.fn(),
|
|
21
|
+
bridge: vi.fn().mockResolvedValue({ txHash: "0x123" }),
|
|
22
|
+
})),
|
|
23
|
+
BridgeChain: {
|
|
24
|
+
Ethereum: "Ethereum",
|
|
25
|
+
Arbitrum: "Arbitrum",
|
|
26
|
+
Avalanche: "Avalanche",
|
|
27
|
+
Base: "Base",
|
|
28
|
+
Optimism: "Optimism",
|
|
29
|
+
Polygon: "Polygon",
|
|
30
|
+
Linea: "Linea",
|
|
31
|
+
Unichain: "Unichain",
|
|
32
|
+
Sonic: "Sonic",
|
|
33
|
+
World_Chain: "World_Chain",
|
|
34
|
+
Monad: "Monad",
|
|
35
|
+
Sei: "Sei",
|
|
36
|
+
XDC: "XDC",
|
|
37
|
+
HyperEVM: "HyperEVM",
|
|
38
|
+
Ink: "Ink",
|
|
39
|
+
Plume: "Plume",
|
|
40
|
+
Codex: "Codex",
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Mock adapter
|
|
45
|
+
vi.mock("@circle-fin/adapter-viem-v2", () => ({
|
|
46
|
+
createViemAdapterFromProvider: vi.fn().mockResolvedValue({}),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
describe("getBridgeChain", () => {
|
|
50
|
+
it("returns correct BridgeChain for known chain IDs", () => {
|
|
51
|
+
expect(getBridgeChain(1)).toBe("Ethereum");
|
|
52
|
+
expect(getBridgeChain(42161)).toBe("Arbitrum");
|
|
53
|
+
expect(getBridgeChain(8453)).toBe("Base");
|
|
54
|
+
expect(getBridgeChain(10)).toBe("Optimism");
|
|
55
|
+
expect(getBridgeChain(137)).toBe("Polygon");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns undefined for unknown chain IDs", () => {
|
|
59
|
+
expect(getBridgeChain(99999)).toBeUndefined();
|
|
60
|
+
expect(getBridgeChain(0)).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("getChainName", () => {
|
|
65
|
+
it("returns chain name for known chains", () => {
|
|
66
|
+
expect(getChainName(1)).toBe("Ethereum");
|
|
67
|
+
expect(getChainName(42161)).toBe("Arbitrum");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns fallback name for unknown chains", () => {
|
|
71
|
+
expect(getChainName(99999)).toBe("Chain_99999");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("useBridge", () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
vi.clearAllMocks();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("initializes with idle state", () => {
|
|
81
|
+
const { result } = renderHook(() => useBridge());
|
|
82
|
+
|
|
83
|
+
expect(result.current.state.status).toBe("idle");
|
|
84
|
+
expect(result.current.state.events).toEqual([]);
|
|
85
|
+
expect(result.current.state.txHash).toBeUndefined();
|
|
86
|
+
expect(result.current.state.error).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("provides bridge function", () => {
|
|
90
|
+
const { result } = renderHook(() => useBridge());
|
|
91
|
+
expect(typeof result.current.bridge).toBe("function");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("provides reset function", () => {
|
|
95
|
+
const { result } = renderHook(() => useBridge());
|
|
96
|
+
expect(typeof result.current.reset).toBe("function");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("reset clears state back to idle", () => {
|
|
100
|
+
const { result } = renderHook(() => useBridge());
|
|
101
|
+
|
|
102
|
+
act(() => {
|
|
103
|
+
result.current.reset();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.current.state.status).toBe("idle");
|
|
107
|
+
expect(result.current.state.events).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("useBridgeQuote", () => {
|
|
112
|
+
it("returns null quote when inputs are invalid", () => {
|
|
113
|
+
const { result } = renderHook(() =>
|
|
114
|
+
useBridgeQuote(undefined, undefined, "")
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(result.current.quote).toBeNull();
|
|
118
|
+
expect(result.current.isLoading).toBe(false);
|
|
119
|
+
expect(result.current.error).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns null quote for zero amount", () => {
|
|
123
|
+
const { result } = renderHook(() => useBridgeQuote(1, 8453, "0"));
|
|
124
|
+
|
|
125
|
+
expect(result.current.quote).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns null quote for negative amount", () => {
|
|
129
|
+
const { result } = renderHook(() => useBridgeQuote(1, 8453, "-100"));
|
|
130
|
+
|
|
131
|
+
expect(result.current.quote).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatNumber,
|
|
4
|
+
parseUSDCAmount,
|
|
5
|
+
isValidPositiveAmount,
|
|
6
|
+
getErrorMessage,
|
|
7
|
+
validateAmountInput,
|
|
8
|
+
validateChainConfig,
|
|
9
|
+
validateChainConfigs,
|
|
10
|
+
MAX_USDC_AMOUNT,
|
|
11
|
+
} from "../utils";
|
|
12
|
+
import type { BridgeChainConfig } from "../types";
|
|
13
|
+
|
|
14
|
+
describe("formatNumber", () => {
|
|
15
|
+
it("formats integers with decimal places", () => {
|
|
16
|
+
expect(formatNumber(1000)).toBe("1,000.00");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("formats decimal numbers", () => {
|
|
20
|
+
expect(formatNumber(1234.567, 2)).toBe("1,234.57");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("formats string numbers", () => {
|
|
24
|
+
expect(formatNumber("1000.5", 2)).toBe("1,000.50");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns '0' for NaN", () => {
|
|
28
|
+
expect(formatNumber("invalid")).toBe("0");
|
|
29
|
+
expect(formatNumber(NaN)).toBe("0");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("respects custom decimal places", () => {
|
|
33
|
+
expect(formatNumber(1234.56789, 4)).toBe("1,234.5679");
|
|
34
|
+
expect(formatNumber(1234.5, 0)).toBe("1,235");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("handles zero correctly", () => {
|
|
38
|
+
expect(formatNumber(0)).toBe("0.00");
|
|
39
|
+
expect(formatNumber("0")).toBe("0.00");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("parseUSDCAmount", () => {
|
|
44
|
+
it("parses valid amount strings", () => {
|
|
45
|
+
expect(parseUSDCAmount("100")).toBe(100000000n);
|
|
46
|
+
expect(parseUSDCAmount("1.5")).toBe(1500000n);
|
|
47
|
+
expect(parseUSDCAmount("0.000001")).toBe(1n);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns null for empty string", () => {
|
|
51
|
+
expect(parseUSDCAmount("")).toBe(null);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns null for negative amounts", () => {
|
|
55
|
+
expect(parseUSDCAmount("-100")).toBe(null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns null for invalid strings", () => {
|
|
59
|
+
expect(parseUSDCAmount("abc")).toBe(null);
|
|
60
|
+
expect(parseUSDCAmount("1.2.3")).toBe(null);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("handles zero", () => {
|
|
64
|
+
expect(parseUSDCAmount("0")).toBe(0n);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("isValidPositiveAmount", () => {
|
|
69
|
+
it("returns true for positive numbers", () => {
|
|
70
|
+
expect(isValidPositiveAmount("100")).toBe(true);
|
|
71
|
+
expect(isValidPositiveAmount("0.001")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns false for zero", () => {
|
|
75
|
+
expect(isValidPositiveAmount("0")).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns false for negative numbers", () => {
|
|
79
|
+
expect(isValidPositiveAmount("-100")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns false for empty string", () => {
|
|
83
|
+
expect(isValidPositiveAmount("")).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns false for invalid strings", () => {
|
|
87
|
+
expect(isValidPositiveAmount("abc")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("getErrorMessage", () => {
|
|
92
|
+
it("extracts message from Error objects", () => {
|
|
93
|
+
expect(getErrorMessage(new Error("Test error"))).toBe("Test error");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns string errors as-is", () => {
|
|
97
|
+
expect(getErrorMessage("String error")).toBe("String error");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("extracts message from error-like objects", () => {
|
|
101
|
+
expect(getErrorMessage({ message: "Object error" })).toBe("Object error");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns default message for unknown errors", () => {
|
|
105
|
+
expect(getErrorMessage(null)).toBe("An unknown error occurred");
|
|
106
|
+
expect(getErrorMessage(undefined)).toBe("An unknown error occurred");
|
|
107
|
+
expect(getErrorMessage(123)).toBe("An unknown error occurred");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("validateAmountInput", () => {
|
|
112
|
+
it("accepts empty string", () => {
|
|
113
|
+
const result = validateAmountInput("");
|
|
114
|
+
expect(result.isValid).toBe(true);
|
|
115
|
+
expect(result.sanitized).toBe("");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("accepts valid decimal numbers", () => {
|
|
119
|
+
expect(validateAmountInput("100").isValid).toBe(true);
|
|
120
|
+
expect(validateAmountInput("100.50").isValid).toBe(true);
|
|
121
|
+
expect(validateAmountInput("0.000001").isValid).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("rejects scientific notation", () => {
|
|
125
|
+
const result = validateAmountInput("1e6");
|
|
126
|
+
expect(result.isValid).toBe(false);
|
|
127
|
+
expect(result.error).toBe("Scientific notation not allowed");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects negative values", () => {
|
|
131
|
+
const result = validateAmountInput("-100");
|
|
132
|
+
expect(result.isValid).toBe(false);
|
|
133
|
+
expect(result.error).toBe("Negative values not allowed");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("accepts partial input like '.' and '0.'", () => {
|
|
137
|
+
expect(validateAmountInput(".").isValid).toBe(true);
|
|
138
|
+
expect(validateAmountInput("0.").isValid).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("truncates to max 6 decimal places", () => {
|
|
142
|
+
const result = validateAmountInput("1.12345678");
|
|
143
|
+
expect(result.isValid).toBe(false);
|
|
144
|
+
expect(result.sanitized).toBe("1.123456");
|
|
145
|
+
expect(result.error).toBe("Maximum 6 decimal places");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("removes leading zeros", () => {
|
|
149
|
+
const result = validateAmountInput("007");
|
|
150
|
+
expect(result.isValid).toBe(true);
|
|
151
|
+
expect(result.sanitized).toBe("7");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("preserves 0. prefix", () => {
|
|
155
|
+
const result = validateAmountInput("0.5");
|
|
156
|
+
expect(result.isValid).toBe(true);
|
|
157
|
+
expect(result.sanitized).toBe("0.5");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("validateChainConfig", () => {
|
|
162
|
+
const validConfig: BridgeChainConfig = {
|
|
163
|
+
chain: { id: 1, name: "Ethereum" } as BridgeChainConfig["chain"],
|
|
164
|
+
usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
165
|
+
tokenMessengerAddress: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
it("accepts valid chain config", () => {
|
|
169
|
+
const result = validateChainConfig(validConfig);
|
|
170
|
+
expect(result.isValid).toBe(true);
|
|
171
|
+
expect(result.errors).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("rejects missing chain object", () => {
|
|
175
|
+
const result = validateChainConfig({
|
|
176
|
+
chain: undefined as unknown as BridgeChainConfig["chain"],
|
|
177
|
+
usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
178
|
+
});
|
|
179
|
+
expect(result.isValid).toBe(false);
|
|
180
|
+
expect(result.errors).toContain("Chain object is required");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("rejects invalid chain ID", () => {
|
|
184
|
+
const result = validateChainConfig({
|
|
185
|
+
...validConfig,
|
|
186
|
+
chain: { id: -1, name: "Test" } as BridgeChainConfig["chain"],
|
|
187
|
+
});
|
|
188
|
+
expect(result.isValid).toBe(false);
|
|
189
|
+
expect(result.errors.some((e) => e.includes("Invalid chain ID"))).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("rejects missing USDC address", () => {
|
|
193
|
+
const result = validateChainConfig({
|
|
194
|
+
chain: { id: 1, name: "Ethereum" } as BridgeChainConfig["chain"],
|
|
195
|
+
usdcAddress: "" as `0x${string}`,
|
|
196
|
+
});
|
|
197
|
+
expect(result.isValid).toBe(false);
|
|
198
|
+
expect(result.errors.some((e) => e.includes("USDC address is required"))).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("rejects invalid USDC address format", () => {
|
|
202
|
+
const result = validateChainConfig({
|
|
203
|
+
...validConfig,
|
|
204
|
+
usdcAddress: "invalid-address" as `0x${string}`,
|
|
205
|
+
});
|
|
206
|
+
expect(result.isValid).toBe(false);
|
|
207
|
+
expect(result.errors.some((e) => e.includes("Invalid USDC address"))).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("validateChainConfigs", () => {
|
|
212
|
+
const validConfigs: BridgeChainConfig[] = [
|
|
213
|
+
{
|
|
214
|
+
chain: { id: 1, name: "Ethereum" } as BridgeChainConfig["chain"],
|
|
215
|
+
usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
chain: { id: 8453, name: "Base" } as BridgeChainConfig["chain"],
|
|
219
|
+
usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
it("accepts valid chain configs array", () => {
|
|
224
|
+
const result = validateChainConfigs(validConfigs);
|
|
225
|
+
expect(result.isValid).toBe(true);
|
|
226
|
+
expect(result.errors).toHaveLength(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("rejects non-array input", () => {
|
|
230
|
+
const result = validateChainConfigs("not an array" as unknown as BridgeChainConfig[]);
|
|
231
|
+
expect(result.isValid).toBe(false);
|
|
232
|
+
expect(result.errors).toContain("Chain configs must be an array");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("rejects empty array", () => {
|
|
236
|
+
const result = validateChainConfigs([]);
|
|
237
|
+
expect(result.isValid).toBe(false);
|
|
238
|
+
expect(result.errors).toContain("At least one chain configuration is required");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("rejects single chain (need at least 2 for bridging)", () => {
|
|
242
|
+
const result = validateChainConfigs([validConfigs[0]]);
|
|
243
|
+
expect(result.isValid).toBe(false);
|
|
244
|
+
expect(result.errors).toContain("At least two chains are required for bridging");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("rejects duplicate chain IDs", () => {
|
|
248
|
+
const result = validateChainConfigs([
|
|
249
|
+
validConfigs[0],
|
|
250
|
+
{ ...validConfigs[0] }, // Same chain ID
|
|
251
|
+
]);
|
|
252
|
+
expect(result.isValid).toBe(false);
|
|
253
|
+
expect(result.errors.some((e) => e.includes("Duplicate chain ID"))).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
});
|