@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
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
|
+
};
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
}
|