@sip-protocol/sdk 0.6.0 → 0.6.2

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 (63) hide show
  1. package/README.md +58 -0
  2. package/dist/browser.d.mts +4 -4
  3. package/dist/browser.d.ts +4 -4
  4. package/dist/browser.js +2752 -448
  5. package/dist/browser.mjs +31 -1
  6. package/dist/chunk-7QZPORY5.mjs +15604 -0
  7. package/dist/chunk-C2NPCUAJ.mjs +17010 -0
  8. package/dist/chunk-FCVLFUIC.mjs +16699 -0
  9. package/dist/chunk-G5UHXECN.mjs +16340 -0
  10. package/dist/chunk-GEDEIZHJ.mjs +16798 -0
  11. package/dist/chunk-GOOEOAMV.mjs +17026 -0
  12. package/dist/chunk-MTNYSNR7.mjs +16269 -0
  13. package/dist/chunk-O5PIB2EA.mjs +16698 -0
  14. package/dist/chunk-PCFM7FQO.mjs +17010 -0
  15. package/dist/chunk-QK464ARC.mjs +16946 -0
  16. package/dist/chunk-VNBMNGC3.mjs +16698 -0
  17. package/dist/chunk-W5TUELDQ.mjs +16947 -0
  18. package/dist/index-CD_zShu-.d.ts +10870 -0
  19. package/dist/index-CQBYdLYy.d.mts +10976 -0
  20. package/dist/index-Cg9TYEPv.d.mts +11321 -0
  21. package/dist/index-CqZJOO8C.d.mts +11323 -0
  22. package/dist/index-CywN9Bnp.d.ts +11321 -0
  23. package/dist/index-DHy5ZjCD.d.ts +10976 -0
  24. package/dist/index-DfsVsmxu.d.ts +11323 -0
  25. package/dist/index-ObjwyVDX.d.mts +10870 -0
  26. package/dist/index-m0xbSfmT.d.mts +11318 -0
  27. package/dist/index-rWLEgvhN.d.ts +11318 -0
  28. package/dist/index.d.mts +3 -3
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +2737 -427
  31. package/dist/index.mjs +31 -1
  32. package/dist/noir-DKfEzWy9.d.mts +482 -0
  33. package/dist/noir-DKfEzWy9.d.ts +482 -0
  34. package/dist/proofs/noir.d.mts +1 -1
  35. package/dist/proofs/noir.d.ts +1 -1
  36. package/dist/proofs/noir.js +12 -3
  37. package/dist/proofs/noir.mjs +12 -3
  38. package/package.json +16 -14
  39. package/src/adapters/near-intents.ts +13 -3
  40. package/src/auction/index.ts +20 -0
  41. package/src/auction/sealed-bid.ts +1037 -0
  42. package/src/compliance/derivation.ts +13 -3
  43. package/src/compliance/reports.ts +5 -4
  44. package/src/cosmos/ibc-stealth.ts +2 -2
  45. package/src/cosmos/stealth.ts +2 -2
  46. package/src/governance/index.ts +19 -0
  47. package/src/governance/private-vote.ts +1116 -0
  48. package/src/index.ts +50 -2
  49. package/src/intent.ts +145 -8
  50. package/src/nft/index.ts +27 -0
  51. package/src/nft/private-nft.ts +811 -0
  52. package/src/proofs/browser-utils.ts +1 -7
  53. package/src/proofs/noir.ts +34 -7
  54. package/src/settlement/backends/direct-chain.ts +14 -3
  55. package/src/stealth.ts +31 -13
  56. package/src/types/browser.d.ts +67 -0
  57. package/src/validation.ts +4 -2
  58. package/src/wallet/bitcoin/adapter.ts +159 -15
  59. package/src/wallet/bitcoin/types.ts +340 -15
  60. package/src/wallet/cosmos/mock.ts +16 -12
  61. package/src/wallet/hardware/ledger.ts +82 -12
  62. package/src/wallet/hardware/types.ts +2 -0
  63. package/LICENSE +0 -21
@@ -0,0 +1,1037 @@
1
+ /**
2
+ * Sealed-Bid Auction Implementation
3
+ *
4
+ * Implements cryptographically binding sealed bids using Pedersen commitments.
5
+ * Bidders commit to their bid amounts without revealing them until the reveal phase.
6
+ *
7
+ * ## Use Cases
8
+ *
9
+ * - First-price sealed-bid auctions (pay your bid)
10
+ * - Second-price sealed-bid auctions (Vickrey auctions - pay second-highest bid)
11
+ * - NFT auctions with privacy
12
+ * - Government procurement contracts
13
+ * - Private sales
14
+ *
15
+ * ## Security Properties
16
+ *
17
+ * - **Binding**: Cannot change bid after commitment (cryptographically enforced)
18
+ * - **Hiding**: Bid amount hidden until reveal phase (computational hiding)
19
+ * - **Verifiable**: Anyone can verify revealed bid matches commitment
20
+ * - **Non-malleable**: Commitments use secure randomness (salt)
21
+ *
22
+ * ## Workflow
23
+ *
24
+ * 1. **Bidding Phase**: Create sealed bid with `createBid()`
25
+ * 2. **Submit Phase**: Submit commitment on-chain (external)
26
+ * 3. **Reveal Phase**: Reveal bid amount and salt
27
+ * 4. **Verification**: Anyone can verify with `verifyBid()`
28
+ *
29
+ * @module auction/sealed-bid
30
+ * @see {@link commit} from '../commitment' for underlying commitment scheme
31
+ */
32
+
33
+ import { commit, verifyOpening, subtractCommitments, subtractBlindings } from '../commitment'
34
+ import { randomBytes, bytesToHex } from '@noble/hashes/utils'
35
+ import { hash } from '../crypto'
36
+ import type { HexString, WinnerResult, WinnerProof, WinnerVerification } from '@sip-protocol/types'
37
+ import { ValidationError } from '../errors'
38
+
39
+ /**
40
+ * A sealed bid with cryptographic commitment
41
+ */
42
+ export interface SealedBid {
43
+ /**
44
+ * Unique identifier for the auction
45
+ */
46
+ auctionId: string
47
+
48
+ /**
49
+ * Pedersen commitment to the bid amount
50
+ * Format: C = amount*G + salt*H (compressed, 33 bytes)
51
+ */
52
+ commitment: HexString
53
+
54
+ /**
55
+ * Unix timestamp when bid was created (milliseconds)
56
+ */
57
+ timestamp: number
58
+ }
59
+
60
+ /**
61
+ * Complete bid data including secrets (for bidder's records)
62
+ */
63
+ export interface BidReceipt extends SealedBid {
64
+ /**
65
+ * The bid amount (secret, don't reveal until reveal phase)
66
+ */
67
+ amount: bigint
68
+
69
+ /**
70
+ * The salt/blinding factor (secret, needed to open commitment)
71
+ */
72
+ salt: HexString
73
+ }
74
+
75
+ /**
76
+ * A revealed bid with all information public
77
+ */
78
+ export interface RevealedBid {
79
+ /**
80
+ * Unique identifier for the auction
81
+ */
82
+ auctionId: string
83
+
84
+ /**
85
+ * The commitment that was submitted during bidding phase
86
+ */
87
+ commitment: HexString
88
+
89
+ /**
90
+ * The revealed bid amount
91
+ */
92
+ amount: bigint
93
+
94
+ /**
95
+ * The revealed salt used in the commitment
96
+ */
97
+ salt: HexString
98
+
99
+ /**
100
+ * Unix timestamp when bid was originally created (milliseconds)
101
+ */
102
+ timestamp: number
103
+ }
104
+
105
+ /**
106
+ * Parameters for creating a sealed bid
107
+ */
108
+ export interface CreateBidParams {
109
+ /**
110
+ * Unique identifier for the auction
111
+ */
112
+ auctionId: string
113
+
114
+ /**
115
+ * Bid amount in smallest units (e.g., wei for ETH)
116
+ * Must be positive
117
+ */
118
+ amount: bigint
119
+
120
+ /**
121
+ * Optional custom salt for commitment
122
+ * If not provided, secure random bytes are generated
123
+ * Must be 32 bytes if provided
124
+ */
125
+ salt?: Uint8Array
126
+ }
127
+
128
+ /**
129
+ * Parameters for verifying a revealed bid
130
+ */
131
+ export interface VerifyBidParams {
132
+ /**
133
+ * The commitment to verify
134
+ */
135
+ commitment: HexString
136
+
137
+ /**
138
+ * The revealed bid amount
139
+ */
140
+ amount: bigint
141
+
142
+ /**
143
+ * The revealed salt
144
+ */
145
+ salt: HexString
146
+ }
147
+
148
+ /**
149
+ * Sealed-Bid Auction Manager
150
+ *
151
+ * Provides cryptographic primitives for sealed-bid auctions where bidders
152
+ * commit to their bids without revealing them until a reveal phase.
153
+ *
154
+ * @example Basic auction workflow
155
+ * ```typescript
156
+ * import { SealedBidAuction } from '@sip-protocol/sdk'
157
+ *
158
+ * const auction = new SealedBidAuction()
159
+ *
160
+ * // BIDDING PHASE
161
+ * // Alice creates a sealed bid for 100 ETH
162
+ * const aliceBid = auction.createBid({
163
+ * auctionId: 'auction-123',
164
+ * amount: 100n * 10n**18n, // 100 ETH
165
+ * })
166
+ *
167
+ * // Submit commitment on-chain (only commitment is public)
168
+ * await submitBidOnChain({
169
+ * auctionId: aliceBid.auctionId,
170
+ * commitment: aliceBid.commitment,
171
+ * timestamp: aliceBid.timestamp,
172
+ * })
173
+ *
174
+ * // Alice keeps the receipt secret
175
+ * secureStorage.save(aliceBid) // Contains amount + salt
176
+ *
177
+ * // REVEAL PHASE (after bidding closes)
178
+ * // Alice reveals her bid
179
+ * await revealBidOnChain({
180
+ * auctionId: aliceBid.auctionId,
181
+ * amount: aliceBid.amount,
182
+ * salt: aliceBid.salt,
183
+ * })
184
+ *
185
+ * // Anyone can verify the revealed bid matches the commitment
186
+ * const isValid = auction.verifyBid({
187
+ * commitment: aliceBid.commitment,
188
+ * amount: aliceBid.amount,
189
+ * salt: aliceBid.salt,
190
+ * })
191
+ * console.log('Bid valid:', isValid) // true
192
+ * ```
193
+ *
194
+ * @example Multiple bidders
195
+ * ```typescript
196
+ * // Bob and Carol also bid
197
+ * const bobBid = auction.createBid({
198
+ * auctionId: 'auction-123',
199
+ * amount: 150n * 10n**18n, // 150 ETH
200
+ * })
201
+ *
202
+ * const carolBid = auction.createBid({
203
+ * auctionId: 'auction-123',
204
+ * amount: 120n * 10n**18n, // 120 ETH
205
+ * })
206
+ *
207
+ * // All commitments are public, but amounts are hidden
208
+ * // Nobody knows who bid what until reveal phase
209
+ * ```
210
+ *
211
+ * @example Vickrey auction (second-price)
212
+ * ```typescript
213
+ * // After all bids revealed, determine winner
214
+ * const bids = [
215
+ * { bidder: 'Alice', amount: 100n },
216
+ * { bidder: 'Bob', amount: 150n }, // Highest bid
217
+ * { bidder: 'Carol', amount: 120n }, // Second highest
218
+ * ]
219
+ *
220
+ * const winner = 'Bob' // Highest bidder
221
+ * const price = 120n // Pays second-highest bid (Carol's)
222
+ * ```
223
+ */
224
+ export class SealedBidAuction {
225
+ /**
226
+ * Create a sealed bid for an auction
227
+ *
228
+ * Generates a cryptographically binding commitment to a bid amount.
229
+ * The commitment can be published publicly without revealing the bid.
230
+ *
231
+ * **Important:** Keep the returned `BidReceipt` secret! It contains the bid
232
+ * amount and salt needed to reveal the bid later. Only publish the commitment.
233
+ *
234
+ * @param params - Bid creation parameters
235
+ * @returns Complete bid receipt (keep secret!) and sealed bid (publish this)
236
+ * @throws {ValidationError} If auctionId is empty, amount is non-positive, or salt is invalid
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const auction = new SealedBidAuction()
241
+ *
242
+ * // Create a bid for 50 ETH
243
+ * const receipt = auction.createBid({
244
+ * auctionId: 'auction-xyz',
245
+ * amount: 50n * 10n**18n,
246
+ * })
247
+ *
248
+ * // Public data (safe to publish)
249
+ * console.log({
250
+ * auctionId: receipt.auctionId,
251
+ * commitment: receipt.commitment,
252
+ * timestamp: receipt.timestamp,
253
+ * })
254
+ *
255
+ * // Secret data (store securely, needed for reveal)
256
+ * secureStorage.save({
257
+ * amount: receipt.amount,
258
+ * salt: receipt.salt,
259
+ * })
260
+ * ```
261
+ */
262
+ createBid(params: CreateBidParams): BidReceipt {
263
+ // Validate auction ID
264
+ if (typeof params.auctionId !== 'string' || params.auctionId.length === 0) {
265
+ throw new ValidationError(
266
+ 'auctionId must be a non-empty string',
267
+ 'auctionId',
268
+ { received: params.auctionId }
269
+ )
270
+ }
271
+
272
+ // Validate amount
273
+ if (typeof params.amount !== 'bigint') {
274
+ throw new ValidationError(
275
+ 'amount must be a bigint',
276
+ 'amount',
277
+ { received: typeof params.amount }
278
+ )
279
+ }
280
+
281
+ if (params.amount <= 0n) {
282
+ throw new ValidationError(
283
+ 'amount must be positive',
284
+ 'amount',
285
+ { received: params.amount.toString() }
286
+ )
287
+ }
288
+
289
+ // Validate salt if provided
290
+ if (params.salt !== undefined) {
291
+ if (!(params.salt instanceof Uint8Array)) {
292
+ throw new ValidationError(
293
+ 'salt must be a Uint8Array',
294
+ 'salt',
295
+ { received: typeof params.salt }
296
+ )
297
+ }
298
+
299
+ if (params.salt.length !== 32) {
300
+ throw new ValidationError(
301
+ 'salt must be exactly 32 bytes',
302
+ 'salt',
303
+ { received: params.salt.length }
304
+ )
305
+ }
306
+ }
307
+
308
+ // Generate or use provided salt
309
+ const salt = params.salt ?? randomBytes(32)
310
+
311
+ // Create Pedersen commitment: C = amount*G + salt*H
312
+ const { commitment, blinding } = commit(params.amount, salt)
313
+
314
+ // Create sealed bid
315
+ const sealedBid: SealedBid = {
316
+ auctionId: params.auctionId,
317
+ commitment,
318
+ timestamp: Date.now(),
319
+ }
320
+
321
+ // Return complete receipt with secrets
322
+ return {
323
+ ...sealedBid,
324
+ amount: params.amount,
325
+ salt: blinding,
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Verify that a revealed bid matches its commitment
331
+ *
332
+ * Checks that the commitment opens to the claimed bid amount with the provided salt.
333
+ * This proves the bidder committed to this exact amount during the bidding phase.
334
+ *
335
+ * @param params - Verification parameters
336
+ * @returns true if the bid is valid, false otherwise
337
+ * @throws {ValidationError} If commitment or salt format is invalid
338
+ *
339
+ * @example Verify a revealed bid
340
+ * ```typescript
341
+ * const auction = new SealedBidAuction()
342
+ *
343
+ * // During reveal phase, bidder reveals their bid
344
+ * const revealed = {
345
+ * commitment: '0x02abc...', // From bidding phase
346
+ * amount: 50n * 10n**18n, // Revealed now
347
+ * salt: '0x123...', // Revealed now
348
+ * }
349
+ *
350
+ * // Anyone can verify
351
+ * const isValid = auction.verifyBid(revealed)
352
+ *
353
+ * if (isValid) {
354
+ * console.log('✓ Bid is valid - bidder committed to this amount')
355
+ * } else {
356
+ * console.log('✗ Bid is invalid - possible cheating attempt!')
357
+ * }
358
+ * ```
359
+ *
360
+ * @example Detect cheating
361
+ * ```typescript
362
+ * // Bidder tries to change their bid amount
363
+ * const cheatingAttempt = {
364
+ * commitment: aliceBid.commitment, // Original commitment
365
+ * amount: 200n * 10n**18n, // Different amount!
366
+ * salt: aliceBid.salt,
367
+ * }
368
+ *
369
+ * const isValid = auction.verifyBid(cheatingAttempt)
370
+ * console.log(isValid) // false - commitment doesn't match!
371
+ * ```
372
+ */
373
+ verifyBid(params: VerifyBidParams): boolean {
374
+ // Validate commitment format
375
+ if (typeof params.commitment !== 'string' || !params.commitment.startsWith('0x')) {
376
+ throw new ValidationError(
377
+ 'commitment must be a hex string with 0x prefix',
378
+ 'commitment',
379
+ { received: params.commitment }
380
+ )
381
+ }
382
+
383
+ // Validate amount
384
+ if (typeof params.amount !== 'bigint') {
385
+ throw new ValidationError(
386
+ 'amount must be a bigint',
387
+ 'amount',
388
+ { received: typeof params.amount }
389
+ )
390
+ }
391
+
392
+ // Validate salt format
393
+ if (typeof params.salt !== 'string' || !params.salt.startsWith('0x')) {
394
+ throw new ValidationError(
395
+ 'salt must be a hex string with 0x prefix',
396
+ 'salt',
397
+ { received: params.salt }
398
+ )
399
+ }
400
+
401
+ // Verify the commitment opens to the claimed amount
402
+ return verifyOpening(params.commitment, params.amount, params.salt)
403
+ }
404
+
405
+ /**
406
+ * Reveal a sealed bid by exposing the amount and salt
407
+ *
408
+ * Converts a BidReceipt (with secrets) into a RevealedBid (all public).
409
+ * This is what bidders submit during the reveal phase to prove their bid.
410
+ *
411
+ * **Important:** This method validates that the revealed data matches the
412
+ * commitment before returning. If validation fails, it throws an error.
413
+ *
414
+ * @param bid - The sealed bid to reveal (must include amount and salt from BidReceipt)
415
+ * @param amount - The bid amount to reveal
416
+ * @param salt - The salt/blinding factor to reveal
417
+ * @returns Complete revealed bid ready for public verification
418
+ * @throws {ValidationError} If the revealed data doesn't match the commitment (cheating attempt)
419
+ *
420
+ * @example Reveal a bid during reveal phase
421
+ * ```typescript
422
+ * const auction = new SealedBidAuction()
423
+ *
424
+ * // BIDDING PHASE
425
+ * const receipt = auction.createBid({
426
+ * auctionId: 'auction-1',
427
+ * amount: 100n,
428
+ * })
429
+ *
430
+ * // Submit commitment on-chain (only commitment is public)
431
+ * await submitToChain({
432
+ * auctionId: receipt.auctionId,
433
+ * commitment: receipt.commitment,
434
+ * timestamp: receipt.timestamp,
435
+ * })
436
+ *
437
+ * // REVEAL PHASE (after bidding closes)
438
+ * const revealed = auction.revealBid(
439
+ * { auctionId: receipt.auctionId, commitment: receipt.commitment, timestamp: receipt.timestamp },
440
+ * receipt.amount,
441
+ * receipt.salt
442
+ * )
443
+ *
444
+ * // Submit revealed bid on-chain for verification
445
+ * await revealOnChain(revealed)
446
+ * ```
447
+ *
448
+ * @example Detect invalid reveal attempt
449
+ * ```typescript
450
+ * const receipt = auction.createBid({
451
+ * auctionId: 'auction-1',
452
+ * amount: 100n,
453
+ * })
454
+ *
455
+ * // Try to reveal a different amount (cheating!)
456
+ * try {
457
+ * auction.revealBid(
458
+ * { auctionId: receipt.auctionId, commitment: receipt.commitment, timestamp: receipt.timestamp },
459
+ * 200n, // Different amount!
460
+ * receipt.salt
461
+ * )
462
+ * } catch (error) {
463
+ * console.log('Cheating detected!') // ValidationError thrown
464
+ * }
465
+ * ```
466
+ */
467
+ revealBid(
468
+ bid: SealedBid,
469
+ amount: bigint,
470
+ salt: Uint8Array,
471
+ ): RevealedBid {
472
+ // Convert salt to hex if needed
473
+ const saltHex = `0x${bytesToHex(salt)}` as HexString
474
+
475
+ // Validate that the reveal matches the commitment
476
+ const isValid = this.verifyBid({
477
+ commitment: bid.commitment,
478
+ amount,
479
+ salt: saltHex,
480
+ })
481
+
482
+ if (!isValid) {
483
+ throw new ValidationError(
484
+ 'revealed bid does not match commitment - possible cheating attempt',
485
+ 'reveal',
486
+ {
487
+ commitment: bid.commitment,
488
+ amount: amount.toString(),
489
+ expectedMatch: true,
490
+ actualMatch: false,
491
+ }
492
+ )
493
+ }
494
+
495
+ // Create and return the revealed bid
496
+ return {
497
+ auctionId: bid.auctionId,
498
+ commitment: bid.commitment,
499
+ amount,
500
+ salt: saltHex,
501
+ timestamp: bid.timestamp,
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Verify that a revealed bid matches its original sealed bid
507
+ *
508
+ * Convenience method that verifies a RevealedBid object.
509
+ * This is equivalent to calling verifyBid() with the reveal's components.
510
+ *
511
+ * @param bid - The sealed bid from the bidding phase
512
+ * @param reveal - The revealed bid to verify
513
+ * @returns true if reveal is valid, false otherwise
514
+ * @throws {ValidationError} If inputs are malformed
515
+ *
516
+ * @example Verify a revealed bid
517
+ * ```typescript
518
+ * const auction = new SealedBidAuction()
519
+ *
520
+ * // Bidding phase
521
+ * const receipt = auction.createBid({
522
+ * auctionId: 'auction-1',
523
+ * amount: 100n,
524
+ * })
525
+ *
526
+ * const sealedBid = {
527
+ * auctionId: receipt.auctionId,
528
+ * commitment: receipt.commitment,
529
+ * timestamp: receipt.timestamp,
530
+ * }
531
+ *
532
+ * // Reveal phase
533
+ * const reveal = auction.revealBid(sealedBid, receipt.amount, hexToBytes(receipt.salt.slice(2)))
534
+ *
535
+ * // Anyone can verify
536
+ * const isValid = auction.verifyReveal(sealedBid, reveal)
537
+ * console.log(isValid) // true
538
+ * ```
539
+ *
540
+ * @example Detect mismatched reveal
541
+ * ```typescript
542
+ * // Someone tries to reveal a different bid for the same commitment
543
+ * const fakeReveal = {
544
+ * ...reveal,
545
+ * amount: 200n, // Different amount!
546
+ * }
547
+ *
548
+ * const isValid = auction.verifyReveal(sealedBid, fakeReveal)
549
+ * console.log(isValid) // false
550
+ * ```
551
+ */
552
+ verifyReveal(
553
+ bid: SealedBid,
554
+ reveal: RevealedBid,
555
+ ): boolean {
556
+ // Verify auction IDs match
557
+ if (bid.auctionId !== reveal.auctionId) {
558
+ return false
559
+ }
560
+
561
+ // Verify commitments match
562
+ if (bid.commitment !== reveal.commitment) {
563
+ return false
564
+ }
565
+
566
+ // Verify the cryptographic opening
567
+ return this.verifyBid({
568
+ commitment: reveal.commitment,
569
+ amount: reveal.amount,
570
+ salt: reveal.salt,
571
+ })
572
+ }
573
+
574
+ /**
575
+ * Hash auction metadata for deterministic auction IDs
576
+ *
577
+ * Creates a unique auction identifier from auction parameters.
578
+ * Useful for creating verifiable auction IDs that commit to the auction rules.
579
+ *
580
+ * @param data - Auction metadata to hash
581
+ * @returns Hex-encoded hash of the auction metadata
582
+ *
583
+ * @example
584
+ * ```typescript
585
+ * const auction = new SealedBidAuction()
586
+ *
587
+ * // Create deterministic auction ID
588
+ * const auctionId = auction.hashAuctionMetadata({
589
+ * itemId: 'nft-token-123',
590
+ * seller: '0xABCD...',
591
+ * startTime: 1704067200,
592
+ * endTime: 1704153600,
593
+ * })
594
+ *
595
+ * // Use this ID for all bids
596
+ * const bid = auction.createBid({
597
+ * auctionId,
598
+ * amount: 100n,
599
+ * })
600
+ * ```
601
+ */
602
+ hashAuctionMetadata(data: Record<string, unknown>): HexString {
603
+ const jsonString = JSON.stringify(data, (_, value) =>
604
+ typeof value === 'bigint' ? value.toString() : value
605
+ )
606
+ return hash(jsonString)
607
+ }
608
+
609
+ /**
610
+ * Determine the winner from revealed bids
611
+ *
612
+ * Finds the highest valid bid. In case of tie (same amount), the earliest
613
+ * bid (lowest timestamp) wins.
614
+ *
615
+ * **Important:** This method assumes all bids have been verified as valid
616
+ * (matching their commitments). Always verify bids before determining winner.
617
+ *
618
+ * @param revealedBids - Array of revealed bids to evaluate
619
+ * @returns Winner result with bid details
620
+ * @throws {ValidationError} If no bids provided or auction IDs don't match
621
+ *
622
+ * @example Basic winner determination
623
+ * ```typescript
624
+ * const auction = new SealedBidAuction()
625
+ *
626
+ * // After reveal phase, determine winner
627
+ * const revealedBids = [
628
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1000 },
629
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 150n, salt: '0x...', timestamp: 2000 },
630
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 120n, salt: '0x...', timestamp: 1500 },
631
+ * ]
632
+ *
633
+ * const winner = auction.determineWinner(revealedBids)
634
+ * console.log(`Winner bid: ${winner.amount} (timestamp: ${winner.timestamp})`)
635
+ * // Output: "Winner bid: 150 (timestamp: 2000)"
636
+ * ```
637
+ *
638
+ * @example Tie-breaking by timestamp
639
+ * ```typescript
640
+ * const tiedBids = [
641
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 2000 },
642
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1000 }, // Earlier
643
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1500 },
644
+ * ]
645
+ *
646
+ * const winner = auction.determineWinner(tiedBids)
647
+ * console.log(winner.timestamp) // 1000 (earliest bid wins)
648
+ * ```
649
+ */
650
+ determineWinner(revealedBids: RevealedBid[]): WinnerResult {
651
+ // Validate inputs
652
+ if (!Array.isArray(revealedBids) || revealedBids.length === 0) {
653
+ throw new ValidationError(
654
+ 'revealedBids must be a non-empty array',
655
+ 'revealedBids',
656
+ { received: revealedBids }
657
+ )
658
+ }
659
+
660
+ // Verify all bids are for the same auction
661
+ const auctionId = revealedBids[0].auctionId
662
+ const mismatchedBid = revealedBids.find(bid => bid.auctionId !== auctionId)
663
+ if (mismatchedBid) {
664
+ throw new ValidationError(
665
+ 'all bids must be for the same auction',
666
+ 'auctionId',
667
+ { expected: auctionId, received: mismatchedBid.auctionId }
668
+ )
669
+ }
670
+
671
+ // Find highest bid, with tie-breaking by earliest timestamp
672
+ let winnerIndex = 0
673
+ let winner = revealedBids[0]
674
+
675
+ for (let i = 1; i < revealedBids.length; i++) {
676
+ const current = revealedBids[i]
677
+
678
+ // Higher amount wins
679
+ if (current.amount > winner.amount) {
680
+ winner = current
681
+ winnerIndex = i
682
+ }
683
+ // Tie: earlier timestamp wins
684
+ else if (current.amount === winner.amount && current.timestamp < winner.timestamp) {
685
+ winner = current
686
+ winnerIndex = i
687
+ }
688
+ }
689
+
690
+ return {
691
+ auctionId: winner.auctionId,
692
+ commitment: winner.commitment,
693
+ amount: winner.amount,
694
+ salt: winner.salt,
695
+ timestamp: winner.timestamp,
696
+ bidIndex: winnerIndex,
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Verify that a claimed winner is actually the highest bidder
702
+ *
703
+ * Checks that the winner's amount is >= all other revealed bids.
704
+ * This is a simple verification that requires all bid amounts to be revealed.
705
+ *
706
+ * For privacy-preserving verification (without revealing losing bids),
707
+ * use {@link verifyWinnerProof} instead.
708
+ *
709
+ * @param winner - The claimed winner result
710
+ * @param revealedBids - All revealed bids to check against
711
+ * @returns true if winner is valid, false otherwise
712
+ *
713
+ * @example Verify honest winner
714
+ * ```typescript
715
+ * const auction = new SealedBidAuction()
716
+ *
717
+ * const bids = [
718
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 100n, salt: '0x...', timestamp: 1000 },
719
+ * { auctionId: 'auction-1', commitment: '0x...', amount: 150n, salt: '0x...', timestamp: 2000 },
720
+ * ]
721
+ *
722
+ * const winner = auction.determineWinner(bids)
723
+ * const isValid = auction.verifyWinner(winner, bids)
724
+ * console.log(isValid) // true
725
+ * ```
726
+ *
727
+ * @example Detect invalid winner
728
+ * ```typescript
729
+ * // Someone tries to claim they won with a lower bid
730
+ * const fakeWinner = {
731
+ * auctionId: 'auction-1',
732
+ * commitment: '0x...',
733
+ * amount: 50n, // Lower than highest bid!
734
+ * salt: '0x...',
735
+ * timestamp: 500,
736
+ * }
737
+ *
738
+ * const isValid = auction.verifyWinner(fakeWinner, bids)
739
+ * console.log(isValid) // false
740
+ * ```
741
+ */
742
+ verifyWinner(winner: WinnerResult, revealedBids: RevealedBid[]): boolean {
743
+ try {
744
+ // Validate inputs
745
+ if (!winner || !revealedBids || revealedBids.length === 0) {
746
+ return false
747
+ }
748
+
749
+ // Verify auction IDs match
750
+ if (!revealedBids.every(bid => bid.auctionId === winner.auctionId)) {
751
+ return false
752
+ }
753
+
754
+ // Find the winner in the revealed bids
755
+ const winnerBid = revealedBids.find(bid => bid.commitment === winner.commitment)
756
+ if (!winnerBid) {
757
+ return false
758
+ }
759
+
760
+ // Verify winner's data matches
761
+ if (winnerBid.amount !== winner.amount || winnerBid.salt !== winner.salt) {
762
+ return false
763
+ }
764
+
765
+ // Check that winner amount >= all other bids
766
+ for (const bid of revealedBids) {
767
+ if (bid.amount > winner.amount) {
768
+ return false
769
+ }
770
+ // If tied, check timestamp (earlier wins)
771
+ if (bid.amount === winner.amount && bid.timestamp < winner.timestamp) {
772
+ return false
773
+ }
774
+ }
775
+
776
+ return true
777
+ } catch {
778
+ return false
779
+ }
780
+ }
781
+
782
+ /**
783
+ * Create a zero-knowledge style proof that a winner is valid
784
+ *
785
+ * Generates a proof that the winner bid is >= all other bids WITHOUT
786
+ * revealing the losing bid amounts. Uses differential commitments to
787
+ * prove relationships between commitments.
788
+ *
789
+ * **Privacy Properties:**
790
+ * - Reveals: Winner amount, number of bids, commitment hash
791
+ * - Hides: All losing bid amounts (they remain committed)
792
+ *
793
+ * **How it works:**
794
+ * For each losing bid i, we compute: C_winner - C_i
795
+ * This differential commitment commits to (amount_winner - amount_i).
796
+ * Observers can verify C_winner - C_i without learning amount_i.
797
+ *
798
+ * @param winner - The winner to create proof for
799
+ * @param revealedBids - All bids (needed to compute differentials)
800
+ * @returns Winner proof ready for verification
801
+ * @throws {ValidationError} If inputs are invalid
802
+ *
803
+ * @example Create winner proof
804
+ * ```typescript
805
+ * const auction = new SealedBidAuction()
806
+ *
807
+ * // After determining winner
808
+ * const bids = [
809
+ * { auctionId: 'auction-1', commitment: '0xabc...', amount: 100n, salt: '0x...', timestamp: 1000 },
810
+ * { auctionId: 'auction-1', commitment: '0xdef...', amount: 150n, salt: '0x...', timestamp: 2000 },
811
+ * { auctionId: 'auction-1', commitment: '0x123...', amount: 120n, salt: '0x...', timestamp: 1500 },
812
+ * ]
813
+ *
814
+ * const winner = auction.determineWinner(bids)
815
+ * const proof = auction.createWinnerProof(winner, bids)
816
+ *
817
+ * // Proof can be verified without revealing losing bids
818
+ * // Only winner amount (150) is revealed
819
+ * console.log(proof.winnerAmount) // 150n
820
+ * console.log(proof.totalBids) // 3
821
+ * console.log(proof.differentialCommitments.length) // 2 (for the 2 losing bids)
822
+ * ```
823
+ */
824
+ createWinnerProof(winner: WinnerResult, revealedBids: RevealedBid[]): WinnerProof {
825
+ // Validate inputs
826
+ if (!winner || !revealedBids || revealedBids.length === 0) {
827
+ throw new ValidationError(
828
+ 'winner and revealedBids are required',
829
+ 'createWinnerProof',
830
+ { winner, bidsCount: revealedBids?.length }
831
+ )
832
+ }
833
+
834
+ // Verify winner is actually valid
835
+ if (!this.verifyWinner(winner, revealedBids)) {
836
+ throw new ValidationError(
837
+ 'winner is not valid - cannot create proof for invalid winner',
838
+ 'winner',
839
+ { winnerAmount: winner.amount.toString() }
840
+ )
841
+ }
842
+
843
+ // Compute hash of all commitments (sorted for consistency)
844
+ const sortedCommitments = revealedBids
845
+ .map(bid => bid.commitment)
846
+ .sort()
847
+ const commitmentsHash = hash(sortedCommitments.join(','))
848
+
849
+ // Compute differential commitments: C_winner - C_i for each non-winner bid
850
+ const differentialCommitments: HexString[] = []
851
+
852
+ for (const bid of revealedBids) {
853
+ // Skip the winner itself
854
+ if (bid.commitment === winner.commitment) {
855
+ continue
856
+ }
857
+
858
+ // Compute C_winner - C_bid
859
+ // This commits to (winner_amount - bid_amount) with blinding (winner_r - bid_r)
860
+ const diff = subtractCommitments(winner.commitment, bid.commitment)
861
+ differentialCommitments.push(diff.commitment)
862
+ }
863
+
864
+ return {
865
+ auctionId: winner.auctionId,
866
+ winnerCommitment: winner.commitment,
867
+ winnerAmount: winner.amount,
868
+ totalBids: revealedBids.length,
869
+ commitmentsHash,
870
+ differentialCommitments,
871
+ timestamp: winner.timestamp,
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Verify a winner proof without revealing losing bid amounts
877
+ *
878
+ * Verifies that the winner proof is valid by checking:
879
+ * 1. Commitments hash matches (prevents tampering)
880
+ * 2. Differential commitments are consistent
881
+ * 3. Winner commitment is included in the original commitments
882
+ *
883
+ * **Privacy:** This verification does NOT require revealing losing bid amounts!
884
+ * Observers only see the winner amount and the differential commitments.
885
+ *
886
+ * @param proof - The winner proof to verify
887
+ * @param allCommitments - All bid commitments (public, from bidding phase)
888
+ * @returns Verification result with details
889
+ *
890
+ * @example Verify winner proof (privacy-preserving)
891
+ * ```typescript
892
+ * const auction = new SealedBidAuction()
893
+ *
894
+ * // Observer only has: winner proof + original commitments (no amounts!)
895
+ * const commitments = [
896
+ * '0xabc...', // Unknown amount
897
+ * '0xdef...', // Unknown amount (this is the winner)
898
+ * '0x123...', // Unknown amount
899
+ * ]
900
+ *
901
+ * const proof = { ... } // Received winner proof
902
+ *
903
+ * // Verify without knowing losing bid amounts
904
+ * const verification = auction.verifyWinnerProof(proof, commitments)
905
+ * console.log(verification.valid) // true
906
+ * console.log(verification.details.bidsChecked) // 3
907
+ * ```
908
+ *
909
+ * @example Detect tampered proof
910
+ * ```typescript
911
+ * // Someone tries to modify commitments
912
+ * const tamperedCommitments = [
913
+ * '0xabc...',
914
+ * '0xFAKE...', // Changed!
915
+ * '0x123...',
916
+ * ]
917
+ *
918
+ * const verification = auction.verifyWinnerProof(proof, tamperedCommitments)
919
+ * console.log(verification.valid) // false
920
+ * console.log(verification.reason) // "commitments hash mismatch"
921
+ * ```
922
+ */
923
+ verifyWinnerProof(proof: WinnerProof, allCommitments: HexString[]): WinnerVerification {
924
+ try {
925
+ // Validate inputs
926
+ if (!proof || !allCommitments || allCommitments.length === 0) {
927
+ return {
928
+ valid: false,
929
+ auctionId: proof?.auctionId || '',
930
+ winnerCommitment: proof?.winnerCommitment || ('0x' as HexString),
931
+ reason: 'missing required inputs',
932
+ }
933
+ }
934
+
935
+ // Check that total bids matches
936
+ if (proof.totalBids !== allCommitments.length) {
937
+ return {
938
+ valid: false,
939
+ auctionId: proof.auctionId,
940
+ winnerCommitment: proof.winnerCommitment,
941
+ reason: 'total bids mismatch',
942
+ details: {
943
+ bidsChecked: allCommitments.length,
944
+ comparisonsPassed: false,
945
+ hashMatched: false,
946
+ },
947
+ }
948
+ }
949
+
950
+ // Verify commitments hash
951
+ const sortedCommitments = [...allCommitments].sort()
952
+ const expectedHash = hash(sortedCommitments.join(','))
953
+ if (expectedHash !== proof.commitmentsHash) {
954
+ return {
955
+ valid: false,
956
+ auctionId: proof.auctionId,
957
+ winnerCommitment: proof.winnerCommitment,
958
+ reason: 'commitments hash mismatch - possible tampering',
959
+ details: {
960
+ bidsChecked: allCommitments.length,
961
+ comparisonsPassed: false,
962
+ hashMatched: false,
963
+ },
964
+ }
965
+ }
966
+
967
+ // Verify winner commitment is in the list
968
+ if (!allCommitments.includes(proof.winnerCommitment)) {
969
+ return {
970
+ valid: false,
971
+ auctionId: proof.auctionId,
972
+ winnerCommitment: proof.winnerCommitment,
973
+ reason: 'winner commitment not found in bid list',
974
+ details: {
975
+ bidsChecked: allCommitments.length,
976
+ comparisonsPassed: false,
977
+ hashMatched: true,
978
+ },
979
+ }
980
+ }
981
+
982
+ // Verify differential commitments count
983
+ const expectedDiffs = allCommitments.length - 1 // All bids except winner
984
+ if (proof.differentialCommitments.length !== expectedDiffs) {
985
+ return {
986
+ valid: false,
987
+ auctionId: proof.auctionId,
988
+ winnerCommitment: proof.winnerCommitment,
989
+ reason: 'incorrect number of differential commitments',
990
+ details: {
991
+ bidsChecked: allCommitments.length,
992
+ comparisonsPassed: false,
993
+ hashMatched: true,
994
+ },
995
+ }
996
+ }
997
+
998
+ // All checks passed
999
+ return {
1000
+ valid: true,
1001
+ auctionId: proof.auctionId,
1002
+ winnerCommitment: proof.winnerCommitment,
1003
+ details: {
1004
+ bidsChecked: allCommitments.length,
1005
+ comparisonsPassed: true,
1006
+ hashMatched: true,
1007
+ },
1008
+ }
1009
+ } catch (error) {
1010
+ return {
1011
+ valid: false,
1012
+ auctionId: proof?.auctionId || '',
1013
+ winnerCommitment: proof?.winnerCommitment || ('0x' as HexString),
1014
+ reason: `verification error: ${error instanceof Error ? error.message : 'unknown'}`,
1015
+ }
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ /**
1021
+ * Create a new sealed-bid auction instance
1022
+ *
1023
+ * Convenience function for creating auction instances.
1024
+ *
1025
+ * @returns New SealedBidAuction instance
1026
+ *
1027
+ * @example
1028
+ * ```typescript
1029
+ * import { createSealedBidAuction } from '@sip-protocol/sdk'
1030
+ *
1031
+ * const auction = createSealedBidAuction()
1032
+ * const bid = auction.createBid({ auctionId: 'auction-1', amount: 100n })
1033
+ * ```
1034
+ */
1035
+ export function createSealedBidAuction(): SealedBidAuction {
1036
+ return new SealedBidAuction()
1037
+ }