@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.
- package/dist/bridge/use-private-teleport.d.ts +1 -1
- package/dist/bridge/use-private-teleport.d.ts.map +1 -1
- package/dist/bridge/use-private-teleport.js +73 -71
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +5 -1
- package/src/bridge/cross-chain-store.ts +210 -0
- package/src/bridge/index.ts +81 -0
- package/src/bridge/private-teleport-types.ts +578 -0
- package/src/bridge/types.ts +125 -0
- package/src/bridge/use-cross-chain-mint.ts +299 -0
- package/src/bridge/use-private-teleport.ts +951 -0
- package/src/index.ts +2 -2
- package/dist/bridge/__tests__/use-private-teleport.test.d.ts +0 -2
- package/dist/bridge/__tests__/use-private-teleport.test.d.ts.map +0 -1
- package/dist/bridge/__tests__/use-private-teleport.test.js +0 -272
|
@@ -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
|
+
}
|