@sip-protocol/sdk 0.2.7 → 0.2.9
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 +349 -0
- package/dist/browser.d.mts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +614 -159
- package/dist/browser.mjs +5 -1
- package/dist/chunk-KXN6IWL5.mjs +10736 -0
- package/dist/chunk-UPTISVCY.mjs +10304 -0
- package/dist/index.d.mts +289 -1
- package/dist/index.d.ts +289 -1
- package/dist/index.js +614 -159
- package/dist/index.mjs +5 -1
- package/package.json +1 -1
- package/src/adapters/near-intents.ts +31 -5
- package/src/index.ts +12 -0
- package/src/zcash/bridge.ts +738 -0
- package/src/zcash/index.ts +36 -1
- package/src/zcash/swap-service.ts +793 -0
|
@@ -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
|
+
}
|