@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/src/chains.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { defineChain } from "viem";
2
+ import {
3
+ mainnet,
4
+ arbitrum,
5
+ avalanche,
6
+ base,
7
+ optimism,
8
+ polygon,
9
+ linea,
10
+ sei,
11
+ worldchain,
12
+ ink,
13
+ sonic,
14
+ xdc,
15
+ // Testnets
16
+ sepolia,
17
+ arbitrumSepolia,
18
+ avalancheFuji,
19
+ baseSepolia,
20
+ optimismSepolia,
21
+ polygonAmoy,
22
+ } from "viem/chains";
23
+ import type { BridgeChainConfig } from "./types";
24
+ import {
25
+ USDC_ADDRESSES,
26
+ TOKEN_MESSENGER_ADDRESSES,
27
+ CHAIN_ICONS,
28
+ } from "./constants";
29
+
30
+ // Custom chain definitions for chains not yet in viem/chains
31
+ export const unichain = defineChain({
32
+ id: 130,
33
+ name: "Unichain",
34
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
35
+ rpcUrls: {
36
+ default: { http: ["https://mainnet.unichain.org"] },
37
+ },
38
+ blockExplorers: {
39
+ default: { name: "Uniscan", url: "https://uniscan.xyz" },
40
+ },
41
+ });
42
+
43
+ export const hyperEvm = defineChain({
44
+ id: 999,
45
+ name: "HyperEVM",
46
+ nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 },
47
+ rpcUrls: {
48
+ default: { http: ["https://rpc.hyperliquid.xyz/evm"] },
49
+ },
50
+ blockExplorers: {
51
+ default: { name: "Hyperscan", url: "https://hyperscan.xyz" },
52
+ },
53
+ });
54
+
55
+ export const plume = defineChain({
56
+ id: 98866,
57
+ name: "Plume",
58
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
59
+ rpcUrls: {
60
+ default: { http: ["https://rpc.plume.org"] },
61
+ },
62
+ blockExplorers: {
63
+ default: { name: "Plume Explorer", url: "https://explorer.plume.org" },
64
+ },
65
+ });
66
+
67
+ export const monad = defineChain({
68
+ id: 10200,
69
+ name: "Monad",
70
+ nativeCurrency: { name: "MON", symbol: "MON", decimals: 18 },
71
+ rpcUrls: {
72
+ default: { http: ["https://rpc.monad.xyz"] },
73
+ },
74
+ blockExplorers: {
75
+ default: { name: "Monad Explorer", url: "https://explorer.monad.xyz" },
76
+ },
77
+ });
78
+
79
+ export const codex = defineChain({
80
+ id: 81224,
81
+ name: "Codex",
82
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
83
+ rpcUrls: {
84
+ default: { http: ["https://rpc.codex.storage"] },
85
+ },
86
+ blockExplorers: {
87
+ default: { name: "Codex Explorer", url: "https://explorer.codex.storage" },
88
+ },
89
+ });
90
+
91
+ // Helper to create chain configs
92
+ export function createChainConfig(
93
+ chain: import("viem").Chain,
94
+ options?: {
95
+ usdcAddress?: `0x${string}`;
96
+ tokenMessengerAddress?: `0x${string}`;
97
+ iconUrl?: string;
98
+ }
99
+ ): BridgeChainConfig {
100
+ return {
101
+ chain,
102
+ usdcAddress: options?.usdcAddress || USDC_ADDRESSES[chain.id],
103
+ tokenMessengerAddress:
104
+ options?.tokenMessengerAddress || TOKEN_MESSENGER_ADDRESSES[chain.id],
105
+ iconUrl: options?.iconUrl || CHAIN_ICONS[chain.id],
106
+ };
107
+ }
108
+
109
+ // All supported CCTP chains
110
+ // Note: Monad is defined but not included in defaults as Circle Bridge Kit doesn't support it yet
111
+ export const DEFAULT_CHAIN_CONFIGS: BridgeChainConfig[] = [
112
+ createChainConfig(mainnet),
113
+ createChainConfig(arbitrum),
114
+ createChainConfig(base),
115
+ createChainConfig(optimism),
116
+ createChainConfig(polygon),
117
+ createChainConfig(avalanche),
118
+ createChainConfig(linea),
119
+ createChainConfig(sonic),
120
+ createChainConfig(worldchain),
121
+ createChainConfig(sei),
122
+ createChainConfig(xdc),
123
+ createChainConfig(ink),
124
+ createChainConfig(unichain),
125
+ createChainConfig(hyperEvm),
126
+ createChainConfig(plume),
127
+ createChainConfig(codex),
128
+ ];
129
+
130
+ // Re-export viem chains for convenience
131
+ export {
132
+ mainnet,
133
+ arbitrum,
134
+ avalanche,
135
+ base,
136
+ optimism,
137
+ polygon,
138
+ linea,
139
+ sei,
140
+ worldchain,
141
+ ink,
142
+ sonic,
143
+ xdc,
144
+ };
145
+
146
+ // =============================================================================
147
+ // TESTNET CHAINS
148
+ // =============================================================================
149
+
150
+ // Testnet USDC addresses (Circle's testnet USDC)
151
+ const TESTNET_USDC_ADDRESSES: Record<number, `0x${string}`> = {
152
+ 11155111: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // Sepolia
153
+ 421614: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", // Arbitrum Sepolia
154
+ 43113: "0x5425890298aed601595a70AB815c96711a31Bc65", // Avalanche Fuji
155
+ 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia
156
+ 11155420: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7", // Optimism Sepolia
157
+ 80002: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582", // Polygon Amoy
158
+ };
159
+
160
+ // Testnet TokenMessenger V2 address (same across testnets)
161
+ const TESTNET_TOKEN_MESSENGER_V2_ADDRESS: `0x${string}` =
162
+ "0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5";
163
+
164
+ /**
165
+ * Create a testnet chain configuration
166
+ */
167
+ export function createTestnetChainConfig(
168
+ chain: import("viem").Chain,
169
+ options?: {
170
+ usdcAddress?: `0x${string}`;
171
+ tokenMessengerAddress?: `0x${string}`;
172
+ iconUrl?: string;
173
+ }
174
+ ): BridgeChainConfig {
175
+ return {
176
+ chain,
177
+ usdcAddress: options?.usdcAddress || TESTNET_USDC_ADDRESSES[chain.id],
178
+ tokenMessengerAddress:
179
+ options?.tokenMessengerAddress || TESTNET_TOKEN_MESSENGER_V2_ADDRESS,
180
+ iconUrl: options?.iconUrl || CHAIN_ICONS[chain.id],
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Testnet chain configurations for development and testing.
186
+ * These use Circle's testnet USDC and TokenMessenger contracts.
187
+ *
188
+ * @example
189
+ * import { TESTNET_CHAIN_CONFIGS } from './chains';
190
+ * <BridgeWidget chains={TESTNET_CHAIN_CONFIGS} />
191
+ */
192
+ export const TESTNET_CHAIN_CONFIGS: BridgeChainConfig[] = [
193
+ createTestnetChainConfig(sepolia),
194
+ createTestnetChainConfig(arbitrumSepolia),
195
+ createTestnetChainConfig(avalancheFuji),
196
+ createTestnetChainConfig(baseSepolia),
197
+ createTestnetChainConfig(optimismSepolia),
198
+ createTestnetChainConfig(polygonAmoy),
199
+ ];
200
+
201
+ // Re-export testnet chains
202
+ export {
203
+ sepolia,
204
+ arbitrumSepolia,
205
+ avalancheFuji,
206
+ baseSepolia,
207
+ optimismSepolia,
208
+ polygonAmoy,
209
+ };
@@ -0,0 +1,97 @@
1
+ // USDC token decimals
2
+ export const USDC_DECIMALS = 6;
3
+
4
+ // USDC brand color for icon
5
+ export const USDC_BRAND_COLOR = "#2775ca";
6
+
7
+ // Maximum USDC amount (100 billion - more than total supply)
8
+ export const MAX_USDC_AMOUNT = "100000000000";
9
+
10
+ // Minimum USDC amount for bridging
11
+ export const MIN_USDC_AMOUNT = "0.000001";
12
+
13
+ // Default locale for number formatting (consistent display in financial apps)
14
+ export const DEFAULT_LOCALE = "en-US";
15
+
16
+ // Default USDC addresses per chain
17
+ // Note: Not all chains listed here are supported by Circle Bridge Kit yet.
18
+ // Check DEFAULT_CHAIN_CONFIGS in chains.ts for currently supported chains.
19
+ export const USDC_ADDRESSES: Record<number, `0x${string}`> = {
20
+ // Original chains
21
+ 1: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum
22
+ 42161: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // Arbitrum
23
+ 43114: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", // Avalanche
24
+ 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base
25
+ 10: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism
26
+ 137: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // Polygon
27
+ 59144: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", // Linea
28
+ // New CCTP V2 chains
29
+ 130: "0x078D782b760474a361dDA0AF3839290b0EF57AD6", // Unichain
30
+ 146: "0x29219dd400f2Bf60E5a23d13Be72B486D4038894", // Sonic
31
+ 480: "0x79A02482A880bCE3F13e09Da970dC34db4CD24d1", // World Chain
32
+ 10200: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", // Monad
33
+ 1329: "0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392", // Sei
34
+ 50: "0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1", // XDC
35
+ 999: "0xb88339CB7199b77E23DB6E890353E22632Ba630f", // HyperEVM
36
+ 57073: "0x2D270e6886d130D724215A266106e6832161EAEd", // Ink
37
+ 98866: "0x222365EF19F7947e5484218551B56bb3965Aa7aF", // Plume
38
+ 81224: "0xd996633a415985DBd7D6D12f4A4343E31f5037cf", // Codex
39
+ };
40
+
41
+ // Circle TokenMessenger V1 addresses (CCTP Legacy)
42
+ export const TOKEN_MESSENGER_V1_ADDRESSES: Record<number, `0x${string}`> = {
43
+ 1: "0xBd3fa81B58Ba92a82136038B25aDec7066af3155", // Ethereum
44
+ 42161: "0x19330d10D9Cc8751218eaf51E8885D058642E08A", // Arbitrum
45
+ 43114: "0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982", // Avalanche
46
+ 8453: "0x1682Ae6375C4E4A97e4B583BC394c861A46D8962", // Base
47
+ 10: "0x2B4069517957735bE00ceE0fadAE88a26365528f", // Optimism
48
+ 137: "0x9daF8c91AEFAE50b9c0E69629D3F6Ca40cA3B3FE", // Polygon
49
+ };
50
+
51
+ // Circle TokenMessengerV2 address (CCTP V2 - same address on all supported chains)
52
+ export const TOKEN_MESSENGER_V2_ADDRESS: `0x${string}` =
53
+ "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d";
54
+
55
+ // Combined TokenMessenger addresses (prefers V2)
56
+ export const TOKEN_MESSENGER_ADDRESSES: Record<number, `0x${string}`> = {
57
+ // All CCTP V2 supported chains use the same address
58
+ 1: TOKEN_MESSENGER_V2_ADDRESS, // Ethereum
59
+ 42161: TOKEN_MESSENGER_V2_ADDRESS, // Arbitrum
60
+ 43114: TOKEN_MESSENGER_V2_ADDRESS, // Avalanche
61
+ 8453: TOKEN_MESSENGER_V2_ADDRESS, // Base
62
+ 10: TOKEN_MESSENGER_V2_ADDRESS, // Optimism
63
+ 137: TOKEN_MESSENGER_V2_ADDRESS, // Polygon
64
+ 59144: TOKEN_MESSENGER_V2_ADDRESS, // Linea
65
+ 130: TOKEN_MESSENGER_V2_ADDRESS, // Unichain
66
+ 146: TOKEN_MESSENGER_V2_ADDRESS, // Sonic
67
+ 480: TOKEN_MESSENGER_V2_ADDRESS, // World Chain
68
+ 10200: TOKEN_MESSENGER_V2_ADDRESS, // Monad
69
+ 1329: TOKEN_MESSENGER_V2_ADDRESS, // Sei
70
+ 50: TOKEN_MESSENGER_V2_ADDRESS, // XDC
71
+ 999: TOKEN_MESSENGER_V2_ADDRESS, // HyperEVM
72
+ 57073: TOKEN_MESSENGER_V2_ADDRESS, // Ink
73
+ 98866: TOKEN_MESSENGER_V2_ADDRESS, // Plume
74
+ 81224: TOKEN_MESSENGER_V2_ADDRESS, // Codex
75
+ };
76
+
77
+ // Chain icon URLs (using DefiLlama's reliable CDN)
78
+ export const CHAIN_ICONS: Record<number, string> = {
79
+ 1: "https://icons.llamao.fi/icons/chains/rsz_ethereum.jpg", // Ethereum
80
+ 42161: "https://icons.llamao.fi/icons/chains/rsz_arbitrum.jpg", // Arbitrum
81
+ 43114: "https://icons.llamao.fi/icons/chains/rsz_avalanche.jpg", // Avalanche
82
+ 8453: "https://icons.llamao.fi/icons/chains/rsz_base.jpg", // Base
83
+ 10: "https://icons.llamao.fi/icons/chains/rsz_optimism.jpg", // Optimism
84
+ 137: "https://icons.llamao.fi/icons/chains/rsz_polygon.jpg", // Polygon
85
+ 59144: "https://icons.llamao.fi/icons/chains/rsz_linea.jpg", // Linea
86
+ 130: "https://icons.llamao.fi/icons/chains/rsz_unichain.jpg", // Unichain
87
+ 146: "https://icons.llamao.fi/icons/chains/rsz_sonic.jpg", // Sonic
88
+ 480: "https://icons.llamao.fi/icons/chains/rsz_world-chain.jpg", // World Chain
89
+ 10200: "https://icons.llamao.fi/icons/chains/rsz_monad.jpg", // Monad
90
+ 1329: "https://icons.llamao.fi/icons/chains/rsz_sei.jpg", // Sei
91
+ 50: "https://icons.llamao.fi/icons/chains/rsz_xdc.jpg", // XDC
92
+ 999: "https://icons.llamao.fi/icons/chains/rsz_hyperevm.jpg", // HyperEVM
93
+ 57073: "https://icons.llamao.fi/icons/chains/rsz_ink.jpg", // Ink
94
+ 98866: "https://icons.llamao.fi/icons/chains/rsz_plume.jpg", // Plume
95
+ 81224:
96
+ "https://raw.githubusercontent.com/0xa3k5/web3icons/main/packages/core/src/svgs/networks/branded/codex.svg", // Codex
97
+ };
package/src/hooks.ts ADDED
@@ -0,0 +1,349 @@
1
+ import { useState, useCallback, useEffect, useMemo } from "react";
2
+ import {
3
+ useAccount,
4
+ useReadContract,
5
+ useReadContracts,
6
+ useWriteContract,
7
+ useWaitForTransactionReceipt,
8
+ } from "wagmi";
9
+ import { formatUnits, parseUnits, erc20Abi } from "viem";
10
+ import type { BridgeChainConfig, BridgeEstimate } from "./types";
11
+ import { USDC_DECIMALS } from "./constants";
12
+ import { formatNumber } from "./utils";
13
+ import { getBridgeChain } from "./useBridge";
14
+
15
+ /**
16
+ * Hook to get USDC balance for a specific chain
17
+ */
18
+ export function useUSDCBalance(chainConfig: BridgeChainConfig | undefined) {
19
+ const { address } = useAccount();
20
+
21
+ const {
22
+ data: balance,
23
+ isLoading,
24
+ refetch,
25
+ } = useReadContract({
26
+ address: chainConfig?.usdcAddress,
27
+ abi: erc20Abi,
28
+ functionName: "balanceOf",
29
+ args: address ? [address] : undefined,
30
+ query: {
31
+ enabled: !!address && !!chainConfig?.usdcAddress,
32
+ },
33
+ });
34
+
35
+ return {
36
+ balance: balance ?? 0n,
37
+ balanceFormatted: balance ? formatUnits(balance, USDC_DECIMALS) : "0",
38
+ isLoading,
39
+ refetch,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Hook to get USDC balances for all configured chains at once.
45
+ * Uses multicall for efficient batch fetching.
46
+ *
47
+ * @param chainConfigs - Array of chain configurations to fetch balances for
48
+ * @returns Object with balances mapped by chain ID, loading state, and refetch function
49
+ *
50
+ * @example
51
+ * const { balances, isLoading, refetch } = useAllUSDCBalances(chains);
52
+ * // balances[1] -> { balance: 1000000n, formatted: "1.00" }
53
+ */
54
+ export function useAllUSDCBalances(chainConfigs: BridgeChainConfig[]): {
55
+ balances: Record<number, { balance: bigint; formatted: string }>;
56
+ isLoading: boolean;
57
+ refetch: () => void;
58
+ } {
59
+ const { address } = useAccount();
60
+
61
+ // Build contract read configs for all chains
62
+ const contracts = useMemo(() => {
63
+ if (!address) return [];
64
+ return chainConfigs
65
+ .filter((config) => config.usdcAddress)
66
+ .map((config) => ({
67
+ address: config.usdcAddress,
68
+ abi: erc20Abi,
69
+ functionName: "balanceOf" as const,
70
+ args: [address] as const,
71
+ chainId: config.chain.id,
72
+ }));
73
+ }, [address, chainConfigs]);
74
+
75
+ const {
76
+ data: results,
77
+ isLoading,
78
+ refetch,
79
+ } = useReadContracts({
80
+ contracts,
81
+ query: {
82
+ enabled: !!address && contracts.length > 0,
83
+ },
84
+ });
85
+
86
+ // Map results to chain IDs
87
+ const balances = useMemo(() => {
88
+ const balanceMap: Record<
89
+ number,
90
+ { balance: bigint; formatted: string }
91
+ > = {};
92
+
93
+ if (!results) return balanceMap;
94
+
95
+ chainConfigs.forEach((config, index) => {
96
+ const result = results[index];
97
+ if (result?.status === "success" && typeof result.result === "bigint") {
98
+ balanceMap[config.chain.id] = {
99
+ balance: result.result,
100
+ formatted: formatUnits(result.result, USDC_DECIMALS),
101
+ };
102
+ } else {
103
+ balanceMap[config.chain.id] = {
104
+ balance: 0n,
105
+ formatted: "0",
106
+ };
107
+ }
108
+ });
109
+
110
+ return balanceMap;
111
+ }, [results, chainConfigs]);
112
+
113
+ return {
114
+ balances,
115
+ isLoading,
116
+ refetch: refetch as () => void,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Hook to check and handle USDC allowance
122
+ */
123
+ export function useUSDCAllowance(
124
+ chainConfig: BridgeChainConfig | undefined,
125
+ spenderAddress?: `0x${string}`
126
+ ) {
127
+ const { address } = useAccount();
128
+ const effectiveSpender =
129
+ spenderAddress || chainConfig?.tokenMessengerAddress;
130
+
131
+ const {
132
+ data: allowance,
133
+ isLoading,
134
+ refetch,
135
+ } = useReadContract({
136
+ address: chainConfig?.usdcAddress,
137
+ abi: erc20Abi,
138
+ functionName: "allowance",
139
+ args:
140
+ address && effectiveSpender ? [address, effectiveSpender] : undefined,
141
+ query: {
142
+ enabled: !!address && !!chainConfig?.usdcAddress && !!effectiveSpender,
143
+ },
144
+ });
145
+
146
+ const { writeContractAsync, isPending: isApproving } = useWriteContract();
147
+ const [approvalTxHash, setApprovalTxHash] = useState<
148
+ `0x${string}` | undefined
149
+ >();
150
+ const [approvalError, setApprovalError] = useState<Error | null>(null);
151
+
152
+ const { isLoading: isConfirming, isSuccess: isApprovalConfirmed } =
153
+ useWaitForTransactionReceipt({
154
+ hash: approvalTxHash,
155
+ });
156
+
157
+ const approve = useCallback(
158
+ async (amount: string): Promise<`0x${string}`> => {
159
+ if (!chainConfig?.usdcAddress || !effectiveSpender) {
160
+ throw new Error("Missing chain config or spender address");
161
+ }
162
+
163
+ setApprovalError(null);
164
+ try {
165
+ const amountBigInt = parseUnits(amount, USDC_DECIMALS);
166
+ const hash = await writeContractAsync({
167
+ address: chainConfig.usdcAddress,
168
+ abi: erc20Abi,
169
+ functionName: "approve",
170
+ args: [effectiveSpender, amountBigInt],
171
+ });
172
+ setApprovalTxHash(hash);
173
+ return hash;
174
+ } catch (error) {
175
+ const err =
176
+ error instanceof Error ? error : new Error("Approval failed");
177
+ setApprovalError(err);
178
+ throw err;
179
+ }
180
+ },
181
+ [chainConfig?.usdcAddress, effectiveSpender, writeContractAsync]
182
+ );
183
+
184
+ useEffect(() => {
185
+ if (isApprovalConfirmed) {
186
+ refetch();
187
+ }
188
+ }, [isApprovalConfirmed, refetch]);
189
+
190
+ const needsApproval = useCallback(
191
+ (amount: string) => {
192
+ // Early return for invalid inputs
193
+ if (!amount || !allowance) return false;
194
+
195
+ const parsedAmount = parseFloat(amount);
196
+ if (isNaN(parsedAmount) || parsedAmount <= 0) return false;
197
+
198
+ try {
199
+ const amountBigInt = parseUnits(amount, USDC_DECIMALS);
200
+ return allowance < amountBigInt;
201
+ } catch {
202
+ // Parsing failed - amount is invalid, return false
203
+ return false;
204
+ }
205
+ },
206
+ [allowance]
207
+ );
208
+
209
+ return {
210
+ allowance: allowance ?? 0n,
211
+ allowanceFormatted: allowance ? formatUnits(allowance, USDC_DECIMALS) : "0",
212
+ isLoading,
213
+ isApproving: isApproving || isConfirming,
214
+ approve,
215
+ needsApproval,
216
+ refetch,
217
+ approvalError,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Hook to estimate bridge costs using Circle Bridge Kit SDK
223
+ *
224
+ * @deprecated This hook is deprecated and will be removed in a future version.
225
+ * Use `useBridgeQuote` from `useBridge.ts` instead for SDK-based estimates.
226
+ *
227
+ * Note: kit.estimate() requires an adapter with wallet connection.
228
+ * For pre-bridge quotes without wallet, we return CCTP standard estimates.
229
+ *
230
+ * @example
231
+ * // Before (deprecated):
232
+ * const { estimate } = useBridgeEstimate(sourceChainId, destChainId, amount);
233
+ *
234
+ * // After (recommended):
235
+ * import { useBridgeQuote } from './useBridge';
236
+ * const { quote } = useBridgeQuote(sourceChainId, destChainId, amount);
237
+ */
238
+ export function useBridgeEstimate(
239
+ sourceChainId: number | undefined,
240
+ destChainId: number | undefined,
241
+ amount: string
242
+ ) {
243
+ // Emit deprecation warning once
244
+ useEffect(() => {
245
+ console.warn(
246
+ "[DEPRECATED] useBridgeEstimate is deprecated and will be removed in a future version. " +
247
+ "Use useBridgeQuote from './useBridge' instead."
248
+ );
249
+ }, []);
250
+
251
+ const [estimate, setEstimate] = useState<BridgeEstimate | null>(null);
252
+ const [isLoading, setIsLoading] = useState(false);
253
+ const [error, setError] = useState<Error | null>(null);
254
+
255
+ const fetchEstimate = useCallback(async () => {
256
+ if (!amount || parseFloat(amount) <= 0 || !sourceChainId || !destChainId) {
257
+ setEstimate(null);
258
+ setError(null);
259
+ return;
260
+ }
261
+
262
+ setIsLoading(true);
263
+ setError(null);
264
+ try {
265
+ const sourceBridgeChain = getBridgeChain(sourceChainId);
266
+ const destBridgeChain = getBridgeChain(destChainId);
267
+
268
+ // If chains are not supported, return basic estimate
269
+ if (!sourceBridgeChain || !destBridgeChain) {
270
+ setEstimate({
271
+ gasFee: "Estimated by wallet",
272
+ bridgeFee: "0.00",
273
+ totalFee: "Gas only",
274
+ estimatedTime: "~15-20 minutes",
275
+ });
276
+ setIsLoading(false);
277
+ return;
278
+ }
279
+
280
+ // Note: kit.estimate() requires an adapter with wallet connection
281
+ // For pre-bridge quotes without wallet, we return CCTP standard estimates
282
+ // CCTP V2 FAST transfers: 1-14 bps fee, SLOW transfers: 0 bps
283
+ setEstimate({
284
+ gasFee: "Estimated by wallet",
285
+ bridgeFee: "0-14 bps (FAST) / 0 (SLOW)",
286
+ totalFee: "Gas + protocol fee",
287
+ estimatedTime: "~15-20 minutes",
288
+ });
289
+ } catch (err) {
290
+ const error =
291
+ err instanceof Error ? err : new Error("Failed to estimate bridge cost");
292
+ setError(error);
293
+ setEstimate(null);
294
+ } finally {
295
+ setIsLoading(false);
296
+ }
297
+ }, [sourceChainId, destChainId, amount]);
298
+
299
+ useEffect(() => {
300
+ // Track if this effect is still active
301
+ let isActive = true;
302
+
303
+ const debounceTimer = setTimeout(() => {
304
+ if (isActive) {
305
+ fetchEstimate();
306
+ }
307
+ }, 500);
308
+
309
+ return () => {
310
+ isActive = false;
311
+ clearTimeout(debounceTimer);
312
+ };
313
+ }, [fetchEstimate]);
314
+
315
+ return { estimate, isLoading, error };
316
+ }
317
+
318
+ /**
319
+ * Hook to format numbers for display
320
+ *
321
+ * @deprecated This hook is deprecated and will be removed in a future version.
322
+ * Use the `formatNumber` utility function directly from `utils.ts` instead.
323
+ * The hook adds unnecessary overhead with useCallback for a pure function.
324
+ *
325
+ * @example
326
+ * // Before (deprecated):
327
+ * const format = useFormatNumber();
328
+ * const formatted = format(1234.56, 2);
329
+ *
330
+ * // After (recommended):
331
+ * import { formatNumber } from './utils';
332
+ * const formatted = formatNumber(1234.56, 2);
333
+ */
334
+ export function useFormatNumber() {
335
+ // Emit deprecation warning once
336
+ useEffect(() => {
337
+ console.warn(
338
+ "[DEPRECATED] useFormatNumber is deprecated and will be removed in a future version. " +
339
+ "Use the formatNumber utility function from './utils' directly instead."
340
+ );
341
+ }, []);
342
+
343
+ return useCallback(
344
+ (value: string | number, decimals: number = 2): string => {
345
+ return formatNumber(value, decimals);
346
+ },
347
+ []
348
+ );
349
+ }