@luxfi/exchange 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * React hook for one-click cross-chain minting
3
+ *
4
+ * Enables users to mint wrapped tokens on C-Chain from X-Chain assets
5
+ * with a single transaction using Warp atomic swaps.
6
+ */
7
+
8
+ import { useCallback, useEffect, useState } from 'react'
9
+ import { usePublicClient, useWalletClient } from 'wagmi'
10
+ import { encodeFunctionData, parseAbi } from 'viem'
11
+ import { useCrossChainStore, type CrossChainStore } from './cross-chain-store'
12
+ import type {
13
+ CrossChainMintRequest,
14
+ CrossChainMintState,
15
+ CrossChainMintStatus,
16
+ SwapRoute,
17
+ AtomicSwapConfig,
18
+ } from './types'
19
+ import { DEFAULT_SWAP_CONFIG, getWrappedToken } from './types'
20
+
21
+ // AtomicSwapBridge contract ABI (minimal interface)
22
+ const BRIDGE_ABI = parseAbi([
23
+ 'function initiateMint(bytes32 swapId, address recipient, bytes32 asset, uint256 amount, uint256 minReceive, uint64 deadline, tuple(address tokenIn, address tokenOut, uint24 poolFee, int24 tickSpacing, address hooks)[] routes) external payable',
24
+ 'function processLockMessage(uint32 messageIndex) external',
25
+ 'function executeSwap(bytes32 swapId, tuple(address tokenIn, address tokenOut, uint24 poolFee, int24 tickSpacing, address hooks)[] routes) external',
26
+ 'function settleSwap(bytes32 swapId, bytes32 preimage) external',
27
+ 'function cancelSwap(bytes32 swapId) external',
28
+ 'function getSwapState(bytes32 swapId) external view returns (uint8 state, address sender, address recipient, bytes32 asset, uint256 amount, uint256 minReceive, uint64 deadline)',
29
+ 'event SwapInitiated(bytes32 indexed swapId, address indexed sender, address indexed recipient, bytes32 asset, uint256 amount)',
30
+ 'event SwapCompleted(bytes32 indexed swapId, uint256 amountReceived)',
31
+ 'event SwapCancelled(bytes32 indexed swapId)',
32
+ ])
33
+
34
+ export interface UseCrossChainMintOptions {
35
+ /** Configuration overrides */
36
+ config?: Partial<AtomicSwapConfig>
37
+ /** Poll interval for status updates (ms) */
38
+ pollInterval?: number
39
+ }
40
+
41
+ export interface UseCrossChainMintReturn {
42
+ /** Initiate a cross-chain mint */
43
+ mint: (request: Omit<CrossChainMintRequest, 'deadline'> & { deadline?: number }) => Promise<string>
44
+ /** Cancel a pending mint after deadline */
45
+ cancel: (swapId: string) => Promise<void>
46
+ /** Current state of a mint by swap ID */
47
+ getMintState: (swapId: string) => CrossChainMintState | undefined
48
+ /** All active mints */
49
+ activeMints: CrossChainStore['pendingMints']
50
+ /** Recent completed mints */
51
+ recentMints: CrossChainStore['recentMints']
52
+ /** Loading state */
53
+ isLoading: boolean
54
+ /** Error message */
55
+ error: string | null
56
+ }
57
+
58
+ export function useCrossChainMint(options: UseCrossChainMintOptions = {}): UseCrossChainMintReturn {
59
+ const { config: configOverrides, pollInterval = 5000 } = options
60
+ const config = { ...DEFAULT_SWAP_CONFIG, ...configOverrides }
61
+
62
+ const publicClient = usePublicClient()
63
+ const { data: walletClient } = useWalletClient()
64
+
65
+ const store = useCrossChainStore()
66
+ const [isLoading, setIsLoading] = useState(false)
67
+ const [error, setError] = useState<string | null>(null)
68
+
69
+ /**
70
+ * Initiate a one-click cross-chain mint
71
+ */
72
+ const mint = useCallback(
73
+ async (
74
+ request: Omit<CrossChainMintRequest, 'deadline'> & { deadline?: number }
75
+ ): Promise<string> => {
76
+ if (!walletClient) {
77
+ throw new Error('Wallet not connected')
78
+ }
79
+
80
+ setIsLoading(true)
81
+ setError(null)
82
+
83
+ try {
84
+ // Validate the asset has a wrapped token mapping
85
+ const wrappedToken = getWrappedToken(request.sourceAsset)
86
+ if (!wrappedToken) {
87
+ throw new Error('Asset not supported for cross-chain mint')
88
+ }
89
+
90
+ // Create the full request with deadline
91
+ const fullRequest: CrossChainMintRequest = {
92
+ ...request,
93
+ deadline: request.deadline ?? Math.floor(Date.now() / 1000) + config.defaultDeadline,
94
+ }
95
+
96
+ // Initialize in store
97
+ const swapId = store.initiateMint(fullRequest)
98
+ store.updateMintStatus(swapId, 'locking')
99
+
100
+ // Build swap routes if target token specified
101
+ const routes: SwapRoute[] = []
102
+ if (request.targetToken && request.targetToken !== wrappedToken) {
103
+ routes.push({
104
+ tokenIn: wrappedToken,
105
+ tokenOut: request.targetToken,
106
+ poolFee: 3000, // 0.3% default
107
+ tickSpacing: 60,
108
+ })
109
+ }
110
+
111
+ // Prepare transaction data
112
+ const txData = encodeFunctionData({
113
+ abi: BRIDGE_ABI,
114
+ functionName: 'initiateMint',
115
+ args: [
116
+ swapId as `0x${string}`,
117
+ request.recipient,
118
+ request.sourceAsset,
119
+ request.amount,
120
+ request.minReceive ?? BigInt(0),
121
+ BigInt(fullRequest.deadline),
122
+ routes.map((r) => ({
123
+ tokenIn: r.tokenIn,
124
+ tokenOut: r.tokenOut,
125
+ poolFee: r.poolFee,
126
+ tickSpacing: r.tickSpacing,
127
+ hooks: r.hooks ?? '0x0000000000000000000000000000000000000000',
128
+ })),
129
+ ],
130
+ })
131
+
132
+ // Send transaction to bridge contract
133
+ const hash = await walletClient.sendTransaction({
134
+ to: config.bridgeContract,
135
+ data: txData,
136
+ value: BigInt(0), // Native assets locked via XVM, not ETH
137
+ } as const)
138
+
139
+ store.updateMintTxHash(swapId, 'sourceTxHash', hash)
140
+ store.updateMintStatus(swapId, 'waiting_confirmation')
141
+
142
+ // Wait for transaction confirmation
143
+ if (publicClient) {
144
+ const receipt = await publicClient.waitForTransactionReceipt({ hash })
145
+
146
+ if (receipt.status === 'success') {
147
+ // Transaction was included, now waiting for Warp message
148
+ store.updateMintStatus(swapId, 'minting')
149
+
150
+ // The bridge contract handles the rest:
151
+ // 1. Warp message is sent to destination chain
152
+ // 2. Validators sign the message
153
+ // 3. Message is processed, wrapped tokens minted
154
+ // 4. Optional DEX swap executed
155
+ // 5. Tokens delivered to recipient
156
+ } else {
157
+ store.failMint(swapId, 'Transaction failed')
158
+ }
159
+ }
160
+
161
+ return swapId
162
+ } catch (err) {
163
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error'
164
+ setError(errorMessage)
165
+ throw err
166
+ } finally {
167
+ setIsLoading(false)
168
+ }
169
+ },
170
+ [walletClient, publicClient, store, config]
171
+ )
172
+
173
+ /**
174
+ * Cancel a pending mint after deadline
175
+ */
176
+ const cancel = useCallback(
177
+ async (swapId: string): Promise<void> => {
178
+ if (!walletClient) {
179
+ throw new Error('Wallet not connected')
180
+ }
181
+
182
+ const pending = store.getPendingMint(swapId)
183
+ if (!pending) {
184
+ throw new Error('Mint not found')
185
+ }
186
+
187
+ // Check deadline
188
+ if (pending.deadline > Date.now() / 1000) {
189
+ throw new Error('Cannot cancel before deadline')
190
+ }
191
+
192
+ const txData = encodeFunctionData({
193
+ abi: BRIDGE_ABI,
194
+ functionName: 'cancelSwap',
195
+ args: [swapId as `0x${string}`],
196
+ })
197
+
198
+ const hash = await walletClient.sendTransaction({
199
+ to: config.bridgeContract,
200
+ data: txData,
201
+ } as const)
202
+
203
+ if (publicClient) {
204
+ await publicClient.waitForTransactionReceipt({ hash })
205
+ }
206
+
207
+ store.cancelMint(swapId)
208
+ },
209
+ [walletClient, publicClient, store, config]
210
+ )
211
+
212
+ /**
213
+ * Get mint state by swap ID
214
+ */
215
+ const getMintState = useCallback(
216
+ (swapId: string): CrossChainMintState | undefined => {
217
+ return store.getPendingMint(swapId)?.state
218
+ },
219
+ [store]
220
+ )
221
+
222
+ /**
223
+ * Poll for status updates on active mints
224
+ */
225
+ useEffect(() => {
226
+ if (!publicClient) return
227
+
228
+ const activeMints = store.getActiveMints()
229
+ if (activeMints.length === 0) return
230
+
231
+ const pollStatuses = async () => {
232
+ for (const mint of activeMints) {
233
+ if (!mint.state.swapId) continue
234
+
235
+ try {
236
+ // Query on-chain state
237
+ const state = await publicClient.readContract({
238
+ address: config.bridgeContract,
239
+ abi: BRIDGE_ABI,
240
+ functionName: 'getSwapState',
241
+ args: [mint.state.swapId],
242
+ })
243
+
244
+ // Map on-chain state to our status
245
+ const [onChainState] = state as [number, ...unknown[]]
246
+ let newStatus: CrossChainMintStatus = mint.state.status
247
+
248
+ switch (onChainState) {
249
+ case 0: // Pending
250
+ newStatus = 'waiting_confirmation'
251
+ break
252
+ case 1: // Locked
253
+ newStatus = 'locking'
254
+ break
255
+ case 2: // Minted
256
+ newStatus = 'minting'
257
+ break
258
+ case 3: // Swapped
259
+ newStatus = 'swapping'
260
+ break
261
+ case 4: // Settled
262
+ newStatus = 'complete'
263
+ store.completeMint(mint.state.swapId)
264
+ break
265
+ case 5: // Cancelled
266
+ newStatus = 'cancelled'
267
+ store.cancelMint(mint.state.swapId)
268
+ break
269
+ case 6: // Expired
270
+ newStatus = 'failed'
271
+ store.failMint(mint.state.swapId, 'Swap expired')
272
+ break
273
+ }
274
+
275
+ if (newStatus !== mint.state.status && onChainState < 4) {
276
+ store.updateMintStatus(mint.state.swapId, newStatus)
277
+ }
278
+ } catch {
279
+ // Ignore polling errors
280
+ }
281
+ }
282
+ }
283
+
284
+ pollStatuses()
285
+ const interval = setInterval(pollStatuses, pollInterval)
286
+
287
+ return () => clearInterval(interval)
288
+ }, [publicClient, store, config, pollInterval])
289
+
290
+ return {
291
+ mint,
292
+ cancel,
293
+ getMintState,
294
+ activeMints: store.pendingMints,
295
+ recentMints: store.recentMints,
296
+ isLoading,
297
+ error,
298
+ }
299
+ }