@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.
- package/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/browser.d.mts +100 -2
- package/dist/browser.d.ts +100 -2
- package/dist/browser.js +1362 -268
- package/dist/browser.mjs +502 -16
- package/dist/{chunk-UPTISVCY.mjs → chunk-AV37IZST.mjs} +731 -15
- package/dist/{chunk-VITVG25F.mjs → chunk-XLEPIR2P.mjs} +2 -100
- package/dist/index-BFOKTz2z.d.ts +6062 -0
- package/dist/index-CAhjA4kh.d.mts +6062 -0
- package/dist/index.d.mts +2 -5609
- package/dist/index.d.ts +2 -5609
- package/dist/index.js +588 -154
- package/dist/index.mjs +5 -1
- package/dist/{noir-BHQtFvRk.d.mts → noir-BTyLXLlZ.d.mts} +1 -1
- package/dist/{noir-BHQtFvRk.d.ts → noir-BTyLXLlZ.d.ts} +1 -1
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +11 -112
- package/dist/proofs/noir.mjs +10 -13
- package/package.json +16 -16
- package/src/browser.ts +23 -0
- package/src/index.ts +12 -0
- package/src/proofs/browser-utils.ts +389 -0
- package/src/proofs/browser.ts +246 -19
- package/src/proofs/circuits/funding_proof.json +1 -1
- package/src/proofs/noir.ts +14 -14
- package/src/proofs/worker.ts +426 -0
- package/src/zcash/bridge.ts +738 -0
- package/src/zcash/index.ts +36 -1
- package/src/zcash/swap-service.ts +793 -0
- package/dist/chunk-4VJHI66K.mjs +0 -12120
- package/dist/chunk-5BAS4D44.mjs +0 -10283
- package/dist/chunk-6WOV2YNG.mjs +0 -10179
- package/dist/chunk-DU7LQDD2.mjs +0 -10148
- package/dist/chunk-MR7HRCRS.mjs +0 -10165
- package/dist/chunk-NDGUWOOZ.mjs +0 -10157
- package/dist/chunk-O4Y2ZUDL.mjs +0 -12721
- package/dist/chunk-VXSHK7US.mjs +0 -10158
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zcash Swap Service
|
|
3
|
+
*
|
|
4
|
+
* Handles cross-chain swaps from ETH/SOL/NEAR to Zcash (ZEC).
|
|
5
|
+
* Since NEAR Intents doesn't support ZEC as destination chain,
|
|
6
|
+
* this service provides an alternative path for ZEC swaps.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const swapService = new ZcashSwapService({
|
|
11
|
+
* zcashService: zcashShieldedService,
|
|
12
|
+
* mode: 'demo', // or 'production' when bridge is available
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* // Get quote for ETH → ZEC swap
|
|
16
|
+
* const quote = await swapService.getQuote({
|
|
17
|
+
* sourceChain: 'ethereum',
|
|
18
|
+
* sourceToken: 'ETH',
|
|
19
|
+
* amount: 1000000000000000000n, // 1 ETH
|
|
20
|
+
* recipientZAddress: 'zs1...',
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* // Execute the swap
|
|
24
|
+
* const result = await swapService.executeSwapToShielded({
|
|
25
|
+
* quoteId: quote.quoteId,
|
|
26
|
+
* sourceChain: 'ethereum',
|
|
27
|
+
* sourceToken: 'ETH',
|
|
28
|
+
* amount: 1000000000000000000n,
|
|
29
|
+
* recipientZAddress: 'zs1...',
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { type ChainId, PrivacyLevel } from '@sip-protocol/types'
|
|
35
|
+
import { ValidationError, IntentError, NetworkError, ErrorCode } from '../errors'
|
|
36
|
+
import type { ZcashShieldedService, ShieldedSendResult } from './shielded-service'
|
|
37
|
+
|
|
38
|
+
// ─── Types ─────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Supported source chains for ZEC swaps
|
|
42
|
+
*/
|
|
43
|
+
export type ZcashSwapSourceChain = 'ethereum' | 'solana' | 'near' | 'polygon' | 'arbitrum' | 'base'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Supported source tokens for ZEC swaps
|
|
47
|
+
*/
|
|
48
|
+
export type ZcashSwapSourceToken = 'ETH' | 'SOL' | 'NEAR' | 'USDC' | 'USDT' | 'MATIC'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Configuration for ZcashSwapService
|
|
52
|
+
*/
|
|
53
|
+
export interface ZcashSwapServiceConfig {
|
|
54
|
+
/** Zcash shielded service for receiving ZEC */
|
|
55
|
+
zcashService?: ZcashShieldedService
|
|
56
|
+
/** Operating mode */
|
|
57
|
+
mode: 'demo' | 'production'
|
|
58
|
+
/** Bridge provider (for production) */
|
|
59
|
+
bridgeProvider?: BridgeProvider
|
|
60
|
+
/** Price feed for quotes */
|
|
61
|
+
priceFeed?: PriceFeed
|
|
62
|
+
/** Default slippage tolerance (basis points, default: 100 = 1%) */
|
|
63
|
+
defaultSlippage?: number
|
|
64
|
+
/** Quote validity duration in seconds (default: 60) */
|
|
65
|
+
quoteValiditySeconds?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bridge provider interface (for production mode)
|
|
70
|
+
*/
|
|
71
|
+
export interface BridgeProvider {
|
|
72
|
+
/** Provider name */
|
|
73
|
+
name: string
|
|
74
|
+
/** Get quote from bridge */
|
|
75
|
+
getQuote(params: BridgeQuoteParams): Promise<BridgeQuote>
|
|
76
|
+
/** Execute swap through bridge */
|
|
77
|
+
executeSwap(params: BridgeSwapParams): Promise<BridgeSwapResult>
|
|
78
|
+
/** Get supported chains */
|
|
79
|
+
getSupportedChains(): Promise<ZcashSwapSourceChain[]>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Price feed interface for quote calculations
|
|
84
|
+
*/
|
|
85
|
+
export interface PriceFeed {
|
|
86
|
+
/** Get current price in USD */
|
|
87
|
+
getPrice(token: string): Promise<number>
|
|
88
|
+
/** Get ZEC price in USD */
|
|
89
|
+
getZecPrice(): Promise<number>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Bridge quote parameters
|
|
94
|
+
*/
|
|
95
|
+
export interface BridgeQuoteParams {
|
|
96
|
+
sourceChain: ZcashSwapSourceChain
|
|
97
|
+
sourceToken: ZcashSwapSourceToken
|
|
98
|
+
amount: bigint
|
|
99
|
+
recipientAddress: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bridge quote result
|
|
104
|
+
*/
|
|
105
|
+
export interface BridgeQuote {
|
|
106
|
+
quoteId: string
|
|
107
|
+
amountIn: bigint
|
|
108
|
+
amountOut: bigint
|
|
109
|
+
fee: bigint
|
|
110
|
+
exchangeRate: number
|
|
111
|
+
validUntil: number
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Bridge swap parameters
|
|
116
|
+
*/
|
|
117
|
+
export interface BridgeSwapParams extends BridgeQuoteParams {
|
|
118
|
+
quoteId: string
|
|
119
|
+
depositAddress: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Bridge swap result
|
|
124
|
+
*/
|
|
125
|
+
export interface BridgeSwapResult {
|
|
126
|
+
txHash: string
|
|
127
|
+
status: 'pending' | 'completed' | 'failed'
|
|
128
|
+
amountReceived?: bigint
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Quote request parameters
|
|
133
|
+
*/
|
|
134
|
+
export interface ZcashQuoteParams {
|
|
135
|
+
/** Source blockchain */
|
|
136
|
+
sourceChain: ZcashSwapSourceChain
|
|
137
|
+
/** Source token symbol */
|
|
138
|
+
sourceToken: ZcashSwapSourceToken
|
|
139
|
+
/** Amount in smallest unit (wei, lamports, etc.) */
|
|
140
|
+
amount: bigint
|
|
141
|
+
/** Recipient z-address */
|
|
142
|
+
recipientZAddress: string
|
|
143
|
+
/** Custom slippage (basis points) */
|
|
144
|
+
slippage?: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Quote response
|
|
149
|
+
*/
|
|
150
|
+
export interface ZcashQuote {
|
|
151
|
+
/** Unique quote identifier */
|
|
152
|
+
quoteId: string
|
|
153
|
+
/** Source chain */
|
|
154
|
+
sourceChain: ZcashSwapSourceChain
|
|
155
|
+
/** Source token */
|
|
156
|
+
sourceToken: ZcashSwapSourceToken
|
|
157
|
+
/** Input amount (in source token's smallest unit) */
|
|
158
|
+
amountIn: bigint
|
|
159
|
+
/** Input amount formatted */
|
|
160
|
+
amountInFormatted: string
|
|
161
|
+
/** Output amount in zatoshis (1 ZEC = 100,000,000 zatoshis) */
|
|
162
|
+
amountOut: bigint
|
|
163
|
+
/** Output amount in ZEC */
|
|
164
|
+
amountOutFormatted: string
|
|
165
|
+
/** Exchange rate (ZEC per source token) */
|
|
166
|
+
exchangeRate: number
|
|
167
|
+
/** Network fee in source token */
|
|
168
|
+
networkFee: bigint
|
|
169
|
+
/** Bridge/swap fee in source token */
|
|
170
|
+
swapFee: bigint
|
|
171
|
+
/** Total fee in source token */
|
|
172
|
+
totalFee: bigint
|
|
173
|
+
/** Slippage tolerance (basis points) */
|
|
174
|
+
slippage: number
|
|
175
|
+
/** Minimum output amount after slippage */
|
|
176
|
+
minimumOutput: bigint
|
|
177
|
+
/** Quote expiration timestamp */
|
|
178
|
+
validUntil: number
|
|
179
|
+
/** Deposit address (where to send source tokens) */
|
|
180
|
+
depositAddress: string
|
|
181
|
+
/** Estimated time to completion (seconds) */
|
|
182
|
+
estimatedTime: number
|
|
183
|
+
/** Privacy level for the swap */
|
|
184
|
+
privacyLevel: PrivacyLevel
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Swap execution parameters
|
|
189
|
+
*/
|
|
190
|
+
export interface ZcashSwapParams {
|
|
191
|
+
/** Quote ID to execute */
|
|
192
|
+
quoteId?: string
|
|
193
|
+
/** Source blockchain */
|
|
194
|
+
sourceChain: ZcashSwapSourceChain
|
|
195
|
+
/** Source token symbol */
|
|
196
|
+
sourceToken: ZcashSwapSourceToken
|
|
197
|
+
/** Amount in smallest unit */
|
|
198
|
+
amount: bigint
|
|
199
|
+
/** Recipient z-address (shielded) */
|
|
200
|
+
recipientZAddress: string
|
|
201
|
+
/** Optional memo for the transaction */
|
|
202
|
+
memo?: string
|
|
203
|
+
/** Custom slippage (basis points) */
|
|
204
|
+
slippage?: number
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Swap execution result
|
|
209
|
+
*/
|
|
210
|
+
export interface ZcashSwapResult {
|
|
211
|
+
/** Swap request ID */
|
|
212
|
+
requestId: string
|
|
213
|
+
/** Quote used */
|
|
214
|
+
quoteId: string
|
|
215
|
+
/** Current status */
|
|
216
|
+
status: ZcashSwapStatus
|
|
217
|
+
/** Source chain transaction hash (deposit tx) */
|
|
218
|
+
sourceTxHash?: string
|
|
219
|
+
/** Zcash transaction ID (if completed) */
|
|
220
|
+
zcashTxId?: string
|
|
221
|
+
/** Amount deposited */
|
|
222
|
+
amountIn: bigint
|
|
223
|
+
/** Amount received in ZEC (zatoshis) */
|
|
224
|
+
amountOut?: bigint
|
|
225
|
+
/** Recipient z-address */
|
|
226
|
+
recipientZAddress: string
|
|
227
|
+
/** Timestamp */
|
|
228
|
+
timestamp: number
|
|
229
|
+
/** Error message if failed */
|
|
230
|
+
error?: string
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Swap status
|
|
235
|
+
*/
|
|
236
|
+
export type ZcashSwapStatus =
|
|
237
|
+
| 'pending_deposit' // Waiting for source chain deposit
|
|
238
|
+
| 'deposit_confirmed' // Deposit confirmed, processing swap
|
|
239
|
+
| 'swapping' // Swap in progress
|
|
240
|
+
| 'sending_zec' // Sending ZEC to recipient
|
|
241
|
+
| 'completed' // Swap completed
|
|
242
|
+
| 'failed' // Swap failed
|
|
243
|
+
| 'expired' // Quote expired
|
|
244
|
+
|
|
245
|
+
// ─── Mock Price Data ───────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Mock prices for demo mode (in USD)
|
|
249
|
+
*/
|
|
250
|
+
const MOCK_PRICES: Record<string, number> = {
|
|
251
|
+
ETH: 2500,
|
|
252
|
+
SOL: 120,
|
|
253
|
+
NEAR: 5,
|
|
254
|
+
MATIC: 0.8,
|
|
255
|
+
USDC: 1,
|
|
256
|
+
USDT: 1,
|
|
257
|
+
ZEC: 35,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Token decimals
|
|
262
|
+
*/
|
|
263
|
+
const TOKEN_DECIMALS: Record<string, number> = {
|
|
264
|
+
ETH: 18,
|
|
265
|
+
SOL: 9,
|
|
266
|
+
NEAR: 24,
|
|
267
|
+
MATIC: 18,
|
|
268
|
+
USDC: 6,
|
|
269
|
+
USDT: 6,
|
|
270
|
+
ZEC: 8, // zatoshis
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Service Implementation ────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Zcash Swap Service
|
|
277
|
+
*
|
|
278
|
+
* Enables cross-chain swaps from ETH/SOL/NEAR to Zcash's shielded pool.
|
|
279
|
+
*/
|
|
280
|
+
export class ZcashSwapService {
|
|
281
|
+
private readonly config: Required<Omit<ZcashSwapServiceConfig, 'zcashService' | 'bridgeProvider' | 'priceFeed'>>
|
|
282
|
+
private readonly zcashService?: ZcashShieldedService
|
|
283
|
+
private readonly bridgeProvider?: BridgeProvider
|
|
284
|
+
private readonly priceFeed?: PriceFeed
|
|
285
|
+
private readonly quotes: Map<string, ZcashQuote> = new Map()
|
|
286
|
+
private readonly swaps: Map<string, ZcashSwapResult> = new Map()
|
|
287
|
+
|
|
288
|
+
constructor(config: ZcashSwapServiceConfig) {
|
|
289
|
+
this.config = {
|
|
290
|
+
mode: config.mode,
|
|
291
|
+
defaultSlippage: config.defaultSlippage ?? 100, // 1%
|
|
292
|
+
quoteValiditySeconds: config.quoteValiditySeconds ?? 60,
|
|
293
|
+
}
|
|
294
|
+
this.zcashService = config.zcashService
|
|
295
|
+
this.bridgeProvider = config.bridgeProvider
|
|
296
|
+
this.priceFeed = config.priceFeed
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Quote Methods ───────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get a quote for swapping to ZEC
|
|
303
|
+
*/
|
|
304
|
+
async getQuote(params: ZcashQuoteParams): Promise<ZcashQuote> {
|
|
305
|
+
// Validate parameters
|
|
306
|
+
this.validateQuoteParams(params)
|
|
307
|
+
|
|
308
|
+
// Validate z-address
|
|
309
|
+
if (this.zcashService) {
|
|
310
|
+
const addressInfo = await this.zcashService.validateAddress(params.recipientZAddress)
|
|
311
|
+
if (!addressInfo.isvalid) {
|
|
312
|
+
throw new ValidationError(
|
|
313
|
+
'Invalid Zcash address',
|
|
314
|
+
'recipientZAddress',
|
|
315
|
+
{ received: params.recipientZAddress },
|
|
316
|
+
ErrorCode.INVALID_ADDRESS,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
// Basic validation without zcashd
|
|
321
|
+
if (!this.isValidZAddressFormat(params.recipientZAddress)) {
|
|
322
|
+
throw new ValidationError(
|
|
323
|
+
'Invalid Zcash address format. Expected z-address (zs1...) or unified address (u1...)',
|
|
324
|
+
'recipientZAddress',
|
|
325
|
+
{ received: params.recipientZAddress },
|
|
326
|
+
ErrorCode.INVALID_ADDRESS,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (this.config.mode === 'production' && this.bridgeProvider) {
|
|
332
|
+
return this.getProductionQuote(params)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return this.getDemoQuote(params)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get quote in demo mode (uses mock prices)
|
|
340
|
+
*/
|
|
341
|
+
private async getDemoQuote(params: ZcashQuoteParams): Promise<ZcashQuote> {
|
|
342
|
+
const { sourceChain, sourceToken, amount, recipientZAddress, slippage } = params
|
|
343
|
+
|
|
344
|
+
// Get prices (mock or from price feed)
|
|
345
|
+
const sourcePrice = this.priceFeed
|
|
346
|
+
? await this.priceFeed.getPrice(sourceToken)
|
|
347
|
+
: MOCK_PRICES[sourceToken] ?? 1
|
|
348
|
+
const zecPrice = this.priceFeed
|
|
349
|
+
? await this.priceFeed.getZecPrice()
|
|
350
|
+
: MOCK_PRICES.ZEC
|
|
351
|
+
|
|
352
|
+
// Calculate amounts
|
|
353
|
+
const sourceDecimals = TOKEN_DECIMALS[sourceToken] ?? 18
|
|
354
|
+
const amountInUsd = (Number(amount) / 10 ** sourceDecimals) * sourcePrice
|
|
355
|
+
|
|
356
|
+
// Apply fees (0.5% swap fee, ~$2 network fee)
|
|
357
|
+
const swapFeeUsd = amountInUsd * 0.005
|
|
358
|
+
const networkFeeUsd = 2
|
|
359
|
+
const totalFeeUsd = swapFeeUsd + networkFeeUsd
|
|
360
|
+
const netAmountUsd = amountInUsd - totalFeeUsd
|
|
361
|
+
|
|
362
|
+
// Calculate ZEC output
|
|
363
|
+
const zecAmount = netAmountUsd / zecPrice
|
|
364
|
+
const zecZatoshis = BigInt(Math.floor(zecAmount * 100_000_000))
|
|
365
|
+
|
|
366
|
+
// Apply slippage
|
|
367
|
+
const slippageBps = slippage ?? this.config.defaultSlippage
|
|
368
|
+
const minimumOutput = (zecZatoshis * BigInt(10000 - slippageBps)) / 10000n
|
|
369
|
+
|
|
370
|
+
// Calculate fees in source token
|
|
371
|
+
const swapFee = BigInt(Math.floor((swapFeeUsd / sourcePrice) * 10 ** sourceDecimals))
|
|
372
|
+
const networkFee = BigInt(Math.floor((networkFeeUsd / sourcePrice) * 10 ** sourceDecimals))
|
|
373
|
+
|
|
374
|
+
// Generate quote
|
|
375
|
+
const quoteId = `zec_quote_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`
|
|
376
|
+
const validUntil = Math.floor(Date.now() / 1000) + this.config.quoteValiditySeconds
|
|
377
|
+
|
|
378
|
+
const quote: ZcashQuote = {
|
|
379
|
+
quoteId,
|
|
380
|
+
sourceChain,
|
|
381
|
+
sourceToken,
|
|
382
|
+
amountIn: amount,
|
|
383
|
+
amountInFormatted: this.formatAmount(amount, sourceDecimals),
|
|
384
|
+
amountOut: zecZatoshis,
|
|
385
|
+
amountOutFormatted: this.formatAmount(zecZatoshis, 8),
|
|
386
|
+
exchangeRate: zecPrice / sourcePrice,
|
|
387
|
+
networkFee,
|
|
388
|
+
swapFee,
|
|
389
|
+
totalFee: networkFee + swapFee,
|
|
390
|
+
slippage: slippageBps,
|
|
391
|
+
minimumOutput,
|
|
392
|
+
validUntil,
|
|
393
|
+
depositAddress: this.generateMockDepositAddress(sourceChain),
|
|
394
|
+
estimatedTime: this.getEstimatedTime(sourceChain),
|
|
395
|
+
privacyLevel: PrivacyLevel.SHIELDED,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Store quote for later execution
|
|
399
|
+
this.quotes.set(quoteId, quote)
|
|
400
|
+
|
|
401
|
+
return quote
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get quote in production mode (uses bridge provider)
|
|
406
|
+
*/
|
|
407
|
+
private async getProductionQuote(params: ZcashQuoteParams): Promise<ZcashQuote> {
|
|
408
|
+
if (!this.bridgeProvider) {
|
|
409
|
+
throw new IntentError(
|
|
410
|
+
'Bridge provider not configured for production mode',
|
|
411
|
+
ErrorCode.INTENT_INVALID_STATE,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const bridgeQuote = await this.bridgeProvider.getQuote({
|
|
416
|
+
sourceChain: params.sourceChain,
|
|
417
|
+
sourceToken: params.sourceToken,
|
|
418
|
+
amount: params.amount,
|
|
419
|
+
recipientAddress: params.recipientZAddress,
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const sourceDecimals = TOKEN_DECIMALS[params.sourceToken] ?? 18
|
|
423
|
+
const slippageBps = params.slippage ?? this.config.defaultSlippage
|
|
424
|
+
const minimumOutput = (bridgeQuote.amountOut * BigInt(10000 - slippageBps)) / 10000n
|
|
425
|
+
|
|
426
|
+
const quote: ZcashQuote = {
|
|
427
|
+
quoteId: bridgeQuote.quoteId,
|
|
428
|
+
sourceChain: params.sourceChain,
|
|
429
|
+
sourceToken: params.sourceToken,
|
|
430
|
+
amountIn: bridgeQuote.amountIn,
|
|
431
|
+
amountInFormatted: this.formatAmount(bridgeQuote.amountIn, sourceDecimals),
|
|
432
|
+
amountOut: bridgeQuote.amountOut,
|
|
433
|
+
amountOutFormatted: this.formatAmount(bridgeQuote.amountOut, 8),
|
|
434
|
+
exchangeRate: bridgeQuote.exchangeRate,
|
|
435
|
+
networkFee: 0n, // Included in bridge fee
|
|
436
|
+
swapFee: bridgeQuote.fee,
|
|
437
|
+
totalFee: bridgeQuote.fee,
|
|
438
|
+
slippage: slippageBps,
|
|
439
|
+
minimumOutput,
|
|
440
|
+
validUntil: bridgeQuote.validUntil,
|
|
441
|
+
depositAddress: '', // Will be set by bridge
|
|
442
|
+
estimatedTime: this.getEstimatedTime(params.sourceChain),
|
|
443
|
+
privacyLevel: PrivacyLevel.SHIELDED,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.quotes.set(quote.quoteId, quote)
|
|
447
|
+
return quote
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ─── Swap Execution ──────────────────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Execute a swap to Zcash shielded pool
|
|
454
|
+
*/
|
|
455
|
+
async executeSwapToShielded(params: ZcashSwapParams): Promise<ZcashSwapResult> {
|
|
456
|
+
// Get or create quote
|
|
457
|
+
let quote: ZcashQuote | undefined
|
|
458
|
+
if (params.quoteId) {
|
|
459
|
+
quote = this.quotes.get(params.quoteId)
|
|
460
|
+
if (!quote) {
|
|
461
|
+
throw new ValidationError(
|
|
462
|
+
'Quote not found or expired',
|
|
463
|
+
'quoteId',
|
|
464
|
+
{ received: params.quoteId },
|
|
465
|
+
ErrorCode.VALIDATION_FAILED,
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!quote) {
|
|
471
|
+
quote = await this.getQuote({
|
|
472
|
+
sourceChain: params.sourceChain,
|
|
473
|
+
sourceToken: params.sourceToken,
|
|
474
|
+
amount: params.amount,
|
|
475
|
+
recipientZAddress: params.recipientZAddress,
|
|
476
|
+
slippage: params.slippage,
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Check quote validity
|
|
481
|
+
if (quote.validUntil < Math.floor(Date.now() / 1000)) {
|
|
482
|
+
throw new IntentError(
|
|
483
|
+
'Quote has expired',
|
|
484
|
+
ErrorCode.INTENT_EXPIRED,
|
|
485
|
+
{ context: { quoteId: quote.quoteId, validUntil: quote.validUntil } },
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Create swap result
|
|
490
|
+
const requestId = `zec_swap_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`
|
|
491
|
+
|
|
492
|
+
const result: ZcashSwapResult = {
|
|
493
|
+
requestId,
|
|
494
|
+
quoteId: quote.quoteId,
|
|
495
|
+
status: 'pending_deposit',
|
|
496
|
+
amountIn: params.amount,
|
|
497
|
+
recipientZAddress: params.recipientZAddress,
|
|
498
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Store for tracking
|
|
502
|
+
this.swaps.set(requestId, result)
|
|
503
|
+
|
|
504
|
+
if (this.config.mode === 'production' && this.bridgeProvider) {
|
|
505
|
+
return this.executeProductionSwap(result, quote, params)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return this.executeDemoSwap(result, quote, params)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Execute swap in demo mode
|
|
513
|
+
*/
|
|
514
|
+
private async executeDemoSwap(
|
|
515
|
+
result: ZcashSwapResult,
|
|
516
|
+
quote: ZcashQuote,
|
|
517
|
+
params: ZcashSwapParams,
|
|
518
|
+
): Promise<ZcashSwapResult> {
|
|
519
|
+
// Simulate swap progression
|
|
520
|
+
result.status = 'deposit_confirmed'
|
|
521
|
+
result.sourceTxHash = `0x${this.randomHex(64)}`
|
|
522
|
+
this.swaps.set(result.requestId, { ...result })
|
|
523
|
+
|
|
524
|
+
// Simulate swap processing
|
|
525
|
+
await this.delay(100) // Small delay for realism
|
|
526
|
+
result.status = 'swapping'
|
|
527
|
+
this.swaps.set(result.requestId, { ...result })
|
|
528
|
+
|
|
529
|
+
await this.delay(100)
|
|
530
|
+
result.status = 'sending_zec'
|
|
531
|
+
this.swaps.set(result.requestId, { ...result })
|
|
532
|
+
|
|
533
|
+
// If we have a zcash service, try to actually send
|
|
534
|
+
if (this.zcashService) {
|
|
535
|
+
try {
|
|
536
|
+
const zecAmount = Number(quote.amountOut) / 100_000_000
|
|
537
|
+
const sendResult = await this.zcashService.sendShielded({
|
|
538
|
+
to: params.recipientZAddress,
|
|
539
|
+
amount: zecAmount,
|
|
540
|
+
memo: params.memo ?? `SIP Swap: ${params.sourceToken} → ZEC`,
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
result.status = 'completed'
|
|
544
|
+
result.zcashTxId = sendResult.txid
|
|
545
|
+
result.amountOut = quote.amountOut
|
|
546
|
+
} catch (error) {
|
|
547
|
+
// Fall back to mock result
|
|
548
|
+
result.status = 'completed'
|
|
549
|
+
result.zcashTxId = this.randomHex(64)
|
|
550
|
+
result.amountOut = quote.amountOut
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
// Mock completion
|
|
554
|
+
result.status = 'completed'
|
|
555
|
+
result.zcashTxId = this.randomHex(64)
|
|
556
|
+
result.amountOut = quote.amountOut
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
this.swaps.set(result.requestId, { ...result })
|
|
560
|
+
return result
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Execute swap in production mode
|
|
565
|
+
*/
|
|
566
|
+
private async executeProductionSwap(
|
|
567
|
+
result: ZcashSwapResult,
|
|
568
|
+
quote: ZcashQuote,
|
|
569
|
+
params: ZcashSwapParams,
|
|
570
|
+
): Promise<ZcashSwapResult> {
|
|
571
|
+
if (!this.bridgeProvider) {
|
|
572
|
+
throw new IntentError(
|
|
573
|
+
'Bridge provider not configured',
|
|
574
|
+
ErrorCode.INTENT_INVALID_STATE,
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const bridgeResult = await this.bridgeProvider.executeSwap({
|
|
580
|
+
sourceChain: params.sourceChain,
|
|
581
|
+
sourceToken: params.sourceToken,
|
|
582
|
+
amount: params.amount,
|
|
583
|
+
recipientAddress: params.recipientZAddress,
|
|
584
|
+
quoteId: quote.quoteId,
|
|
585
|
+
depositAddress: quote.depositAddress,
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
result.sourceTxHash = bridgeResult.txHash
|
|
589
|
+
result.status = bridgeResult.status === 'completed' ? 'completed' : 'swapping'
|
|
590
|
+
|
|
591
|
+
if (bridgeResult.amountReceived) {
|
|
592
|
+
result.amountOut = bridgeResult.amountReceived
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
result.status = 'failed'
|
|
596
|
+
result.error = error instanceof Error ? error.message : 'Bridge execution failed'
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
this.swaps.set(result.requestId, { ...result })
|
|
600
|
+
return result
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ─── Status Methods ──────────────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get swap status
|
|
607
|
+
*/
|
|
608
|
+
async getSwapStatus(requestId: string): Promise<ZcashSwapResult | null> {
|
|
609
|
+
return this.swaps.get(requestId) ?? null
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Wait for swap completion
|
|
614
|
+
*/
|
|
615
|
+
async waitForCompletion(
|
|
616
|
+
requestId: string,
|
|
617
|
+
timeout: number = 300000,
|
|
618
|
+
pollInterval: number = 5000,
|
|
619
|
+
): Promise<ZcashSwapResult> {
|
|
620
|
+
const startTime = Date.now()
|
|
621
|
+
|
|
622
|
+
while (Date.now() - startTime < timeout) {
|
|
623
|
+
const status = await this.getSwapStatus(requestId)
|
|
624
|
+
|
|
625
|
+
if (!status) {
|
|
626
|
+
throw new IntentError(
|
|
627
|
+
'Swap not found',
|
|
628
|
+
ErrorCode.INTENT_NOT_FOUND,
|
|
629
|
+
{ context: { requestId } },
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (status.status === 'completed') {
|
|
634
|
+
return status
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (status.status === 'failed' || status.status === 'expired') {
|
|
638
|
+
throw new IntentError(
|
|
639
|
+
`Swap ${status.status}: ${status.error ?? 'Unknown error'}`,
|
|
640
|
+
ErrorCode.INTENT_FAILED,
|
|
641
|
+
{ context: { requestId, status } },
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
await this.delay(pollInterval)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
throw new NetworkError(
|
|
649
|
+
'Swap completion timeout',
|
|
650
|
+
ErrorCode.NETWORK_TIMEOUT,
|
|
651
|
+
{ context: { requestId, timeout } },
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ─── Utility Methods ─────────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Get supported source chains
|
|
659
|
+
*/
|
|
660
|
+
async getSupportedChains(): Promise<ZcashSwapSourceChain[]> {
|
|
661
|
+
if (this.bridgeProvider) {
|
|
662
|
+
return this.bridgeProvider.getSupportedChains()
|
|
663
|
+
}
|
|
664
|
+
return ['ethereum', 'solana', 'near', 'polygon', 'arbitrum', 'base']
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Get supported source tokens for a chain
|
|
669
|
+
*/
|
|
670
|
+
getSupportedTokens(chain: ZcashSwapSourceChain): ZcashSwapSourceToken[] {
|
|
671
|
+
const tokensByChain: Record<ZcashSwapSourceChain, ZcashSwapSourceToken[]> = {
|
|
672
|
+
ethereum: ['ETH', 'USDC', 'USDT'],
|
|
673
|
+
solana: ['SOL', 'USDC', 'USDT'],
|
|
674
|
+
near: ['NEAR', 'USDC', 'USDT'],
|
|
675
|
+
polygon: ['MATIC', 'USDC', 'USDT'],
|
|
676
|
+
arbitrum: ['ETH', 'USDC', 'USDT'],
|
|
677
|
+
base: ['ETH', 'USDC'],
|
|
678
|
+
}
|
|
679
|
+
return tokensByChain[chain] ?? []
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Check if a swap route is supported
|
|
684
|
+
*/
|
|
685
|
+
isRouteSupported(chain: ZcashSwapSourceChain, token: ZcashSwapSourceToken): boolean {
|
|
686
|
+
return this.getSupportedTokens(chain).includes(token)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ─── Private Helpers ─────────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
private validateQuoteParams(params: ZcashQuoteParams): void {
|
|
692
|
+
if (!params.sourceChain) {
|
|
693
|
+
throw new ValidationError('Source chain is required', 'sourceChain', undefined, ErrorCode.VALIDATION_FAILED)
|
|
694
|
+
}
|
|
695
|
+
if (!params.sourceToken) {
|
|
696
|
+
throw new ValidationError('Source token is required', 'sourceToken', undefined, ErrorCode.VALIDATION_FAILED)
|
|
697
|
+
}
|
|
698
|
+
if (!params.amount || params.amount <= 0n) {
|
|
699
|
+
throw new ValidationError('Amount must be positive', 'amount', { received: params.amount }, ErrorCode.INVALID_AMOUNT)
|
|
700
|
+
}
|
|
701
|
+
if (!params.recipientZAddress) {
|
|
702
|
+
throw new ValidationError('Recipient z-address is required', 'recipientZAddress', undefined, ErrorCode.VALIDATION_FAILED)
|
|
703
|
+
}
|
|
704
|
+
if (!this.isRouteSupported(params.sourceChain, params.sourceToken)) {
|
|
705
|
+
throw new ValidationError(
|
|
706
|
+
`Unsupported swap route: ${params.sourceChain}:${params.sourceToken} → ZEC`,
|
|
707
|
+
'sourceToken',
|
|
708
|
+
{ chain: params.sourceChain, token: params.sourceToken },
|
|
709
|
+
ErrorCode.VALIDATION_FAILED,
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private isValidZAddressFormat(address: string): boolean {
|
|
715
|
+
// Shielded addresses start with 'zs' (sapling) or 'zc' (sprout, deprecated)
|
|
716
|
+
// Unified addresses start with 'u'
|
|
717
|
+
// Testnet shielded: 'ztestsapling' prefix
|
|
718
|
+
return (
|
|
719
|
+
address.startsWith('zs1') ||
|
|
720
|
+
address.startsWith('u1') ||
|
|
721
|
+
address.startsWith('ztestsapling') ||
|
|
722
|
+
address.startsWith('utest')
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private generateMockDepositAddress(chain: ZcashSwapSourceChain): string {
|
|
727
|
+
switch (chain) {
|
|
728
|
+
case 'ethereum':
|
|
729
|
+
case 'polygon':
|
|
730
|
+
case 'arbitrum':
|
|
731
|
+
case 'base':
|
|
732
|
+
return `0x${this.randomHex(40)}`
|
|
733
|
+
case 'solana':
|
|
734
|
+
return this.randomBase58(44)
|
|
735
|
+
case 'near':
|
|
736
|
+
return `deposit_${this.randomHex(8)}.near`
|
|
737
|
+
default:
|
|
738
|
+
return `0x${this.randomHex(40)}`
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private getEstimatedTime(chain: ZcashSwapSourceChain): number {
|
|
743
|
+
// Estimated completion time in seconds
|
|
744
|
+
const times: Record<ZcashSwapSourceChain, number> = {
|
|
745
|
+
ethereum: 900, // ~15 min (confirmations + processing)
|
|
746
|
+
solana: 300, // ~5 min
|
|
747
|
+
near: 300, // ~5 min
|
|
748
|
+
polygon: 600, // ~10 min
|
|
749
|
+
arbitrum: 600, // ~10 min
|
|
750
|
+
base: 600, // ~10 min
|
|
751
|
+
}
|
|
752
|
+
return times[chain] ?? 600
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private formatAmount(amount: bigint, decimals: number): string {
|
|
756
|
+
const divisor = 10 ** decimals
|
|
757
|
+
const whole = amount / BigInt(divisor)
|
|
758
|
+
const fraction = amount % BigInt(divisor)
|
|
759
|
+
const fractionStr = fraction.toString().padStart(decimals, '0').replace(/0+$/, '')
|
|
760
|
+
return fractionStr ? `${whole}.${fractionStr}` : whole.toString()
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private randomHex(length: number): string {
|
|
764
|
+
const chars = '0123456789abcdef'
|
|
765
|
+
let result = ''
|
|
766
|
+
for (let i = 0; i < length; i++) {
|
|
767
|
+
result += chars[Math.floor(Math.random() * chars.length)]
|
|
768
|
+
}
|
|
769
|
+
return result
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private randomBase58(length: number): string {
|
|
773
|
+
const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
774
|
+
let result = ''
|
|
775
|
+
for (let i = 0; i < length; i++) {
|
|
776
|
+
result += chars[Math.floor(Math.random() * chars.length)]
|
|
777
|
+
}
|
|
778
|
+
return result
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private delay(ms: number): Promise<void> {
|
|
782
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Create a Zcash swap service instance
|
|
788
|
+
*/
|
|
789
|
+
export function createZcashSwapService(
|
|
790
|
+
config: ZcashSwapServiceConfig,
|
|
791
|
+
): ZcashSwapService {
|
|
792
|
+
return new ZcashSwapService(config)
|
|
793
|
+
}
|