@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,951 @@
1
+ /**
2
+ * Private Teleport Hook
3
+ *
4
+ * React hook for cross-chain private teleportation
5
+ * Enables: XVM UTXO → ZNote (shielded) → Z-Chain AMM → destination
6
+ */
7
+
8
+ import { useCallback, useEffect, useState } from 'react'
9
+ import { usePublicClient, useWalletClient } from 'wagmi'
10
+ import { encodeFunctionData, keccak256, toHex } from 'viem'
11
+
12
+ import {
13
+ type PrivateTeleportConfig,
14
+ type PrivateTeleportRequest,
15
+ type TeleportRecord,
16
+ TeleportState,
17
+ type TeleportStateString,
18
+ type PedersenCommitment,
19
+ type EncryptedValue,
20
+ type RangeProof,
21
+ type MerkleProof,
22
+ DEFAULT_PRIVATE_TELEPORT_CONFIG,
23
+ PRIVATE_TELEPORT_ABI,
24
+ ZNOTE_ABI,
25
+ ZCHAIN_AMM_ABI,
26
+ } from './private-teleport-types'
27
+
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // HOOK OPTIONS & RETURN TYPE
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+
32
+ export interface UsePrivateTeleportOptions {
33
+ /** Custom config */
34
+ config?: Partial<PrivateTeleportConfig>
35
+ /** Enable auto-polling for teleport status */
36
+ pollInterval?: number
37
+ /** Callback when teleport state changes */
38
+ onStateChange?: (teleportId: string, state: TeleportState) => void
39
+ }
40
+
41
+ /** Private transfer request to another recipient */
42
+ export interface PrivateTransferRequest {
43
+ teleportId: string
44
+ /** Recipient's viewing public key (for note encryption) */
45
+ recipientViewKey: `0x${string}`
46
+ /** Amount to send (will generate new commitment) */
47
+ amount: bigint
48
+ /** Optional: change amount back to sender */
49
+ changeAmount?: bigint
50
+ }
51
+
52
+ /** Unshield request to X-Chain */
53
+ export interface UnshieldToXChainRequest {
54
+ teleportId: string
55
+ /** X-Chain destination address (bech32 format) */
56
+ destinationAddress: string
57
+ /** Amount to unshield (must match commitment) */
58
+ amount: bigint
59
+ }
60
+
61
+ export interface UsePrivateTeleportReturn {
62
+ /** Initiate a private teleport */
63
+ teleport: (request: PrivateTeleportRequest) => Promise<string>
64
+ /** Execute private swap on Z-Chain AMM */
65
+ executeSwap: (teleportId: string, poolId: string, minOutput: bigint) => Promise<void>
66
+ /** Export to C-Chain (unshield) */
67
+ exportToDestination: (teleportId: string) => Promise<void>
68
+ /** Unshield back to X-Chain UTXO */
69
+ unshieldToXChain: (request: UnshieldToXChainRequest) => Promise<string>
70
+ /** Private transfer to another recipient (stays shielded) */
71
+ privateTransfer: (request: PrivateTransferRequest) => Promise<number>
72
+ /** Split note into payment + change */
73
+ splitAndTransfer: (teleportId: string, outputs: Array<{recipient: `0x${string}`, amount: bigint}>) => Promise<number[]>
74
+ /** Complete teleport after confirmation */
75
+ completeTeleport: (teleportId: string, warpConfirmation: `0x${string}`) => Promise<void>
76
+ /** Cancel teleport */
77
+ cancelTeleport: (teleportId: string) => Promise<void>
78
+ /** Get teleport record */
79
+ getTeleport: (teleportId: string) => Promise<TeleportRecord | null>
80
+ /** Check if teleport is complete */
81
+ isComplete: (teleportId: string) => Promise<boolean>
82
+ /** Current teleport ID (if any) */
83
+ currentTeleportId: string | null
84
+ /** Current teleport state */
85
+ currentState: TeleportState | null
86
+ /** Loading state */
87
+ isLoading: boolean
88
+ /** Error state */
89
+ error: Error | null
90
+ }
91
+
92
+ // ═══════════════════════════════════════════════════════════════════════════
93
+ // CRYPTO UTILITIES (STUB - REAL IMPL USES @luxfi/crypto)
94
+ // ═══════════════════════════════════════════════════════════════════════════
95
+
96
+ /**
97
+ * Generate Pedersen commitment to an amount
98
+ * In production: Uses @luxfi/crypto Pedersen commitment scheme
99
+ */
100
+ async function generateCommitment(
101
+ amount: bigint,
102
+ blindingFactor?: `0x${string}`
103
+ ): Promise<PedersenCommitment> {
104
+ // Generate random blinding factor if not provided
105
+ const bf = blindingFactor ?? (toHex(BigInt(Math.random() * Number.MAX_SAFE_INTEGER)) as `0x${string}`)
106
+
107
+ // Pedersen: C = g^amount * h^blinding
108
+ // Simplified for now - real implementation uses elliptic curve ops
109
+ const commitment = keccak256(
110
+ `${toHex(amount)}${bf.slice(2)}` as `0x${string}`
111
+ )
112
+
113
+ return {
114
+ commitment,
115
+ blindingFactor: bf,
116
+ }
117
+ }
118
+
119
+ /**
120
+ * FHE-encrypt a value
121
+ * In production: Uses @luxfi/fhe TFHE encryption
122
+ */
123
+ async function fheEncrypt(
124
+ value: bigint,
125
+ publicKey: `0x${string}`
126
+ ): Promise<EncryptedValue> {
127
+ // Simplified - real implementation uses TFHE
128
+ const ciphertext = keccak256(
129
+ `${toHex(value)}${publicKey.slice(2)}` as `0x${string}`
130
+ )
131
+
132
+ return {
133
+ ciphertext,
134
+ publicKey,
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Generate Bulletproof range proof
140
+ * In production: Uses @luxfi/crypto Bulletproof implementation
141
+ */
142
+ async function generateRangeProof(
143
+ amount: bigint,
144
+ commitment: `0x${string}`,
145
+ blindingFactor: `0x${string}`,
146
+ rangeBits: number = 64
147
+ ): Promise<RangeProof> {
148
+ // Simplified - real implementation generates actual Bulletproof
149
+ const proof = keccak256(
150
+ `${commitment}${blindingFactor.slice(2)}${toHex(amount).slice(2)}` as `0x${string}`
151
+ )
152
+
153
+ return {
154
+ proof,
155
+ commitment,
156
+ rangeBits,
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Generate nullifier for spending a note
162
+ */
163
+ function generateNullifier(
164
+ commitment: `0x${string}`,
165
+ spendingKey: `0x${string}`,
166
+ noteIndex: number
167
+ ): `0x${string}` {
168
+ return keccak256(
169
+ `${commitment}${spendingKey.slice(2)}${toHex(noteIndex).slice(2)}` as `0x${string}`
170
+ )
171
+ }
172
+
173
+ // ═══════════════════════════════════════════════════════════════════════════
174
+ // STATE MAPPING
175
+ // ═══════════════════════════════════════════════════════════════════════════
176
+
177
+ const stateMap: Record<number, TeleportState> = {
178
+ 0: TeleportState.INITIATED,
179
+ 1: TeleportState.SHIELDED,
180
+ 2: TeleportState.SWAP_COMPLETE,
181
+ 3: TeleportState.EXPORTED,
182
+ 4: TeleportState.COMPLETED,
183
+ 5: TeleportState.CANCELLED,
184
+ 6: TeleportState.EXPIRED,
185
+ }
186
+
187
+ // ═══════════════════════════════════════════════════════════════════════════
188
+ // HOOK IMPLEMENTATION
189
+ // ═══════════════════════════════════════════════════════════════════════════
190
+
191
+ export function usePrivateTeleport(
192
+ options: UsePrivateTeleportOptions = {}
193
+ ): UsePrivateTeleportReturn {
194
+ const {
195
+ config: configOverrides,
196
+ pollInterval = 5000,
197
+ onStateChange,
198
+ } = options
199
+
200
+ const config: PrivateTeleportConfig = {
201
+ ...DEFAULT_PRIVATE_TELEPORT_CONFIG,
202
+ ...configOverrides,
203
+ }
204
+
205
+ const publicClient = usePublicClient()
206
+ const { data: walletClient } = useWalletClient()
207
+
208
+ const [currentTeleportId, setCurrentTeleportId] = useState<string | null>(null)
209
+ const [currentState, setCurrentState] = useState<TeleportState | null>(null)
210
+ const [isLoading, setIsLoading] = useState(false)
211
+ const [error, setError] = useState<Error | null>(null)
212
+
213
+ // Store commitment secrets for later use
214
+ const [secrets, setSecrets] = useState<Map<string, {
215
+ blindingFactor: `0x${string}`
216
+ spendingKey: `0x${string}`
217
+ noteIndex: number
218
+ }>>(new Map())
219
+
220
+ // ─────────────────────────────────────────────────────────────────────────
221
+ // INITIATE TELEPORT
222
+ // ─────────────────────────────────────────────────────────────────────────
223
+
224
+ const teleport = useCallback(async (request: PrivateTeleportRequest): Promise<string> => {
225
+ if (!walletClient || !publicClient) {
226
+ throw new Error('Wallet not connected')
227
+ }
228
+
229
+ setIsLoading(true)
230
+ setError(null)
231
+
232
+ try {
233
+ // Generate Pedersen commitment
234
+ const commitment = await generateCommitment(request.amount)
235
+
236
+ // FHE-encrypt the amount
237
+ const fhePublicKey = '0x' + '00'.repeat(32) as `0x${string}` // Would fetch from ZNote
238
+ const encrypted = await fheEncrypt(request.amount, fhePublicKey)
239
+
240
+ // Build Warp message (would be signed by validators in production)
241
+ const warpMessage = encodeFunctionData({
242
+ abi: [{ name: 'teleport', type: 'function', inputs: [
243
+ { name: 'sourceChain', type: 'bytes32' },
244
+ { name: 'sourceAsset', type: 'bytes32' },
245
+ { name: 'sender', type: 'address' },
246
+ { name: 'deadline', type: 'uint256' },
247
+ ], outputs: [] }],
248
+ functionName: 'teleport',
249
+ args: [
250
+ request.sourceChain,
251
+ request.sourceAsset,
252
+ walletClient.account.address,
253
+ BigInt(request.deadline),
254
+ ],
255
+ })
256
+
257
+ // Call initiateTeleport
258
+ const txData = encodeFunctionData({
259
+ abi: PRIVATE_TELEPORT_ABI,
260
+ functionName: 'initiateTeleport',
261
+ args: [
262
+ warpMessage,
263
+ commitment.commitment,
264
+ encrypted.ciphertext,
265
+ request.recipient,
266
+ request.destChain,
267
+ request.destAsset ?? request.sourceAsset,
268
+ request.privateSwap,
269
+ ],
270
+ })
271
+
272
+ const hash = await walletClient.sendTransaction({
273
+ to: config.teleportContract,
274
+ data: txData,
275
+ value: BigInt(0),
276
+ })
277
+
278
+ // Wait for confirmation
279
+ const receipt = await publicClient.waitForTransactionReceipt({ hash })
280
+
281
+ // Extract teleportId from logs (simplified)
282
+ const teleportId = receipt.logs[0]?.topics[1] ?? hash
283
+
284
+ // Store secrets for later
285
+ const spendingKey = keccak256(toHex(Date.now())) as `0x${string}`
286
+ setSecrets(prev => new Map(prev).set(teleportId, {
287
+ blindingFactor: commitment.blindingFactor!,
288
+ spendingKey,
289
+ noteIndex: 0, // Would be extracted from event
290
+ }))
291
+
292
+ setCurrentTeleportId(teleportId)
293
+ setCurrentState(TeleportState.INITIATED)
294
+
295
+ return teleportId
296
+ } catch (err) {
297
+ const e = err instanceof Error ? err : new Error(String(err))
298
+ setError(e)
299
+ throw e
300
+ } finally {
301
+ setIsLoading(false)
302
+ }
303
+ }, [walletClient, publicClient, config])
304
+
305
+ // ─────────────────────────────────────────────────────────────────────────
306
+ // EXECUTE PRIVATE SWAP
307
+ // ─────────────────────────────────────────────────────────────────────────
308
+
309
+ const executeSwap = useCallback(async (
310
+ teleportId: string,
311
+ poolId: string,
312
+ minOutput: bigint
313
+ ): Promise<void> => {
314
+ if (!walletClient || !publicClient) {
315
+ throw new Error('Wallet not connected')
316
+ }
317
+
318
+ setIsLoading(true)
319
+ setError(null)
320
+
321
+ try {
322
+ // Encrypt minimum output
323
+ const fhePublicKey = '0x' + '00'.repeat(32) as `0x${string}`
324
+ const encryptedMinOutput = await fheEncrypt(minOutput, fhePublicKey)
325
+
326
+ // Generate swap proof (stub)
327
+ const swapProof = keccak256(teleportId as `0x${string}`) as `0x${string}`
328
+
329
+ const txData = encodeFunctionData({
330
+ abi: PRIVATE_TELEPORT_ABI,
331
+ functionName: 'executePrivateSwap',
332
+ args: [
333
+ teleportId as `0x${string}`,
334
+ poolId as `0x${string}`,
335
+ encryptedMinOutput.ciphertext,
336
+ swapProof,
337
+ ],
338
+ })
339
+
340
+ const hash = await walletClient.sendTransaction({
341
+ to: config.teleportContract,
342
+ data: txData,
343
+ value: BigInt(0),
344
+ })
345
+
346
+ await publicClient.waitForTransactionReceipt({ hash })
347
+ setCurrentState(TeleportState.SWAP_COMPLETE)
348
+ onStateChange?.(teleportId, TeleportState.SWAP_COMPLETE)
349
+ } catch (err) {
350
+ const e = err instanceof Error ? err : new Error(String(err))
351
+ setError(e)
352
+ throw e
353
+ } finally {
354
+ setIsLoading(false)
355
+ }
356
+ }, [walletClient, publicClient, config, onStateChange])
357
+
358
+ // ─────────────────────────────────────────────────────────────────────────
359
+ // GET TELEPORT RECORD (defined early for use by other callbacks)
360
+ // ─────────────────────────────────────────────────────────────────────────
361
+
362
+ const getTeleportRecord = useCallback(async (teleportId: string): Promise<TeleportRecord | null> => {
363
+ if (!publicClient) {
364
+ throw new Error('Client not connected')
365
+ }
366
+
367
+ try {
368
+ const result = await publicClient.readContract({
369
+ address: config.teleportContract,
370
+ abi: PRIVATE_TELEPORT_ABI,
371
+ functionName: 'getTeleport',
372
+ args: [teleportId as `0x${string}`],
373
+ })
374
+
375
+ const tuple = result as unknown as [
376
+ `0x${string}`, number, `0x${string}`, `0x${string}`, `0x${string}`, `0x${string}`,
377
+ `0x${string}`, `0x${string}`, `0x${string}`, `0x${string}`, `0x${string}`,
378
+ bigint, bigint, boolean
379
+ ]
380
+ const [
381
+ id, state, sourceChain, destChain, sourceAsset, destAsset,
382
+ noteCommitment, encryptedAmount, nullifierHash, sender, recipient,
383
+ deadline, createdBlock, privateSwap
384
+ ] = tuple
385
+
386
+ if (id === '0x' + '00'.repeat(32)) {
387
+ return null
388
+ }
389
+
390
+ return {
391
+ teleportId: id,
392
+ state: stateMap[state] ?? TeleportState.INITIATED,
393
+ sourceChain,
394
+ destChain,
395
+ sourceAsset,
396
+ destAsset,
397
+ noteCommitment,
398
+ encryptedAmount,
399
+ nullifierHash: nullifierHash !== '0x' + '00'.repeat(32) ? nullifierHash : undefined,
400
+ sender,
401
+ recipient,
402
+ deadline: Number(deadline),
403
+ createdBlock: Number(createdBlock),
404
+ privateSwap,
405
+ }
406
+ } catch {
407
+ return null
408
+ }
409
+ }, [publicClient, config])
410
+
411
+ // ─────────────────────────────────────────────────────────────────────────
412
+ // EXPORT TO DESTINATION
413
+ // ─────────────────────────────────────────────────────────────────────────
414
+
415
+ const exportToDestination = useCallback(async (teleportId: string): Promise<void> => {
416
+ if (!walletClient || !publicClient) {
417
+ throw new Error('Wallet not connected')
418
+ }
419
+
420
+ const secret = secrets.get(teleportId)
421
+ if (!secret) {
422
+ throw new Error('Secrets not found for teleport')
423
+ }
424
+
425
+ setIsLoading(true)
426
+ setError(null)
427
+
428
+ try {
429
+ // Get teleport record to get commitment
430
+ const record = await getTeleportRecord(teleportId)
431
+ if (!record) {
432
+ throw new Error('Teleport not found')
433
+ }
434
+
435
+ // Generate range proof
436
+ const rangeProof = await generateRangeProof(
437
+ BigInt(0), // Would get actual amount from decrypted value
438
+ record.noteCommitment,
439
+ secret.blindingFactor
440
+ )
441
+
442
+ // Generate nullifier
443
+ const nullifier = generateNullifier(
444
+ record.noteCommitment,
445
+ secret.spendingKey,
446
+ secret.noteIndex
447
+ )
448
+
449
+ // Get Merkle proof from ZNote
450
+ const merkleProofData = await publicClient.readContract({
451
+ address: config.zNoteContract,
452
+ abi: ZNOTE_ABI,
453
+ functionName: 'getMerkleProof',
454
+ args: [BigInt(secret.noteIndex)],
455
+ }) as `0x${string}`[]
456
+
457
+ const txData = encodeFunctionData({
458
+ abi: PRIVATE_TELEPORT_ABI,
459
+ functionName: 'exportToDestination',
460
+ args: [
461
+ teleportId as `0x${string}`,
462
+ rangeProof.proof,
463
+ nullifier,
464
+ merkleProofData,
465
+ ],
466
+ })
467
+
468
+ const hash = await walletClient.sendTransaction({
469
+ to: config.teleportContract,
470
+ data: txData,
471
+ value: BigInt(0),
472
+ })
473
+
474
+ await publicClient.waitForTransactionReceipt({ hash })
475
+ setCurrentState(TeleportState.EXPORTED)
476
+ onStateChange?.(teleportId, TeleportState.EXPORTED)
477
+ } catch (err) {
478
+ const e = err instanceof Error ? err : new Error(String(err))
479
+ setError(e)
480
+ throw e
481
+ } finally {
482
+ setIsLoading(false)
483
+ }
484
+ }, [walletClient, publicClient, config, secrets, onStateChange])
485
+
486
+ // ─────────────────────────────────────────────────────────────────────────
487
+ // COMPLETE TELEPORT
488
+ // ─────────────────────────────────────────────────────────────────────────
489
+
490
+ const completeTeleport = useCallback(async (
491
+ teleportId: string,
492
+ warpConfirmation: `0x${string}`
493
+ ): Promise<void> => {
494
+ if (!walletClient || !publicClient) {
495
+ throw new Error('Wallet not connected')
496
+ }
497
+
498
+ setIsLoading(true)
499
+ setError(null)
500
+
501
+ try {
502
+ const txData = encodeFunctionData({
503
+ abi: PRIVATE_TELEPORT_ABI,
504
+ functionName: 'completeTeleport',
505
+ args: [teleportId as `0x${string}`, warpConfirmation],
506
+ })
507
+
508
+ const hash = await walletClient.sendTransaction({
509
+ to: config.teleportContract,
510
+ data: txData,
511
+ value: BigInt(0),
512
+ })
513
+
514
+ await publicClient.waitForTransactionReceipt({ hash })
515
+ setCurrentState(TeleportState.COMPLETED)
516
+ onStateChange?.(teleportId, TeleportState.COMPLETED)
517
+
518
+ // Clean up secrets
519
+ setSecrets(prev => {
520
+ const next = new Map(prev)
521
+ next.delete(teleportId)
522
+ return next
523
+ })
524
+ } catch (err) {
525
+ const e = err instanceof Error ? err : new Error(String(err))
526
+ setError(e)
527
+ throw e
528
+ } finally {
529
+ setIsLoading(false)
530
+ }
531
+ }, [walletClient, publicClient, config, onStateChange])
532
+
533
+ // ─────────────────────────────────────────────────────────────────────────
534
+ // UNSHIELD TO X-CHAIN
535
+ // ─────────────────────────────────────────────────────────────────────────
536
+
537
+ const unshieldToXChain = useCallback(async (request: UnshieldToXChainRequest): Promise<string> => {
538
+ if (!walletClient || !publicClient) {
539
+ throw new Error('Wallet not connected')
540
+ }
541
+
542
+ const secret = secrets.get(request.teleportId)
543
+ if (!secret) {
544
+ throw new Error('Secrets not found for teleport')
545
+ }
546
+
547
+ setIsLoading(true)
548
+ setError(null)
549
+
550
+ try {
551
+ const record = await getTeleportRecord(request.teleportId)
552
+ if (!record) {
553
+ throw new Error('Teleport not found')
554
+ }
555
+
556
+ // Generate range proof (proves amount matches commitment)
557
+ const rangeProof = await generateRangeProof(
558
+ request.amount,
559
+ record.noteCommitment,
560
+ secret.blindingFactor
561
+ )
562
+
563
+ // Generate nullifier
564
+ const nullifier = generateNullifier(
565
+ record.noteCommitment,
566
+ secret.spendingKey,
567
+ secret.noteIndex
568
+ )
569
+
570
+ // Get Merkle proof
571
+ const merkleProofData = await publicClient.readContract({
572
+ address: config.zNoteContract,
573
+ abi: ZNOTE_ABI,
574
+ functionName: 'getMerkleProof',
575
+ args: [BigInt(secret.noteIndex)],
576
+ }) as `0x${string}`[]
577
+
578
+ // Encode destination address
579
+ const destinationBytes = toHex(new TextEncoder().encode(request.destinationAddress))
580
+
581
+ const txData = encodeFunctionData({
582
+ abi: PRIVATE_TELEPORT_ABI,
583
+ functionName: 'unshieldToXChain',
584
+ args: [
585
+ request.teleportId as `0x${string}`,
586
+ destinationBytes as `0x${string}`,
587
+ BigInt(request.amount),
588
+ nullifier,
589
+ merkleProofData,
590
+ rangeProof.proof,
591
+ ],
592
+ })
593
+
594
+ const hash = await walletClient.sendTransaction({
595
+ to: config.teleportContract,
596
+ data: txData,
597
+ value: BigInt(0),
598
+ })
599
+
600
+ const receipt = await publicClient.waitForTransactionReceipt({ hash })
601
+
602
+ // Extract exportTxId from logs
603
+ const exportTxId = receipt.logs[0]?.topics[1] ?? hash
604
+
605
+ setCurrentState(TeleportState.COMPLETED)
606
+ onStateChange?.(request.teleportId, TeleportState.COMPLETED)
607
+
608
+ // Clean up secrets
609
+ setSecrets(prev => {
610
+ const next = new Map(prev)
611
+ next.delete(request.teleportId)
612
+ return next
613
+ })
614
+
615
+ return exportTxId
616
+ } catch (err) {
617
+ const e = err instanceof Error ? err : new Error(String(err))
618
+ setError(e)
619
+ throw e
620
+ } finally {
621
+ setIsLoading(false)
622
+ }
623
+ }, [walletClient, publicClient, config, secrets, onStateChange, getTeleportRecord])
624
+
625
+ // ─────────────────────────────────────────────────────────────────────────
626
+ // PRIVATE TRANSFER TO RECIPIENT
627
+ // ─────────────────────────────────────────────────────────────────────────
628
+
629
+ const privateTransfer = useCallback(async (request: PrivateTransferRequest): Promise<number> => {
630
+ if (!walletClient || !publicClient) {
631
+ throw new Error('Wallet not connected')
632
+ }
633
+
634
+ const secret = secrets.get(request.teleportId)
635
+ if (!secret) {
636
+ throw new Error('Secrets not found for teleport')
637
+ }
638
+
639
+ setIsLoading(true)
640
+ setError(null)
641
+
642
+ try {
643
+ const record = await getTeleportRecord(request.teleportId)
644
+ if (!record) {
645
+ throw new Error('Teleport not found')
646
+ }
647
+
648
+ // Generate new commitment for recipient
649
+ const recipientCommitment = await generateCommitment(request.amount)
650
+
651
+ // Encrypt note to recipient's viewing key
652
+ const encryptedNote = keccak256(
653
+ `${request.recipientViewKey}${recipientCommitment.commitment.slice(2)}` as `0x${string}`
654
+ )
655
+
656
+ // Generate nullifier
657
+ const nullifier = generateNullifier(
658
+ record.noteCommitment,
659
+ secret.spendingKey,
660
+ secret.noteIndex
661
+ )
662
+
663
+ // Get Merkle proof
664
+ const merkleProofData = await publicClient.readContract({
665
+ address: config.zNoteContract,
666
+ abi: ZNOTE_ABI,
667
+ functionName: 'getMerkleProof',
668
+ args: [BigInt(secret.noteIndex)],
669
+ }) as `0x${string}`[]
670
+
671
+ // Generate transfer proof (proves amount conservation)
672
+ const transferProof = keccak256(
673
+ `${record.noteCommitment}${recipientCommitment.commitment.slice(2)}` as `0x${string}`
674
+ )
675
+
676
+ const txData = encodeFunctionData({
677
+ abi: PRIVATE_TELEPORT_ABI,
678
+ functionName: 'privateTransferToRecipient',
679
+ args: [
680
+ request.teleportId as `0x${string}`,
681
+ recipientCommitment.commitment,
682
+ encryptedNote,
683
+ nullifier,
684
+ merkleProofData,
685
+ transferProof,
686
+ ],
687
+ })
688
+
689
+ const hash = await walletClient.sendTransaction({
690
+ to: config.teleportContract,
691
+ data: txData,
692
+ value: BigInt(0),
693
+ })
694
+
695
+ const receipt = await publicClient.waitForTransactionReceipt({ hash })
696
+
697
+ // Extract note index from logs (simplified)
698
+ const newNoteIndex = Number(receipt.logs[0]?.data?.slice(0, 66) ?? '0')
699
+
700
+ setCurrentState(TeleportState.COMPLETED)
701
+ onStateChange?.(request.teleportId, TeleportState.COMPLETED)
702
+
703
+ // Clean up secrets for this teleport
704
+ setSecrets(prev => {
705
+ const next = new Map(prev)
706
+ next.delete(request.teleportId)
707
+ return next
708
+ })
709
+
710
+ return newNoteIndex
711
+ } catch (err) {
712
+ const e = err instanceof Error ? err : new Error(String(err))
713
+ setError(e)
714
+ throw e
715
+ } finally {
716
+ setIsLoading(false)
717
+ }
718
+ }, [walletClient, publicClient, config, secrets, onStateChange, getTeleportRecord])
719
+
720
+ // ─────────────────────────────────────────────────────────────────────────
721
+ // SPLIT AND TRANSFER
722
+ // ─────────────────────────────────────────────────────────────────────────
723
+
724
+ const splitAndTransfer = useCallback(async (
725
+ teleportId: string,
726
+ outputs: Array<{recipient: `0x${string}`, amount: bigint}>
727
+ ): Promise<number[]> => {
728
+ if (!walletClient || !publicClient) {
729
+ throw new Error('Wallet not connected')
730
+ }
731
+
732
+ const secret = secrets.get(teleportId)
733
+ if (!secret) {
734
+ throw new Error('Secrets not found for teleport')
735
+ }
736
+
737
+ if (outputs.length === 0 || outputs.length > 16) {
738
+ throw new Error('Invalid number of outputs (1-16)')
739
+ }
740
+
741
+ setIsLoading(true)
742
+ setError(null)
743
+
744
+ try {
745
+ const record = await getTeleportRecord(teleportId)
746
+ if (!record) {
747
+ throw new Error('Teleport not found')
748
+ }
749
+
750
+ // Generate commitments for each output
751
+ const outputNotes = await Promise.all(
752
+ outputs.map(async (output) => {
753
+ const commitment = await generateCommitment(output.amount)
754
+ const encryptedNote = keccak256(
755
+ `${output.recipient}${commitment.commitment.slice(2)}` as `0x${string}`
756
+ )
757
+
758
+ return {
759
+ commitment: commitment.commitment,
760
+ encryptedNote,
761
+ encryptedMemo: '0x' as `0x${string}`,
762
+ }
763
+ })
764
+ )
765
+
766
+ // Generate nullifier
767
+ const nullifier = generateNullifier(
768
+ record.noteCommitment,
769
+ secret.spendingKey,
770
+ secret.noteIndex
771
+ )
772
+
773
+ // Get Merkle proof
774
+ const merkleProofData = await publicClient.readContract({
775
+ address: config.zNoteContract,
776
+ abi: ZNOTE_ABI,
777
+ functionName: 'getMerkleProof',
778
+ args: [BigInt(secret.noteIndex)],
779
+ }) as `0x${string}`[]
780
+
781
+ // Generate split proof (proves sum of outputs = input)
782
+ const splitProof = keccak256(
783
+ `${record.noteCommitment}${outputNotes.map(n => n.commitment.slice(2)).join('')}` as `0x${string}`
784
+ )
785
+
786
+ const txData = encodeFunctionData({
787
+ abi: PRIVATE_TELEPORT_ABI,
788
+ functionName: 'splitAndTransfer',
789
+ args: [
790
+ teleportId as `0x${string}`,
791
+ outputNotes,
792
+ nullifier,
793
+ merkleProofData,
794
+ splitProof,
795
+ ],
796
+ })
797
+
798
+ const hash = await walletClient.sendTransaction({
799
+ to: config.teleportContract,
800
+ data: txData,
801
+ value: BigInt(0),
802
+ })
803
+
804
+ const receipt = await publicClient.waitForTransactionReceipt({ hash })
805
+
806
+ // Extract note indices from logs (simplified)
807
+ const noteIndices = outputs.map((_, i) => i)
808
+
809
+ setCurrentState(TeleportState.COMPLETED)
810
+ onStateChange?.(teleportId, TeleportState.COMPLETED)
811
+
812
+ // Clean up secrets for this teleport
813
+ setSecrets(prev => {
814
+ const next = new Map(prev)
815
+ next.delete(teleportId)
816
+ return next
817
+ })
818
+
819
+ return noteIndices
820
+ } catch (err) {
821
+ const e = err instanceof Error ? err : new Error(String(err))
822
+ setError(e)
823
+ throw e
824
+ } finally {
825
+ setIsLoading(false)
826
+ }
827
+ }, [walletClient, publicClient, config, secrets, onStateChange, getTeleportRecord])
828
+
829
+ // ─────────────────────────────────────────────────────────────────────────
830
+ // CANCEL TELEPORT
831
+ // ─────────────────────────────────────────────────────────────────────────
832
+
833
+ const cancelTeleport = useCallback(async (teleportId: string): Promise<void> => {
834
+ if (!walletClient || !publicClient) {
835
+ throw new Error('Wallet not connected')
836
+ }
837
+
838
+ setIsLoading(true)
839
+ setError(null)
840
+
841
+ try {
842
+ const txData = encodeFunctionData({
843
+ abi: PRIVATE_TELEPORT_ABI,
844
+ functionName: 'cancelTeleport',
845
+ args: [teleportId as `0x${string}`],
846
+ })
847
+
848
+ const hash = await walletClient.sendTransaction({
849
+ to: config.teleportContract,
850
+ data: txData,
851
+ value: BigInt(0),
852
+ })
853
+
854
+ await publicClient.waitForTransactionReceipt({ hash })
855
+ setCurrentState(TeleportState.CANCELLED)
856
+ onStateChange?.(teleportId, TeleportState.CANCELLED)
857
+
858
+ // Clean up secrets
859
+ setSecrets(prev => {
860
+ const next = new Map(prev)
861
+ next.delete(teleportId)
862
+ return next
863
+ })
864
+ } catch (err) {
865
+ const e = err instanceof Error ? err : new Error(String(err))
866
+ setError(e)
867
+ throw e
868
+ } finally {
869
+ setIsLoading(false)
870
+ }
871
+ }, [walletClient, publicClient, config, onStateChange])
872
+
873
+ // Wrapper for external API
874
+ const getTeleport = useCallback(async (teleportId: string): Promise<TeleportRecord | null> => {
875
+ return getTeleportRecord(teleportId)
876
+ }, [getTeleportRecord])
877
+
878
+ // ─────────────────────────────────────────────────────────────────────────
879
+ // CHECK COMPLETION
880
+ // ─────────────────────────────────────────────────────────────────────────
881
+
882
+ const isComplete = useCallback(async (teleportId: string): Promise<boolean> => {
883
+ if (!publicClient) {
884
+ throw new Error('Client not connected')
885
+ }
886
+
887
+ try {
888
+ const result = await publicClient.readContract({
889
+ address: config.teleportContract,
890
+ abi: PRIVATE_TELEPORT_ABI,
891
+ functionName: 'isComplete',
892
+ args: [teleportId as `0x${string}`],
893
+ })
894
+ return result as boolean
895
+ } catch {
896
+ return false
897
+ }
898
+ }, [publicClient, config])
899
+
900
+ // ─────────────────────────────────────────────────────────────────────────
901
+ // POLLING
902
+ // ─────────────────────────────────────────────────────────────────────────
903
+
904
+ useEffect(() => {
905
+ if (!currentTeleportId || !publicClient || currentState === TeleportState.COMPLETED || currentState === TeleportState.CANCELLED) {
906
+ return
907
+ }
908
+
909
+ const poll = async () => {
910
+ const record = await getTeleportRecord(currentTeleportId)
911
+ if (record && record.state !== currentState) {
912
+ setCurrentState(record.state)
913
+ onStateChange?.(currentTeleportId, record.state)
914
+ }
915
+ }
916
+
917
+ const interval = setInterval(poll, pollInterval)
918
+ return () => clearInterval(interval)
919
+ }, [currentTeleportId, currentState, publicClient, pollInterval, onStateChange, getTeleportRecord])
920
+
921
+ return {
922
+ // Initiate
923
+ teleport,
924
+ executeSwap,
925
+ // Export (unshield)
926
+ exportToDestination,
927
+ unshieldToXChain,
928
+ // Private transfers (stay shielded)
929
+ privateTransfer,
930
+ splitAndTransfer,
931
+ // Complete/cancel
932
+ completeTeleport,
933
+ cancelTeleport,
934
+ // Queries
935
+ getTeleport,
936
+ isComplete,
937
+ // State
938
+ currentTeleportId,
939
+ currentState,
940
+ isLoading,
941
+ error,
942
+ }
943
+ }
944
+
945
+ // Re-export types for convenience
946
+ export type {
947
+ PrivateTeleportRequest,
948
+ TeleportRecord,
949
+ TeleportState,
950
+ PrivateTeleportConfig,
951
+ } from './private-teleport-types'