@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3

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 (142) hide show
  1. package/README.md +214 -108
  2. package/{packages/sdk/dist → dist}/client.d.ts +25 -1
  3. package/{packages/sdk/dist → dist}/client.js +36 -1
  4. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  5. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  6. package/{packages/sdk/dist → dist}/index.d.ts +3 -3
  7. package/{packages/sdk/dist → dist}/index.js +3 -3
  8. package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
  9. package/{packages/sdk/dist → dist}/instructions.js +45 -15
  10. package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
  11. package/{packages/sdk/dist → dist}/stealth.js +146 -14
  12. package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
  13. package/{packages/sdk/dist → dist}/taproot.js +54 -2
  14. package/package.json +86 -63
  15. package/src/announcement-client.ts +457 -0
  16. package/src/auditor-ciphertext.ts +181 -0
  17. package/src/auditor.ts +409 -0
  18. package/src/bitcoin/ika.ts +103 -0
  19. package/src/bitcoin/index.ts +5 -0
  20. package/src/bound-params.ts +322 -0
  21. package/src/chadbuffer.ts +603 -0
  22. package/src/circomlibjs.d.ts +51 -0
  23. package/src/claim-link.ts +53 -0
  24. package/src/client.ts +638 -0
  25. package/src/commitment-tree.ts +736 -0
  26. package/src/config.ts +772 -0
  27. package/src/core/esplora.ts +332 -0
  28. package/src/core/mempool.ts +159 -0
  29. package/src/crypto-babyjub.ts +385 -0
  30. package/src/crypto-ed25519.ts +297 -0
  31. package/src/crypto.ts +199 -0
  32. package/src/event-client.ts +231 -0
  33. package/src/events.ts +384 -0
  34. package/src/explorer.ts +300 -0
  35. package/src/index.ts +902 -0
  36. package/src/instructions.ts +2820 -0
  37. package/src/keys.ts +1228 -0
  38. package/src/logger.ts +41 -0
  39. package/src/magicblock.ts +278 -0
  40. package/src/merkle.ts +197 -0
  41. package/src/note.ts +754 -0
  42. package/src/pda.ts +516 -0
  43. package/src/pool-state.ts +176 -0
  44. package/src/poseidon.ts +175 -0
  45. package/src/prover/index.ts +19 -0
  46. package/src/prover/mobile.ts +303 -0
  47. package/src/prover/web.ts +771 -0
  48. package/src/psbt.ts +333 -0
  49. package/src/selective-disclosure.ts +284 -0
  50. package/src/sender-memo.ts +343 -0
  51. package/src/snarkjs.d.ts +19 -0
  52. package/src/sns-resolver.ts +333 -0
  53. package/src/solana/connection.ts +189 -0
  54. package/src/solana/priority-fee.ts +201 -0
  55. package/src/spend-doc.ts +163 -0
  56. package/src/stealth.ts +1477 -0
  57. package/src/taproot.ts +707 -0
  58. package/src/token-registry.ts +207 -0
  59. package/src/utils/encoding.ts +33 -0
  60. package/src/vk-registry.ts +295 -0
  61. package/LICENSE +0 -21
  62. package/packages/btc-client/src/esplora-client.ts +0 -153
  63. package/packages/btc-client/src/index.ts +0 -3
  64. package/packages/btc-client/src/op-return.ts +0 -93
  65. package/packages/btc-client/src/types.ts +0 -112
  66. package/packages/sdk/README.md +0 -277
  67. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  68. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  69. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  70. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  71. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  73. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  75. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  79. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  81. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  83. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  85. /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/config.js +0 -0
  87. /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
  89. /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
  90. /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
  91. /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
  92. /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
  93. /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
  94. /package/{packages/sdk/dist → dist}/crypto.js +0 -0
  95. /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
  96. /package/{packages/sdk/dist → dist}/event-client.js +0 -0
  97. /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
  98. /package/{packages/sdk/dist → dist}/events.js +0 -0
  99. /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
  100. /package/{packages/sdk/dist → dist}/explorer.js +0 -0
  101. /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
  102. /package/{packages/sdk/dist → dist}/keys.js +0 -0
  103. /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
  104. /package/{packages/sdk/dist → dist}/logger.js +0 -0
  105. /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
  106. /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
  107. /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
  108. /package/{packages/sdk/dist → dist}/merkle.js +0 -0
  109. /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
  110. /package/{packages/sdk/dist → dist}/note.js +0 -0
  111. /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
  112. /package/{packages/sdk/dist → dist}/pda.js +0 -0
  113. /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
  114. /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
  115. /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
  116. /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
  117. /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
  118. /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
  119. /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
  120. /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
  121. /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
  122. /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
  123. /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
  124. /package/{packages/sdk/dist → dist}/psbt.js +0 -0
  125. /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  127. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  131. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
  135. /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
  137. /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
  138. /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
  139. /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
  140. /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
  141. /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
  142. /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
package/src/note.ts ADDED
@@ -0,0 +1,754 @@
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
+
14
+ import { randomFieldElement, bigintToBytes, sha256Hash, bytesToBigint, BN254_FIELD_PRIME, babyJubMul, BABYJUB_BASE8 } from "./crypto";
15
+ import { computeUnifiedCommitmentSync, computeNullifierSync, hashNullifierSync, computeJoinSplitCommitmentSync, computeNPKSync, computeMPKSync, computeJoinSplitNullifierSync } from "./poseidon";
16
+
17
+ /**
18
+ * Note structure for shielded amounts
19
+ *
20
+ * Core secrets (always required):
21
+ * - amount, nullifier, secret
22
+ *
23
+ * Computed values (from circom circuits):
24
+ * - note, commitment, nullifierHash
25
+ * These are 0n until computed by circuits
26
+ */
27
+ export interface Note {
28
+ // Amount in satoshis
29
+ amount: bigint;
30
+ // Random nullifier (field element)
31
+ nullifier: bigint;
32
+ // Random secret (field element)
33
+ secret: bigint;
34
+ // note = Poseidon(nullifier, secret) - computed by circuit
35
+ note: bigint;
36
+ // commitment = Poseidon(note, amount) - computed by circuit
37
+ commitment: bigint;
38
+ // nullifierHash = Poseidon(nullifier) - computed by circuit
39
+ nullifierHash: bigint;
40
+ // 32-byte representations
41
+ nullifierBytes: Uint8Array;
42
+ secretBytes: Uint8Array;
43
+ commitmentBytes: Uint8Array;
44
+ nullifierHashBytes: Uint8Array;
45
+ }
46
+
47
+ /**
48
+ * Serializable note data (for storage/transmission)
49
+ * Only stores essential secrets - hashes recomputed by circuits
50
+ */
51
+ export interface SerializedNote {
52
+ amount: string;
53
+ nullifier: string;
54
+ secret: string;
55
+ // Optional pre-computed values
56
+ commitment?: string;
57
+ nullifierHash?: string;
58
+ }
59
+
60
+ /**
61
+ * Generate a new note with random nullifier and secret
62
+ *
63
+ * Note: Hash values are NOT computed here. They will be computed
64
+ * by the circom circuits during proof generation.
65
+ *
66
+ * @param amountSats - Amount in satoshis
67
+ * @returns Note with secrets (hashes are 0n until circuit execution)
68
+ */
69
+ export function generateNote(amountSats: bigint): Note {
70
+ const nullifier = randomFieldElement();
71
+ const secret = randomFieldElement();
72
+
73
+ return createNoteFromSecrets(nullifier, secret, amountSats);
74
+ }
75
+
76
+ /**
77
+ * Create a note from known secrets
78
+ *
79
+ * @param nullifier - Nullifier field element
80
+ * @param secret - Secret field element
81
+ * @param amountSats - Amount in satoshis
82
+ * @param commitment - Optional pre-computed commitment (from circuit)
83
+ * @param nullifierHash - Optional pre-computed nullifier hash (from circuit)
84
+ * @returns Note structure
85
+ */
86
+ export function createNoteFromSecrets(
87
+ nullifier: bigint,
88
+ secret: bigint,
89
+ amountSats: bigint,
90
+ commitment?: bigint,
91
+ nullifierHash?: bigint
92
+ ): Note {
93
+ // Computed values are 0n until provided from circuit outputs
94
+ const comm = commitment ?? 0n;
95
+ const nullHash = nullifierHash ?? 0n;
96
+ const noteHash = 0n; // Intermediate hash not typically exposed
97
+
98
+ return {
99
+ amount: amountSats,
100
+ nullifier,
101
+ secret,
102
+ note: noteHash,
103
+ commitment: comm,
104
+ nullifierHash: nullHash,
105
+ nullifierBytes: bigintToBytes(nullifier),
106
+ secretBytes: bigintToBytes(secret),
107
+ commitmentBytes: bigintToBytes(comm),
108
+ nullifierHashBytes: bigintToBytes(nullHash),
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Update note with computed hash values from circom circuit execution
114
+ *
115
+ * @param note - Note to update
116
+ * @param commitment - Commitment from circuit output
117
+ * @param nullifierHash - Nullifier hash from circuit output
118
+ * @returns Updated note
119
+ */
120
+ export function updateNoteWithHashes(
121
+ note: Note,
122
+ commitment: bigint,
123
+ nullifierHash: bigint
124
+ ): Note {
125
+ return {
126
+ ...note,
127
+ commitment,
128
+ nullifierHash,
129
+ commitmentBytes: bigintToBytes(commitment),
130
+ nullifierHashBytes: bigintToBytes(nullifierHash),
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Serialize a note for storage or transmission
136
+ *
137
+ * Only stores the essential data (amount, nullifier, secret).
138
+ * Optionally includes pre-computed hash values.
139
+ */
140
+ export function serializeNote(note: Note): SerializedNote {
141
+ const serialized: SerializedNote = {
142
+ amount: note.amount.toString(),
143
+ nullifier: note.nullifier.toString(),
144
+ secret: note.secret.toString(),
145
+ };
146
+
147
+ // Include computed values if available
148
+ if (note.commitment !== 0n) {
149
+ serialized.commitment = note.commitment.toString();
150
+ }
151
+ if (note.nullifierHash !== 0n) {
152
+ serialized.nullifierHash = note.nullifierHash.toString();
153
+ }
154
+
155
+ return serialized;
156
+ }
157
+
158
+ /**
159
+ * Deserialize and restore a note from stored data
160
+ */
161
+ export function deserializeNote(data: SerializedNote): Note {
162
+ const amount = BigInt(data.amount);
163
+ const nullifier = BigInt(data.nullifier);
164
+ const secret = BigInt(data.secret);
165
+ const commitment = data.commitment ? BigInt(data.commitment) : undefined;
166
+ const nullifierHash = data.nullifierHash
167
+ ? BigInt(data.nullifierHash)
168
+ : undefined;
169
+
170
+ return createNoteFromSecrets(
171
+ nullifier,
172
+ secret,
173
+ amount,
174
+ commitment,
175
+ nullifierHash
176
+ );
177
+ }
178
+
179
+ /**
180
+ * Format satoshis as BTC string
181
+ */
182
+ export function formatBtc(sats: bigint): string {
183
+ const btc = Number(sats) / 100_000_000;
184
+ return btc.toFixed(8) + " BTC";
185
+ }
186
+
187
+ /**
188
+ * Parse BTC string to satoshis
189
+ */
190
+ export function parseBtc(btcString: string): bigint {
191
+ const btc = parseFloat(btcString.replace(" BTC", ""));
192
+ return BigInt(Math.round(btc * 100_000_000));
193
+ }
194
+
195
+ /**
196
+ * Check if a note has computed hash values
197
+ */
198
+ export function noteHasComputedHashes(note: Note): boolean {
199
+ return note.commitment !== 0n && note.nullifierHash !== 0n;
200
+ }
201
+
202
+ // ============================================================================
203
+ // Note Commitment/Nullifier Helpers
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Get the public key X coordinate from a note's nullifier (used as private key)
208
+ *
209
+ * In the unified model: pubKey = nullifier * BASE8 (on Baby Jubjub curve)
210
+ * Returns pubKey.x for commitment computation
211
+ */
212
+ export function getNotePublicKeyX(note: Note): bigint {
213
+ const pubKey = babyJubMul(note.nullifier, BABYJUB_BASE8);
214
+ return pubKey.x;
215
+ }
216
+
217
+ /**
218
+ * Compute the commitment for a note
219
+ *
220
+ * commitment = Poseidon(pubKeyX, amount)
221
+ * where pubKeyX = (nullifier * G).x
222
+ *
223
+ * Returns the note with commitment fields populated
224
+ */
225
+ export function computeNoteCommitment(note: Note): Note {
226
+ const pubKeyX = getNotePublicKeyX(note);
227
+ const commitment = computeUnifiedCommitmentSync(pubKeyX, note.amount);
228
+ const commitmentBytes = bigintToBytes(commitment);
229
+
230
+ return {
231
+ ...note,
232
+ commitment,
233
+ commitmentBytes,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Compute the nullifier and nullifier hash for a note at a given leaf index
239
+ *
240
+ * nullifier = Poseidon(privKey, leafIndex)
241
+ * nullifierHash = Poseidon(nullifier)
242
+ *
243
+ * @returns Object with computed nullifier values
244
+ */
245
+ export function computeNoteNullifier(
246
+ note: Note,
247
+ leafIndex: bigint
248
+ ): {
249
+ nullifier: bigint;
250
+ nullifierHash: bigint;
251
+ nullifierHashBytes: Uint8Array;
252
+ } {
253
+ const nullifier = computeNullifierSync(note.nullifier, leafIndex);
254
+ const nullifierHash = hashNullifierSync(nullifier);
255
+ const nullifierHashBytes = bigintToBytes(nullifierHash);
256
+
257
+ return {
258
+ nullifier,
259
+ nullifierHash,
260
+ nullifierHashBytes,
261
+ };
262
+ }
263
+
264
+ // ============================================================================
265
+ // Deterministic Note Derivation (HD-style)
266
+ // ============================================================================
267
+
268
+ /**
269
+ * Derive a note deterministically from a seed phrase/name and index.
270
+ *
271
+ * This allows recovery of all notes from just the seed + index.
272
+ * Similar to HD wallets in Bitcoin.
273
+ *
274
+ * The seed is first hashed to create a 32-byte master key, then
275
+ * nullifier and secret are derived from that master key.
276
+ *
277
+ * @param seed - Seed phrase, name, or password (any string)
278
+ * @param index - Note index (0, 1, 2, ...)
279
+ * @param amountSats - Amount in satoshis
280
+ * @returns Note with deterministically derived secrets
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * // Derive notes from a name
285
+ * const note0 = deriveNote("albertgogogo", 0, 100_000n);
286
+ * const note1 = deriveNote("albertgogogo", 1, 50_000n);
287
+ *
288
+ * // Later: recover the same notes
289
+ * const recovered = deriveNote("albertgogogo", 0, 100_000n);
290
+ * // recovered.nullifier === note0.nullifier ✓
291
+ * ```
292
+ */
293
+ export function deriveNote(
294
+ seed: string,
295
+ index: number,
296
+ amountSats: bigint
297
+ ): Note {
298
+ // Step 1: Hash seed to get 32-byte master key (normalizes any input)
299
+ const master = deriveMasterKey(seed);
300
+
301
+ // Step 2: Derive nullifier and secret from master + index
302
+ const nullifier = deriveFromMaster(master, index, 0); // 0 = nullifier domain
303
+ const secret = deriveFromMaster(master, index, 1); // 1 = secret domain
304
+
305
+ return createNoteFromSecrets(nullifier, secret, amountSats);
306
+ }
307
+
308
+ /**
309
+ * Derive master key from seed (32 bytes)
310
+ *
311
+ * master = SHA256(seed)
312
+ *
313
+ * You can cache this and use deriveNoteFromMaster() for efficiency.
314
+ */
315
+ export function deriveMasterKey(seed: string): Uint8Array {
316
+ const encoder = new TextEncoder();
317
+ const seedBytes = encoder.encode(seed);
318
+ return sha256Hash(seedBytes);
319
+ }
320
+
321
+ /**
322
+ * Derive a note from a pre-computed master key
323
+ *
324
+ * More efficient if deriving many notes from the same seed.
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * const master = deriveMasterKey("albertgogogo");
329
+ * const note0 = deriveNoteFromMaster(master, 0, 100_000n);
330
+ * const note1 = deriveNoteFromMaster(master, 1, 50_000n);
331
+ * ```
332
+ */
333
+ export function deriveNoteFromMaster(
334
+ master: Uint8Array,
335
+ index: number,
336
+ amountSats: bigint
337
+ ): Note {
338
+ const nullifier = deriveFromMaster(master, index, 0);
339
+ const secret = deriveFromMaster(master, index, 1);
340
+ return createNoteFromSecrets(nullifier, secret, amountSats);
341
+ }
342
+
343
+ /**
344
+ * Derive field element from master key + index + domain
345
+ *
346
+ * result = SHA256(master || index || domain) mod BN254_PRIME
347
+ *
348
+ * @param master - 32-byte master key
349
+ * @param index - Note index
350
+ * @param domain - 0 for nullifier, 1 for secret
351
+ */
352
+ function deriveFromMaster(
353
+ master: Uint8Array,
354
+ index: number,
355
+ domain: number
356
+ ): bigint {
357
+ // Build input: master (32) || index (4) || domain (1) = 37 bytes
358
+ const input = new Uint8Array(37);
359
+ input.set(master, 0);
360
+
361
+ // Index as 4 bytes little-endian
362
+ const view = new DataView(input.buffer);
363
+ view.setUint32(32, index, true);
364
+
365
+ // Domain as 1 byte
366
+ input[36] = domain;
367
+
368
+ // Hash and reduce to field
369
+ const hash = sha256Hash(input);
370
+ return bytesToBigint(hash) % BN254_FIELD_PRIME;
371
+ }
372
+
373
+ /**
374
+ * Derive multiple notes at once from a seed
375
+ *
376
+ * More efficient than calling deriveNote() multiple times
377
+ * because master key is computed only once.
378
+ *
379
+ * @param seed - Seed phrase
380
+ * @param amounts - Array of amounts for each note
381
+ * @param startIndex - Starting index (default 0)
382
+ * @returns Array of derived notes
383
+ *
384
+ * @example
385
+ * ```typescript
386
+ * // Create a wallet with 3 notes
387
+ * const notes = deriveNotes("albertgogogo", [100_000n, 50_000n, 25_000n]);
388
+ * ```
389
+ */
390
+ export function deriveNotes(
391
+ seed: string,
392
+ amounts: bigint[],
393
+ startIndex: number = 0
394
+ ): Note[] {
395
+ // Compute master once, derive all notes from it
396
+ const master = deriveMasterKey(seed);
397
+ return amounts.map((amount, i) => deriveNoteFromMaster(master, startIndex + i, amount));
398
+ }
399
+
400
+ /**
401
+ * Check the strength of a seed phrase
402
+ *
403
+ * Returns estimated bits of entropy.
404
+ * Recommended: >= 80 bits for moderate security, >= 128 bits for high security
405
+ */
406
+ export function estimateSeedStrength(seed: string): {
407
+ bits: number;
408
+ strength: "weak" | "moderate" | "strong" | "very_strong";
409
+ warning?: string;
410
+ } {
411
+ // Very rough entropy estimation
412
+ const length = seed.length;
413
+ const hasLower = /[a-z]/.test(seed);
414
+ const hasUpper = /[A-Z]/.test(seed);
415
+ const hasDigit = /[0-9]/.test(seed);
416
+ const hasSpecial = /[^a-zA-Z0-9]/.test(seed);
417
+
418
+ let charsetSize = 0;
419
+ if (hasLower) charsetSize += 26;
420
+ if (hasUpper) charsetSize += 26;
421
+ if (hasDigit) charsetSize += 10;
422
+ if (hasSpecial) charsetSize += 32;
423
+
424
+ // Entropy = length * log2(charsetSize)
425
+ const bits = Math.floor(length * Math.log2(charsetSize || 1));
426
+
427
+ let strength: "weak" | "moderate" | "strong" | "very_strong";
428
+ let warning: string | undefined;
429
+
430
+ if (bits < 40) {
431
+ strength = "weak";
432
+ warning = "DANGER: This seed can be easily brute-forced. Use a longer passphrase!";
433
+ } else if (bits < 80) {
434
+ strength = "moderate";
435
+ warning = "Consider using a longer passphrase for better security.";
436
+ } else if (bits < 128) {
437
+ strength = "strong";
438
+ } else {
439
+ strength = "very_strong";
440
+ }
441
+
442
+ return { bits, strength, warning };
443
+ }
444
+
445
+ // ============================================================================
446
+ // Circom-Compatible Note Helpers
447
+ // ============================================================================
448
+
449
+ /**
450
+ * Simple note data structure (for circom circuit inputs)
451
+ *
452
+ * NOTE: When using circom circuits, the commitment is computed INSIDE the circuit
453
+ * using Poseidon. The SDK just provides the raw note data.
454
+ */
455
+ export interface NoteData {
456
+ nullifier: bigint;
457
+ secret: bigint;
458
+ amount: bigint;
459
+ }
460
+
461
+ // Poseidon initialization state (for API compatibility)
462
+
463
+ /**
464
+ * Create a simple note (alias for generateNote but returns NoteData)
465
+ *
466
+ * Use this to generate note data for circom circuit inputs.
467
+ *
468
+ * @param amount - Amount in satoshis
469
+ * @returns NoteData with nullifier, secret, and amount
470
+ */
471
+ export function createNote(amount: bigint): NoteData {
472
+ const note = generateNote(amount);
473
+ return {
474
+ nullifier: note.nullifier,
475
+ secret: note.secret,
476
+ amount: note.amount,
477
+ };
478
+ }
479
+
480
+ // initPoseidon/isPoseidonReady used to live here as well, over a flag local to this module. The
481
+ // barrel exported initPoseidon from poseidon.ts and isPoseidonReady from here, so they read
482
+ // different state: `await initPoseidon(); isPoseidonReady()` returned false forever. Both now
483
+ // belong to poseidon.ts, which owns the actual hasher.
484
+
485
+ /**
486
+ * Prepare withdrawal - creates change note for remaining balance
487
+ *
488
+ * PRIVACY: All withdrawals are indistinguishable on-chain.
489
+ * Change amount can be >= 0, and the commitment always looks random.
490
+ *
491
+ * @param inputNote - Note being spent
492
+ * @param withdrawAmount - Amount to withdraw
493
+ * @returns Change note with remaining balance (can be 0)
494
+ */
495
+ export function prepareWithdrawal(
496
+ inputNote: NoteData,
497
+ withdrawAmount: bigint
498
+ ): { changeNote: NoteData; changeAmount: bigint } {
499
+ if (withdrawAmount <= 0n) {
500
+ throw new Error("Withdraw amount must be positive");
501
+ }
502
+ if (withdrawAmount > inputNote.amount) {
503
+ throw new Error("Withdraw amount exceeds note balance");
504
+ }
505
+
506
+ const changeAmount = inputNote.amount - withdrawAmount;
507
+ const changeNote = createNote(changeAmount);
508
+
509
+ return { changeNote, changeAmount };
510
+ }
511
+
512
+ // ============================================================================
513
+ // Stealth Note Types (Dual-Key ECDH Support)
514
+ // ============================================================================
515
+
516
+ /**
517
+ * Stealth note structure for dual-key ECDH system
518
+ *
519
+ * Key differences from basic Note:
520
+ * - Uses random value instead of nullifier/secret for commitment
521
+ * - Stores ephemeral spending pubkey for proof generation
522
+ * - Nullifier derived from (spendingPrivKey, leafIndex) in circuit
523
+ */
524
+ export interface StealthNote {
525
+ /** Amount in satoshis */
526
+ amount: bigint;
527
+
528
+ /** Random value for commitment */
529
+ random: bigint;
530
+
531
+ /** Ephemeral spending public key (from sender) */
532
+ ephemeralSpendPubX: bigint;
533
+ ephemeralSpendPubY: bigint;
534
+
535
+ /** Leaf index in Merkle tree (set when commitment added on-chain) */
536
+ leafIndex: number;
537
+
538
+ /** Note public key = Poseidon(ECDHShared.x, ECDHShared.y) */
539
+ notePubKey: bigint;
540
+
541
+ /** Commitment = Poseidon(notePubKey, amount, random) */
542
+ commitment: bigint;
543
+
544
+ /** Byte representations */
545
+ randomBytes: Uint8Array;
546
+ commitmentBytes: Uint8Array;
547
+ }
548
+
549
+ /**
550
+ * Serializable stealth note data
551
+ */
552
+ export interface SerializedStealthNote {
553
+ amount: string;
554
+ random: string;
555
+ ephemeralSpendPubX: string;
556
+ ephemeralSpendPubY: string;
557
+ leafIndex: number;
558
+ notePubKey?: string;
559
+ commitment?: string;
560
+ }
561
+
562
+ /**
563
+ * Create a stealth note from scanned announcement data
564
+ *
565
+ * @param amount - Decrypted amount
566
+ * @param random - Decrypted random value
567
+ * @param ephemeralSpendPub - Sender's ephemeral pubkey
568
+ * @param leafIndex - Merkle tree leaf index
569
+ * @returns StealthNote structure
570
+ */
571
+ export function createStealthNote(
572
+ amount: bigint,
573
+ random: bigint,
574
+ ephemeralSpendPub: { x: bigint; y: bigint },
575
+ leafIndex: number
576
+ ): StealthNote {
577
+ return {
578
+ amount,
579
+ random,
580
+ ephemeralSpendPubX: ephemeralSpendPub.x,
581
+ ephemeralSpendPubY: ephemeralSpendPub.y,
582
+ leafIndex,
583
+ notePubKey: 0n, // Computed in circuit
584
+ commitment: 0n, // Computed in circuit
585
+ randomBytes: bigintToBytes(random),
586
+ commitmentBytes: new Uint8Array(32),
587
+ };
588
+ }
589
+
590
+ /**
591
+ * Update stealth note with computed values from circuit
592
+ */
593
+ export function updateStealthNoteWithHashes(
594
+ note: StealthNote,
595
+ notePubKey: bigint,
596
+ commitment: bigint
597
+ ): StealthNote {
598
+ return {
599
+ ...note,
600
+ notePubKey,
601
+ commitment,
602
+ commitmentBytes: bigintToBytes(commitment),
603
+ };
604
+ }
605
+
606
+ /**
607
+ * Serialize stealth note for storage
608
+ */
609
+ export function serializeStealthNote(note: StealthNote): SerializedStealthNote {
610
+ const serialized: SerializedStealthNote = {
611
+ amount: note.amount.toString(),
612
+ random: note.random.toString(),
613
+ ephemeralSpendPubX: note.ephemeralSpendPubX.toString(),
614
+ ephemeralSpendPubY: note.ephemeralSpendPubY.toString(),
615
+ leafIndex: note.leafIndex,
616
+ };
617
+
618
+ if (note.notePubKey !== 0n) {
619
+ serialized.notePubKey = note.notePubKey.toString();
620
+ }
621
+ if (note.commitment !== 0n) {
622
+ serialized.commitment = note.commitment.toString();
623
+ }
624
+
625
+ return serialized;
626
+ }
627
+
628
+ /**
629
+ * Deserialize stealth note from storage
630
+ */
631
+ export function deserializeStealthNote(data: SerializedStealthNote): StealthNote {
632
+ const note = createStealthNote(
633
+ BigInt(data.amount),
634
+ BigInt(data.random),
635
+ {
636
+ x: BigInt(data.ephemeralSpendPubX),
637
+ y: BigInt(data.ephemeralSpendPubY),
638
+ },
639
+ data.leafIndex
640
+ );
641
+
642
+ if (data.notePubKey && data.commitment) {
643
+ return updateStealthNoteWithHashes(
644
+ note,
645
+ BigInt(data.notePubKey),
646
+ BigInt(data.commitment)
647
+ );
648
+ }
649
+
650
+ return note;
651
+ }
652
+
653
+ /**
654
+ * Check if stealth note has computed hashes
655
+ */
656
+ export function stealthNoteHasComputedHashes(note: StealthNote): boolean {
657
+ return note.notePubKey !== 0n && note.commitment !== 0n;
658
+ }
659
+
660
+ // ============================================================================
661
+ // JoinSplit Note (Railgun-aligned 3-key model)
662
+ // ============================================================================
663
+
664
+ /**
665
+ * JoinSplit note structure
666
+ *
667
+ * Commitment = Poseidon(npk, token, amount)
668
+ * where npk = Poseidon(MPK, random)
669
+ * and MPK = Poseidon(pkX, pkY, nullifyingKey)
670
+ */
671
+ export interface JoinSplitNote {
672
+ /** Note public key: Poseidon(MPK, random) */
673
+ npk: bigint;
674
+ /** Token identifier: computeTokenId(mintBytes) */
675
+ token: bigint;
676
+ /** Amount in token's native units */
677
+ amount: bigint;
678
+ /** Blinding factor (used in NPK derivation) */
679
+ random: bigint;
680
+ /** Merkle tree leaf position (-1 if not yet inserted) */
681
+ leafIndex: number;
682
+ /** Commitment: Poseidon(npk, token, amount) */
683
+ commitment: bigint;
684
+ }
685
+
686
+ /**
687
+ * Create a JoinSplit note from key components
688
+ *
689
+ * @param tokenId - Token identifier from computeTokenId(mintBytes)
690
+ */
691
+ export function createJoinSplitNote(
692
+ mpk: bigint,
693
+ random: bigint,
694
+ amount: bigint,
695
+ tokenId: bigint,
696
+ leafIndex: number = -1,
697
+ ): JoinSplitNote {
698
+ const npk = computeNPKSync(mpk, random);
699
+ const commitment = computeJoinSplitCommitmentSync(npk, tokenId, amount);
700
+
701
+ return {
702
+ npk,
703
+ token: tokenId,
704
+ amount,
705
+ random,
706
+ leafIndex,
707
+ commitment,
708
+ };
709
+ }
710
+
711
+ /**
712
+ * Compute nullifier for a JoinSplit note
713
+ */
714
+ export function computeJoinSplitNoteNullifier(
715
+ nullifyingKey: bigint,
716
+ leafIndex: number,
717
+ ): bigint {
718
+ return computeJoinSplitNullifierSync(nullifyingKey, BigInt(leafIndex));
719
+ }
720
+
721
+ /**
722
+ * Serialize a JoinSplit note for storage
723
+ */
724
+ export interface SerializedJoinSplitNote {
725
+ npk: string;
726
+ token: string;
727
+ amount: string;
728
+ random: string;
729
+ leafIndex: number;
730
+ commitment: string;
731
+ }
732
+
733
+ export function serializeJoinSplitNote(note: JoinSplitNote): SerializedJoinSplitNote {
734
+ return {
735
+ npk: note.npk.toString(),
736
+ token: note.token.toString(),
737
+ amount: note.amount.toString(),
738
+ random: note.random.toString(),
739
+ leafIndex: note.leafIndex,
740
+ commitment: note.commitment.toString(),
741
+ };
742
+ }
743
+
744
+ export function deserializeJoinSplitNote(data: SerializedJoinSplitNote): JoinSplitNote {
745
+ return {
746
+ npk: BigInt(data.npk),
747
+ token: BigInt(data.token),
748
+ amount: BigInt(data.amount),
749
+ random: BigInt(data.random),
750
+ leafIndex: data.leafIndex,
751
+ commitment: BigInt(data.commitment),
752
+ };
753
+ }
754
+