@utxopia/sdk 0.1.0-alpha.1

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/package.json +108 -0
  4. package/packages/btc-client/src/esplora-client.ts +153 -0
  5. package/packages/btc-client/src/index.ts +3 -0
  6. package/packages/btc-client/src/op-return.ts +93 -0
  7. package/packages/btc-client/src/types.ts +112 -0
  8. package/packages/sdk/README.md +277 -0
  9. package/packages/sdk/dist/announcement-client.d.ts +64 -0
  10. package/packages/sdk/dist/announcement-client.js +337 -0
  11. package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
  12. package/packages/sdk/dist/auditor-ciphertext.js +135 -0
  13. package/packages/sdk/dist/auditor.d.ts +115 -0
  14. package/packages/sdk/dist/auditor.js +292 -0
  15. package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
  16. package/packages/sdk/dist/bitcoin/ika.js +74 -0
  17. package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
  18. package/packages/sdk/dist/bitcoin/index.js +1 -0
  19. package/packages/sdk/dist/bound-params.d.ts +96 -0
  20. package/packages/sdk/dist/bound-params.js +249 -0
  21. package/packages/sdk/dist/chadbuffer.d.ts +124 -0
  22. package/packages/sdk/dist/chadbuffer.js +405 -0
  23. package/packages/sdk/dist/claim-link.d.ts +30 -0
  24. package/packages/sdk/dist/claim-link.js +50 -0
  25. package/packages/sdk/dist/client.d.ts +233 -0
  26. package/packages/sdk/dist/client.js +416 -0
  27. package/packages/sdk/dist/commitment-tree.d.ts +268 -0
  28. package/packages/sdk/dist/commitment-tree.js +564 -0
  29. package/packages/sdk/dist/config.d.ts +177 -0
  30. package/packages/sdk/dist/config.js +426 -0
  31. package/packages/sdk/dist/core/esplora.d.ts +110 -0
  32. package/packages/sdk/dist/core/esplora.js +187 -0
  33. package/packages/sdk/dist/core/mempool.d.ts +60 -0
  34. package/packages/sdk/dist/core/mempool.js +107 -0
  35. package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
  36. package/packages/sdk/dist/crypto-babyjub.js +312 -0
  37. package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
  38. package/packages/sdk/dist/crypto-ed25519.js +235 -0
  39. package/packages/sdk/dist/crypto.d.ts +59 -0
  40. package/packages/sdk/dist/crypto.js +140 -0
  41. package/packages/sdk/dist/event-client.d.ts +64 -0
  42. package/packages/sdk/dist/event-client.js +165 -0
  43. package/packages/sdk/dist/events.d.ts +127 -0
  44. package/packages/sdk/dist/events.js +305 -0
  45. package/packages/sdk/dist/explorer.d.ts +86 -0
  46. package/packages/sdk/dist/explorer.js +197 -0
  47. package/packages/sdk/dist/index.d.ts +61 -0
  48. package/packages/sdk/dist/index.js +216 -0
  49. package/packages/sdk/dist/instructions.d.ts +1125 -0
  50. package/packages/sdk/dist/instructions.js +1760 -0
  51. package/packages/sdk/dist/keys.d.ts +467 -0
  52. package/packages/sdk/dist/keys.js +799 -0
  53. package/packages/sdk/dist/logger.d.ts +10 -0
  54. package/packages/sdk/dist/logger.js +40 -0
  55. package/packages/sdk/dist/magicblock.d.ts +90 -0
  56. package/packages/sdk/dist/magicblock.js +164 -0
  57. package/packages/sdk/dist/merkle.d.ts +82 -0
  58. package/packages/sdk/dist/merkle.js +141 -0
  59. package/packages/sdk/dist/note.d.ts +348 -0
  60. package/packages/sdk/dist/note.js +483 -0
  61. package/packages/sdk/dist/pda.d.ts +160 -0
  62. package/packages/sdk/dist/pda.js +384 -0
  63. package/packages/sdk/dist/pool-state.d.ts +100 -0
  64. package/packages/sdk/dist/pool-state.js +126 -0
  65. package/packages/sdk/dist/poseidon.d.ts +53 -0
  66. package/packages/sdk/dist/poseidon.js +136 -0
  67. package/packages/sdk/dist/prover/index.d.ts +10 -0
  68. package/packages/sdk/dist/prover/index.js +10 -0
  69. package/packages/sdk/dist/prover/mobile.d.ts +45 -0
  70. package/packages/sdk/dist/prover/mobile.js +227 -0
  71. package/packages/sdk/dist/prover/web.d.ts +136 -0
  72. package/packages/sdk/dist/prover/web.js +597 -0
  73. package/packages/sdk/dist/psbt.d.ts +73 -0
  74. package/packages/sdk/dist/psbt.js +202 -0
  75. package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
  76. package/packages/sdk/dist/selective-disclosure.js +172 -0
  77. package/packages/sdk/dist/sender-memo.d.ts +149 -0
  78. package/packages/sdk/dist/sender-memo.js +250 -0
  79. package/packages/sdk/dist/sns-resolver.d.ts +121 -0
  80. package/packages/sdk/dist/sns-resolver.js +229 -0
  81. package/packages/sdk/dist/solana/connection.d.ts +77 -0
  82. package/packages/sdk/dist/solana/connection.js +133 -0
  83. package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
  84. package/packages/sdk/dist/solana/priority-fee.js +137 -0
  85. package/packages/sdk/dist/stealth.d.ts +429 -0
  86. package/packages/sdk/dist/stealth.js +703 -0
  87. package/packages/sdk/dist/taproot.d.ts +172 -0
  88. package/packages/sdk/dist/taproot.js +494 -0
  89. package/packages/sdk/dist/token-registry.d.ts +96 -0
  90. package/packages/sdk/dist/token-registry.js +122 -0
  91. package/packages/sdk/dist/utils/encoding.d.ts +19 -0
  92. package/packages/sdk/dist/utils/encoding.js +29 -0
  93. package/packages/sdk/dist/vk-registry.d.ts +100 -0
  94. package/packages/sdk/dist/vk-registry.js +212 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Note (shielded commitment) utilities for UTXOpia
3
+ *
4
+ * A Note represents a shielded deposit with:
5
+ * - nullifier: Random secret for spending
6
+ * - secret: Additional entropy
7
+ * - amount: Value in satoshis
8
+ *
9
+ * Hash values (commitment, nullifierHash) are computed by circom circuits
10
+ * using Poseidon. This SDK stores the raw secrets and optionally
11
+ * accepts pre-computed hash values.
12
+ */
13
+ import { randomFieldElement, bigintToBytes, sha256Hash, bytesToBigint, BN254_FIELD_PRIME, babyJubMul, BABYJUB_BASE8 } from "./crypto";
14
+ import { computeUnifiedCommitmentSync, computeNullifierSync, hashNullifierSync, computeJoinSplitCommitmentSync, computeNPKSync, computeJoinSplitNullifierSync } from "./poseidon";
15
+ /**
16
+ * Generate a new note with random nullifier and secret
17
+ *
18
+ * Note: Hash values are NOT computed here. They will be computed
19
+ * by the circom circuits during proof generation.
20
+ *
21
+ * @param amountSats - Amount in satoshis
22
+ * @returns Note with secrets (hashes are 0n until circuit execution)
23
+ */
24
+ export function generateNote(amountSats) {
25
+ const nullifier = randomFieldElement();
26
+ const secret = randomFieldElement();
27
+ return createNoteFromSecrets(nullifier, secret, amountSats);
28
+ }
29
+ /**
30
+ * Create a note from known secrets
31
+ *
32
+ * @param nullifier - Nullifier field element
33
+ * @param secret - Secret field element
34
+ * @param amountSats - Amount in satoshis
35
+ * @param commitment - Optional pre-computed commitment (from circuit)
36
+ * @param nullifierHash - Optional pre-computed nullifier hash (from circuit)
37
+ * @returns Note structure
38
+ */
39
+ export function createNoteFromSecrets(nullifier, secret, amountSats, commitment, nullifierHash) {
40
+ // Computed values are 0n until provided from circuit outputs
41
+ const comm = commitment ?? 0n;
42
+ const nullHash = nullifierHash ?? 0n;
43
+ const noteHash = 0n; // Intermediate hash not typically exposed
44
+ return {
45
+ amount: amountSats,
46
+ nullifier,
47
+ secret,
48
+ note: noteHash,
49
+ commitment: comm,
50
+ nullifierHash: nullHash,
51
+ nullifierBytes: bigintToBytes(nullifier),
52
+ secretBytes: bigintToBytes(secret),
53
+ commitmentBytes: bigintToBytes(comm),
54
+ nullifierHashBytes: bigintToBytes(nullHash),
55
+ };
56
+ }
57
+ /**
58
+ * Update note with computed hash values from circom circuit execution
59
+ *
60
+ * @param note - Note to update
61
+ * @param commitment - Commitment from circuit output
62
+ * @param nullifierHash - Nullifier hash from circuit output
63
+ * @returns Updated note
64
+ */
65
+ export function updateNoteWithHashes(note, commitment, nullifierHash) {
66
+ return {
67
+ ...note,
68
+ commitment,
69
+ nullifierHash,
70
+ commitmentBytes: bigintToBytes(commitment),
71
+ nullifierHashBytes: bigintToBytes(nullifierHash),
72
+ };
73
+ }
74
+ /**
75
+ * Serialize a note for storage or transmission
76
+ *
77
+ * Only stores the essential data (amount, nullifier, secret).
78
+ * Optionally includes pre-computed hash values.
79
+ */
80
+ export function serializeNote(note) {
81
+ const serialized = {
82
+ amount: note.amount.toString(),
83
+ nullifier: note.nullifier.toString(),
84
+ secret: note.secret.toString(),
85
+ };
86
+ // Include computed values if available
87
+ if (note.commitment !== 0n) {
88
+ serialized.commitment = note.commitment.toString();
89
+ }
90
+ if (note.nullifierHash !== 0n) {
91
+ serialized.nullifierHash = note.nullifierHash.toString();
92
+ }
93
+ return serialized;
94
+ }
95
+ /**
96
+ * Deserialize and restore a note from stored data
97
+ */
98
+ export function deserializeNote(data) {
99
+ const amount = BigInt(data.amount);
100
+ const nullifier = BigInt(data.nullifier);
101
+ const secret = BigInt(data.secret);
102
+ const commitment = data.commitment ? BigInt(data.commitment) : undefined;
103
+ const nullifierHash = data.nullifierHash
104
+ ? BigInt(data.nullifierHash)
105
+ : undefined;
106
+ return createNoteFromSecrets(nullifier, secret, amount, commitment, nullifierHash);
107
+ }
108
+ /**
109
+ * Format satoshis as BTC string
110
+ */
111
+ export function formatBtc(sats) {
112
+ const btc = Number(sats) / 100000000;
113
+ return btc.toFixed(8) + " BTC";
114
+ }
115
+ /**
116
+ * Parse BTC string to satoshis
117
+ */
118
+ export function parseBtc(btcString) {
119
+ const btc = parseFloat(btcString.replace(" BTC", ""));
120
+ return BigInt(Math.round(btc * 100000000));
121
+ }
122
+ /**
123
+ * Check if a note has computed hash values
124
+ */
125
+ export function noteHasComputedHashes(note) {
126
+ return note.commitment !== 0n && note.nullifierHash !== 0n;
127
+ }
128
+ // ============================================================================
129
+ // Note Commitment/Nullifier Helpers
130
+ // ============================================================================
131
+ /**
132
+ * Get the public key X coordinate from a note's nullifier (used as private key)
133
+ *
134
+ * In the unified model: pubKey = nullifier * BASE8 (on Baby Jubjub curve)
135
+ * Returns pubKey.x for commitment computation
136
+ */
137
+ export function getNotePublicKeyX(note) {
138
+ const pubKey = babyJubMul(note.nullifier, BABYJUB_BASE8);
139
+ return pubKey.x;
140
+ }
141
+ /**
142
+ * Compute the commitment for a note
143
+ *
144
+ * commitment = Poseidon(pubKeyX, amount)
145
+ * where pubKeyX = (nullifier * G).x
146
+ *
147
+ * Returns the note with commitment fields populated
148
+ */
149
+ export function computeNoteCommitment(note) {
150
+ const pubKeyX = getNotePublicKeyX(note);
151
+ const commitment = computeUnifiedCommitmentSync(pubKeyX, note.amount);
152
+ const commitmentBytes = bigintToBytes(commitment);
153
+ return {
154
+ ...note,
155
+ commitment,
156
+ commitmentBytes,
157
+ };
158
+ }
159
+ /**
160
+ * Compute the nullifier and nullifier hash for a note at a given leaf index
161
+ *
162
+ * nullifier = Poseidon(privKey, leafIndex)
163
+ * nullifierHash = Poseidon(nullifier)
164
+ *
165
+ * @returns Object with computed nullifier values
166
+ */
167
+ export function computeNoteNullifier(note, leafIndex) {
168
+ const nullifier = computeNullifierSync(note.nullifier, leafIndex);
169
+ const nullifierHash = hashNullifierSync(nullifier);
170
+ const nullifierHashBytes = bigintToBytes(nullifierHash);
171
+ return {
172
+ nullifier,
173
+ nullifierHash,
174
+ nullifierHashBytes,
175
+ };
176
+ }
177
+ // ============================================================================
178
+ // Deterministic Note Derivation (HD-style)
179
+ // ============================================================================
180
+ /**
181
+ * Derive a note deterministically from a seed phrase/name and index.
182
+ *
183
+ * This allows recovery of all notes from just the seed + index.
184
+ * Similar to HD wallets in Bitcoin.
185
+ *
186
+ * The seed is first hashed to create a 32-byte master key, then
187
+ * nullifier and secret are derived from that master key.
188
+ *
189
+ * @param seed - Seed phrase, name, or password (any string)
190
+ * @param index - Note index (0, 1, 2, ...)
191
+ * @param amountSats - Amount in satoshis
192
+ * @returns Note with deterministically derived secrets
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * // Derive notes from a name
197
+ * const note0 = deriveNote("albertgogogo", 0, 100_000n);
198
+ * const note1 = deriveNote("albertgogogo", 1, 50_000n);
199
+ *
200
+ * // Later: recover the same notes
201
+ * const recovered = deriveNote("albertgogogo", 0, 100_000n);
202
+ * // recovered.nullifier === note0.nullifier ✓
203
+ * ```
204
+ */
205
+ export function deriveNote(seed, index, amountSats) {
206
+ // Step 1: Hash seed to get 32-byte master key (normalizes any input)
207
+ const master = deriveMasterKey(seed);
208
+ // Step 2: Derive nullifier and secret from master + index
209
+ const nullifier = deriveFromMaster(master, index, 0); // 0 = nullifier domain
210
+ const secret = deriveFromMaster(master, index, 1); // 1 = secret domain
211
+ return createNoteFromSecrets(nullifier, secret, amountSats);
212
+ }
213
+ /**
214
+ * Derive master key from seed (32 bytes)
215
+ *
216
+ * master = SHA256(seed)
217
+ *
218
+ * You can cache this and use deriveNoteFromMaster() for efficiency.
219
+ */
220
+ export function deriveMasterKey(seed) {
221
+ const encoder = new TextEncoder();
222
+ const seedBytes = encoder.encode(seed);
223
+ return sha256Hash(seedBytes);
224
+ }
225
+ /**
226
+ * Derive a note from a pre-computed master key
227
+ *
228
+ * More efficient if deriving many notes from the same seed.
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * const master = deriveMasterKey("albertgogogo");
233
+ * const note0 = deriveNoteFromMaster(master, 0, 100_000n);
234
+ * const note1 = deriveNoteFromMaster(master, 1, 50_000n);
235
+ * ```
236
+ */
237
+ export function deriveNoteFromMaster(master, index, amountSats) {
238
+ const nullifier = deriveFromMaster(master, index, 0);
239
+ const secret = deriveFromMaster(master, index, 1);
240
+ return createNoteFromSecrets(nullifier, secret, amountSats);
241
+ }
242
+ /**
243
+ * Derive field element from master key + index + domain
244
+ *
245
+ * result = SHA256(master || index || domain) mod BN254_PRIME
246
+ *
247
+ * @param master - 32-byte master key
248
+ * @param index - Note index
249
+ * @param domain - 0 for nullifier, 1 for secret
250
+ */
251
+ function deriveFromMaster(master, index, domain) {
252
+ // Build input: master (32) || index (4) || domain (1) = 37 bytes
253
+ const input = new Uint8Array(37);
254
+ input.set(master, 0);
255
+ // Index as 4 bytes little-endian
256
+ const view = new DataView(input.buffer);
257
+ view.setUint32(32, index, true);
258
+ // Domain as 1 byte
259
+ input[36] = domain;
260
+ // Hash and reduce to field
261
+ const hash = sha256Hash(input);
262
+ return bytesToBigint(hash) % BN254_FIELD_PRIME;
263
+ }
264
+ /**
265
+ * Derive multiple notes at once from a seed
266
+ *
267
+ * More efficient than calling deriveNote() multiple times
268
+ * because master key is computed only once.
269
+ *
270
+ * @param seed - Seed phrase
271
+ * @param amounts - Array of amounts for each note
272
+ * @param startIndex - Starting index (default 0)
273
+ * @returns Array of derived notes
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * // Create a wallet with 3 notes
278
+ * const notes = deriveNotes("albertgogogo", [100_000n, 50_000n, 25_000n]);
279
+ * ```
280
+ */
281
+ export function deriveNotes(seed, amounts, startIndex = 0) {
282
+ // Compute master once, derive all notes from it
283
+ const master = deriveMasterKey(seed);
284
+ return amounts.map((amount, i) => deriveNoteFromMaster(master, startIndex + i, amount));
285
+ }
286
+ /**
287
+ * Check the strength of a seed phrase
288
+ *
289
+ * Returns estimated bits of entropy.
290
+ * Recommended: >= 80 bits for moderate security, >= 128 bits for high security
291
+ */
292
+ export function estimateSeedStrength(seed) {
293
+ // Very rough entropy estimation
294
+ const length = seed.length;
295
+ const hasLower = /[a-z]/.test(seed);
296
+ const hasUpper = /[A-Z]/.test(seed);
297
+ const hasDigit = /[0-9]/.test(seed);
298
+ const hasSpecial = /[^a-zA-Z0-9]/.test(seed);
299
+ let charsetSize = 0;
300
+ if (hasLower)
301
+ charsetSize += 26;
302
+ if (hasUpper)
303
+ charsetSize += 26;
304
+ if (hasDigit)
305
+ charsetSize += 10;
306
+ if (hasSpecial)
307
+ charsetSize += 32;
308
+ // Entropy = length * log2(charsetSize)
309
+ const bits = Math.floor(length * Math.log2(charsetSize || 1));
310
+ let strength;
311
+ let warning;
312
+ if (bits < 40) {
313
+ strength = "weak";
314
+ warning = "DANGER: This seed can be easily brute-forced. Use a longer passphrase!";
315
+ }
316
+ else if (bits < 80) {
317
+ strength = "moderate";
318
+ warning = "Consider using a longer passphrase for better security.";
319
+ }
320
+ else if (bits < 128) {
321
+ strength = "strong";
322
+ }
323
+ else {
324
+ strength = "very_strong";
325
+ }
326
+ return { bits, strength, warning };
327
+ }
328
+ // Poseidon initialization state (for API compatibility)
329
+ /**
330
+ * Create a simple note (alias for generateNote but returns NoteData)
331
+ *
332
+ * Use this to generate note data for circom circuit inputs.
333
+ *
334
+ * @param amount - Amount in satoshis
335
+ * @returns NoteData with nullifier, secret, and amount
336
+ */
337
+ export function createNote(amount) {
338
+ const note = generateNote(amount);
339
+ return {
340
+ nullifier: note.nullifier,
341
+ secret: note.secret,
342
+ amount: note.amount,
343
+ };
344
+ }
345
+ // initPoseidon/isPoseidonReady used to live here as well, over a flag local to this module. The
346
+ // barrel exported initPoseidon from poseidon.ts and isPoseidonReady from here, so they read
347
+ // different state: `await initPoseidon(); isPoseidonReady()` returned false forever. Both now
348
+ // belong to poseidon.ts, which owns the actual hasher.
349
+ /**
350
+ * Prepare withdrawal - creates change note for remaining balance
351
+ *
352
+ * PRIVACY: All withdrawals are indistinguishable on-chain.
353
+ * Change amount can be >= 0, and the commitment always looks random.
354
+ *
355
+ * @param inputNote - Note being spent
356
+ * @param withdrawAmount - Amount to withdraw
357
+ * @returns Change note with remaining balance (can be 0)
358
+ */
359
+ export function prepareWithdrawal(inputNote, withdrawAmount) {
360
+ if (withdrawAmount <= 0n) {
361
+ throw new Error("Withdraw amount must be positive");
362
+ }
363
+ if (withdrawAmount > inputNote.amount) {
364
+ throw new Error("Withdraw amount exceeds note balance");
365
+ }
366
+ const changeAmount = inputNote.amount - withdrawAmount;
367
+ const changeNote = createNote(changeAmount);
368
+ return { changeNote, changeAmount };
369
+ }
370
+ /**
371
+ * Create a stealth note from scanned announcement data
372
+ *
373
+ * @param amount - Decrypted amount
374
+ * @param random - Decrypted random value
375
+ * @param ephemeralSpendPub - Sender's ephemeral pubkey
376
+ * @param leafIndex - Merkle tree leaf index
377
+ * @returns StealthNote structure
378
+ */
379
+ export function createStealthNote(amount, random, ephemeralSpendPub, leafIndex) {
380
+ return {
381
+ amount,
382
+ random,
383
+ ephemeralSpendPubX: ephemeralSpendPub.x,
384
+ ephemeralSpendPubY: ephemeralSpendPub.y,
385
+ leafIndex,
386
+ notePubKey: 0n, // Computed in circuit
387
+ commitment: 0n, // Computed in circuit
388
+ randomBytes: bigintToBytes(random),
389
+ commitmentBytes: new Uint8Array(32),
390
+ };
391
+ }
392
+ /**
393
+ * Update stealth note with computed values from circuit
394
+ */
395
+ export function updateStealthNoteWithHashes(note, notePubKey, commitment) {
396
+ return {
397
+ ...note,
398
+ notePubKey,
399
+ commitment,
400
+ commitmentBytes: bigintToBytes(commitment),
401
+ };
402
+ }
403
+ /**
404
+ * Serialize stealth note for storage
405
+ */
406
+ export function serializeStealthNote(note) {
407
+ const serialized = {
408
+ amount: note.amount.toString(),
409
+ random: note.random.toString(),
410
+ ephemeralSpendPubX: note.ephemeralSpendPubX.toString(),
411
+ ephemeralSpendPubY: note.ephemeralSpendPubY.toString(),
412
+ leafIndex: note.leafIndex,
413
+ };
414
+ if (note.notePubKey !== 0n) {
415
+ serialized.notePubKey = note.notePubKey.toString();
416
+ }
417
+ if (note.commitment !== 0n) {
418
+ serialized.commitment = note.commitment.toString();
419
+ }
420
+ return serialized;
421
+ }
422
+ /**
423
+ * Deserialize stealth note from storage
424
+ */
425
+ export function deserializeStealthNote(data) {
426
+ const note = createStealthNote(BigInt(data.amount), BigInt(data.random), {
427
+ x: BigInt(data.ephemeralSpendPubX),
428
+ y: BigInt(data.ephemeralSpendPubY),
429
+ }, data.leafIndex);
430
+ if (data.notePubKey && data.commitment) {
431
+ return updateStealthNoteWithHashes(note, BigInt(data.notePubKey), BigInt(data.commitment));
432
+ }
433
+ return note;
434
+ }
435
+ /**
436
+ * Check if stealth note has computed hashes
437
+ */
438
+ export function stealthNoteHasComputedHashes(note) {
439
+ return note.notePubKey !== 0n && note.commitment !== 0n;
440
+ }
441
+ /**
442
+ * Create a JoinSplit note from key components
443
+ *
444
+ * @param tokenId - Token identifier from computeTokenId(mintBytes)
445
+ */
446
+ export function createJoinSplitNote(mpk, random, amount, tokenId, leafIndex = -1) {
447
+ const npk = computeNPKSync(mpk, random);
448
+ const commitment = computeJoinSplitCommitmentSync(npk, tokenId, amount);
449
+ return {
450
+ npk,
451
+ token: tokenId,
452
+ amount,
453
+ random,
454
+ leafIndex,
455
+ commitment,
456
+ };
457
+ }
458
+ /**
459
+ * Compute nullifier for a JoinSplit note
460
+ */
461
+ export function computeJoinSplitNoteNullifier(nullifyingKey, leafIndex) {
462
+ return computeJoinSplitNullifierSync(nullifyingKey, BigInt(leafIndex));
463
+ }
464
+ export function serializeJoinSplitNote(note) {
465
+ return {
466
+ npk: note.npk.toString(),
467
+ token: note.token.toString(),
468
+ amount: note.amount.toString(),
469
+ random: note.random.toString(),
470
+ leafIndex: note.leafIndex,
471
+ commitment: note.commitment.toString(),
472
+ };
473
+ }
474
+ export function deserializeJoinSplitNote(data) {
475
+ return {
476
+ npk: BigInt(data.npk),
477
+ token: BigInt(data.token),
478
+ amount: BigInt(data.amount),
479
+ random: BigInt(data.random),
480
+ leafIndex: data.leafIndex,
481
+ commitment: BigInt(data.commitment),
482
+ };
483
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * PDA (Program Derived Address) Derivation Utilities
3
+ *
4
+ * Centralized module for all UTXOpia PDA derivations.
5
+ * Prevents code duplication across api.ts, pda.ts, etc.
6
+ *
7
+ * @module pda
8
+ */
9
+ import { type Address } from "@solana/kit";
10
+ export declare const PDA_SEEDS: {
11
+ readonly POOL_STATE: "pool_state";
12
+ readonly COMMITMENT_TREE: "commitment_tree";
13
+ readonly LIGHT_CLIENT: "btc_light_client";
14
+ readonly BLOCK_HEADER: "block";
15
+ readonly HEIGHT_INDEX: "height_index";
16
+ readonly VERIFIED_TX: "verified_tx";
17
+ readonly DEPOSIT: "deposit";
18
+ readonly NULLIFIER: "nullifier";
19
+ readonly POLICY_APPROVAL: "policy_approval";
20
+ readonly EXIT_DESTINATION: "exit_destination";
21
+ readonly VK_REGISTRY: "vk_registry";
22
+ readonly TOKEN_CONFIG: "token_config";
23
+ readonly POOL_CONFIG: "pool_config";
24
+ };
25
+ /** `["pool_state", pool_id]` — pool_id is the pool's zkBTC mint. */
26
+ export declare function poolStateSeeds(poolId: Address | Uint8Array): Uint8Array[];
27
+ /** `["commitment_tree", pool_state, tree_index_le]`. */
28
+ export declare function commitmentTreeSeeds(poolState: Address | Uint8Array, treeIndex?: number): Uint8Array[];
29
+ /** `["token_config", pool_state, mint]`. */
30
+ export declare function tokenConfigSeeds(poolState: Address | Uint8Array, mint: Address | Uint8Array): Uint8Array[];
31
+ /** `["pool_config", pool_state]`. */
32
+ export declare function poolConfigSeeds(poolState: Address | Uint8Array): Uint8Array[];
33
+ /** `["nullifier", pool_state, nullifier]` on tree 0, and
34
+ * `["nullifier", pool_state, tree_index_le, nullifier]` after a rotation.
35
+ *
36
+ * A nullifier is Poseidon(nullifyingKey, leafIndex), so it names a note only
37
+ * within one pool and one tree — leaf indices restart at 0 in each new tree.
38
+ * Drop either scope and two distinct notes collapse onto one PDA, where
39
+ * spending either strands the other. Tree 0 keeps the shorter seeds so records
40
+ * already on chain stay reachable (`joinsplit_common.rs`). */
41
+ export declare function nullifierRecordSeeds(nullifierHash: Uint8Array, poolState: Address | Uint8Array, treeIndex?: number): Uint8Array[];
42
+ /** `["redemption", pool_state, user, nonce_le]` (`redeem.rs`). */
43
+ export declare function redemptionRequestSeeds(poolState: Address | Uint8Array, userPubkey: Address | Uint8Array, nonce: bigint): Uint8Array[];
44
+ /** `["vk_registry", [n_inputs], [n_outputs]]`. */
45
+ export declare function vkRegistrySeeds(nInputs: number, nOutputs: number): Uint8Array[];
46
+ /** `["deposit_receipt", txid]`, or `[..., vout_le]` for the OP_RETURN-free
47
+ * `verify_deposit` flow. See deriveDepositReceiptPDA for which is which. */
48
+ export declare function depositReceiptSeeds(depositTxid: Uint8Array, depositVout?: number): Uint8Array[];
49
+ /** `["policy_approval", pool_state, request_hash, nonce]`. */
50
+ export declare function policyApprovalSeeds(poolState: Address | Uint8Array, requestHash: Uint8Array, nonce: Uint8Array): Uint8Array[];
51
+ /** `["exit_destination", pool_state, [kind], key]`. */
52
+ export declare function exitDestinationSeeds(poolState: Address | Uint8Array, kind: number, key: Uint8Array): Uint8Array[];
53
+ /** `["btc_light_client"]`. */
54
+ export declare function lightClientSeeds(): Uint8Array[];
55
+ /** `["block", block_hash]`. */
56
+ export declare function blockHeaderSeeds(blockHash: Uint8Array): Uint8Array[];
57
+ /** `["height_index", height_le(8)]`. */
58
+ export declare function heightIndexSeeds(height: number | bigint): Uint8Array[];
59
+ /** `["verified_tx", block_hash, txid]`. */
60
+ export declare function verifiedTransactionSeeds(blockHash: Uint8Array, txid: Uint8Array): Uint8Array[];
61
+ /**
62
+ * Derive Pool State PDA
63
+ */
64
+ export declare function derivePoolStatePDA(poolId: Address | Uint8Array, programId?: Address): Promise<[Address, number]>;
65
+ /**
66
+ * Derive Commitment Tree PDA
67
+ *
68
+ * @param treeIndex - Tree rotation index (default 0).
69
+ */
70
+ export declare function deriveCommitmentTreePDA(poolState: Address | Uint8Array, programId?: Address, treeIndex?: number): Promise<[Address, number]>;
71
+ /**
72
+ * Derive TokenConfig PDA for a specific mint
73
+ * Seeds: ["token_config", mint_pubkey_bytes]
74
+ */
75
+ export declare function deriveTokenConfigPDA(poolState: Address | Uint8Array, mintPubkey: Uint8Array, programId?: Address): Promise<[Address, number]>;
76
+ /** Derive ["pool_config", pool_state]. */
77
+ export declare function derivePoolConfigPDA(poolState: Address | Uint8Array, programId?: Address): Promise<[Address, number]>;
78
+ /**
79
+ * Derive Nullifier Record PDA.
80
+ *
81
+ * Seeds are `["nullifier", pool_state, nullifier]` on tree 0, and
82
+ * `["nullifier", pool_state, tree_index_le, nullifier]` on any tree a rotation
83
+ * created. Both scopes exist because a nullifier is Poseidon(nullifyingKey,
84
+ * leafIndex) and so identifies a note only within one pool and one tree:
85
+ *
86
+ * - Without the pool, the same seed spending into two vaults derives one PDA,
87
+ * and spending in either strands the twin note in the other.
88
+ * - Without the tree, the same happens across a rotation, because leaf indices
89
+ * restart at 0 in every new tree.
90
+ *
91
+ * Tree 0 keeps the shorter seeds so the records already on chain stay reachable
92
+ * — re-deriving them would make every already-spent note spendable again.
93
+ */
94
+ export declare function deriveNullifierRecordPDA(nullifierHash: Uint8Array, poolState: Address | Uint8Array, treeIndex?: number, programId?: Address): Promise<[Address, number]>;
95
+ /**
96
+ * Derive one-time PolicyApproval PDA.
97
+ * Seeds: ["policy_approval", pool_state, request_hash, nonce]
98
+ */
99
+ export declare function derivePolicyApprovalPDA(poolState: Address | Uint8Array, requestHash: Uint8Array, nonce: Uint8Array, programId?: Address): Promise<[Address, number]>;
100
+ /** Destination kinds in the exit registry. Kind is a PDA seed, so a Solana
101
+ * owner and a BTC script hash sharing the same 32 bytes stay distinct. */
102
+ export declare const EXIT_KIND_SOLANA_OWNER = 0;
103
+ export declare const EXIT_KIND_BTC_SCRIPT = 1;
104
+ /**
105
+ * Derive an ExitDestination PDA — the append-only registry of destinations a
106
+ * permissioned pool's ragequit path may pay.
107
+ *
108
+ * Seeds: ["exit_destination", pool_state, [kind], key]
109
+ *
110
+ * `key` is the recipient token account's OWNER for `EXIT_KIND_SOLANA_OWNER`,
111
+ * or `sha256(btcScript)` for `EXIT_KIND_BTC_SCRIPT`.
112
+ */
113
+ export declare function deriveExitDestinationPDA(poolState: Address | Uint8Array, kind: number, key: Uint8Array, programId: Address): Promise<[Address, number]>;
114
+ /**
115
+ * Derive BTC Light Client PDA
116
+ */
117
+ export declare function deriveLightClientPDA(programId?: Address): Promise<[Address, number]>;
118
+ /**
119
+ * Derive Block Header PDA (hash-based)
120
+ * Seeds: ["block", blockHash(32)]
121
+ */
122
+ export declare function deriveBlockHeaderPDA(blockHash: Uint8Array, programId?: Address): Promise<[Address, number]>;
123
+ /**
124
+ * Derive HeightIndex PDA
125
+ * Seeds: ["height_index", height_le_bytes(8)]
126
+ */
127
+ export declare function deriveHeightIndexPDA(height: number | bigint, programId?: Address): Promise<[Address, number]>;
128
+ /**
129
+ * Derive VerifiedTransaction PDA (btc-light-client)
130
+ *
131
+ * Seeds: ["verified_tx", blockHash(32), txid(32)]
132
+ */
133
+ export declare function deriveVerifiedTransactionPDA(blockHash: Uint8Array, txid: Uint8Array, programId?: Address): Promise<[Address, number]>;
134
+ /**
135
+ * Derive Redemption Request PDA
136
+ *
137
+ * Seeds: ["redemption", pool_state, user_pubkey(32), nonce_le(8)]
138
+ */
139
+ export declare function deriveRedemptionRequestPDA(poolState: Address | Uint8Array, userPubkey: Uint8Array, nonce: bigint, programId?: Address): Promise<[Address, number]>;
140
+ /**
141
+ * Derive VK Registry PDA for a JoinSplit variant
142
+ *
143
+ * Seeds: ["vk_registry", &[n_inputs], &[n_outputs]]
144
+ */
145
+ export declare function deriveVkRegistryPDA(nInputs: number, nOutputs: number, programId?: Address): Promise<[Address, number]>;
146
+ /**
147
+ * Derive Deposit Receipt PDA.
148
+ *
149
+ * Two on-chain schemes exist:
150
+ * - `complete_deposit` (disc 11, the active direct-vault flow): seeds ["deposit_receipt", txid] —
151
+ * call WITHOUT `depositVout`.
152
+ * - `verify_deposit` (disc 25, OP_RETURN-free flow): seeds ["deposit_receipt", txid, vout(4 LE)] —
153
+ * pass `depositVout` so a funding tx with multiple independent deposit outputs gets one receipt
154
+ * per output (each creditable once) instead of the first output blocking the rest.
155
+ */
156
+ export declare function deriveDepositReceiptPDA(depositTxid: Uint8Array, depositVout?: number, programId?: Address): Promise<[Address, number]>;
157
+ /**
158
+ * Convert bigint commitment to bytes for PDA derivation
159
+ */
160
+ export declare function commitmentToBytes(commitment: bigint): Uint8Array;