@sip-protocol/sdk 0.1.9 → 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,884 @@
1
+ /**
2
+ * Browser Noir Proof Provider
3
+ *
4
+ * Production-ready ZK proof provider for browser environments.
5
+ * Uses Web Workers for non-blocking proof generation and WASM for computation.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { BrowserNoirProvider } from '@sip-protocol/sdk/browser'
10
+ *
11
+ * const provider = new BrowserNoirProvider()
12
+ * await provider.initialize() // Loads WASM
13
+ *
14
+ * const proof = await provider.generateFundingProof(inputs)
15
+ * ```
16
+ *
17
+ * @see docs/specs/ZK-ARCHITECTURE.md
18
+ * @see https://github.com/sip-protocol/sip-protocol/issues/121
19
+ */
20
+
21
+ import type { ZKProof } from '@sip-protocol/types'
22
+ import type {
23
+ ProofProvider,
24
+ ProofFramework,
25
+ FundingProofParams,
26
+ ValidityProofParams,
27
+ FulfillmentProofParams,
28
+ ProofResult,
29
+ } from './interface'
30
+ import { ProofGenerationError } from './interface'
31
+ import { ProofError, ErrorCode } from '../errors'
32
+ import {
33
+ isBrowser,
34
+ supportsWebWorkers,
35
+ supportsSharedArrayBuffer,
36
+ hexToBytes,
37
+ bytesToHex,
38
+ getBrowserInfo,
39
+ } from './browser-utils'
40
+
41
+ // Import Noir JS (works in browser with WASM)
42
+ import { Noir } from '@noir-lang/noir_js'
43
+ import type { CompiledCircuit } from '@noir-lang/types'
44
+ import { UltraHonkBackend } from '@aztec/bb.js'
45
+ import { secp256k1 } from '@noble/curves/secp256k1'
46
+
47
+ // Import compiled circuit artifacts
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ import fundingCircuitArtifact from './circuits/funding_proof.json'
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ import validityCircuitArtifact from './circuits/validity_proof.json'
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ import fulfillmentCircuitArtifact from './circuits/fulfillment_proof.json'
54
+
55
+ /**
56
+ * Public key coordinates for secp256k1
57
+ */
58
+ export interface PublicKeyCoordinates {
59
+ /** X coordinate as 32-byte array */
60
+ x: number[]
61
+ /** Y coordinate as 32-byte array */
62
+ y: number[]
63
+ }
64
+
65
+ /**
66
+ * Browser Noir Provider Configuration
67
+ */
68
+ export interface BrowserNoirProviderConfig {
69
+ /**
70
+ * Use Web Workers for proof generation (non-blocking)
71
+ * @default true
72
+ */
73
+ useWorker?: boolean
74
+
75
+ /**
76
+ * Enable verbose logging for debugging
77
+ * @default false
78
+ */
79
+ verbose?: boolean
80
+
81
+ /**
82
+ * Oracle public key for verifying attestations in fulfillment proofs
83
+ * Required for production use.
84
+ */
85
+ oraclePublicKey?: PublicKeyCoordinates
86
+
87
+ /**
88
+ * Maximum time for proof generation before timeout (ms)
89
+ * @default 60000 (60 seconds)
90
+ */
91
+ timeout?: number
92
+ }
93
+
94
+ /**
95
+ * Proof generation progress callback
96
+ */
97
+ export type ProofProgressCallback = (progress: {
98
+ stage: 'initializing' | 'witness' | 'proving' | 'verifying' | 'complete'
99
+ percent: number
100
+ message: string
101
+ }) => void
102
+
103
+ /**
104
+ * Browser-compatible Noir Proof Provider
105
+ *
106
+ * Designed for browser environments with:
107
+ * - WASM-based proof generation
108
+ * - Optional Web Worker support for non-blocking UI
109
+ * - Memory-efficient initialization
110
+ * - Progress callbacks for UX
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const provider = new BrowserNoirProvider({ useWorker: true })
115
+ *
116
+ * await provider.initialize((progress) => {
117
+ * console.log(`${progress.stage}: ${progress.percent}%`)
118
+ * })
119
+ *
120
+ * const result = await provider.generateFundingProof(params, (progress) => {
121
+ * updateProgressBar(progress.percent)
122
+ * })
123
+ * ```
124
+ */
125
+ export class BrowserNoirProvider implements ProofProvider {
126
+ readonly framework: ProofFramework = 'noir'
127
+ private _isReady = false
128
+ private config: Required<BrowserNoirProviderConfig>
129
+
130
+ // Circuit instances
131
+ private fundingNoir: Noir | null = null
132
+ private fundingBackend: UltraHonkBackend | null = null
133
+ private validityNoir: Noir | null = null
134
+ private validityBackend: UltraHonkBackend | null = null
135
+ private fulfillmentNoir: Noir | null = null
136
+ private fulfillmentBackend: UltraHonkBackend | null = null
137
+
138
+ // Worker instance (optional)
139
+ private worker: Worker | null = null
140
+ private workerPending: Map<
141
+ string,
142
+ { resolve: (result: ProofResult) => void; reject: (error: Error) => void }
143
+ > = new Map()
144
+
145
+ constructor(config: BrowserNoirProviderConfig = {}) {
146
+ this.config = {
147
+ useWorker: config.useWorker ?? true,
148
+ verbose: config.verbose ?? false,
149
+ oraclePublicKey: config.oraclePublicKey ?? undefined,
150
+ timeout: config.timeout ?? 60000,
151
+ } as Required<BrowserNoirProviderConfig>
152
+
153
+ // Warn if not in browser
154
+ if (!isBrowser()) {
155
+ console.warn(
156
+ '[BrowserNoirProvider] Not running in browser environment. ' +
157
+ 'Consider using NoirProofProvider for Node.js.'
158
+ )
159
+ }
160
+ }
161
+
162
+ get isReady(): boolean {
163
+ return this._isReady
164
+ }
165
+
166
+ /**
167
+ * Get browser environment info
168
+ */
169
+ static getBrowserInfo() {
170
+ return getBrowserInfo()
171
+ }
172
+
173
+ /**
174
+ * Check if browser supports all required features
175
+ */
176
+ static checkBrowserSupport(): {
177
+ supported: boolean
178
+ missing: string[]
179
+ } {
180
+ const missing: string[] = []
181
+
182
+ if (!isBrowser()) {
183
+ missing.push('browser environment')
184
+ }
185
+
186
+ // Check for WebAssembly
187
+ if (typeof WebAssembly === 'undefined') {
188
+ missing.push('WebAssembly')
189
+ }
190
+
191
+ // SharedArrayBuffer is required for Barretenberg WASM
192
+ if (!supportsSharedArrayBuffer()) {
193
+ missing.push('SharedArrayBuffer (requires COOP/COEP headers)')
194
+ }
195
+
196
+ return {
197
+ supported: missing.length === 0,
198
+ missing,
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Derive secp256k1 public key coordinates from a private key
204
+ */
205
+ static derivePublicKey(privateKey: Uint8Array): PublicKeyCoordinates {
206
+ const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
207
+ const x = Array.from(uncompressedPubKey.slice(1, 33))
208
+ const y = Array.from(uncompressedPubKey.slice(33, 65))
209
+ return { x, y }
210
+ }
211
+
212
+ /**
213
+ * Initialize the browser provider
214
+ *
215
+ * Loads WASM and circuit artifacts. This should be called before any
216
+ * proof generation. Consider showing a loading indicator during init.
217
+ *
218
+ * @param onProgress - Optional progress callback
219
+ */
220
+ async initialize(onProgress?: ProofProgressCallback): Promise<void> {
221
+ if (this._isReady) {
222
+ return
223
+ }
224
+
225
+ const { supported, missing } = BrowserNoirProvider.checkBrowserSupport()
226
+ if (!supported) {
227
+ throw new ProofError(
228
+ `Browser missing required features: ${missing.join(', ')}`,
229
+ ErrorCode.PROOF_PROVIDER_NOT_READY
230
+ )
231
+ }
232
+
233
+ try {
234
+ onProgress?.({
235
+ stage: 'initializing',
236
+ percent: 0,
237
+ message: 'Loading WASM runtime...',
238
+ })
239
+
240
+ if (this.config.verbose) {
241
+ console.log('[BrowserNoirProvider] Initializing...')
242
+ console.log('[BrowserNoirProvider] Browser info:', getBrowserInfo())
243
+ }
244
+
245
+ // Initialize circuits in parallel for faster loading
246
+ const fundingCircuit = fundingCircuitArtifact as unknown as CompiledCircuit
247
+ const validityCircuit = validityCircuitArtifact as unknown as CompiledCircuit
248
+ const fulfillmentCircuit = fulfillmentCircuitArtifact as unknown as CompiledCircuit
249
+
250
+ onProgress?.({
251
+ stage: 'initializing',
252
+ percent: 20,
253
+ message: 'Creating proof backends...',
254
+ })
255
+
256
+ // Create backends (this loads WASM)
257
+ this.fundingBackend = new UltraHonkBackend(fundingCircuit.bytecode)
258
+ this.validityBackend = new UltraHonkBackend(validityCircuit.bytecode)
259
+ this.fulfillmentBackend = new UltraHonkBackend(fulfillmentCircuit.bytecode)
260
+
261
+ onProgress?.({
262
+ stage: 'initializing',
263
+ percent: 60,
264
+ message: 'Initializing Noir circuits...',
265
+ })
266
+
267
+ // Create Noir instances for witness generation
268
+ this.fundingNoir = new Noir(fundingCircuit)
269
+ this.validityNoir = new Noir(validityCircuit)
270
+ this.fulfillmentNoir = new Noir(fulfillmentCircuit)
271
+
272
+ onProgress?.({
273
+ stage: 'initializing',
274
+ percent: 90,
275
+ message: 'Setting up worker...',
276
+ })
277
+
278
+ // Initialize worker if enabled and supported
279
+ if (this.config.useWorker && supportsWebWorkers()) {
280
+ await this.initializeWorker()
281
+ }
282
+
283
+ this._isReady = true
284
+
285
+ onProgress?.({
286
+ stage: 'complete',
287
+ percent: 100,
288
+ message: 'Ready for proof generation',
289
+ })
290
+
291
+ if (this.config.verbose) {
292
+ console.log('[BrowserNoirProvider] Initialization complete')
293
+ }
294
+ } catch (error) {
295
+ throw new ProofError(
296
+ `Failed to initialize BrowserNoirProvider: ${error instanceof Error ? error.message : String(error)}`,
297
+ ErrorCode.PROOF_NOT_IMPLEMENTED,
298
+ { context: { error } }
299
+ )
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Initialize Web Worker for off-main-thread proof generation
305
+ */
306
+ private async initializeWorker(): Promise<void> {
307
+ // Worker initialization is optional - proof gen works on main thread too
308
+ // For now, we'll do main-thread proof gen with async/await
309
+ // Full worker implementation would require bundling worker code separately
310
+ if (this.config.verbose) {
311
+ console.log('[BrowserNoirProvider] Worker support: using async main-thread')
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Generate a Funding Proof
317
+ *
318
+ * Proves: balance >= minimumRequired without revealing balance
319
+ *
320
+ * @param params - Funding proof parameters
321
+ * @param onProgress - Optional progress callback
322
+ */
323
+ async generateFundingProof(
324
+ params: FundingProofParams,
325
+ onProgress?: ProofProgressCallback
326
+ ): Promise<ProofResult> {
327
+ this.ensureReady()
328
+
329
+ if (!this.fundingNoir || !this.fundingBackend) {
330
+ throw new ProofGenerationError('funding', 'Funding circuit not initialized')
331
+ }
332
+
333
+ try {
334
+ onProgress?.({
335
+ stage: 'witness',
336
+ percent: 10,
337
+ message: 'Preparing witness inputs...',
338
+ })
339
+
340
+ // Compute commitment hash
341
+ const { commitmentHash, blindingField } = await this.computeCommitmentHash(
342
+ params.balance,
343
+ params.blindingFactor,
344
+ params.assetId
345
+ )
346
+
347
+ const witnessInputs = {
348
+ commitment_hash: commitmentHash,
349
+ minimum_required: params.minimumRequired.toString(),
350
+ asset_id: this.assetIdToField(params.assetId),
351
+ balance: params.balance.toString(),
352
+ blinding: blindingField,
353
+ }
354
+
355
+ onProgress?.({
356
+ stage: 'witness',
357
+ percent: 30,
358
+ message: 'Generating witness...',
359
+ })
360
+
361
+ // Generate witness
362
+ const { witness } = await this.fundingNoir.execute(witnessInputs)
363
+
364
+ onProgress?.({
365
+ stage: 'proving',
366
+ percent: 50,
367
+ message: 'Generating proof (this may take a moment)...',
368
+ })
369
+
370
+ // Generate proof
371
+ const proofData = await this.fundingBackend.generateProof(witness)
372
+
373
+ onProgress?.({
374
+ stage: 'complete',
375
+ percent: 100,
376
+ message: 'Proof generated successfully',
377
+ })
378
+
379
+ const publicInputs: `0x${string}`[] = [
380
+ `0x${commitmentHash}`,
381
+ `0x${params.minimumRequired.toString(16).padStart(16, '0')}`,
382
+ `0x${this.assetIdToField(params.assetId)}`,
383
+ ]
384
+
385
+ const proof: ZKProof = {
386
+ type: 'funding',
387
+ proof: `0x${bytesToHex(proofData.proof)}`,
388
+ publicInputs,
389
+ }
390
+
391
+ return { proof, publicInputs }
392
+ } catch (error) {
393
+ const message = error instanceof Error ? error.message : String(error)
394
+ throw new ProofGenerationError(
395
+ 'funding',
396
+ `Failed to generate funding proof: ${message}`,
397
+ error instanceof Error ? error : undefined
398
+ )
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Generate a Validity Proof
404
+ *
405
+ * Proves: Intent is authorized by sender without revealing identity
406
+ */
407
+ async generateValidityProof(
408
+ params: ValidityProofParams,
409
+ onProgress?: ProofProgressCallback
410
+ ): Promise<ProofResult> {
411
+ this.ensureReady()
412
+
413
+ if (!this.validityNoir || !this.validityBackend) {
414
+ throw new ProofGenerationError('validity', 'Validity circuit not initialized')
415
+ }
416
+
417
+ try {
418
+ onProgress?.({
419
+ stage: 'witness',
420
+ percent: 10,
421
+ message: 'Preparing validity witness...',
422
+ })
423
+
424
+ // Convert inputs to field elements
425
+ const intentHashField = this.hexToField(params.intentHash)
426
+ const senderAddressField = this.hexToField(params.senderAddress)
427
+ const senderBlindingField = this.bytesToField(params.senderBlinding)
428
+ const senderSecretField = this.bytesToField(params.senderSecret)
429
+ const nonceField = this.bytesToField(params.nonce)
430
+
431
+ // Compute derived values
432
+ const { commitmentX, commitmentY } = await this.computeSenderCommitment(
433
+ senderAddressField,
434
+ senderBlindingField
435
+ )
436
+ const nullifier = await this.computeNullifier(senderSecretField, intentHashField, nonceField)
437
+
438
+ const signature = Array.from(params.authorizationSignature)
439
+ const messageHash = this.fieldToBytes32(intentHashField)
440
+
441
+ // Get public key
442
+ let pubKeyX: number[]
443
+ let pubKeyY: number[]
444
+ if (params.senderPublicKey) {
445
+ pubKeyX = Array.from(params.senderPublicKey.x)
446
+ pubKeyY = Array.from(params.senderPublicKey.y)
447
+ } else {
448
+ const coords = this.getPublicKeyCoordinates(params.senderSecret)
449
+ pubKeyX = coords.x
450
+ pubKeyY = coords.y
451
+ }
452
+
453
+ const witnessInputs = {
454
+ intent_hash: intentHashField,
455
+ sender_commitment_x: commitmentX,
456
+ sender_commitment_y: commitmentY,
457
+ nullifier: nullifier,
458
+ timestamp: params.timestamp.toString(),
459
+ expiry: params.expiry.toString(),
460
+ sender_address: senderAddressField,
461
+ sender_blinding: senderBlindingField,
462
+ sender_secret: senderSecretField,
463
+ pub_key_x: pubKeyX,
464
+ pub_key_y: pubKeyY,
465
+ signature: signature,
466
+ message_hash: messageHash,
467
+ nonce: nonceField,
468
+ }
469
+
470
+ onProgress?.({
471
+ stage: 'witness',
472
+ percent: 30,
473
+ message: 'Generating witness...',
474
+ })
475
+
476
+ const { witness } = await this.validityNoir.execute(witnessInputs)
477
+
478
+ onProgress?.({
479
+ stage: 'proving',
480
+ percent: 50,
481
+ message: 'Generating validity proof...',
482
+ })
483
+
484
+ const proofData = await this.validityBackend.generateProof(witness)
485
+
486
+ onProgress?.({
487
+ stage: 'complete',
488
+ percent: 100,
489
+ message: 'Validity proof generated',
490
+ })
491
+
492
+ const publicInputs: `0x${string}`[] = [
493
+ `0x${intentHashField}`,
494
+ `0x${commitmentX}`,
495
+ `0x${commitmentY}`,
496
+ `0x${nullifier}`,
497
+ `0x${params.timestamp.toString(16).padStart(16, '0')}`,
498
+ `0x${params.expiry.toString(16).padStart(16, '0')}`,
499
+ ]
500
+
501
+ const proof: ZKProof = {
502
+ type: 'validity',
503
+ proof: `0x${bytesToHex(proofData.proof)}`,
504
+ publicInputs,
505
+ }
506
+
507
+ return { proof, publicInputs }
508
+ } catch (error) {
509
+ const message = error instanceof Error ? error.message : String(error)
510
+ throw new ProofGenerationError(
511
+ 'validity',
512
+ `Failed to generate validity proof: ${message}`,
513
+ error instanceof Error ? error : undefined
514
+ )
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Generate a Fulfillment Proof
520
+ *
521
+ * Proves: Solver correctly executed the intent
522
+ */
523
+ async generateFulfillmentProof(
524
+ params: FulfillmentProofParams,
525
+ onProgress?: ProofProgressCallback
526
+ ): Promise<ProofResult> {
527
+ this.ensureReady()
528
+
529
+ if (!this.fulfillmentNoir || !this.fulfillmentBackend) {
530
+ throw new ProofGenerationError('fulfillment', 'Fulfillment circuit not initialized')
531
+ }
532
+
533
+ try {
534
+ onProgress?.({
535
+ stage: 'witness',
536
+ percent: 10,
537
+ message: 'Preparing fulfillment witness...',
538
+ })
539
+
540
+ const intentHashField = this.hexToField(params.intentHash)
541
+ const recipientStealthField = this.hexToField(params.recipientStealth)
542
+
543
+ const { commitmentX, commitmentY } = await this.computeOutputCommitment(
544
+ params.outputAmount,
545
+ params.outputBlinding
546
+ )
547
+
548
+ const solverSecretField = this.bytesToField(params.solverSecret)
549
+ const solverId = await this.computeSolverId(solverSecretField)
550
+ const outputBlindingField = this.bytesToField(params.outputBlinding)
551
+
552
+ const attestation = params.oracleAttestation
553
+ const attestationRecipientField = this.hexToField(attestation.recipient)
554
+ const attestationTxHashField = this.hexToField(attestation.txHash)
555
+ const oracleSignature = Array.from(attestation.signature)
556
+ const oracleMessageHash = await this.computeOracleMessageHash(
557
+ attestation.recipient,
558
+ attestation.amount,
559
+ attestation.txHash,
560
+ attestation.blockNumber
561
+ )
562
+
563
+ const oraclePubKeyX = this.config.oraclePublicKey?.x ?? new Array(32).fill(0)
564
+ const oraclePubKeyY = this.config.oraclePublicKey?.y ?? new Array(32).fill(0)
565
+
566
+ const witnessInputs = {
567
+ intent_hash: intentHashField,
568
+ output_commitment_x: commitmentX,
569
+ output_commitment_y: commitmentY,
570
+ recipient_stealth: recipientStealthField,
571
+ min_output_amount: params.minOutputAmount.toString(),
572
+ solver_id: solverId,
573
+ fulfillment_time: params.fulfillmentTime.toString(),
574
+ expiry: params.expiry.toString(),
575
+ output_amount: params.outputAmount.toString(),
576
+ output_blinding: outputBlindingField,
577
+ solver_secret: solverSecretField,
578
+ attestation_recipient: attestationRecipientField,
579
+ attestation_amount: attestation.amount.toString(),
580
+ attestation_tx_hash: attestationTxHashField,
581
+ attestation_block: attestation.blockNumber.toString(),
582
+ oracle_signature: oracleSignature,
583
+ oracle_message_hash: oracleMessageHash,
584
+ oracle_pub_key_x: oraclePubKeyX,
585
+ oracle_pub_key_y: oraclePubKeyY,
586
+ }
587
+
588
+ onProgress?.({
589
+ stage: 'witness',
590
+ percent: 30,
591
+ message: 'Generating witness...',
592
+ })
593
+
594
+ const { witness } = await this.fulfillmentNoir.execute(witnessInputs)
595
+
596
+ onProgress?.({
597
+ stage: 'proving',
598
+ percent: 50,
599
+ message: 'Generating fulfillment proof...',
600
+ })
601
+
602
+ const proofData = await this.fulfillmentBackend.generateProof(witness)
603
+
604
+ onProgress?.({
605
+ stage: 'complete',
606
+ percent: 100,
607
+ message: 'Fulfillment proof generated',
608
+ })
609
+
610
+ const publicInputs: `0x${string}`[] = [
611
+ `0x${intentHashField}`,
612
+ `0x${commitmentX}`,
613
+ `0x${commitmentY}`,
614
+ `0x${recipientStealthField}`,
615
+ `0x${params.minOutputAmount.toString(16).padStart(16, '0')}`,
616
+ `0x${solverId}`,
617
+ `0x${params.fulfillmentTime.toString(16).padStart(16, '0')}`,
618
+ `0x${params.expiry.toString(16).padStart(16, '0')}`,
619
+ ]
620
+
621
+ const proof: ZKProof = {
622
+ type: 'fulfillment',
623
+ proof: `0x${bytesToHex(proofData.proof)}`,
624
+ publicInputs,
625
+ }
626
+
627
+ return { proof, publicInputs }
628
+ } catch (error) {
629
+ const message = error instanceof Error ? error.message : String(error)
630
+ throw new ProofGenerationError(
631
+ 'fulfillment',
632
+ `Failed to generate fulfillment proof: ${message}`,
633
+ error instanceof Error ? error : undefined
634
+ )
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Verify a proof
640
+ */
641
+ async verifyProof(proof: ZKProof): Promise<boolean> {
642
+ this.ensureReady()
643
+
644
+ let backend: UltraHonkBackend | null = null
645
+
646
+ switch (proof.type) {
647
+ case 'funding':
648
+ backend = this.fundingBackend
649
+ break
650
+ case 'validity':
651
+ backend = this.validityBackend
652
+ break
653
+ case 'fulfillment':
654
+ backend = this.fulfillmentBackend
655
+ break
656
+ default:
657
+ throw new ProofError(`Unknown proof type: ${proof.type}`, ErrorCode.PROOF_NOT_IMPLEMENTED)
658
+ }
659
+
660
+ if (!backend) {
661
+ throw new ProofError(
662
+ `${proof.type} backend not initialized`,
663
+ ErrorCode.PROOF_PROVIDER_NOT_READY
664
+ )
665
+ }
666
+
667
+ try {
668
+ const proofHex = proof.proof.startsWith('0x') ? proof.proof.slice(2) : proof.proof
669
+ const proofBytes = hexToBytes(proofHex)
670
+
671
+ const isValid = await backend.verifyProof({
672
+ proof: proofBytes,
673
+ publicInputs: proof.publicInputs.map((input) =>
674
+ input.startsWith('0x') ? input.slice(2) : input
675
+ ),
676
+ })
677
+
678
+ return isValid
679
+ } catch (error) {
680
+ if (this.config.verbose) {
681
+ console.error('[BrowserNoirProvider] Verification error:', error)
682
+ }
683
+ return false
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Destroy the provider and free resources
689
+ */
690
+ async destroy(): Promise<void> {
691
+ if (this.fundingBackend) {
692
+ await this.fundingBackend.destroy()
693
+ this.fundingBackend = null
694
+ }
695
+ if (this.validityBackend) {
696
+ await this.validityBackend.destroy()
697
+ this.validityBackend = null
698
+ }
699
+ if (this.fulfillmentBackend) {
700
+ await this.fulfillmentBackend.destroy()
701
+ this.fulfillmentBackend = null
702
+ }
703
+ if (this.worker) {
704
+ this.worker.terminate()
705
+ this.worker = null
706
+ }
707
+ this.fundingNoir = null
708
+ this.validityNoir = null
709
+ this.fulfillmentNoir = null
710
+ this._isReady = false
711
+ }
712
+
713
+ // ─── Private Utility Methods ────────────────────────────────────────────────
714
+
715
+ private ensureReady(): void {
716
+ if (!this._isReady) {
717
+ throw new ProofError(
718
+ 'BrowserNoirProvider not initialized. Call initialize() first.',
719
+ ErrorCode.PROOF_PROVIDER_NOT_READY
720
+ )
721
+ }
722
+ }
723
+
724
+ private async computeCommitmentHash(
725
+ balance: bigint,
726
+ blindingFactor: Uint8Array,
727
+ assetId: string
728
+ ): Promise<{ commitmentHash: string; blindingField: string }> {
729
+ const blindingField = this.bytesToField(blindingFactor)
730
+ const { sha256 } = await import('@noble/hashes/sha256')
731
+ const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
732
+
733
+ const preimage = new Uint8Array([
734
+ ...this.bigintToBytes(balance, 8),
735
+ ...blindingFactor.slice(0, 32),
736
+ ...hexToBytes(this.assetIdToField(assetId)),
737
+ ])
738
+
739
+ const hash = sha256(preimage)
740
+ const commitmentHash = nobleToHex(hash)
741
+
742
+ return { commitmentHash, blindingField }
743
+ }
744
+
745
+ private assetIdToField(assetId: string): string {
746
+ if (assetId.startsWith('0x')) {
747
+ return assetId.slice(2).padStart(64, '0')
748
+ }
749
+ const encoder = new TextEncoder()
750
+ const bytes = encoder.encode(assetId)
751
+ let result = 0n
752
+ for (let i = 0; i < bytes.length && i < 31; i++) {
753
+ result = result * 256n + BigInt(bytes[i])
754
+ }
755
+ return result.toString(16).padStart(64, '0')
756
+ }
757
+
758
+ private bytesToField(bytes: Uint8Array): string {
759
+ let result = 0n
760
+ const len = Math.min(bytes.length, 31)
761
+ for (let i = 0; i < len; i++) {
762
+ result = result * 256n + BigInt(bytes[i])
763
+ }
764
+ return result.toString()
765
+ }
766
+
767
+ private bigintToBytes(value: bigint, length: number): Uint8Array {
768
+ const bytes = new Uint8Array(length)
769
+ let v = value
770
+ for (let i = length - 1; i >= 0; i--) {
771
+ bytes[i] = Number(v & 0xffn)
772
+ v = v >> 8n
773
+ }
774
+ return bytes
775
+ }
776
+
777
+ private hexToField(hex: string): string {
778
+ const h = hex.startsWith('0x') ? hex.slice(2) : hex
779
+ return h.padStart(64, '0')
780
+ }
781
+
782
+ private fieldToBytes32(field: string): number[] {
783
+ const hex = field.padStart(64, '0')
784
+ const bytes: number[] = []
785
+ for (let i = 0; i < 32; i++) {
786
+ bytes.push(parseInt(hex.slice(i * 2, i * 2 + 2), 16))
787
+ }
788
+ return bytes
789
+ }
790
+
791
+ private async computeSenderCommitment(
792
+ senderAddressField: string,
793
+ senderBlindingField: string
794
+ ): Promise<{ commitmentX: string; commitmentY: string }> {
795
+ const { sha256 } = await import('@noble/hashes/sha256')
796
+ const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
797
+
798
+ const addressBytes = hexToBytes(senderAddressField)
799
+ const blindingBytes = hexToBytes(senderBlindingField.padStart(64, '0'))
800
+ const preimage = new Uint8Array([...addressBytes, ...blindingBytes])
801
+ const hash = sha256(preimage)
802
+
803
+ const commitmentX = nobleToHex(hash.slice(0, 16)).padStart(64, '0')
804
+ const commitmentY = nobleToHex(hash.slice(16, 32)).padStart(64, '0')
805
+
806
+ return { commitmentX, commitmentY }
807
+ }
808
+
809
+ private async computeNullifier(
810
+ senderSecretField: string,
811
+ intentHashField: string,
812
+ nonceField: string
813
+ ): Promise<string> {
814
+ const { sha256 } = await import('@noble/hashes/sha256')
815
+ const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
816
+
817
+ const secretBytes = hexToBytes(senderSecretField.padStart(64, '0'))
818
+ const intentBytes = hexToBytes(intentHashField)
819
+ const nonceBytes = hexToBytes(nonceField.padStart(64, '0'))
820
+ const preimage = new Uint8Array([...secretBytes, ...intentBytes, ...nonceBytes])
821
+ const hash = sha256(preimage)
822
+
823
+ return nobleToHex(hash)
824
+ }
825
+
826
+ private async computeOutputCommitment(
827
+ outputAmount: bigint,
828
+ outputBlinding: Uint8Array
829
+ ): Promise<{ commitmentX: string; commitmentY: string }> {
830
+ const { sha256 } = await import('@noble/hashes/sha256')
831
+ const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
832
+
833
+ const amountBytes = this.bigintToBytes(outputAmount, 8)
834
+ const blindingBytes = outputBlinding.slice(0, 32)
835
+ const preimage = new Uint8Array([...amountBytes, ...blindingBytes])
836
+ const hash = sha256(preimage)
837
+
838
+ const commitmentX = nobleToHex(hash.slice(0, 16)).padStart(64, '0')
839
+ const commitmentY = nobleToHex(hash.slice(16, 32)).padStart(64, '0')
840
+
841
+ return { commitmentX, commitmentY }
842
+ }
843
+
844
+ private async computeSolverId(solverSecretField: string): Promise<string> {
845
+ const { sha256 } = await import('@noble/hashes/sha256')
846
+ const { bytesToHex: nobleToHex } = await import('@noble/hashes/utils')
847
+
848
+ const secretBytes = hexToBytes(solverSecretField.padStart(64, '0'))
849
+ const hash = sha256(secretBytes)
850
+
851
+ return nobleToHex(hash)
852
+ }
853
+
854
+ private async computeOracleMessageHash(
855
+ recipient: string,
856
+ amount: bigint,
857
+ txHash: string,
858
+ blockNumber: bigint
859
+ ): Promise<number[]> {
860
+ const { sha256 } = await import('@noble/hashes/sha256')
861
+
862
+ const recipientBytes = hexToBytes(this.hexToField(recipient))
863
+ const amountBytes = this.bigintToBytes(amount, 8)
864
+ const txHashBytes = hexToBytes(this.hexToField(txHash))
865
+ const blockBytes = this.bigintToBytes(blockNumber, 8)
866
+
867
+ const preimage = new Uint8Array([
868
+ ...recipientBytes,
869
+ ...amountBytes,
870
+ ...txHashBytes,
871
+ ...blockBytes,
872
+ ])
873
+ const hash = sha256(preimage)
874
+
875
+ return Array.from(hash)
876
+ }
877
+
878
+ private getPublicKeyCoordinates(privateKey: Uint8Array): PublicKeyCoordinates {
879
+ const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
880
+ const x = Array.from(uncompressedPubKey.slice(1, 33))
881
+ const y = Array.from(uncompressedPubKey.slice(33, 65))
882
+ return { x, y }
883
+ }
884
+ }