@sip-protocol/sdk 0.2.8 → 0.2.10

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -0
  3. package/dist/browser.d.mts +100 -2
  4. package/dist/browser.d.ts +100 -2
  5. package/dist/browser.js +1362 -268
  6. package/dist/browser.mjs +502 -16
  7. package/dist/{chunk-UPTISVCY.mjs → chunk-AV37IZST.mjs} +731 -15
  8. package/dist/{chunk-VITVG25F.mjs → chunk-XLEPIR2P.mjs} +2 -100
  9. package/dist/index-BFOKTz2z.d.ts +6062 -0
  10. package/dist/index-CAhjA4kh.d.mts +6062 -0
  11. package/dist/index.d.mts +2 -5609
  12. package/dist/index.d.ts +2 -5609
  13. package/dist/index.js +588 -154
  14. package/dist/index.mjs +5 -1
  15. package/dist/{noir-BHQtFvRk.d.mts → noir-BTyLXLlZ.d.mts} +1 -1
  16. package/dist/{noir-BHQtFvRk.d.ts → noir-BTyLXLlZ.d.ts} +1 -1
  17. package/dist/proofs/noir.d.mts +1 -1
  18. package/dist/proofs/noir.d.ts +1 -1
  19. package/dist/proofs/noir.js +11 -112
  20. package/dist/proofs/noir.mjs +10 -13
  21. package/package.json +16 -16
  22. package/src/browser.ts +23 -0
  23. package/src/index.ts +12 -0
  24. package/src/proofs/browser-utils.ts +389 -0
  25. package/src/proofs/browser.ts +246 -19
  26. package/src/proofs/circuits/funding_proof.json +1 -1
  27. package/src/proofs/noir.ts +14 -14
  28. package/src/proofs/worker.ts +426 -0
  29. package/src/zcash/bridge.ts +738 -0
  30. package/src/zcash/index.ts +36 -1
  31. package/src/zcash/swap-service.ts +793 -0
  32. package/dist/chunk-4VJHI66K.mjs +0 -12120
  33. package/dist/chunk-5BAS4D44.mjs +0 -10283
  34. package/dist/chunk-6WOV2YNG.mjs +0 -10179
  35. package/dist/chunk-DU7LQDD2.mjs +0 -10148
  36. package/dist/chunk-MR7HRCRS.mjs +0 -10165
  37. package/dist/chunk-NDGUWOOZ.mjs +0 -10157
  38. package/dist/chunk-O4Y2ZUDL.mjs +0 -12721
  39. package/dist/chunk-VXSHK7US.mjs +0 -10158
@@ -0,0 +1,738 @@
1
+ /**
2
+ * Zcash Bridge Module
3
+ *
4
+ * Bridges source chain tokens (ETH, SOL, etc.) to ZEC and optionally
5
+ * shields them to z-addresses in a single operation.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const bridge = new ZcashBridge({
10
+ * zcashService: zcashShieldedService,
11
+ * mode: 'demo',
12
+ * })
13
+ *
14
+ * // Get supported routes
15
+ * const routes = bridge.getSupportedRoutes()
16
+ *
17
+ * // Bridge ETH to shielded ZEC
18
+ * const result = await bridge.bridgeToShielded({
19
+ * sourceChain: 'ethereum',
20
+ * sourceToken: 'ETH',
21
+ * amount: 1000000000000000000n, // 1 ETH
22
+ * recipientZAddress: 'zs1...',
23
+ * })
24
+ * ```
25
+ */
26
+
27
+ import { ValidationError, IntentError, ErrorCode } from '../errors'
28
+ import type { ZcashShieldedService } from './shielded-service'
29
+ import type {
30
+ ZcashSwapSourceChain,
31
+ ZcashSwapSourceToken,
32
+ BridgeProvider,
33
+ PriceFeed,
34
+ } from './swap-service'
35
+
36
+ // ─── Types ─────────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Bridge route definition
40
+ */
41
+ export interface BridgeRoute {
42
+ /** Source chain identifier */
43
+ sourceChain: ZcashSwapSourceChain
44
+ /** Source token symbol */
45
+ sourceToken: ZcashSwapSourceToken
46
+ /** Whether route is currently active */
47
+ active: boolean
48
+ /** Minimum amount in smallest unit */
49
+ minAmount: bigint
50
+ /** Maximum amount in smallest unit */
51
+ maxAmount: bigint
52
+ /** Estimated time in seconds */
53
+ estimatedTime: number
54
+ /** Fee percentage (basis points) */
55
+ feeBps: number
56
+ }
57
+
58
+ /**
59
+ * Bridge parameters for bridging to shielded ZEC
60
+ */
61
+ export interface BridgeParams {
62
+ /** Source blockchain */
63
+ sourceChain: ZcashSwapSourceChain
64
+ /** Source token symbol */
65
+ sourceToken: ZcashSwapSourceToken
66
+ /** Amount in smallest unit (wei, lamports, etc.) */
67
+ amount: bigint
68
+ /** Recipient z-address (shielded) or t-address (transparent) */
69
+ recipientAddress: string
70
+ /** Whether to shield after bridging (default: true) */
71
+ shield?: boolean
72
+ /** Optional memo for shielded transaction */
73
+ memo?: string
74
+ /** Sender's source chain address (for tracking) */
75
+ senderAddress?: string
76
+ /** Custom slippage tolerance (basis points) */
77
+ slippage?: number
78
+ }
79
+
80
+ /**
81
+ * Bridge result
82
+ */
83
+ export interface BridgeResult {
84
+ /** Unique bridge request ID */
85
+ requestId: string
86
+ /** Current status */
87
+ status: BridgeStatus
88
+ /** Source chain transaction hash */
89
+ sourceTxHash?: string
90
+ /** Intermediate transparent address (if shielding) */
91
+ transparentAddress?: string
92
+ /** Transparent receive transaction (if shielding) */
93
+ transparentTxHash?: string
94
+ /** Final shielded transaction ID */
95
+ shieldedTxId?: string
96
+ /** Amount received in ZEC (zatoshis) */
97
+ amountReceived?: bigint
98
+ /** Amount received in ZEC formatted */
99
+ amountReceivedFormatted?: string
100
+ /** Recipient address */
101
+ recipientAddress: string
102
+ /** Total fee paid (zatoshis) */
103
+ totalFee: bigint
104
+ /** Timestamp of request */
105
+ timestamp: number
106
+ /** Error message if failed */
107
+ error?: string
108
+ }
109
+
110
+ /**
111
+ * Bridge status
112
+ */
113
+ export type BridgeStatus =
114
+ | 'pending' // Waiting to start
115
+ | 'bridging' // Cross-chain bridge in progress
116
+ | 'bridge_confirmed' // Bridge complete, ZEC received at t-addr
117
+ | 'shielding' // Shielding transparent ZEC
118
+ | 'completed' // All done
119
+ | 'failed' // Bridge failed
120
+
121
+ /**
122
+ * Bridge configuration
123
+ */
124
+ export interface ZcashBridgeConfig {
125
+ /** Zcash shielded service (required for shielding) */
126
+ zcashService?: ZcashShieldedService
127
+ /** Operating mode */
128
+ mode: 'demo' | 'production'
129
+ /** External bridge provider for production */
130
+ bridgeProvider?: BridgeProvider
131
+ /** Price feed for conversions */
132
+ priceFeed?: PriceFeed
133
+ /** Default slippage (basis points, default: 100 = 1%) */
134
+ defaultSlippage?: number
135
+ }
136
+
137
+ // ─── Constants ─────────────────────────────────────────────────────────────────
138
+
139
+ /**
140
+ * Token decimals by symbol
141
+ */
142
+ const TOKEN_DECIMALS: Record<string, number> = {
143
+ ETH: 18,
144
+ SOL: 9,
145
+ NEAR: 24,
146
+ MATIC: 18,
147
+ USDC: 6,
148
+ USDT: 6,
149
+ ZEC: 8,
150
+ }
151
+
152
+ /**
153
+ * Mock prices for demo mode (USD)
154
+ */
155
+ const MOCK_PRICES: Record<string, number> = {
156
+ ETH: 2500,
157
+ SOL: 120,
158
+ NEAR: 5,
159
+ MATIC: 0.8,
160
+ USDC: 1,
161
+ USDT: 1,
162
+ ZEC: 35,
163
+ }
164
+
165
+ /**
166
+ * Route configurations
167
+ */
168
+ const ROUTE_CONFIG: Record<string, { minUsd: number; maxUsd: number; feeBps: number; time: number }> = {
169
+ 'ethereum:ETH': { minUsd: 10, maxUsd: 100000, feeBps: 50, time: 900 },
170
+ 'ethereum:USDC': { minUsd: 10, maxUsd: 100000, feeBps: 30, time: 900 },
171
+ 'ethereum:USDT': { minUsd: 10, maxUsd: 100000, feeBps: 30, time: 900 },
172
+ 'solana:SOL': { minUsd: 10, maxUsd: 50000, feeBps: 50, time: 300 },
173
+ 'solana:USDC': { minUsd: 10, maxUsd: 50000, feeBps: 30, time: 300 },
174
+ 'solana:USDT': { minUsd: 10, maxUsd: 50000, feeBps: 30, time: 300 },
175
+ 'near:NEAR': { minUsd: 10, maxUsd: 25000, feeBps: 50, time: 300 },
176
+ 'near:USDC': { minUsd: 10, maxUsd: 25000, feeBps: 30, time: 300 },
177
+ 'polygon:MATIC': { minUsd: 10, maxUsd: 50000, feeBps: 50, time: 600 },
178
+ 'polygon:USDC': { minUsd: 10, maxUsd: 50000, feeBps: 30, time: 600 },
179
+ 'arbitrum:ETH': { minUsd: 10, maxUsd: 100000, feeBps: 50, time: 600 },
180
+ 'arbitrum:USDC': { minUsd: 10, maxUsd: 100000, feeBps: 30, time: 600 },
181
+ 'base:ETH': { minUsd: 10, maxUsd: 50000, feeBps: 50, time: 600 },
182
+ 'base:USDC': { minUsd: 10, maxUsd: 50000, feeBps: 30, time: 600 },
183
+ }
184
+
185
+ // ─── Bridge Implementation ─────────────────────────────────────────────────────
186
+
187
+ /**
188
+ * Zcash Bridge
189
+ *
190
+ * Bridges tokens from Ethereum, Solana, and other chains to Zcash,
191
+ * with optional shielding to z-addresses.
192
+ */
193
+ export class ZcashBridge {
194
+ private readonly config: Required<Omit<ZcashBridgeConfig, 'zcashService' | 'bridgeProvider' | 'priceFeed'>>
195
+ private readonly zcashService?: ZcashShieldedService
196
+ private readonly bridgeProvider?: BridgeProvider
197
+ private readonly priceFeed?: PriceFeed
198
+ private readonly bridgeRequests: Map<string, BridgeResult> = new Map()
199
+
200
+ constructor(config: ZcashBridgeConfig) {
201
+ this.config = {
202
+ mode: config.mode,
203
+ defaultSlippage: config.defaultSlippage ?? 100,
204
+ }
205
+ this.zcashService = config.zcashService
206
+ this.bridgeProvider = config.bridgeProvider
207
+ this.priceFeed = config.priceFeed
208
+ }
209
+
210
+ // ─── Route Discovery ─────────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Get all supported bridge routes
214
+ */
215
+ getSupportedRoutes(): BridgeRoute[] {
216
+ const routes: BridgeRoute[] = []
217
+
218
+ for (const [key, routeConfig] of Object.entries(ROUTE_CONFIG)) {
219
+ const [chain, token] = key.split(':') as [ZcashSwapSourceChain, ZcashSwapSourceToken]
220
+ const decimals = TOKEN_DECIMALS[token] ?? 18
221
+ const price = MOCK_PRICES[token] ?? 1
222
+
223
+ // Calculate min/max in token's smallest unit
224
+ const minAmount = BigInt(Math.floor((routeConfig.minUsd / price) * 10 ** decimals))
225
+ const maxAmount = BigInt(Math.floor((routeConfig.maxUsd / price) * 10 ** decimals))
226
+
227
+ routes.push({
228
+ sourceChain: chain,
229
+ sourceToken: token,
230
+ active: true,
231
+ minAmount,
232
+ maxAmount,
233
+ estimatedTime: routeConfig.time,
234
+ feeBps: routeConfig.feeBps,
235
+ })
236
+ }
237
+
238
+ return routes
239
+ }
240
+
241
+ /**
242
+ * Get routes for a specific source chain
243
+ */
244
+ getRoutesForChain(chain: ZcashSwapSourceChain): BridgeRoute[] {
245
+ return this.getSupportedRoutes().filter((r) => r.sourceChain === chain)
246
+ }
247
+
248
+ /**
249
+ * Check if a specific route is supported
250
+ */
251
+ isRouteSupported(chain: ZcashSwapSourceChain, token: ZcashSwapSourceToken): boolean {
252
+ const key = `${chain}:${token}`
253
+ return key in ROUTE_CONFIG
254
+ }
255
+
256
+ /**
257
+ * Get route details
258
+ */
259
+ getRoute(chain: ZcashSwapSourceChain, token: ZcashSwapSourceToken): BridgeRoute | null {
260
+ const routes = this.getSupportedRoutes()
261
+ return routes.find((r) => r.sourceChain === chain && r.sourceToken === token) ?? null
262
+ }
263
+
264
+ // ─── Bridge Operations ───────────────────────────────────────────────────────
265
+
266
+ /**
267
+ * Bridge tokens to shielded ZEC
268
+ *
269
+ * This is the main entry point for bridging. It:
270
+ * 1. Bridges source tokens to a Zcash transparent address
271
+ * 2. Optionally shields the ZEC to a z-address
272
+ *
273
+ * @param params - Bridge parameters
274
+ * @returns Bridge result with transaction details
275
+ */
276
+ async bridgeToShielded(params: BridgeParams): Promise<BridgeResult> {
277
+ // Validate parameters
278
+ this.validateParams(params)
279
+
280
+ // Validate recipient address
281
+ await this.validateRecipientAddress(params.recipientAddress, params.shield ?? true)
282
+
283
+ // Create request
284
+ const requestId = this.generateRequestId()
285
+ const result: BridgeResult = {
286
+ requestId,
287
+ status: 'pending',
288
+ recipientAddress: params.recipientAddress,
289
+ totalFee: 0n,
290
+ timestamp: Math.floor(Date.now() / 1000),
291
+ }
292
+
293
+ this.bridgeRequests.set(requestId, result)
294
+
295
+ // Execute based on mode
296
+ if (this.config.mode === 'production') {
297
+ if (!this.bridgeProvider) {
298
+ throw new IntentError(
299
+ 'Bridge provider not configured for production mode',
300
+ ErrorCode.INTENT_INVALID_STATE,
301
+ )
302
+ }
303
+ return this.executeProductionBridge(result, params)
304
+ }
305
+
306
+ return this.executeDemoBridge(result, params)
307
+ }
308
+
309
+ /**
310
+ * Execute bridge in demo mode
311
+ */
312
+ private async executeDemoBridge(result: BridgeResult, params: BridgeParams): Promise<BridgeResult> {
313
+ const shouldShield = params.shield !== false
314
+
315
+ try {
316
+ // Step 1: Simulate cross-chain bridge
317
+ result.status = 'bridging'
318
+ result.sourceTxHash = `0x${this.randomHex(64)}`
319
+ this.bridgeRequests.set(result.requestId, { ...result })
320
+
321
+ // Calculate ZEC amount
322
+ const zecAmount = await this.calculateZecAmount(params)
323
+ const fee = await this.calculateFee(params)
324
+
325
+ await this.delay(50) // Simulate bridge time
326
+
327
+ // Step 2: Bridge confirmed - ZEC at transparent address
328
+ result.status = 'bridge_confirmed'
329
+ if (shouldShield) {
330
+ result.transparentAddress = this.generateMockTransparentAddress()
331
+ result.transparentTxHash = this.randomHex(64)
332
+ }
333
+ this.bridgeRequests.set(result.requestId, { ...result })
334
+
335
+ // Step 3: Shield to z-address (if requested)
336
+ if (shouldShield && this.zcashService) {
337
+ result.status = 'shielding'
338
+ this.bridgeRequests.set(result.requestId, { ...result })
339
+
340
+ try {
341
+ const zecAmountFormatted = Number(zecAmount) / 100_000_000
342
+ const sendResult = await this.zcashService.sendShielded({
343
+ to: params.recipientAddress,
344
+ amount: zecAmountFormatted,
345
+ memo: params.memo ?? `SIP Bridge: ${params.sourceToken} → ZEC`,
346
+ })
347
+
348
+ result.shieldedTxId = sendResult.txid
349
+ result.amountReceived = zecAmount
350
+ result.amountReceivedFormatted = zecAmountFormatted.toFixed(8)
351
+ } catch {
352
+ // Fall back to mock if zcashd not available
353
+ result.shieldedTxId = this.randomHex(64)
354
+ result.amountReceived = zecAmount
355
+ result.amountReceivedFormatted = (Number(zecAmount) / 100_000_000).toFixed(8)
356
+ }
357
+ } else if (shouldShield) {
358
+ // No zcash service, mock shielding
359
+ result.status = 'shielding'
360
+ this.bridgeRequests.set(result.requestId, { ...result })
361
+
362
+ await this.delay(50)
363
+
364
+ result.shieldedTxId = this.randomHex(64)
365
+ result.amountReceived = zecAmount
366
+ result.amountReceivedFormatted = (Number(zecAmount) / 100_000_000).toFixed(8)
367
+ } else {
368
+ // No shielding - direct to transparent
369
+ result.amountReceived = zecAmount
370
+ result.amountReceivedFormatted = (Number(zecAmount) / 100_000_000).toFixed(8)
371
+ }
372
+
373
+ result.status = 'completed'
374
+ result.totalFee = fee
375
+ this.bridgeRequests.set(result.requestId, { ...result })
376
+
377
+ return result
378
+ } catch (error) {
379
+ result.status = 'failed'
380
+ result.error = error instanceof Error ? error.message : 'Bridge failed'
381
+ this.bridgeRequests.set(result.requestId, { ...result })
382
+ throw error
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Execute bridge in production mode
388
+ */
389
+ private async executeProductionBridge(result: BridgeResult, params: BridgeParams): Promise<BridgeResult> {
390
+ if (!this.bridgeProvider) {
391
+ throw new IntentError(
392
+ 'Bridge provider not configured for production mode',
393
+ ErrorCode.INTENT_INVALID_STATE,
394
+ )
395
+ }
396
+
397
+ const shouldShield = params.shield !== false
398
+
399
+ try {
400
+ // Step 1: Get quote from bridge provider
401
+ const quote = await this.bridgeProvider.getQuote({
402
+ sourceChain: params.sourceChain,
403
+ sourceToken: params.sourceToken,
404
+ amount: params.amount,
405
+ recipientAddress: shouldShield
406
+ ? this.generateMockTransparentAddress() // Intermediate t-addr
407
+ : params.recipientAddress,
408
+ })
409
+
410
+ result.status = 'bridging'
411
+ this.bridgeRequests.set(result.requestId, { ...result })
412
+
413
+ // Step 2: Execute bridge
414
+ const bridgeResult = await this.bridgeProvider.executeSwap({
415
+ sourceChain: params.sourceChain,
416
+ sourceToken: params.sourceToken,
417
+ amount: params.amount,
418
+ recipientAddress: params.recipientAddress,
419
+ quoteId: quote.quoteId,
420
+ depositAddress: '', // Provider handles this
421
+ })
422
+
423
+ result.sourceTxHash = bridgeResult.txHash
424
+
425
+ if (bridgeResult.status === 'failed') {
426
+ result.status = 'failed'
427
+ result.error = 'Bridge execution failed'
428
+ this.bridgeRequests.set(result.requestId, { ...result })
429
+ return result
430
+ }
431
+
432
+ result.status = 'bridge_confirmed'
433
+ this.bridgeRequests.set(result.requestId, { ...result })
434
+
435
+ // Step 3: Shield if requested
436
+ if (shouldShield && this.zcashService && bridgeResult.amountReceived) {
437
+ result.status = 'shielding'
438
+ this.bridgeRequests.set(result.requestId, { ...result })
439
+
440
+ const zecAmount = Number(bridgeResult.amountReceived) / 100_000_000
441
+ const sendResult = await this.zcashService.sendShielded({
442
+ to: params.recipientAddress,
443
+ amount: zecAmount,
444
+ memo: params.memo ?? `SIP Bridge: ${params.sourceToken} → ZEC`,
445
+ })
446
+
447
+ result.shieldedTxId = sendResult.txid
448
+ result.amountReceived = bridgeResult.amountReceived
449
+ result.amountReceivedFormatted = zecAmount.toFixed(8)
450
+ } else if (bridgeResult.amountReceived) {
451
+ result.amountReceived = bridgeResult.amountReceived
452
+ result.amountReceivedFormatted = (Number(bridgeResult.amountReceived) / 100_000_000).toFixed(8)
453
+ }
454
+
455
+ result.status = 'completed'
456
+ result.totalFee = quote.fee
457
+ this.bridgeRequests.set(result.requestId, { ...result })
458
+
459
+ return result
460
+ } catch (error) {
461
+ result.status = 'failed'
462
+ result.error = error instanceof Error ? error.message : 'Production bridge failed'
463
+ this.bridgeRequests.set(result.requestId, { ...result })
464
+ throw error
465
+ }
466
+ }
467
+
468
+ // ─── Status Methods ──────────────────────────────────────────────────────────
469
+
470
+ /**
471
+ * Get bridge request status
472
+ */
473
+ getStatus(requestId: string): BridgeResult | null {
474
+ return this.bridgeRequests.get(requestId) ?? null
475
+ }
476
+
477
+ /**
478
+ * Wait for bridge completion
479
+ */
480
+ async waitForCompletion(
481
+ requestId: string,
482
+ timeout: number = 600000,
483
+ pollInterval: number = 5000,
484
+ ): Promise<BridgeResult> {
485
+ const startTime = Date.now()
486
+
487
+ while (Date.now() - startTime < timeout) {
488
+ const status = this.getStatus(requestId)
489
+
490
+ if (!status) {
491
+ throw new IntentError(
492
+ 'Bridge request not found',
493
+ ErrorCode.INTENT_NOT_FOUND,
494
+ { context: { requestId } },
495
+ )
496
+ }
497
+
498
+ if (status.status === 'completed') {
499
+ return status
500
+ }
501
+
502
+ if (status.status === 'failed') {
503
+ throw new IntentError(
504
+ `Bridge failed: ${status.error ?? 'Unknown error'}`,
505
+ ErrorCode.INTENT_FAILED,
506
+ { context: { requestId, status } },
507
+ )
508
+ }
509
+
510
+ await this.delay(pollInterval)
511
+ }
512
+
513
+ throw new IntentError(
514
+ 'Bridge timeout',
515
+ ErrorCode.NETWORK_TIMEOUT,
516
+ { context: { requestId, timeout } },
517
+ )
518
+ }
519
+
520
+ // ─── Validation ──────────────────────────────────────────────────────────────
521
+
522
+ private validateParams(params: BridgeParams): void {
523
+ if (!params.sourceChain) {
524
+ throw new ValidationError(
525
+ 'Source chain is required',
526
+ 'sourceChain',
527
+ undefined,
528
+ ErrorCode.VALIDATION_FAILED,
529
+ )
530
+ }
531
+
532
+ if (!params.sourceToken) {
533
+ throw new ValidationError(
534
+ 'Source token is required',
535
+ 'sourceToken',
536
+ undefined,
537
+ ErrorCode.VALIDATION_FAILED,
538
+ )
539
+ }
540
+
541
+ if (!params.amount || params.amount <= 0n) {
542
+ throw new ValidationError(
543
+ 'Amount must be positive',
544
+ 'amount',
545
+ { received: params.amount },
546
+ ErrorCode.INVALID_AMOUNT,
547
+ )
548
+ }
549
+
550
+ if (!params.recipientAddress) {
551
+ throw new ValidationError(
552
+ 'Recipient address is required',
553
+ 'recipientAddress',
554
+ undefined,
555
+ ErrorCode.VALIDATION_FAILED,
556
+ )
557
+ }
558
+
559
+ if (!this.isRouteSupported(params.sourceChain, params.sourceToken)) {
560
+ throw new ValidationError(
561
+ `Unsupported route: ${params.sourceChain}:${params.sourceToken} → ZEC`,
562
+ 'sourceChain',
563
+ { chain: params.sourceChain, token: params.sourceToken },
564
+ ErrorCode.VALIDATION_FAILED,
565
+ )
566
+ }
567
+
568
+ // Validate amount is within limits
569
+ const route = this.getRoute(params.sourceChain, params.sourceToken)
570
+ if (route) {
571
+ if (params.amount < route.minAmount) {
572
+ throw new ValidationError(
573
+ `Amount below minimum: ${params.amount} < ${route.minAmount}`,
574
+ 'amount',
575
+ { received: params.amount, minimum: route.minAmount },
576
+ ErrorCode.INVALID_AMOUNT,
577
+ )
578
+ }
579
+ if (params.amount > route.maxAmount) {
580
+ throw new ValidationError(
581
+ `Amount above maximum: ${params.amount} > ${route.maxAmount}`,
582
+ 'amount',
583
+ { received: params.amount, maximum: route.maxAmount },
584
+ ErrorCode.INVALID_AMOUNT,
585
+ )
586
+ }
587
+ }
588
+ }
589
+
590
+ private async validateRecipientAddress(address: string, requireShielded: boolean): Promise<void> {
591
+ if (this.zcashService) {
592
+ const info = await this.zcashService.validateAddress(address)
593
+ if (!info.isvalid) {
594
+ throw new ValidationError(
595
+ 'Invalid Zcash address',
596
+ 'recipientAddress',
597
+ { received: address },
598
+ ErrorCode.INVALID_ADDRESS,
599
+ )
600
+ }
601
+
602
+ if (requireShielded) {
603
+ const isShielded = await this.zcashService.isShieldedAddress(address)
604
+ if (!isShielded) {
605
+ throw new ValidationError(
606
+ 'Shielded address (z-address) required for bridgeToShielded',
607
+ 'recipientAddress',
608
+ { received: address },
609
+ ErrorCode.INVALID_ADDRESS,
610
+ )
611
+ }
612
+ }
613
+ } else {
614
+ // Basic format validation without zcashd
615
+ if (requireShielded) {
616
+ if (!this.isShieldedAddressFormat(address)) {
617
+ throw new ValidationError(
618
+ 'Invalid shielded address format. Expected zs1... or u1...',
619
+ 'recipientAddress',
620
+ { received: address },
621
+ ErrorCode.INVALID_ADDRESS,
622
+ )
623
+ }
624
+ } else {
625
+ if (!this.isValidZcashAddressFormat(address)) {
626
+ throw new ValidationError(
627
+ 'Invalid Zcash address format',
628
+ 'recipientAddress',
629
+ { received: address },
630
+ ErrorCode.INVALID_ADDRESS,
631
+ )
632
+ }
633
+ }
634
+ }
635
+ }
636
+
637
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
638
+
639
+ private async calculateZecAmount(params: BridgeParams): Promise<bigint> {
640
+ const sourcePrice = this.priceFeed
641
+ ? await this.priceFeed.getPrice(params.sourceToken)
642
+ : MOCK_PRICES[params.sourceToken] ?? 1
643
+
644
+ const zecPrice = this.priceFeed
645
+ ? await this.priceFeed.getZecPrice()
646
+ : MOCK_PRICES.ZEC
647
+
648
+ const sourceDecimals = TOKEN_DECIMALS[params.sourceToken] ?? 18
649
+ const amountInUsd = (Number(params.amount) / 10 ** sourceDecimals) * sourcePrice
650
+
651
+ // Get route fee
652
+ const route = this.getRoute(params.sourceChain, params.sourceToken)
653
+ const feeBps = route?.feeBps ?? 50
654
+ const feeAmount = amountInUsd * (feeBps / 10000)
655
+ const networkFee = 2 // ~$2 network fee
656
+
657
+ const netAmountUsd = amountInUsd - feeAmount - networkFee
658
+ const zecAmount = netAmountUsd / zecPrice
659
+
660
+ return BigInt(Math.floor(zecAmount * 100_000_000))
661
+ }
662
+
663
+ private async calculateFee(params: BridgeParams): Promise<bigint> {
664
+ const sourcePrice = this.priceFeed
665
+ ? await this.priceFeed.getPrice(params.sourceToken)
666
+ : MOCK_PRICES[params.sourceToken] ?? 1
667
+
668
+ const zecPrice = this.priceFeed
669
+ ? await this.priceFeed.getZecPrice()
670
+ : MOCK_PRICES.ZEC
671
+
672
+ const sourceDecimals = TOKEN_DECIMALS[params.sourceToken] ?? 18
673
+ const amountInUsd = (Number(params.amount) / 10 ** sourceDecimals) * sourcePrice
674
+
675
+ const route = this.getRoute(params.sourceChain, params.sourceToken)
676
+ const feeBps = route?.feeBps ?? 50
677
+ const feeUsd = amountInUsd * (feeBps / 10000) + 2 // +$2 network
678
+
679
+ // Convert fee to zatoshis
680
+ const feeZec = feeUsd / zecPrice
681
+ return BigInt(Math.floor(feeZec * 100_000_000))
682
+ }
683
+
684
+ private isShieldedAddressFormat(address: string): boolean {
685
+ return (
686
+ address.startsWith('zs1') ||
687
+ address.startsWith('u1') ||
688
+ address.startsWith('ztestsapling') ||
689
+ address.startsWith('utest')
690
+ )
691
+ }
692
+
693
+ private isValidZcashAddressFormat(address: string): boolean {
694
+ return (
695
+ this.isShieldedAddressFormat(address) ||
696
+ address.startsWith('t1') || // mainnet transparent
697
+ address.startsWith('t3') || // mainnet P2SH
698
+ address.startsWith('tm') // testnet transparent
699
+ )
700
+ }
701
+
702
+ private generateMockTransparentAddress(): string {
703
+ return `t1${this.randomBase58(33)}`
704
+ }
705
+
706
+ private generateRequestId(): string {
707
+ return `bridge_${Date.now()}_${this.randomHex(8)}`
708
+ }
709
+
710
+ private randomHex(length: number): string {
711
+ const chars = '0123456789abcdef'
712
+ let result = ''
713
+ for (let i = 0; i < length; i++) {
714
+ result += chars[Math.floor(Math.random() * chars.length)]
715
+ }
716
+ return result
717
+ }
718
+
719
+ private randomBase58(length: number): string {
720
+ const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
721
+ let result = ''
722
+ for (let i = 0; i < length; i++) {
723
+ result += chars[Math.floor(Math.random() * chars.length)]
724
+ }
725
+ return result
726
+ }
727
+
728
+ private delay(ms: number): Promise<void> {
729
+ return new Promise((resolve) => setTimeout(resolve, ms))
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Create a Zcash bridge instance
735
+ */
736
+ export function createZcashBridge(config: ZcashBridgeConfig): ZcashBridge {
737
+ return new ZcashBridge(config)
738
+ }