@hongming-wang/usdc-bridge-widget 0.1.1 → 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/dist/index.mjs CHANGED
@@ -542,7 +542,7 @@ function useUSDCBalance(chainConfig) {
542
542
  const { address } = useAccount2();
543
543
  const {
544
544
  data: balance,
545
- isLoading,
545
+ isLoading: queryLoading,
546
546
  refetch
547
547
  } = useReadContract({
548
548
  address: chainConfig?.usdcAddress,
@@ -553,6 +553,7 @@ function useUSDCBalance(chainConfig) {
553
553
  enabled: !!address && !!chainConfig?.usdcAddress
554
554
  }
555
555
  });
556
+ const isLoading = !!address && queryLoading;
556
557
  return {
557
558
  balance: balance ?? 0n,
558
559
  balanceFormatted: balance ? formatUnits(balance, USDC_DECIMALS) : "0",
@@ -574,7 +575,7 @@ function useAllUSDCBalances(chainConfigs) {
574
575
  }, [address, chainConfigs]);
575
576
  const {
576
577
  data: results,
577
- isLoading,
578
+ isLoading: queryLoading,
578
579
  refetch
579
580
  } = useReadContracts({
580
581
  contracts,
@@ -582,6 +583,7 @@ function useAllUSDCBalances(chainConfigs) {
582
583
  enabled: !!address && contracts.length > 0
583
584
  }
584
585
  });
586
+ const isLoading = !!address && queryLoading;
585
587
  const balances = useMemo(() => {
586
588
  const balanceMap = {};
587
589
  if (!results) return balanceMap;
@@ -612,7 +614,7 @@ function useUSDCAllowance(chainConfig, spenderAddress) {
612
614
  const effectiveSpender = spenderAddress || chainConfig?.tokenMessengerAddress;
613
615
  const {
614
616
  data: allowance,
615
- isLoading,
617
+ isLoading: queryLoading,
616
618
  refetch
617
619
  } = useReadContract({
618
620
  address: chainConfig?.usdcAddress,
@@ -623,6 +625,7 @@ function useUSDCAllowance(chainConfig, spenderAddress) {
623
625
  enabled: !!address && !!chainConfig?.usdcAddress && !!effectiveSpender
624
626
  }
625
627
  });
628
+ const isLoading = !!address && queryLoading;
626
629
  const { writeContractAsync, isPending: isApproving } = useWriteContract();
627
630
  const [approvalTxHash, setApprovalTxHash] = useState2();
628
631
  const [approvalError, setApprovalError] = useState2(null);
@@ -1209,12 +1212,40 @@ function WalletIcon({
1209
1212
 
1210
1213
  // src/BridgeWidget.tsx
1211
1214
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1215
+ var TYPE_AHEAD_RESET_MS = 1e3;
1216
+ var DROPDOWN_MAX_HEIGHT = 300;
1217
+ var BOX_SHADOW_COLOR = "rgba(0,0,0,0.3)";
1218
+ var DISABLED_BUTTON_BACKGROUND = "rgba(255,255,255,0.1)";
1219
+ function getBorderlessStyles(borderless, theme, options) {
1220
+ const bgColor = options?.useBackgroundColor ? theme.backgroundColor : theme.cardBackgroundColor;
1221
+ return {
1222
+ borderRadius: borderless ? 0 : `${theme.borderRadius}px`,
1223
+ background: borderless ? "transparent" : bgColor,
1224
+ border: borderless ? "none" : `1px solid ${theme.borderColor}`,
1225
+ ...options?.includeBoxShadow && {
1226
+ boxShadow: borderless ? "none" : `0 4px 24px ${BOX_SHADOW_COLOR}`
1227
+ }
1228
+ };
1229
+ }
1230
+ var SPINNER_KEYFRAMES = `@keyframes cc-balance-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`;
1231
+ var KEYFRAMES_ATTR = "data-cc-spinner-keyframes";
1232
+ function injectSpinnerKeyframes() {
1233
+ if (typeof document === "undefined") return;
1234
+ if (document.querySelector(`style[${KEYFRAMES_ATTR}]`)) return;
1235
+ const style = document.createElement("style");
1236
+ style.setAttribute(KEYFRAMES_ATTR, "true");
1237
+ style.textContent = SPINNER_KEYFRAMES;
1238
+ document.head.appendChild(style);
1239
+ }
1212
1240
  function ChainIcon({
1213
1241
  chainConfig,
1214
1242
  theme,
1215
1243
  size = 24
1216
1244
  }) {
1217
1245
  const [hasError, setHasError] = useState3(false);
1246
+ useEffect3(() => {
1247
+ setHasError(false);
1248
+ }, [chainConfig.iconUrl]);
1218
1249
  if (!chainConfig.iconUrl || hasError) {
1219
1250
  return /* @__PURE__ */ jsx2(
1220
1251
  "div",
@@ -1248,6 +1279,9 @@ function ChainIcon({
1248
1279
  );
1249
1280
  }
1250
1281
  function BalanceSpinner({ size = 12 }) {
1282
+ useEffect3(() => {
1283
+ injectSpinnerKeyframes();
1284
+ }, []);
1251
1285
  return /* @__PURE__ */ jsxs2(
1252
1286
  "svg",
1253
1287
  {
@@ -1263,7 +1297,6 @@ function BalanceSpinner({ size = 12 }) {
1263
1297
  },
1264
1298
  "aria-hidden": "true",
1265
1299
  children: [
1266
- /* @__PURE__ */ jsx2("style", { children: `@keyframes cc-balance-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }` }),
1267
1300
  /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.25" }),
1268
1301
  /* @__PURE__ */ jsx2("path", { d: "M12 2a10 10 0 0 1 10 10", strokeLinecap: "round" })
1269
1302
  ]
@@ -1280,7 +1313,8 @@ function ChainSelector({
1280
1313
  id,
1281
1314
  balances,
1282
1315
  isLoadingBalances,
1283
- disabled
1316
+ disabled,
1317
+ borderless
1284
1318
  }) {
1285
1319
  const [isOpen, setIsOpen] = useState3(false);
1286
1320
  const [focusedIndex, setFocusedIndex] = useState3(-1);
@@ -1288,8 +1322,9 @@ function ChainSelector({
1288
1322
  const typeAheadTimeoutRef = useRef2(null);
1289
1323
  const buttonRef = useRef2(null);
1290
1324
  const listRef = useRef2(null);
1291
- const availableChains = chains.filter(
1292
- (c) => c.chain.id !== excludeChainId
1325
+ const availableChains = useMemo2(
1326
+ () => chains.filter((c) => c.chain.id !== excludeChainId),
1327
+ [chains, excludeChainId]
1293
1328
  );
1294
1329
  useEffect3(() => {
1295
1330
  return () => {
@@ -1352,7 +1387,7 @@ function ChainSelector({
1352
1387
  }
1353
1388
  typeAheadTimeoutRef.current = setTimeout(() => {
1354
1389
  setTypeAhead("");
1355
- }, 1e3);
1390
+ }, TYPE_AHEAD_RESET_MS);
1356
1391
  const matchIndex = availableChains.findIndex(
1357
1392
  (chain) => chain.chain.name.toLowerCase().startsWith(newTypeAhead)
1358
1393
  );
@@ -1429,9 +1464,7 @@ function ChainSelector({
1429
1464
  alignItems: "center",
1430
1465
  justifyContent: "space-between",
1431
1466
  padding: "10px 12px",
1432
- borderRadius: `${theme.borderRadius}px`,
1433
- background: theme.cardBackgroundColor,
1434
- border: `1px solid ${theme.borderColor}`,
1467
+ ...getBorderlessStyles(borderless, theme),
1435
1468
  cursor: disabled ? "not-allowed" : "pointer",
1436
1469
  opacity: disabled ? 0.6 : 1,
1437
1470
  transition: "all 0.2s"
@@ -1466,7 +1499,7 @@ function ChainSelector({
1466
1499
  " Loading..."
1467
1500
  ]
1468
1501
  }
1469
- ) : selectedBalance ? /* @__PURE__ */ jsxs2(
1502
+ ) : balances && selectedBalance ? /* @__PURE__ */ jsxs2(
1470
1503
  "span",
1471
1504
  {
1472
1505
  style: {
@@ -1524,11 +1557,11 @@ function ChainSelector({
1524
1557
  width: "100%",
1525
1558
  marginTop: "8px",
1526
1559
  borderRadius: `${theme.borderRadius}px`,
1527
- boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
1560
+ boxShadow: `0 10px 40px ${BOX_SHADOW_COLOR}`,
1528
1561
  background: theme.cardBackgroundColor,
1529
1562
  backdropFilter: "blur(10px)",
1530
1563
  border: `1px solid ${theme.borderColor}`,
1531
- maxHeight: "300px",
1564
+ maxHeight: `${DROPDOWN_MAX_HEIGHT}px`,
1532
1565
  overflowY: "auto",
1533
1566
  overflowX: "hidden",
1534
1567
  padding: 0,
@@ -1540,6 +1573,7 @@ function ChainSelector({
1540
1573
  const chainBalance = balances?.[chainConfig.chain.id];
1541
1574
  const isFocused = index === focusedIndex;
1542
1575
  const isSelected = chainConfig.chain.id === selectedChain.chain.id;
1576
+ const hasPositiveBalance = chainBalance ? parseFloat(chainBalance.formatted) > 0 : false;
1543
1577
  return /* @__PURE__ */ jsxs2(
1544
1578
  "li",
1545
1579
  {
@@ -1589,28 +1623,19 @@ function ChainSelector({
1589
1623
  },
1590
1624
  children: /* @__PURE__ */ jsx2(BalanceSpinner, { size: 10 })
1591
1625
  }
1592
- ) : chainBalance ? /* @__PURE__ */ jsxs2(
1626
+ ) : balances && chainBalance ? /* @__PURE__ */ jsxs2(
1593
1627
  "span",
1594
1628
  {
1595
1629
  style: {
1596
1630
  fontSize: "10px",
1597
- color: parseFloat(chainBalance.formatted) > 0 ? theme.successColor : theme.mutedTextColor
1631
+ color: hasPositiveBalance ? theme.successColor : theme.mutedTextColor
1598
1632
  },
1599
1633
  children: [
1600
1634
  formatNumber(chainBalance.formatted, 2),
1601
1635
  " USDC"
1602
1636
  ]
1603
1637
  }
1604
- ) : /* @__PURE__ */ jsx2(
1605
- "span",
1606
- {
1607
- style: {
1608
- fontSize: "10px",
1609
- color: theme.mutedTextColor
1610
- },
1611
- children: "0.00 USDC"
1612
- }
1613
- )
1638
+ ) : null
1614
1639
  ] })
1615
1640
  ]
1616
1641
  },
@@ -1662,7 +1687,9 @@ function AmountInput({
1662
1687
  onMaxClick,
1663
1688
  theme,
1664
1689
  id,
1665
- disabled
1690
+ disabled,
1691
+ showBalance = true,
1692
+ borderless
1666
1693
  }) {
1667
1694
  const inputId = `${id}-input`;
1668
1695
  const labelId = `${id}-label`;
@@ -1708,7 +1735,7 @@ function AmountInput({
1708
1735
  children: "Amount"
1709
1736
  }
1710
1737
  ),
1711
- /* @__PURE__ */ jsxs2(
1738
+ showBalance && /* @__PURE__ */ jsxs2(
1712
1739
  "span",
1713
1740
  {
1714
1741
  style: { fontSize: "10px", color: theme.mutedTextColor },
@@ -1732,10 +1759,8 @@ function AmountInput({
1732
1759
  style: {
1733
1760
  display: "flex",
1734
1761
  alignItems: "center",
1735
- borderRadius: `${theme.borderRadius}px`,
1736
1762
  overflow: "hidden",
1737
- background: theme.cardBackgroundColor,
1738
- border: `1px solid ${theme.borderColor}`,
1763
+ ...getBorderlessStyles(borderless, theme),
1739
1764
  opacity: disabled ? 0.6 : 1
1740
1765
  },
1741
1766
  children: [
@@ -1862,6 +1887,7 @@ function BridgeWidget({
1862
1887
  onBridgeError,
1863
1888
  onConnectWallet,
1864
1889
  theme: themeOverrides,
1890
+ borderless = false,
1865
1891
  className,
1866
1892
  style
1867
1893
  }) {
@@ -1875,7 +1901,6 @@ function BridgeWidget({
1875
1901
  const validation = validateChainConfigs(chains);
1876
1902
  if (!validation.isValid) {
1877
1903
  const errorMsg = validation.errors.join("; ");
1878
- console.error("[BridgeWidget] Invalid chain configuration:", errorMsg);
1879
1904
  setConfigError(errorMsg);
1880
1905
  } else {
1881
1906
  setConfigError(null);
@@ -1902,22 +1927,19 @@ function BridgeWidget({
1902
1927
  const [txHash, setTxHash] = useState3();
1903
1928
  const [error, setError] = useState3(null);
1904
1929
  const { balances: allBalances, isLoading: isLoadingAllBalances, refetch: refetchAllBalances } = useAllUSDCBalances(chains);
1905
- const { balanceFormatted, refetch: refetchBalance } = useUSDCBalance(
1906
- sourceChainConfig
1907
- );
1930
+ const balanceFormatted = useMemo2(() => {
1931
+ return allBalances[sourceChainConfig.chain.id]?.formatted ?? "0";
1932
+ }, [allBalances, sourceChainConfig.chain.id]);
1933
+ const parsedBalance = useMemo2(() => parseFloat(balanceFormatted), [balanceFormatted]);
1934
+ const parsedAmount = useMemo2(() => parseFloat(amount) || 0, [amount]);
1908
1935
  const { needsApproval, approve, isApproving } = useUSDCAllowance(
1909
1936
  sourceChainConfig
1910
1937
  );
1911
- const refetchBalances = useCallback3(() => {
1912
- refetchBalance();
1913
- refetchAllBalances();
1914
- }, [refetchBalance, refetchAllBalances]);
1915
1938
  useEffect3(() => {
1916
1939
  if (address) {
1917
1940
  refetchAllBalances();
1918
- refetchBalance();
1919
1941
  }
1920
- }, [address, refetchAllBalances, refetchBalance]);
1942
+ }, [address, refetchAllBalances]);
1921
1943
  const { bridge: executeBridge, state: bridgeState, reset: resetBridge } = useBridge();
1922
1944
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt2({
1923
1945
  hash: txHash
@@ -1930,11 +1952,11 @@ function BridgeWidget({
1930
1952
  onBridgeErrorRef.current = onBridgeError;
1931
1953
  const needsChainSwitch = isConnected && currentChainId !== sourceChainConfig.chain.id;
1932
1954
  const handleSwapChains = useCallback3(() => {
1933
- setSourceChainConfig((prev) => {
1934
- setDestChainConfig(prev);
1935
- return destChainConfig;
1936
- });
1937
- }, [destChainConfig]);
1955
+ const newSource = destChainConfig;
1956
+ const newDest = sourceChainConfig;
1957
+ setSourceChainConfig(newSource);
1958
+ setDestChainConfig(newDest);
1959
+ }, [destChainConfig, sourceChainConfig]);
1938
1960
  const handleMaxClick = useCallback3(() => {
1939
1961
  setAmount(balanceFormatted);
1940
1962
  }, [balanceFormatted]);
@@ -1946,7 +1968,7 @@ function BridgeWidget({
1946
1968
  }
1947
1969
  }, [switchChainAsync, sourceChainConfig.chain.id]);
1948
1970
  const handleBridge = useCallback3(async () => {
1949
- if (!address || !amount || parseFloat(amount) <= 0) return;
1971
+ if (!address || !amount || parsedAmount <= 0) return;
1950
1972
  setError(null);
1951
1973
  resetBridge();
1952
1974
  try {
@@ -1980,6 +2002,7 @@ function BridgeWidget({
1980
2002
  }, [
1981
2003
  address,
1982
2004
  amount,
2005
+ parsedAmount,
1983
2006
  needsApproval,
1984
2007
  approve,
1985
2008
  executeBridge,
@@ -2010,7 +2033,7 @@ function BridgeWidget({
2010
2033
  const currentDestChainId = destChainConfig.chain.id;
2011
2034
  const currentTxHash = bridgeState.txHash;
2012
2035
  setAmount("");
2013
- refetchBalances();
2036
+ refetchAllBalances();
2014
2037
  if (currentTxHash) {
2015
2038
  onBridgeSuccessRef.current?.({
2016
2039
  sourceChainId: currentSourceChainId,
@@ -2026,12 +2049,12 @@ function BridgeWidget({
2026
2049
  bridgeState.status,
2027
2050
  bridgeState.txHash,
2028
2051
  bridgeState.error,
2029
- refetchBalances,
2052
+ refetchAllBalances,
2030
2053
  amount,
2031
2054
  sourceChainConfig.chain.id,
2032
2055
  destChainConfig.chain.id
2033
2056
  ]);
2034
- const isButtonDisabled = !isConnected || needsChainSwitch || !amount || parseFloat(amount) <= 0 || parseFloat(amount) > parseFloat(balanceFormatted) || isConfirming || isApproving || isBridging;
2057
+ const isButtonDisabled = !isConnected || needsChainSwitch || !amount || parsedAmount <= 0 || parsedAmount > parsedBalance || isConfirming || isApproving || isBridging;
2035
2058
  const isButtonActuallyDisabled = isButtonDisabled && !needsChainSwitch && isConnected;
2036
2059
  const getButtonText = useCallback3(() => {
2037
2060
  if (!isConnected) return "Connect Wallet";
@@ -2044,8 +2067,8 @@ function BridgeWidget({
2044
2067
  if (isConfirming || isApproving) {
2045
2068
  return "Approving...";
2046
2069
  }
2047
- if (!amount || parseFloat(amount) <= 0) return "Enter Amount";
2048
- if (parseFloat(amount) > parseFloat(balanceFormatted)) {
2070
+ if (!amount || parsedAmount <= 0) return "Enter Amount";
2071
+ if (parsedAmount > parsedBalance) {
2049
2072
  return "Insufficient Balance";
2050
2073
  }
2051
2074
  if (needsApproval(amount)) return "Approve & Bridge USDC";
@@ -2058,7 +2081,8 @@ function BridgeWidget({
2058
2081
  isConfirming,
2059
2082
  isApproving,
2060
2083
  amount,
2061
- balanceFormatted,
2084
+ parsedAmount,
2085
+ parsedBalance,
2062
2086
  needsApproval
2063
2087
  ]);
2064
2088
  const handleButtonClick = useCallback3(() => {
@@ -2098,7 +2122,7 @@ function BridgeWidget({
2098
2122
  cursor: isButtonActuallyDisabled ? "not-allowed" : "pointer",
2099
2123
  transition: "all 0.2s",
2100
2124
  color: isButtonActuallyDisabled ? theme.mutedTextColor : theme.textColor,
2101
- background: isButtonActuallyDisabled ? "rgba(255,255,255,0.1)" : `linear-gradient(135deg, ${theme.primaryColor} 0%, ${theme.secondaryColor} 100%)`,
2125
+ background: isButtonActuallyDisabled ? DISABLED_BUTTON_BACKGROUND : `linear-gradient(135deg, ${theme.primaryColor} 0%, ${theme.secondaryColor} 100%)`,
2102
2126
  boxShadow: isButtonActuallyDisabled ? "none" : `0 4px 14px ${theme.primaryColor}60, inset 0 1px 0 rgba(255,255,255,0.2)`
2103
2127
  }),
2104
2128
  [
@@ -2120,11 +2144,11 @@ function BridgeWidget({
2120
2144
  fontFamily: theme.fontFamily,
2121
2145
  maxWidth: "480px",
2122
2146
  width: "100%",
2123
- borderRadius: `${theme.borderRadius}px`,
2124
2147
  padding: "16px",
2125
- background: theme.backgroundColor,
2126
- border: `1px solid ${theme.borderColor}`,
2127
- boxShadow: "0 4px 24px rgba(0,0,0,0.3)",
2148
+ ...getBorderlessStyles(borderless, theme, {
2149
+ includeBoxShadow: true,
2150
+ useBackgroundColor: true
2151
+ }),
2128
2152
  ...style
2129
2153
  },
2130
2154
  children: [
@@ -2148,9 +2172,10 @@ function BridgeWidget({
2148
2172
  onSelect: setSourceChainConfig,
2149
2173
  excludeChainId: destChainConfig.chain.id,
2150
2174
  theme,
2151
- balances: allBalances,
2152
- isLoadingBalances: isLoadingAllBalances,
2153
- disabled: isOperationPending
2175
+ balances: isConnected ? allBalances : void 0,
2176
+ isLoadingBalances: isConnected && isLoadingAllBalances,
2177
+ disabled: isOperationPending,
2178
+ borderless
2154
2179
  }
2155
2180
  ),
2156
2181
  /* @__PURE__ */ jsx2(SwapButton, { onClick: handleSwapChains, theme, disabled: isOperationPending }),
@@ -2164,9 +2189,10 @@ function BridgeWidget({
2164
2189
  onSelect: setDestChainConfig,
2165
2190
  excludeChainId: sourceChainConfig.chain.id,
2166
2191
  theme,
2167
- balances: allBalances,
2168
- isLoadingBalances: isLoadingAllBalances,
2169
- disabled: isOperationPending
2192
+ balances: isConnected ? allBalances : void 0,
2193
+ isLoadingBalances: isConnected && isLoadingAllBalances,
2194
+ disabled: isOperationPending,
2195
+ borderless
2170
2196
  }
2171
2197
  )
2172
2198
  ]
@@ -2181,7 +2207,9 @@ function BridgeWidget({
2181
2207
  balance: balanceFormatted,
2182
2208
  onMaxClick: handleMaxClick,
2183
2209
  theme,
2184
- disabled: isOperationPending
2210
+ disabled: isOperationPending,
2211
+ showBalance: isConnected,
2212
+ borderless
2185
2213
  }
2186
2214
  ) }),
2187
2215
  configError && /* @__PURE__ */ jsxs2(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hongming-wang/usdc-bridge-widget",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A reusable USDC cross-chain bridge widget powered by Circle CCTP",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,14 +17,6 @@
17
17
  "dist",
18
18
  "src"
19
19
  ],
20
- "scripts": {
21
- "build": "tsup src/index.tsx --format cjs,esm --dts --external react --external react-dom --external wagmi --external viem --external @tanstack/react-query",
22
- "dev": "tsup src/index.tsx --format cjs,esm --dts --watch",
23
- "typecheck": "tsc --noEmit",
24
- "test": "vitest run",
25
- "test:watch": "vitest",
26
- "test:coverage": "vitest run --coverage"
27
- },
28
20
  "dependencies": {
29
21
  "@circle-fin/bridge-kit": "^1.0.0",
30
22
  "@circle-fin/adapter-viem-v2": "^1.3.0"
@@ -59,5 +51,13 @@
59
51
  "repository": {
60
52
  "type": "git",
61
53
  "url": "https://github.com/hongming-wang/usdc-bridge-widget"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup src/index.tsx --format cjs,esm --dts --external react --external react-dom --external wagmi --external viem --external @tanstack/react-query",
57
+ "dev": "tsup src/index.tsx --format cjs,esm --dts --watch",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "test:coverage": "vitest run --coverage"
62
62
  }
63
- }
63
+ }