@hongming-wang/usdc-bridge-widget 0.1.0 → 0.2.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 +17 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +97 -63
- package/dist/index.mjs +97 -63
- package/package.json +10 -10
- package/src/BridgeWidget.tsx +132 -69
- package/src/__tests__/BridgeWidget.test.tsx +29 -0
- package/src/__tests__/hooks.test.ts +20 -1
- package/src/hooks.ts +12 -3
- package/src/types.ts +2 -0
package/src/BridgeWidget.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, useId, useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useAccount,
|
|
4
4
|
useChainId,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
BridgeWidgetTheme,
|
|
12
12
|
BridgeChainConfig,
|
|
13
13
|
} from "./types";
|
|
14
|
-
import {
|
|
14
|
+
import { useUSDCAllowance, useAllUSDCBalances } from "./hooks";
|
|
15
15
|
import { useBridge } from "./useBridge";
|
|
16
16
|
import { DEFAULT_CHAIN_CONFIGS } from "./chains";
|
|
17
17
|
import { USDC_BRAND_COLOR } from "./constants";
|
|
@@ -19,6 +19,46 @@ import { formatNumber, getErrorMessage, validateAmountInput, validateChainConfig
|
|
|
19
19
|
import { mergeTheme } from "./theme";
|
|
20
20
|
import { ChevronDownIcon, SwapIcon } from "./icons";
|
|
21
21
|
|
|
22
|
+
// Constants
|
|
23
|
+
const TYPE_AHEAD_RESET_MS = 1000;
|
|
24
|
+
const DROPDOWN_MAX_HEIGHT = 300;
|
|
25
|
+
const BOX_SHADOW_COLOR = "rgba(0,0,0,0.3)";
|
|
26
|
+
const DISABLED_BUTTON_BACKGROUND = "rgba(255,255,255,0.1)";
|
|
27
|
+
|
|
28
|
+
// Helper function for borderless styles
|
|
29
|
+
function getBorderlessStyles(
|
|
30
|
+
borderless: boolean | undefined,
|
|
31
|
+
theme: Required<BridgeWidgetTheme>,
|
|
32
|
+
options?: { includeBoxShadow?: boolean; useBackgroundColor?: boolean }
|
|
33
|
+
) {
|
|
34
|
+
const bgColor = options?.useBackgroundColor
|
|
35
|
+
? theme.backgroundColor
|
|
36
|
+
: theme.cardBackgroundColor;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
borderRadius: borderless ? 0 : `${theme.borderRadius}px`,
|
|
40
|
+
background: borderless ? "transparent" : bgColor,
|
|
41
|
+
border: borderless ? "none" : `1px solid ${theme.borderColor}`,
|
|
42
|
+
...(options?.includeBoxShadow && {
|
|
43
|
+
boxShadow: borderless ? "none" : `0 4px 24px ${BOX_SHADOW_COLOR}`,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Shared keyframes style - injected once per document
|
|
49
|
+
const SPINNER_KEYFRAMES = `@keyframes cc-balance-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`;
|
|
50
|
+
const KEYFRAMES_ATTR = "data-cc-spinner-keyframes";
|
|
51
|
+
|
|
52
|
+
function injectSpinnerKeyframes() {
|
|
53
|
+
if (typeof document === "undefined") return;
|
|
54
|
+
// Check if already injected using a data attribute on the style element
|
|
55
|
+
if (document.querySelector(`style[${KEYFRAMES_ATTR}]`)) return;
|
|
56
|
+
const style = document.createElement("style");
|
|
57
|
+
style.setAttribute(KEYFRAMES_ATTR, "true");
|
|
58
|
+
style.textContent = SPINNER_KEYFRAMES;
|
|
59
|
+
document.head.appendChild(style);
|
|
60
|
+
}
|
|
61
|
+
|
|
22
62
|
// Chain icon with fallback
|
|
23
63
|
function ChainIcon({
|
|
24
64
|
chainConfig,
|
|
@@ -31,6 +71,11 @@ function ChainIcon({
|
|
|
31
71
|
}) {
|
|
32
72
|
const [hasError, setHasError] = useState(false);
|
|
33
73
|
|
|
74
|
+
// Reset error state when chainConfig changes
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
setHasError(false);
|
|
77
|
+
}, [chainConfig.iconUrl]);
|
|
78
|
+
|
|
34
79
|
if (!chainConfig.iconUrl || hasError) {
|
|
35
80
|
return (
|
|
36
81
|
<div
|
|
@@ -66,6 +111,11 @@ function ChainIcon({
|
|
|
66
111
|
|
|
67
112
|
// Small loading spinner for balance display
|
|
68
113
|
function BalanceSpinner({ size = 12 }: { size?: number }) {
|
|
114
|
+
// Inject keyframes once on first render
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
injectSpinnerKeyframes();
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
69
119
|
return (
|
|
70
120
|
<svg
|
|
71
121
|
width={size}
|
|
@@ -80,7 +130,6 @@ function BalanceSpinner({ size = 12 }: { size?: number }) {
|
|
|
80
130
|
}}
|
|
81
131
|
aria-hidden="true"
|
|
82
132
|
>
|
|
83
|
-
<style>{`@keyframes cc-balance-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}</style>
|
|
84
133
|
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
85
134
|
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
|
86
135
|
</svg>
|
|
@@ -99,6 +148,7 @@ function ChainSelector({
|
|
|
99
148
|
balances,
|
|
100
149
|
isLoadingBalances,
|
|
101
150
|
disabled,
|
|
151
|
+
borderless,
|
|
102
152
|
}: {
|
|
103
153
|
label: string;
|
|
104
154
|
chains: BridgeChainConfig[];
|
|
@@ -110,6 +160,7 @@ function ChainSelector({
|
|
|
110
160
|
balances?: Record<number, { balance: bigint; formatted: string }>;
|
|
111
161
|
isLoadingBalances?: boolean;
|
|
112
162
|
disabled?: boolean;
|
|
163
|
+
borderless?: boolean;
|
|
113
164
|
}) {
|
|
114
165
|
const [isOpen, setIsOpen] = useState(false);
|
|
115
166
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
@@ -117,8 +168,11 @@ function ChainSelector({
|
|
|
117
168
|
const typeAheadTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
118
169
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
119
170
|
const listRef = useRef<HTMLUListElement>(null);
|
|
120
|
-
|
|
121
|
-
|
|
171
|
+
|
|
172
|
+
// Memoize filtered chains to avoid recalculation on every render
|
|
173
|
+
const availableChains = useMemo(
|
|
174
|
+
() => chains.filter((c) => c.chain.id !== excludeChainId),
|
|
175
|
+
[chains, excludeChainId]
|
|
122
176
|
);
|
|
123
177
|
|
|
124
178
|
// Clear type-ahead timer on unmount
|
|
@@ -189,10 +243,10 @@ function ChainSelector({
|
|
|
189
243
|
clearTimeout(typeAheadTimeoutRef.current);
|
|
190
244
|
}
|
|
191
245
|
|
|
192
|
-
// Reset type-ahead after
|
|
246
|
+
// Reset type-ahead after timeout
|
|
193
247
|
typeAheadTimeoutRef.current = setTimeout(() => {
|
|
194
248
|
setTypeAhead("");
|
|
195
|
-
},
|
|
249
|
+
}, TYPE_AHEAD_RESET_MS);
|
|
196
250
|
|
|
197
251
|
// Find matching chain
|
|
198
252
|
const matchIndex = availableChains.findIndex((chain) =>
|
|
@@ -280,9 +334,7 @@ function ChainSelector({
|
|
|
280
334
|
alignItems: "center",
|
|
281
335
|
justifyContent: "space-between",
|
|
282
336
|
padding: "10px 12px",
|
|
283
|
-
|
|
284
|
-
background: theme.cardBackgroundColor,
|
|
285
|
-
border: `1px solid ${theme.borderColor}`,
|
|
337
|
+
...getBorderlessStyles(borderless, theme),
|
|
286
338
|
cursor: disabled ? "not-allowed" : "pointer",
|
|
287
339
|
opacity: disabled ? 0.6 : 1,
|
|
288
340
|
transition: "all 0.2s",
|
|
@@ -312,7 +364,7 @@ function ChainSelector({
|
|
|
312
364
|
>
|
|
313
365
|
<BalanceSpinner size={10} /> Loading...
|
|
314
366
|
</span>
|
|
315
|
-
) : selectedBalance ? (
|
|
367
|
+
) : balances && selectedBalance ? (
|
|
316
368
|
<span
|
|
317
369
|
style={{
|
|
318
370
|
fontSize: "10px",
|
|
@@ -363,11 +415,11 @@ function ChainSelector({
|
|
|
363
415
|
width: "100%",
|
|
364
416
|
marginTop: "8px",
|
|
365
417
|
borderRadius: `${theme.borderRadius}px`,
|
|
366
|
-
boxShadow:
|
|
418
|
+
boxShadow: `0 10px 40px ${BOX_SHADOW_COLOR}`,
|
|
367
419
|
background: theme.cardBackgroundColor,
|
|
368
420
|
backdropFilter: "blur(10px)",
|
|
369
421
|
border: `1px solid ${theme.borderColor}`,
|
|
370
|
-
maxHeight:
|
|
422
|
+
maxHeight: `${DROPDOWN_MAX_HEIGHT}px`,
|
|
371
423
|
overflowY: "auto",
|
|
372
424
|
overflowX: "hidden",
|
|
373
425
|
padding: 0,
|
|
@@ -380,6 +432,8 @@ function ChainSelector({
|
|
|
380
432
|
const chainBalance = balances?.[chainConfig.chain.id];
|
|
381
433
|
const isFocused = index === focusedIndex;
|
|
382
434
|
const isSelected = chainConfig.chain.id === selectedChain.chain.id;
|
|
435
|
+
// Pre-compute parsed balance to avoid parseFloat in render
|
|
436
|
+
const hasPositiveBalance = chainBalance ? parseFloat(chainBalance.formatted) > 0 : false;
|
|
383
437
|
|
|
384
438
|
return (
|
|
385
439
|
<li
|
|
@@ -428,25 +482,16 @@ function ChainSelector({
|
|
|
428
482
|
>
|
|
429
483
|
<BalanceSpinner size={10} />
|
|
430
484
|
</span>
|
|
431
|
-
) : chainBalance ? (
|
|
485
|
+
) : balances && chainBalance ? (
|
|
432
486
|
<span
|
|
433
487
|
style={{
|
|
434
488
|
fontSize: "10px",
|
|
435
|
-
color:
|
|
489
|
+
color: hasPositiveBalance ? theme.successColor : theme.mutedTextColor,
|
|
436
490
|
}}
|
|
437
491
|
>
|
|
438
492
|
{formatNumber(chainBalance.formatted, 2)} USDC
|
|
439
493
|
</span>
|
|
440
|
-
) :
|
|
441
|
-
<span
|
|
442
|
-
style={{
|
|
443
|
-
fontSize: "10px",
|
|
444
|
-
color: theme.mutedTextColor,
|
|
445
|
-
}}
|
|
446
|
-
>
|
|
447
|
-
0.00 USDC
|
|
448
|
-
</span>
|
|
449
|
-
)}
|
|
494
|
+
) : null}
|
|
450
495
|
</div>
|
|
451
496
|
</li>
|
|
452
497
|
);
|
|
@@ -507,6 +552,8 @@ function AmountInput({
|
|
|
507
552
|
theme,
|
|
508
553
|
id,
|
|
509
554
|
disabled,
|
|
555
|
+
showBalance = true,
|
|
556
|
+
borderless,
|
|
510
557
|
}: {
|
|
511
558
|
value: string;
|
|
512
559
|
onChange: (value: string) => void;
|
|
@@ -515,6 +562,8 @@ function AmountInput({
|
|
|
515
562
|
theme: Required<BridgeWidgetTheme>;
|
|
516
563
|
id: string;
|
|
517
564
|
disabled?: boolean;
|
|
565
|
+
showBalance?: boolean;
|
|
566
|
+
borderless?: boolean;
|
|
518
567
|
}) {
|
|
519
568
|
const inputId = `${id}-input`;
|
|
520
569
|
const labelId = `${id}-label`;
|
|
@@ -564,24 +613,24 @@ function AmountInput({
|
|
|
564
613
|
>
|
|
565
614
|
Amount
|
|
566
615
|
</label>
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
{
|
|
616
|
+
{showBalance && (
|
|
617
|
+
<span
|
|
618
|
+
style={{ fontSize: "10px", color: theme.mutedTextColor }}
|
|
619
|
+
aria-live="polite"
|
|
620
|
+
>
|
|
621
|
+
Balance:{" "}
|
|
622
|
+
<span style={{ color: theme.textColor }}>
|
|
623
|
+
{formatNumber(balance)} USDC
|
|
624
|
+
</span>
|
|
574
625
|
</span>
|
|
575
|
-
|
|
626
|
+
)}
|
|
576
627
|
</div>
|
|
577
628
|
<div
|
|
578
629
|
style={{
|
|
579
630
|
display: "flex",
|
|
580
631
|
alignItems: "center",
|
|
581
|
-
borderRadius: `${theme.borderRadius}px`,
|
|
582
632
|
overflow: "hidden",
|
|
583
|
-
|
|
584
|
-
border: `1px solid ${theme.borderColor}`,
|
|
633
|
+
...getBorderlessStyles(borderless, theme),
|
|
585
634
|
opacity: disabled ? 0.6 : 1,
|
|
586
635
|
}}
|
|
587
636
|
>
|
|
@@ -690,6 +739,7 @@ export function BridgeWidget({
|
|
|
690
739
|
onBridgeError,
|
|
691
740
|
onConnectWallet,
|
|
692
741
|
theme: themeOverrides,
|
|
742
|
+
borderless = false,
|
|
693
743
|
className,
|
|
694
744
|
style,
|
|
695
745
|
}: BridgeWidgetProps) {
|
|
@@ -705,7 +755,6 @@ export function BridgeWidget({
|
|
|
705
755
|
const validation = validateChainConfigs(chains);
|
|
706
756
|
if (!validation.isValid) {
|
|
707
757
|
const errorMsg = validation.errors.join("; ");
|
|
708
|
-
console.error("[BridgeWidget] Invalid chain configuration:", errorMsg);
|
|
709
758
|
setConfigError(errorMsg);
|
|
710
759
|
} else {
|
|
711
760
|
setConfigError(null);
|
|
@@ -741,20 +790,28 @@ export function BridgeWidget({
|
|
|
741
790
|
const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
|
|
742
791
|
const [error, setError] = useState<string | null>(null);
|
|
743
792
|
|
|
744
|
-
// Hooks - fetch balances for all chains
|
|
793
|
+
// Hooks - fetch balances for all chains (single batch request via multicall)
|
|
745
794
|
const { balances: allBalances, isLoading: isLoadingAllBalances, refetch: refetchAllBalances } = useAllUSDCBalances(chains);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
)
|
|
795
|
+
|
|
796
|
+
// Derive source chain balance from the batch-fetched balances (no extra network request)
|
|
797
|
+
const balanceFormatted = useMemo(() => {
|
|
798
|
+
return allBalances[sourceChainConfig.chain.id]?.formatted ?? "0";
|
|
799
|
+
}, [allBalances, sourceChainConfig.chain.id]);
|
|
800
|
+
|
|
801
|
+
// Memoize parsed values to avoid repeated parsing
|
|
802
|
+
const parsedBalance = useMemo(() => parseFloat(balanceFormatted), [balanceFormatted]);
|
|
803
|
+
const parsedAmount = useMemo(() => parseFloat(amount) || 0, [amount]);
|
|
804
|
+
|
|
749
805
|
const { needsApproval, approve, isApproving } = useUSDCAllowance(
|
|
750
806
|
sourceChainConfig
|
|
751
807
|
);
|
|
752
808
|
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
809
|
+
// Refetch balances when wallet connects or address changes
|
|
810
|
+
useEffect(() => {
|
|
811
|
+
if (address) {
|
|
812
|
+
refetchAllBalances();
|
|
813
|
+
}
|
|
814
|
+
}, [address, refetchAllBalances]);
|
|
758
815
|
|
|
759
816
|
// Bridge hook
|
|
760
817
|
const { bridge: executeBridge, state: bridgeState, reset: resetBridge } = useBridge();
|
|
@@ -785,11 +842,11 @@ export function BridgeWidget({
|
|
|
785
842
|
|
|
786
843
|
// Swap chains
|
|
787
844
|
const handleSwapChains = useCallback(() => {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
}, [destChainConfig]);
|
|
845
|
+
const newSource = destChainConfig;
|
|
846
|
+
const newDest = sourceChainConfig;
|
|
847
|
+
setSourceChainConfig(newSource);
|
|
848
|
+
setDestChainConfig(newDest);
|
|
849
|
+
}, [destChainConfig, sourceChainConfig]);
|
|
793
850
|
|
|
794
851
|
// Handle max click
|
|
795
852
|
const handleMaxClick = useCallback(() => {
|
|
@@ -807,7 +864,7 @@ export function BridgeWidget({
|
|
|
807
864
|
|
|
808
865
|
// Handle bridge
|
|
809
866
|
const handleBridge = useCallback(async () => {
|
|
810
|
-
if (!address || !amount ||
|
|
867
|
+
if (!address || !amount || parsedAmount <= 0) return;
|
|
811
868
|
|
|
812
869
|
setError(null);
|
|
813
870
|
resetBridge();
|
|
@@ -848,6 +905,7 @@ export function BridgeWidget({
|
|
|
848
905
|
}, [
|
|
849
906
|
address,
|
|
850
907
|
amount,
|
|
908
|
+
parsedAmount,
|
|
851
909
|
needsApproval,
|
|
852
910
|
approve,
|
|
853
911
|
executeBridge,
|
|
@@ -891,7 +949,7 @@ export function BridgeWidget({
|
|
|
891
949
|
const currentTxHash = bridgeState.txHash;
|
|
892
950
|
|
|
893
951
|
setAmount("");
|
|
894
|
-
|
|
952
|
+
refetchAllBalances();
|
|
895
953
|
|
|
896
954
|
if (currentTxHash) {
|
|
897
955
|
onBridgeSuccessRef.current?.({
|
|
@@ -908,19 +966,19 @@ export function BridgeWidget({
|
|
|
908
966
|
bridgeState.status,
|
|
909
967
|
bridgeState.txHash,
|
|
910
968
|
bridgeState.error,
|
|
911
|
-
|
|
969
|
+
refetchAllBalances,
|
|
912
970
|
amount,
|
|
913
971
|
sourceChainConfig.chain.id,
|
|
914
972
|
destChainConfig.chain.id,
|
|
915
973
|
]);
|
|
916
974
|
|
|
917
|
-
// Computed disabled state
|
|
975
|
+
// Computed disabled state using memoized values
|
|
918
976
|
const isButtonDisabled =
|
|
919
977
|
!isConnected ||
|
|
920
978
|
needsChainSwitch ||
|
|
921
979
|
!amount ||
|
|
922
|
-
|
|
923
|
-
|
|
980
|
+
parsedAmount <= 0 ||
|
|
981
|
+
parsedAmount > parsedBalance ||
|
|
924
982
|
isConfirming ||
|
|
925
983
|
isApproving ||
|
|
926
984
|
isBridging;
|
|
@@ -944,8 +1002,8 @@ export function BridgeWidget({
|
|
|
944
1002
|
return "Approving...";
|
|
945
1003
|
}
|
|
946
1004
|
|
|
947
|
-
if (!amount ||
|
|
948
|
-
if (
|
|
1005
|
+
if (!amount || parsedAmount <= 0) return "Enter Amount";
|
|
1006
|
+
if (parsedAmount > parsedBalance) {
|
|
949
1007
|
return "Insufficient Balance";
|
|
950
1008
|
}
|
|
951
1009
|
if (needsApproval(amount)) return "Approve & Bridge USDC";
|
|
@@ -958,7 +1016,8 @@ export function BridgeWidget({
|
|
|
958
1016
|
isConfirming,
|
|
959
1017
|
isApproving,
|
|
960
1018
|
amount,
|
|
961
|
-
|
|
1019
|
+
parsedAmount,
|
|
1020
|
+
parsedBalance,
|
|
962
1021
|
needsApproval,
|
|
963
1022
|
]);
|
|
964
1023
|
|
|
@@ -1003,7 +1062,7 @@ export function BridgeWidget({
|
|
|
1003
1062
|
transition: "all 0.2s",
|
|
1004
1063
|
color: isButtonActuallyDisabled ? theme.mutedTextColor : theme.textColor,
|
|
1005
1064
|
background: isButtonActuallyDisabled
|
|
1006
|
-
?
|
|
1065
|
+
? DISABLED_BUTTON_BACKGROUND
|
|
1007
1066
|
: `linear-gradient(135deg, ${theme.primaryColor} 0%, ${theme.secondaryColor} 100%)`,
|
|
1008
1067
|
boxShadow: isButtonActuallyDisabled
|
|
1009
1068
|
? "none"
|
|
@@ -1028,11 +1087,11 @@ export function BridgeWidget({
|
|
|
1028
1087
|
fontFamily: theme.fontFamily,
|
|
1029
1088
|
maxWidth: "480px",
|
|
1030
1089
|
width: "100%",
|
|
1031
|
-
borderRadius: `${theme.borderRadius}px`,
|
|
1032
1090
|
padding: "16px",
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1091
|
+
...getBorderlessStyles(borderless, theme, {
|
|
1092
|
+
includeBoxShadow: true,
|
|
1093
|
+
useBackgroundColor: true,
|
|
1094
|
+
}),
|
|
1036
1095
|
...style,
|
|
1037
1096
|
}}
|
|
1038
1097
|
>
|
|
@@ -1053,9 +1112,10 @@ export function BridgeWidget({
|
|
|
1053
1112
|
onSelect={setSourceChainConfig}
|
|
1054
1113
|
excludeChainId={destChainConfig.chain.id}
|
|
1055
1114
|
theme={theme}
|
|
1056
|
-
balances={allBalances}
|
|
1057
|
-
isLoadingBalances={isLoadingAllBalances}
|
|
1115
|
+
balances={isConnected ? allBalances : undefined}
|
|
1116
|
+
isLoadingBalances={isConnected && isLoadingAllBalances}
|
|
1058
1117
|
disabled={isOperationPending}
|
|
1118
|
+
borderless={borderless}
|
|
1059
1119
|
/>
|
|
1060
1120
|
<SwapButton onClick={handleSwapChains} theme={theme} disabled={isOperationPending} />
|
|
1061
1121
|
<ChainSelector
|
|
@@ -1066,9 +1126,10 @@ export function BridgeWidget({
|
|
|
1066
1126
|
onSelect={setDestChainConfig}
|
|
1067
1127
|
excludeChainId={sourceChainConfig.chain.id}
|
|
1068
1128
|
theme={theme}
|
|
1069
|
-
balances={allBalances}
|
|
1070
|
-
isLoadingBalances={isLoadingAllBalances}
|
|
1129
|
+
balances={isConnected ? allBalances : undefined}
|
|
1130
|
+
isLoadingBalances={isConnected && isLoadingAllBalances}
|
|
1071
1131
|
disabled={isOperationPending}
|
|
1132
|
+
borderless={borderless}
|
|
1072
1133
|
/>
|
|
1073
1134
|
</div>
|
|
1074
1135
|
|
|
@@ -1082,6 +1143,8 @@ export function BridgeWidget({
|
|
|
1082
1143
|
onMaxClick={handleMaxClick}
|
|
1083
1144
|
theme={theme}
|
|
1084
1145
|
disabled={isOperationPending}
|
|
1146
|
+
showBalance={isConnected}
|
|
1147
|
+
borderless={borderless}
|
|
1085
1148
|
/>
|
|
1086
1149
|
</div>
|
|
1087
1150
|
|
|
@@ -217,6 +217,22 @@ describe("BridgeWidget", () => {
|
|
|
217
217
|
expect(widget.style.backgroundColor).toBe("red");
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
it("applies borderless style when borderless prop is true", () => {
|
|
221
|
+
render(<BridgeWidget borderless />);
|
|
222
|
+
const widget = screen.getByRole("region", { name: "USDC Bridge Widget" });
|
|
223
|
+
// Check that borderless styles are applied
|
|
224
|
+
expect(widget.style.background).toBe("transparent");
|
|
225
|
+
// JSDOM normalizes "0px" to "0"
|
|
226
|
+
expect(widget.style.borderRadius).toBe("0");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("applies default borders when borderless prop is false", () => {
|
|
230
|
+
render(<BridgeWidget borderless={false} />);
|
|
231
|
+
const widget = screen.getByRole("region", { name: "USDC Bridge Widget" });
|
|
232
|
+
// Check that default styles have border radius (not 0)
|
|
233
|
+
expect(widget.style.borderRadius).not.toBe("0");
|
|
234
|
+
});
|
|
235
|
+
|
|
220
236
|
it("calls onBridgeStart when bridge is initiated", async () => {
|
|
221
237
|
const onBridgeStart = vi.fn();
|
|
222
238
|
render(<BridgeWidget onBridgeStart={onBridgeStart} />);
|
|
@@ -264,6 +280,19 @@ describe("BridgeWidget - Disconnected State", () => {
|
|
|
264
280
|
|
|
265
281
|
expect(onConnectWallet).toHaveBeenCalled();
|
|
266
282
|
});
|
|
283
|
+
|
|
284
|
+
it("does not show balance in amount input when wallet is disconnected", () => {
|
|
285
|
+
render(<BridgeWidget />);
|
|
286
|
+
// Balance label should not be present when disconnected
|
|
287
|
+
expect(screen.queryByText(/Balance:/)).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("does not show balance in chain selectors when wallet is disconnected", () => {
|
|
291
|
+
render(<BridgeWidget />);
|
|
292
|
+
// USDC balance text should not appear in chain selectors
|
|
293
|
+
expect(screen.queryByText(/1,000.00 USDC/)).toBeNull();
|
|
294
|
+
expect(screen.queryByText(/500.00 USDC/)).toBeNull();
|
|
295
|
+
});
|
|
267
296
|
});
|
|
268
297
|
|
|
269
298
|
describe("BridgeWidget - Chain Switch Required", () => {
|
|
@@ -5,10 +5,11 @@ import type { BridgeChainConfig } from "../types";
|
|
|
5
5
|
|
|
6
6
|
// Create mock functions
|
|
7
7
|
const mockUseReadContracts = vi.fn();
|
|
8
|
+
const mockUseAccount = vi.fn();
|
|
8
9
|
|
|
9
10
|
// Mock wagmi hooks
|
|
10
11
|
vi.mock("wagmi", () => ({
|
|
11
|
-
useAccount:
|
|
12
|
+
useAccount: () => mockUseAccount(),
|
|
12
13
|
useReadContract: vi.fn(() => ({
|
|
13
14
|
data: 1000000000n,
|
|
14
15
|
isLoading: false,
|
|
@@ -58,6 +59,8 @@ describe("useAllUSDCBalances", () => {
|
|
|
58
59
|
|
|
59
60
|
beforeEach(() => {
|
|
60
61
|
vi.clearAllMocks();
|
|
62
|
+
// Default to connected wallet
|
|
63
|
+
mockUseAccount.mockReturnValue({ address: "0x1234567890123456789012345678901234567890" });
|
|
61
64
|
mockUseReadContracts.mockReturnValue({
|
|
62
65
|
data: [
|
|
63
66
|
{ status: "success", result: 1000000000n },
|
|
@@ -124,4 +127,20 @@ describe("useAllUSDCBalances", () => {
|
|
|
124
127
|
// Successful call should work
|
|
125
128
|
expect(result.current.balances[8453].balance).toBe(500000000n);
|
|
126
129
|
});
|
|
130
|
+
|
|
131
|
+
it("returns isLoading false when wallet not connected", () => {
|
|
132
|
+
// Set wallet as disconnected
|
|
133
|
+
mockUseAccount.mockReturnValue({ address: undefined });
|
|
134
|
+
mockUseReadContracts.mockReturnValue({
|
|
135
|
+
data: undefined,
|
|
136
|
+
isLoading: true, // Query reports loading but...
|
|
137
|
+
refetch: vi.fn(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const { result } = renderHook(() => useAllUSDCBalances(mockChainConfigs));
|
|
141
|
+
|
|
142
|
+
// ...isLoading should be false since wallet is not connected
|
|
143
|
+
expect(result.current.isLoading).toBe(false);
|
|
144
|
+
expect(result.current.balances).toEqual({});
|
|
145
|
+
});
|
|
127
146
|
});
|
package/src/hooks.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function useUSDCBalance(chainConfig: BridgeChainConfig | undefined) {
|
|
|
20
20
|
|
|
21
21
|
const {
|
|
22
22
|
data: balance,
|
|
23
|
-
isLoading,
|
|
23
|
+
isLoading: queryLoading,
|
|
24
24
|
refetch,
|
|
25
25
|
} = useReadContract({
|
|
26
26
|
address: chainConfig?.usdcAddress,
|
|
@@ -32,6 +32,9 @@ export function useUSDCBalance(chainConfig: BridgeChainConfig | undefined) {
|
|
|
32
32
|
},
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
// Only show loading when wallet is connected and query is actually running
|
|
36
|
+
const isLoading = !!address && queryLoading;
|
|
37
|
+
|
|
35
38
|
return {
|
|
36
39
|
balance: balance ?? 0n,
|
|
37
40
|
balanceFormatted: balance ? formatUnits(balance, USDC_DECIMALS) : "0",
|
|
@@ -74,7 +77,7 @@ export function useAllUSDCBalances(chainConfigs: BridgeChainConfig[]): {
|
|
|
74
77
|
|
|
75
78
|
const {
|
|
76
79
|
data: results,
|
|
77
|
-
isLoading,
|
|
80
|
+
isLoading: queryLoading,
|
|
78
81
|
refetch,
|
|
79
82
|
} = useReadContracts({
|
|
80
83
|
contracts,
|
|
@@ -83,6 +86,9 @@ export function useAllUSDCBalances(chainConfigs: BridgeChainConfig[]): {
|
|
|
83
86
|
},
|
|
84
87
|
});
|
|
85
88
|
|
|
89
|
+
// Only show loading when wallet is connected and query is actually running
|
|
90
|
+
const isLoading = !!address && queryLoading;
|
|
91
|
+
|
|
86
92
|
// Map results to chain IDs
|
|
87
93
|
const balances = useMemo(() => {
|
|
88
94
|
const balanceMap: Record<
|
|
@@ -130,7 +136,7 @@ export function useUSDCAllowance(
|
|
|
130
136
|
|
|
131
137
|
const {
|
|
132
138
|
data: allowance,
|
|
133
|
-
isLoading,
|
|
139
|
+
isLoading: queryLoading,
|
|
134
140
|
refetch,
|
|
135
141
|
} = useReadContract({
|
|
136
142
|
address: chainConfig?.usdcAddress,
|
|
@@ -143,6 +149,9 @@ export function useUSDCAllowance(
|
|
|
143
149
|
},
|
|
144
150
|
});
|
|
145
151
|
|
|
152
|
+
// Only show loading when wallet is connected and query is actually running
|
|
153
|
+
const isLoading = !!address && queryLoading;
|
|
154
|
+
|
|
146
155
|
const { writeContractAsync, isPending: isApproving } = useWriteContract();
|
|
147
156
|
const [approvalTxHash, setApprovalTxHash] = useState<
|
|
148
157
|
`0x${string}` | undefined
|
package/src/types.ts
CHANGED
|
@@ -123,6 +123,8 @@ export interface BridgeWidgetProps {
|
|
|
123
123
|
onConnectWallet?: () => void;
|
|
124
124
|
/** Custom theme overrides to customize the widget appearance */
|
|
125
125
|
theme?: BridgeWidgetTheme;
|
|
126
|
+
/** Remove all borders from the widget for seamless integration */
|
|
127
|
+
borderless?: boolean;
|
|
126
128
|
/** Custom CSS class name to apply to the widget container */
|
|
127
129
|
className?: string;
|
|
128
130
|
/** Custom inline styles to apply to the widget container */
|