@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/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# USDC Bridge Widget
|
|
2
|
+
|
|
3
|
+
A reusable React widget for cross-chain USDC transfers powered by Circle's CCTP (Cross-Chain Transfer Protocol).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Cross-chain USDC transfers with native token minting
|
|
8
|
+
- No bridge fees (only gas costs)
|
|
9
|
+
- Customizable theme and styling
|
|
10
|
+
- Built-in chain switching
|
|
11
|
+
- TypeScript support
|
|
12
|
+
- Works with any wagmi-compatible wallet
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @honeypot-finance/usdc-bridge-widget
|
|
18
|
+
# or
|
|
19
|
+
yarn add @honeypot-finance/usdc-bridge-widget
|
|
20
|
+
# or
|
|
21
|
+
pnpm add @honeypot-finance/usdc-bridge-widget
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Peer Dependencies
|
|
25
|
+
|
|
26
|
+
Make sure you have these peer dependencies installed:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install react react-dom wagmi viem @tanstack/react-query
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { BridgeWidget } from "@honeypot-finance/usdc-bridge-widget";
|
|
36
|
+
import { WagmiProvider, createConfig, http } from "wagmi";
|
|
37
|
+
import { mainnet, arbitrum, base, optimism, polygon } from "viem/chains";
|
|
38
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
39
|
+
import { injected } from "wagmi/connectors";
|
|
40
|
+
|
|
41
|
+
// Create wagmi config
|
|
42
|
+
const config = createConfig({
|
|
43
|
+
chains: [mainnet, arbitrum, base, optimism, polygon],
|
|
44
|
+
connectors: [injected()],
|
|
45
|
+
transports: {
|
|
46
|
+
[mainnet.id]: http(),
|
|
47
|
+
[arbitrum.id]: http(),
|
|
48
|
+
[base.id]: http(),
|
|
49
|
+
[optimism.id]: http(),
|
|
50
|
+
[polygon.id]: http(),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const queryClient = new QueryClient();
|
|
55
|
+
|
|
56
|
+
function App() {
|
|
57
|
+
return (
|
|
58
|
+
<WagmiProvider config={config}>
|
|
59
|
+
<QueryClientProvider client={queryClient}>
|
|
60
|
+
{/* Minimal - uses all 17 CCTP chains by default */}
|
|
61
|
+
<BridgeWidget />
|
|
62
|
+
|
|
63
|
+
{/* Or with options */}
|
|
64
|
+
<BridgeWidget
|
|
65
|
+
defaultSourceChainId={1}
|
|
66
|
+
defaultDestinationChainId={8453}
|
|
67
|
+
onBridgeSuccess={({ txHash, amount }) => {
|
|
68
|
+
console.log(`Bridged ${amount} USDC: ${txHash}`);
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</QueryClientProvider>
|
|
72
|
+
</WagmiProvider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Props
|
|
78
|
+
|
|
79
|
+
### BridgeWidgetProps
|
|
80
|
+
|
|
81
|
+
| Prop | Type | Default | Description |
|
|
82
|
+
|------|------|---------|-------------|
|
|
83
|
+
| `chains` | `BridgeChainConfig[]` | All CCTP chains | Array of supported chains |
|
|
84
|
+
| `defaultSourceChainId` | `number` | First chain | Default source chain ID |
|
|
85
|
+
| `defaultDestinationChainId` | `number` | Second chain | Default destination chain ID |
|
|
86
|
+
| `onBridgeStart` | `function` | - | Called when bridge starts |
|
|
87
|
+
| `onBridgeSuccess` | `function` | - | Called on successful bridge |
|
|
88
|
+
| `onBridgeError` | `function` | - | Called on bridge error |
|
|
89
|
+
| `onConnectWallet` | `function` | - | Called when "Connect Wallet" clicked |
|
|
90
|
+
| `theme` | `BridgeWidgetTheme` | Default theme | Custom theme overrides |
|
|
91
|
+
| `className` | `string` | - | Custom CSS class |
|
|
92
|
+
| `style` | `CSSProperties` | - | Custom inline styles |
|
|
93
|
+
|
|
94
|
+
### BridgeChainConfig
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface BridgeChainConfig {
|
|
98
|
+
chain: Chain; // viem Chain object
|
|
99
|
+
usdcAddress: `0x${string}`; // USDC contract address
|
|
100
|
+
tokenMessengerAddress?: `0x${string}`; // Circle TokenMessenger address
|
|
101
|
+
iconUrl?: string; // Optional chain icon URL
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### BridgeWidgetTheme
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
interface BridgeWidgetTheme {
|
|
109
|
+
primaryColor?: string; // Default: "#6366f1"
|
|
110
|
+
secondaryColor?: string; // Default: "#a855f7"
|
|
111
|
+
backgroundColor?: string; // Default: "rgba(15, 15, 25, 0.8)"
|
|
112
|
+
cardBackgroundColor?: string;
|
|
113
|
+
textColor?: string; // Default: "#ffffff"
|
|
114
|
+
mutedTextColor?: string;
|
|
115
|
+
borderColor?: string;
|
|
116
|
+
successColor?: string; // Default: "#22c55e"
|
|
117
|
+
errorColor?: string; // Default: "#ef4444"
|
|
118
|
+
hoverColor?: string; // Default: "rgba(255, 255, 255, 0.05)"
|
|
119
|
+
borderRadius?: number; // Default: 12
|
|
120
|
+
fontFamily?: string;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Custom Chain Configuration
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { BridgeWidget, createChainConfig, USDC_ADDRESSES } from "@honeypot-finance/usdc-bridge-widget";
|
|
128
|
+
import { mainnet, arbitrum, base } from "viem/chains";
|
|
129
|
+
import { defineChain } from "viem";
|
|
130
|
+
|
|
131
|
+
// Use helper function
|
|
132
|
+
const chains = [
|
|
133
|
+
createChainConfig(mainnet, {
|
|
134
|
+
iconUrl: "https://example.com/eth-icon.png",
|
|
135
|
+
}),
|
|
136
|
+
createChainConfig(arbitrum),
|
|
137
|
+
createChainConfig(base),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Or define manually
|
|
141
|
+
const customChain = defineChain({
|
|
142
|
+
id: 12345,
|
|
143
|
+
name: "Custom Chain",
|
|
144
|
+
// ... other chain config
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const customChainConfig = {
|
|
148
|
+
chain: customChain,
|
|
149
|
+
usdcAddress: "0x..." as `0x${string}`,
|
|
150
|
+
tokenMessengerAddress: "0x..." as `0x${string}`,
|
|
151
|
+
iconUrl: "https://example.com/custom-icon.png",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
<BridgeWidget chains={[...chains, customChainConfig]} />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Custom Theme
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<BridgeWidget
|
|
161
|
+
chains={DEFAULT_CHAIN_CONFIGS}
|
|
162
|
+
theme={{
|
|
163
|
+
primaryColor: "#00ff00",
|
|
164
|
+
secondaryColor: "#00cc00",
|
|
165
|
+
backgroundColor: "#1a1a2e",
|
|
166
|
+
borderRadius: 16,
|
|
167
|
+
fontFamily: "Inter, sans-serif",
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Callbacks
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<BridgeWidget
|
|
176
|
+
chains={DEFAULT_CHAIN_CONFIGS}
|
|
177
|
+
onBridgeStart={({ sourceChainId, destChainId, amount }) => {
|
|
178
|
+
console.log(`Starting bridge: ${amount} USDC from ${sourceChainId} to ${destChainId}`);
|
|
179
|
+
}}
|
|
180
|
+
onBridgeSuccess={({ sourceChainId, destChainId, amount, txHash }) => {
|
|
181
|
+
console.log(`Bridge successful! TX: ${txHash}`);
|
|
182
|
+
}}
|
|
183
|
+
onBridgeError={(error) => {
|
|
184
|
+
console.error("Bridge failed:", error.message);
|
|
185
|
+
}}
|
|
186
|
+
onConnectWallet={() => {
|
|
187
|
+
// Open your wallet modal
|
|
188
|
+
openWalletModal();
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Using Individual Hooks
|
|
194
|
+
|
|
195
|
+
The widget exports its internal hooks for advanced usage:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
import {
|
|
199
|
+
useUSDCBalance,
|
|
200
|
+
useAllUSDCBalances,
|
|
201
|
+
useUSDCAllowance,
|
|
202
|
+
useBridge,
|
|
203
|
+
useBridgeQuote,
|
|
204
|
+
} from "@honeypot-finance/usdc-bridge-widget";
|
|
205
|
+
|
|
206
|
+
function CustomComponent() {
|
|
207
|
+
const chainConfig = { chain: mainnet, usdcAddress: "0x..." };
|
|
208
|
+
|
|
209
|
+
// Balance hooks
|
|
210
|
+
const { balance, balanceFormatted } = useUSDCBalance(chainConfig);
|
|
211
|
+
const { balances, isLoading } = useAllUSDCBalances([chainConfig]);
|
|
212
|
+
|
|
213
|
+
// Allowance hook
|
|
214
|
+
const { needsApproval, approve, isApproving } = useUSDCAllowance(chainConfig);
|
|
215
|
+
|
|
216
|
+
// Bridge execution hook
|
|
217
|
+
const { bridge, state, reset } = useBridge();
|
|
218
|
+
// state.status: "idle" | "loading" | "approving" | "burning" | "fetching-attestation" | "minting" | "success" | "error"
|
|
219
|
+
|
|
220
|
+
// Quote hook (returns static CCTP estimates)
|
|
221
|
+
const { quote, isLoading: quoteLoading } = useBridgeQuote(1, 8453, "100");
|
|
222
|
+
|
|
223
|
+
// Build your own UI
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Deprecated Hooks
|
|
228
|
+
|
|
229
|
+
The following hooks are deprecated and will be removed in a future version:
|
|
230
|
+
|
|
231
|
+
- `useBridgeEstimate` - Use `useBridgeQuote` instead
|
|
232
|
+
- `useFormatNumber` - Use the `formatNumber` utility function directly
|
|
233
|
+
|
|
234
|
+
## Supported Chains
|
|
235
|
+
|
|
236
|
+
The widget includes pre-configured USDC and TokenMessenger addresses for all CCTP V2 EVM chains:
|
|
237
|
+
|
|
238
|
+
| Chain | Chain ID |
|
|
239
|
+
|-------|----------|
|
|
240
|
+
| Ethereum | 1 |
|
|
241
|
+
| Arbitrum | 42161 |
|
|
242
|
+
| Base | 8453 |
|
|
243
|
+
| Optimism | 10 |
|
|
244
|
+
| Polygon | 137 |
|
|
245
|
+
| Avalanche | 43114 |
|
|
246
|
+
| Linea | 59144 |
|
|
247
|
+
| Unichain | 130 |
|
|
248
|
+
| Sonic | 146 |
|
|
249
|
+
| World Chain | 480 |
|
|
250
|
+
| Monad | 10200 |
|
|
251
|
+
| Sei | 1329 |
|
|
252
|
+
| XDC | 50 |
|
|
253
|
+
| HyperEVM | 999 |
|
|
254
|
+
| Ink | 57073 |
|
|
255
|
+
| Plume | 98866 |
|
|
256
|
+
| Codex | 81224 |
|
|
257
|
+
|
|
258
|
+
See [Circle CCTP Docs](https://developers.circle.com/cctp/cctp-supported-blockchains) for the latest supported chains
|
|
259
|
+
|
|
260
|
+
## Circle Bridge Kit Integration
|
|
261
|
+
|
|
262
|
+
For full bridge functionality, install Circle's Bridge Kit:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
npm install @circle-fin/bridge-kit @circle-fin/adapter-viem-v2
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Then integrate with the widget's callbacks to execute actual transfers.
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// src/useBridge.ts
|
|
2
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import { useConnectorClient } from "wagmi";
|
|
4
|
+
var CHAIN_NAME_MAP = {
|
|
5
|
+
1: "Ethereum",
|
|
6
|
+
42161: "Arbitrum",
|
|
7
|
+
43114: "Avalanche",
|
|
8
|
+
8453: "Base",
|
|
9
|
+
10: "OP_Mainnet",
|
|
10
|
+
137: "Polygon",
|
|
11
|
+
59144: "Linea",
|
|
12
|
+
130: "Unichain",
|
|
13
|
+
146: "Sonic",
|
|
14
|
+
480: "World_Chain",
|
|
15
|
+
143: "Monad",
|
|
16
|
+
1329: "Sei",
|
|
17
|
+
50: "XDC",
|
|
18
|
+
999: "HyperEVM",
|
|
19
|
+
57073: "Ink",
|
|
20
|
+
98866: "Plume",
|
|
21
|
+
81224: "Codex"
|
|
22
|
+
};
|
|
23
|
+
function getChainName(chainId) {
|
|
24
|
+
return CHAIN_NAME_MAP[chainId] || `Chain_${chainId}`;
|
|
25
|
+
}
|
|
26
|
+
function useBridge() {
|
|
27
|
+
const { data: connectorClient } = useConnectorClient();
|
|
28
|
+
const [state, setState] = useState({
|
|
29
|
+
status: "idle",
|
|
30
|
+
events: []
|
|
31
|
+
});
|
|
32
|
+
const isMountedRef = useRef(true);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
isMountedRef.current = true;
|
|
35
|
+
return () => {
|
|
36
|
+
isMountedRef.current = false;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
const addEvent = useCallback((type, data) => {
|
|
40
|
+
if (!isMountedRef.current) return;
|
|
41
|
+
setState((prev) => ({
|
|
42
|
+
...prev,
|
|
43
|
+
events: [...prev.events, { type, timestamp: Date.now(), data }]
|
|
44
|
+
}));
|
|
45
|
+
}, []);
|
|
46
|
+
const reset = useCallback(() => {
|
|
47
|
+
setState({ status: "idle", events: [] });
|
|
48
|
+
}, []);
|
|
49
|
+
const bridge = useCallback(
|
|
50
|
+
async (params) => {
|
|
51
|
+
const { sourceChainConfig, destChainConfig, amount, recipientAddress } = params;
|
|
52
|
+
if (!connectorClient) {
|
|
53
|
+
setState({
|
|
54
|
+
status: "error",
|
|
55
|
+
error: new Error("Wallet not connected"),
|
|
56
|
+
events: []
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setState({ status: "loading", events: [] });
|
|
61
|
+
addEvent("start", { amount, sourceChain: sourceChainConfig.chain.id, destChain: destChainConfig.chain.id });
|
|
62
|
+
try {
|
|
63
|
+
let BridgingKit;
|
|
64
|
+
let createAdapterFromProvider;
|
|
65
|
+
try {
|
|
66
|
+
const bridgingKitModule = await import("@circle-fin/bridging-kit");
|
|
67
|
+
const adapterModule = await import("@circle-fin/adapter-viem-v2");
|
|
68
|
+
BridgingKit = bridgingKitModule.BridgingKit;
|
|
69
|
+
createAdapterFromProvider = adapterModule.createAdapterFromProvider;
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Circle Bridge Kit packages not installed. Run: npm install @circle-fin/bridging-kit @circle-fin/adapter-viem-v2"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const clientWithTransport = connectorClient;
|
|
76
|
+
const provider = clientWithTransport?.transport?.value?.provider;
|
|
77
|
+
if (!provider) {
|
|
78
|
+
throw new Error("Could not get wallet provider from connector");
|
|
79
|
+
}
|
|
80
|
+
const adapter = await createAdapterFromProvider({ provider });
|
|
81
|
+
const kit = new BridgingKit();
|
|
82
|
+
kit.on("approve", (event) => {
|
|
83
|
+
addEvent("approve", event);
|
|
84
|
+
if (isMountedRef.current) {
|
|
85
|
+
setState((prev) => ({
|
|
86
|
+
...prev,
|
|
87
|
+
status: "approving",
|
|
88
|
+
txHash: event.values?.txHash
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
kit.on("burn", (event) => {
|
|
93
|
+
addEvent("burn", event);
|
|
94
|
+
if (isMountedRef.current) {
|
|
95
|
+
setState((prev) => ({
|
|
96
|
+
...prev,
|
|
97
|
+
status: "burning",
|
|
98
|
+
txHash: event.values?.txHash
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
kit.on("mint", (event) => {
|
|
103
|
+
addEvent("mint", event);
|
|
104
|
+
if (isMountedRef.current) {
|
|
105
|
+
setState((prev) => ({
|
|
106
|
+
...prev,
|
|
107
|
+
status: "minting",
|
|
108
|
+
txHash: event.values?.txHash
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const sourceChainName = getChainName(sourceChainConfig.chain.id);
|
|
113
|
+
const destChainName = getChainName(destChainConfig.chain.id);
|
|
114
|
+
const bridgeParams = {
|
|
115
|
+
from: { adapter, chain: sourceChainName },
|
|
116
|
+
to: { adapter, chain: destChainName },
|
|
117
|
+
amount
|
|
118
|
+
};
|
|
119
|
+
if (recipientAddress) {
|
|
120
|
+
bridgeParams.recipientAddress = recipientAddress;
|
|
121
|
+
}
|
|
122
|
+
const result = await kit.bridge(bridgeParams);
|
|
123
|
+
addEvent("complete", result);
|
|
124
|
+
if (isMountedRef.current) {
|
|
125
|
+
setState((prev) => ({
|
|
126
|
+
...prev,
|
|
127
|
+
status: "success",
|
|
128
|
+
txHash: result?.txHash
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
addEvent("error", error);
|
|
133
|
+
if (isMountedRef.current) {
|
|
134
|
+
setState((prev) => ({
|
|
135
|
+
...prev,
|
|
136
|
+
status: "error",
|
|
137
|
+
error: error instanceof Error ? error : new Error("Bridge transfer failed")
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
[connectorClient, addEvent]
|
|
144
|
+
);
|
|
145
|
+
return { bridge, state, reset };
|
|
146
|
+
}
|
|
147
|
+
function useBridgeQuote(sourceChainId, destChainId, amount) {
|
|
148
|
+
const [quote, setQuote] = useState(null);
|
|
149
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
150
|
+
const [error, setError] = useState(null);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!sourceChainId || !destChainId || !amount || parseFloat(amount) <= 0) {
|
|
153
|
+
setQuote(null);
|
|
154
|
+
setError(null);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const fetchQuote = async () => {
|
|
158
|
+
setIsLoading(true);
|
|
159
|
+
setError(null);
|
|
160
|
+
try {
|
|
161
|
+
let BridgingKit;
|
|
162
|
+
try {
|
|
163
|
+
const bridgingKitModule = await import("@circle-fin/bridging-kit");
|
|
164
|
+
BridgingKit = bridgingKitModule.BridgingKit;
|
|
165
|
+
} catch {
|
|
166
|
+
setError(new Error("Bridge Kit not installed. Run: npm install @circle-fin/bridging-kit"));
|
|
167
|
+
setQuote(null);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const kit = new BridgingKit();
|
|
171
|
+
const sourceChainName = getChainName(sourceChainId);
|
|
172
|
+
const destChainName = getChainName(destChainId);
|
|
173
|
+
if (typeof kit.getQuote === "function") {
|
|
174
|
+
const result = await kit.getQuote({
|
|
175
|
+
from: { chain: sourceChainName },
|
|
176
|
+
to: { chain: destChainName },
|
|
177
|
+
amount
|
|
178
|
+
});
|
|
179
|
+
setQuote({
|
|
180
|
+
estimatedGasFee: result.estimatedGasFee || "0.00",
|
|
181
|
+
bridgeFee: result.bridgeFee || "0.00",
|
|
182
|
+
totalFee: result.totalFee || result.estimatedGasFee || "0.00",
|
|
183
|
+
estimatedTime: result.estimatedTime || "~15-20 minutes",
|
|
184
|
+
expiresAt: result.expiresAt
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
setQuote({
|
|
188
|
+
estimatedGasFee: "Estimated by wallet",
|
|
189
|
+
bridgeFee: "0.00",
|
|
190
|
+
totalFee: "Gas only",
|
|
191
|
+
estimatedTime: "~15-20 minutes"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
setError(err instanceof Error ? err : new Error("Failed to get quote"));
|
|
196
|
+
setQuote(null);
|
|
197
|
+
} finally {
|
|
198
|
+
setIsLoading(false);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const debounceTimer = setTimeout(fetchQuote, 500);
|
|
202
|
+
return () => clearTimeout(debounceTimer);
|
|
203
|
+
}, [sourceChainId, destChainId, amount]);
|
|
204
|
+
return { quote, isLoading, error };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export {
|
|
208
|
+
getChainName,
|
|
209
|
+
useBridge,
|
|
210
|
+
useBridgeQuote
|
|
211
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// src/useBridge.ts
|
|
2
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import { useConnectorClient } from "wagmi";
|
|
4
|
+
var CHAIN_NAME_MAP = {
|
|
5
|
+
1: "Ethereum",
|
|
6
|
+
42161: "Arbitrum",
|
|
7
|
+
43114: "Avalanche",
|
|
8
|
+
8453: "Base",
|
|
9
|
+
10: "OP_Mainnet",
|
|
10
|
+
137: "Polygon",
|
|
11
|
+
59144: "Linea",
|
|
12
|
+
130: "Unichain",
|
|
13
|
+
146: "Sonic",
|
|
14
|
+
480: "World_Chain",
|
|
15
|
+
143: "Monad",
|
|
16
|
+
1329: "Sei",
|
|
17
|
+
50: "XDC",
|
|
18
|
+
999: "HyperEVM",
|
|
19
|
+
57073: "Ink",
|
|
20
|
+
98866: "Plume",
|
|
21
|
+
81224: "Codex"
|
|
22
|
+
};
|
|
23
|
+
function getChainName(chainId) {
|
|
24
|
+
return CHAIN_NAME_MAP[chainId] || `Chain_${chainId}`;
|
|
25
|
+
}
|
|
26
|
+
function useBridge() {
|
|
27
|
+
const { data: connectorClient } = useConnectorClient();
|
|
28
|
+
const [state, setState] = useState({
|
|
29
|
+
status: "idle",
|
|
30
|
+
events: []
|
|
31
|
+
});
|
|
32
|
+
const isMountedRef = useRef(true);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
isMountedRef.current = true;
|
|
35
|
+
return () => {
|
|
36
|
+
isMountedRef.current = false;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
const addEvent = useCallback((type, data) => {
|
|
40
|
+
if (!isMountedRef.current) return;
|
|
41
|
+
setState((prev) => ({
|
|
42
|
+
...prev,
|
|
43
|
+
events: [...prev.events, { type, timestamp: Date.now(), data }]
|
|
44
|
+
}));
|
|
45
|
+
}, []);
|
|
46
|
+
const reset = useCallback(() => {
|
|
47
|
+
setState({ status: "idle", events: [] });
|
|
48
|
+
}, []);
|
|
49
|
+
const bridge = useCallback(
|
|
50
|
+
async (params) => {
|
|
51
|
+
const { sourceChainConfig, destChainConfig, amount, recipientAddress } = params;
|
|
52
|
+
if (!connectorClient) {
|
|
53
|
+
setState({
|
|
54
|
+
status: "error",
|
|
55
|
+
error: new Error("Wallet not connected"),
|
|
56
|
+
events: []
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setState({ status: "loading", events: [] });
|
|
61
|
+
addEvent("start", { amount, sourceChain: sourceChainConfig.chain.id, destChain: destChainConfig.chain.id });
|
|
62
|
+
try {
|
|
63
|
+
let BridgingKit;
|
|
64
|
+
let createAdapterFromProvider;
|
|
65
|
+
try {
|
|
66
|
+
const bridgingKitPath = ["@circle-fin", "bridging-kit"].join("/");
|
|
67
|
+
const adapterPath = ["@circle-fin", "adapter-viem-v2"].join("/");
|
|
68
|
+
const dynamicImport = Function("m", "return import(m)");
|
|
69
|
+
const [bridgingKitModule, adapterModule] = await Promise.all([
|
|
70
|
+
dynamicImport(bridgingKitPath),
|
|
71
|
+
dynamicImport(adapterPath)
|
|
72
|
+
]);
|
|
73
|
+
BridgingKit = bridgingKitModule.BridgingKit;
|
|
74
|
+
createAdapterFromProvider = adapterModule.createAdapterFromProvider;
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"Circle Bridge Kit packages not installed. Run: npm install @circle-fin/bridging-kit @circle-fin/adapter-viem-v2"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const clientWithTransport = connectorClient;
|
|
81
|
+
const provider = clientWithTransport?.transport?.value?.provider;
|
|
82
|
+
if (!provider) {
|
|
83
|
+
throw new Error("Could not get wallet provider from connector");
|
|
84
|
+
}
|
|
85
|
+
const adapter = await createAdapterFromProvider({ provider });
|
|
86
|
+
const kit = new BridgingKit();
|
|
87
|
+
kit.on("approve", (event) => {
|
|
88
|
+
addEvent("approve", event);
|
|
89
|
+
if (isMountedRef.current) {
|
|
90
|
+
setState((prev) => ({
|
|
91
|
+
...prev,
|
|
92
|
+
status: "approving",
|
|
93
|
+
txHash: event.values?.txHash
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
kit.on("burn", (event) => {
|
|
98
|
+
addEvent("burn", event);
|
|
99
|
+
if (isMountedRef.current) {
|
|
100
|
+
setState((prev) => ({
|
|
101
|
+
...prev,
|
|
102
|
+
status: "burning",
|
|
103
|
+
txHash: event.values?.txHash
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
kit.on("mint", (event) => {
|
|
108
|
+
addEvent("mint", event);
|
|
109
|
+
if (isMountedRef.current) {
|
|
110
|
+
setState((prev) => ({
|
|
111
|
+
...prev,
|
|
112
|
+
status: "minting",
|
|
113
|
+
txHash: event.values?.txHash
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const sourceChainName = getChainName(sourceChainConfig.chain.id);
|
|
118
|
+
const destChainName = getChainName(destChainConfig.chain.id);
|
|
119
|
+
const bridgeParams = {
|
|
120
|
+
from: { adapter, chain: sourceChainName },
|
|
121
|
+
to: { adapter, chain: destChainName },
|
|
122
|
+
amount
|
|
123
|
+
};
|
|
124
|
+
if (recipientAddress) {
|
|
125
|
+
bridgeParams.recipientAddress = recipientAddress;
|
|
126
|
+
}
|
|
127
|
+
const result = await kit.bridge(bridgeParams);
|
|
128
|
+
addEvent("complete", result);
|
|
129
|
+
if (isMountedRef.current) {
|
|
130
|
+
setState((prev) => ({
|
|
131
|
+
...prev,
|
|
132
|
+
status: "success",
|
|
133
|
+
txHash: result?.txHash
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
addEvent("error", error);
|
|
138
|
+
if (isMountedRef.current) {
|
|
139
|
+
setState((prev) => ({
|
|
140
|
+
...prev,
|
|
141
|
+
status: "error",
|
|
142
|
+
error: error instanceof Error ? error : new Error("Bridge transfer failed")
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
[connectorClient, addEvent]
|
|
149
|
+
);
|
|
150
|
+
return { bridge, state, reset };
|
|
151
|
+
}
|
|
152
|
+
function useBridgeQuote(sourceChainId, destChainId, amount) {
|
|
153
|
+
const [quote, setQuote] = useState(null);
|
|
154
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
155
|
+
const [error, setError] = useState(null);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!sourceChainId || !destChainId || !amount || parseFloat(amount) <= 0) {
|
|
158
|
+
setQuote(null);
|
|
159
|
+
setError(null);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const fetchQuote = async () => {
|
|
163
|
+
setIsLoading(true);
|
|
164
|
+
setError(null);
|
|
165
|
+
try {
|
|
166
|
+
let BridgingKit;
|
|
167
|
+
try {
|
|
168
|
+
const modulePath = ["@circle-fin", "bridging-kit"].join("/");
|
|
169
|
+
const dynamicImport = Function("m", "return import(m)");
|
|
170
|
+
const bridgingKitModule = await dynamicImport(modulePath);
|
|
171
|
+
BridgingKit = bridgingKitModule.BridgingKit;
|
|
172
|
+
} catch {
|
|
173
|
+
setError(new Error("Bridge Kit not installed. Run: npm install @circle-fin/bridging-kit"));
|
|
174
|
+
setQuote(null);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const kit = new BridgingKit();
|
|
178
|
+
const sourceChainName = getChainName(sourceChainId);
|
|
179
|
+
const destChainName = getChainName(destChainId);
|
|
180
|
+
if (typeof kit.getQuote === "function") {
|
|
181
|
+
const result = await kit.getQuote({
|
|
182
|
+
from: { chain: sourceChainName },
|
|
183
|
+
to: { chain: destChainName },
|
|
184
|
+
amount
|
|
185
|
+
});
|
|
186
|
+
setQuote({
|
|
187
|
+
estimatedGasFee: result.estimatedGasFee || "0.00",
|
|
188
|
+
bridgeFee: result.bridgeFee || "0.00",
|
|
189
|
+
totalFee: result.totalFee || result.estimatedGasFee || "0.00",
|
|
190
|
+
estimatedTime: result.estimatedTime || "~15-20 minutes",
|
|
191
|
+
expiresAt: result.expiresAt
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
setQuote({
|
|
195
|
+
estimatedGasFee: "Estimated by wallet",
|
|
196
|
+
bridgeFee: "0.00",
|
|
197
|
+
totalFee: "Gas only",
|
|
198
|
+
estimatedTime: "~15-20 minutes"
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
setError(err instanceof Error ? err : new Error("Failed to get quote"));
|
|
203
|
+
setQuote(null);
|
|
204
|
+
} finally {
|
|
205
|
+
setIsLoading(false);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const debounceTimer = setTimeout(fetchQuote, 500);
|
|
209
|
+
return () => clearTimeout(debounceTimer);
|
|
210
|
+
}, [sourceChainId, destChainId, amount]);
|
|
211
|
+
return { quote, isLoading, error };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export {
|
|
215
|
+
getChainName,
|
|
216
|
+
useBridge,
|
|
217
|
+
useBridgeQuote
|
|
218
|
+
};
|