@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
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Baby Jubjub curve operations for UTXOpia
3
+ *
4
+ * Twisted Edwards curve matching circomlib's BabyJubjub:
5
+ * a*x^2 + y^2 = 1 + d*x^2*y^2
6
+ * a = 168700, d = 168696
7
+ *
8
+ * Over BN254 scalar field.
9
+ * Used for spending keys and in-circuit key derivation via BabyPbk().
10
+ *
11
+ * @see https://eips.ethereum.org/EIPS/eip-2494
12
+ * @see circomlib/circuits/babyjub.circom
13
+ */
14
+
15
+ import { sha256 } from "@noble/hashes/sha2.js";
16
+
17
+ // =============================================================================
18
+ // Field Constants
19
+ // =============================================================================
20
+
21
+ /** BN254 scalar field prime (Baby Jubjub base field) */
22
+ export const BABYJUB_FIELD_PRIME =
23
+ 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
24
+
25
+ /** Baby Jubjub curve parameter a */
26
+ export const BABYJUB_A = 168700n;
27
+
28
+ /** Baby Jubjub curve parameter d */
29
+ export const BABYJUB_D = 168696n;
30
+
31
+ /**
32
+ * Baby Jubjub subgroup order (order of BASE8 generator)
33
+ * = field_prime / 8 (cofactor 8)
34
+ */
35
+ export const BABYJUB_ORDER =
36
+ 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
37
+
38
+ /**
39
+ * Baby Jubjub cofactor
40
+ */
41
+ export const BABYJUB_COFACTOR = 8n;
42
+
43
+ /**
44
+ * Generator point (BASE8) - matches circomlib's BabyPbk() generator
45
+ * This is the base point of the prime-order subgroup (cofactor-cleared).
46
+ */
47
+ export const BABYJUB_BASE8: BabyJubPoint = {
48
+ x: 5299619240641551281634865583518297030282874472190772894086521144482721001553n,
49
+ y: 16950150798460657717958625567821834550301663161624707787222815936182638968203n,
50
+ };
51
+
52
+ // =============================================================================
53
+ // Types
54
+ // =============================================================================
55
+
56
+ /** Point on the Baby Jubjub curve (affine coordinates) */
57
+ export interface BabyJubPoint {
58
+ x: bigint;
59
+ y: bigint;
60
+ }
61
+
62
+ /** Identity element (point at infinity for twisted Edwards) */
63
+ export const BABYJUB_IDENTITY: BabyJubPoint = { x: 0n, y: 1n };
64
+
65
+ // =============================================================================
66
+ // Modular Arithmetic
67
+ // =============================================================================
68
+
69
+ const P = BABYJUB_FIELD_PRIME;
70
+
71
+ function mod(n: bigint, p: bigint = P): bigint {
72
+ const result = n % p;
73
+ return result >= 0n ? result : result + p;
74
+ }
75
+
76
+ function modInverse(a: bigint, p: bigint = P): bigint {
77
+ let [old_r, r] = [a, p];
78
+ let [old_s, s] = [1n, 0n];
79
+
80
+ while (r !== 0n) {
81
+ const q = old_r / r;
82
+ [old_r, r] = [r, old_r - q * r];
83
+ [old_s, s] = [s, old_s - q * s];
84
+ }
85
+
86
+ return mod(old_s, p);
87
+ }
88
+
89
+ // =============================================================================
90
+ // Curve Operations
91
+ // =============================================================================
92
+
93
+ /**
94
+ * Check if a point is the identity element
95
+ */
96
+ export function isIdentity(point: BabyJubPoint): boolean {
97
+ return point.x === 0n && point.y === 1n;
98
+ }
99
+
100
+ /**
101
+ * Verify a point is on the Baby Jubjub curve
102
+ *
103
+ * a*x^2 + y^2 = 1 + d*x^2*y^2
104
+ */
105
+ export function isOnBabyJubCurve(point: BabyJubPoint): boolean {
106
+ if (isIdentity(point)) return true;
107
+
108
+ const { x, y } = point;
109
+ const x2 = mod(x * x);
110
+ const y2 = mod(y * y);
111
+
112
+ const lhs = mod(BABYJUB_A * x2 + y2);
113
+ const rhs = mod(1n + BABYJUB_D * x2 * y2);
114
+
115
+ return lhs === rhs;
116
+ }
117
+
118
+ /**
119
+ * Point addition on Baby Jubjub (twisted Edwards addition law)
120
+ *
121
+ * For twisted Edwards: a*x^2 + y^2 = 1 + d*x^2*y^2
122
+ * x3 = (x1*y2 + y1*x2) / (1 + d*x1*x2*y1*y2)
123
+ * y3 = (y1*y2 - a*x1*x2) / (1 - d*x1*x2*y1*y2)
124
+ */
125
+ export function babyJubAdd(p1: BabyJubPoint, p2: BabyJubPoint): BabyJubPoint {
126
+ const { x: x1, y: y1 } = p1;
127
+ const { x: x2, y: y2 } = p2;
128
+
129
+ const x1x2 = mod(x1 * x2);
130
+ const y1y2 = mod(y1 * y2);
131
+ const dx1x2y1y2 = mod(BABYJUB_D * x1x2 * y1y2);
132
+
133
+ const x3Num = mod(x1 * y2 + y1 * x2);
134
+ const x3Den = mod(1n + dx1x2y1y2);
135
+
136
+ const y3Num = mod(y1y2 - BABYJUB_A * x1x2);
137
+ const y3Den = mod(1n - dx1x2y1y2);
138
+
139
+ const x3 = mod(x3Num * modInverse(x3Den));
140
+ const y3 = mod(y3Num * modInverse(y3Den));
141
+
142
+ return { x: x3, y: y3 };
143
+ }
144
+
145
+ /**
146
+ * Point doubling on Baby Jubjub
147
+ */
148
+ export function babyJubDouble(point: BabyJubPoint): BabyJubPoint {
149
+ return babyJubAdd(point, point);
150
+ }
151
+
152
+ /**
153
+ * Scalar multiplication using double-and-add (constant-time Montgomery ladder)
154
+ *
155
+ * SECURITY: Uses Montgomery ladder for constant-time execution.
156
+ */
157
+ export function babyJubMul(scalar: bigint, point: BabyJubPoint): BabyJubPoint {
158
+ scalar = mod(scalar, BABYJUB_ORDER);
159
+
160
+ if (scalar === 0n) return BABYJUB_IDENTITY;
161
+ if (isIdentity(point)) return BABYJUB_IDENTITY;
162
+
163
+ // Montgomery ladder
164
+ let r0 = BABYJUB_IDENTITY;
165
+ let r1 = point;
166
+
167
+ // Process all bits of the scalar
168
+ const bits = scalar.toString(2).length;
169
+ for (let i = bits - 1; i >= 0; i--) {
170
+ const bit = (scalar >> BigInt(i)) & 1n;
171
+
172
+ if (bit === 1n) {
173
+ r0 = babyJubAdd(r0, r1);
174
+ r1 = babyJubDouble(r1);
175
+ } else {
176
+ r1 = babyJubAdd(r0, r1);
177
+ r0 = babyJubDouble(r0);
178
+ }
179
+ }
180
+
181
+ return r0;
182
+ }
183
+
184
+ /**
185
+ * Negate a point (flip x coordinate for twisted Edwards)
186
+ */
187
+ export function babyJubNegate(point: BabyJubPoint): BabyJubPoint {
188
+ if (isIdentity(point)) return point;
189
+ return {
190
+ x: mod(-point.x),
191
+ y: point.y,
192
+ };
193
+ }
194
+
195
+ // =============================================================================
196
+ // Compression / Serialization
197
+ // =============================================================================
198
+
199
+ /**
200
+ * Compress a Baby Jubjub point to 32 bytes
201
+ *
202
+ * Format: y-coordinate with sign of x stored in MSB of last byte
203
+ * This matches circomlib's pointbits.circom compression.
204
+ */
205
+ export function babyJubCompress(point: BabyJubPoint): Uint8Array {
206
+ const bytes = new Uint8Array(32);
207
+ let y = point.y;
208
+
209
+ // Store y in little-endian (matching circomlib convention)
210
+ let temp = y;
211
+ for (let i = 0; i < 32; i++) {
212
+ bytes[i] = Number(temp & 0xffn);
213
+ temp = temp >> 8n;
214
+ }
215
+
216
+ // Set MSB of last byte to sign of x (1 if x is odd)
217
+ if ((point.x & 1n) === 1n) {
218
+ bytes[31] |= 0x80;
219
+ }
220
+
221
+ return bytes;
222
+ }
223
+
224
+ /**
225
+ * Decompress a 32-byte representation to a Baby Jubjub point
226
+ */
227
+ export function babyJubDecompress(bytes: Uint8Array): BabyJubPoint {
228
+ if (bytes.length !== 32) {
229
+ throw new Error("Expected 32 bytes for compressed Baby Jubjub point");
230
+ }
231
+
232
+ // Extract sign of x from MSB
233
+ const xSign = (bytes[31] & 0x80) !== 0;
234
+
235
+ // Read y (little-endian), clearing the sign bit
236
+ const cleanBytes = new Uint8Array(bytes);
237
+ cleanBytes[31] &= 0x7f;
238
+
239
+ let y = 0n;
240
+ for (let i = 31; i >= 0; i--) {
241
+ y = (y << 8n) | BigInt(cleanBytes[i]);
242
+ }
243
+
244
+ // Recover x from curve equation: a*x^2 + y^2 = 1 + d*x^2*y^2
245
+ // x^2 = (1 - y^2) / (a - d*y^2)
246
+ const y2 = mod(y * y);
247
+ const num = mod(1n - y2);
248
+ const den = mod(BABYJUB_A - BABYJUB_D * y2);
249
+ const x2 = mod(num * modInverse(den));
250
+
251
+ // Compute x via Tonelli-Shanks square root
252
+ let x = modSqrt(x2);
253
+
254
+ // Adjust sign of x
255
+ const xIsOdd = (x & 1n) === 1n;
256
+ if (xIsOdd !== xSign) {
257
+ x = mod(-x);
258
+ }
259
+
260
+ const point = { x, y };
261
+ if (!isOnBabyJubCurve(point)) {
262
+ throw new Error("Decompressed point is not on the Baby Jubjub curve");
263
+ }
264
+
265
+ return point;
266
+ }
267
+
268
+ /**
269
+ * Tonelli-Shanks modular square root
270
+ */
271
+ function modSqrt(n: bigint): bigint {
272
+ if (n === 0n) return 0n;
273
+
274
+ // For p ≡ 3 (mod 4), sqrt = n^((p+1)/4)
275
+ // BN254 scalar field: p ≡ 1 (mod 4), so we need full Tonelli-Shanks
276
+ const p = P;
277
+
278
+ // Check if n is a quadratic residue
279
+ const euler = modPow(n, (p - 1n) / 2n);
280
+ if (euler !== 1n) {
281
+ throw new Error("No square root exists (not a quadratic residue)");
282
+ }
283
+
284
+ // Factor out powers of 2 from p-1
285
+ let Q = p - 1n;
286
+ let S = 0n;
287
+ while ((Q & 1n) === 0n) {
288
+ Q >>= 1n;
289
+ S++;
290
+ }
291
+
292
+ // Find a non-residue z
293
+ let z = 2n;
294
+ while (modPow(z, (p - 1n) / 2n) !== p - 1n) {
295
+ z++;
296
+ }
297
+
298
+ let M = S;
299
+ let c = modPow(z, Q);
300
+ let t = modPow(n, Q);
301
+ let R = modPow(n, (Q + 1n) / 2n);
302
+
303
+ while (true) {
304
+ if (t === 1n) return R;
305
+
306
+ let i = 1n;
307
+ let temp = mod(t * t);
308
+ while (temp !== 1n) {
309
+ temp = mod(temp * temp);
310
+ i++;
311
+ }
312
+
313
+ const b = modPow(c, 1n << (M - i - 1n));
314
+ M = i;
315
+ c = mod(b * b);
316
+ t = mod(t * c);
317
+ R = mod(R * b);
318
+ }
319
+ }
320
+
321
+ function modPow(base: bigint, exp: bigint): bigint {
322
+ let result = 1n;
323
+ base = mod(base);
324
+
325
+ while (exp > 0n) {
326
+ if (exp & 1n) {
327
+ result = mod(result * base);
328
+ }
329
+ exp >>= 1n;
330
+ base = mod(base * base);
331
+ }
332
+
333
+ return result;
334
+ }
335
+
336
+ // =============================================================================
337
+ // Key Generation
338
+ // =============================================================================
339
+
340
+ /**
341
+ * Generate a random Baby Jubjub keypair
342
+ */
343
+ export function generateBabyJubKeyPair(): { privKey: bigint; pubKey: BabyJubPoint } {
344
+ const bytes = new Uint8Array(32);
345
+ crypto.getRandomValues(bytes);
346
+ const privKey = babyJubScalarFromBytes(bytes);
347
+ const pubKey = babyJubMul(privKey, BABYJUB_BASE8);
348
+ return { privKey, pubKey };
349
+ }
350
+
351
+ /**
352
+ * Derive a Baby Jubjub keypair from a seed (deterministic)
353
+ */
354
+ export function deriveBabyJubKeyFromSeed(
355
+ seed: Uint8Array
356
+ ): { privKey: bigint; pubKey: BabyJubPoint } {
357
+ const hash = sha256(seed);
358
+ const privKey = babyJubScalarFromBytes(hash);
359
+ const pubKey = babyJubMul(privKey, BABYJUB_BASE8);
360
+ return { privKey, pubKey };
361
+ }
362
+
363
+ /**
364
+ * Derive a scalar from bytes (reduces modulo Baby Jubjub order)
365
+ */
366
+ export function babyJubScalarFromBytes(bytes: Uint8Array): bigint {
367
+ let result = 0n;
368
+ for (let i = 0; i < bytes.length; i++) {
369
+ result = (result << 8n) | BigInt(bytes[i]);
370
+ }
371
+ return mod(result, BABYJUB_ORDER);
372
+ }
373
+
374
+ /**
375
+ * Convert a Baby Jubjub scalar to 32 bytes (big-endian)
376
+ */
377
+ export function babyJubScalarToBytes(scalar: bigint): Uint8Array {
378
+ const bytes = new Uint8Array(32);
379
+ let temp = mod(scalar, BABYJUB_ORDER);
380
+ for (let i = 31; i >= 0; i--) {
381
+ bytes[i] = Number(temp & 0xffn);
382
+ temp >>= 8n;
383
+ }
384
+ return bytes;
385
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Ed25519/X25519 utilities for UTXOpia viewing keys
3
+ *
4
+ * Uses @noble/curves for Ed25519 key generation and X25519 ECDH.
5
+ * Ed25519 is used for viewing keys (off-chain only, fast and standard).
6
+ *
7
+ * Viewing key operations:
8
+ * - Key generation: Ed25519 keypair
9
+ * - ECDH: X25519 (Montgomery form of Curve25519)
10
+ * - Shared secret: 32 bytes from X25519
11
+ *
12
+ * @see https://github.com/paulmillr/noble-curves
13
+ */
14
+
15
+ import { ed25519 } from "@noble/curves/ed25519.js";
16
+ import { x25519 } from "@noble/curves/ed25519.js";
17
+ import { sha256 } from "@noble/hashes/sha2.js";
18
+
19
+ // =============================================================================
20
+ // Key Generation
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Generate an Ed25519 keypair for viewing key use
25
+ *
26
+ * @returns 32-byte private key and 32-byte public key
27
+ */
28
+ export function ed25519GenerateKeyPair(): { privKey: Uint8Array; pubKey: Uint8Array } {
29
+ const privKey = ed25519.utils.randomSecretKey();
30
+ const pubKey = ed25519.getPublicKey(privKey);
31
+ return { privKey, pubKey };
32
+ }
33
+
34
+ /**
35
+ * Derive an Ed25519 keypair deterministically from secret material.
36
+ *
37
+ * Used for deposit ephemeral keys, where a random keypair is a liability: the
38
+ * deposit address commits to the ephemeral pubkey, so losing it makes the coins
39
+ * unspendable by anyone, forever. Deriving it from the owner's seed means the
40
+ * seed alone can rebuild every address they were ever handed.
41
+ *
42
+ * @param material - secret bytes; hashed, so it need not be uniform
43
+ */
44
+ export function ed25519KeyPairFromMaterial(material: Uint8Array): {
45
+ privKey: Uint8Array;
46
+ pubKey: Uint8Array;
47
+ } {
48
+ const privKey = sha256(material);
49
+ return { privKey, pubKey: ed25519.getPublicKey(privKey) };
50
+ }
51
+
52
+ /**
53
+ * Derive an Ed25519 public key from a private key
54
+ *
55
+ * @param privKey - 32-byte Ed25519 private key
56
+ * @returns 32-byte Ed25519 public key
57
+ */
58
+ export function ed25519GetPublicKey(privKey: Uint8Array): Uint8Array {
59
+ return ed25519.getPublicKey(privKey);
60
+ }
61
+
62
+ /**
63
+ * Derive an Ed25519 keypair from a seed (deterministic)
64
+ *
65
+ * @param seed - Arbitrary bytes to derive from (will be SHA256'd to 32 bytes)
66
+ * @returns Ed25519 keypair
67
+ */
68
+ export function ed25519DeriveKeyFromSeed(seed: Uint8Array): { privKey: Uint8Array; pubKey: Uint8Array } {
69
+ const privKey = sha256(seed);
70
+ const pubKey = ed25519.getPublicKey(privKey);
71
+ return { privKey, pubKey };
72
+ }
73
+
74
+ // =============================================================================
75
+ // ECDH (X25519)
76
+ // =============================================================================
77
+
78
+ /**
79
+ * Convert Ed25519 public key to X25519 (Montgomery form) for ECDH
80
+ *
81
+ * Uses the birational map from twisted Edwards to Montgomery form.
82
+ */
83
+ export function ed25519PubToX25519(edPub: Uint8Array): Uint8Array {
84
+ return ed25519.utils.toMontgomery(edPub);
85
+ }
86
+
87
+ /**
88
+ * Perform X25519 ECDH key exchange
89
+ *
90
+ * @param privKey - 32-byte Ed25519 private key (will be converted internally)
91
+ * @param pubKey - 32-byte Ed25519 public key (will be converted to X25519)
92
+ * @returns 32-byte shared secret
93
+ */
94
+ export function x25519Ecdh(privKey: Uint8Array, pubKey: Uint8Array): Uint8Array {
95
+ // Convert Ed25519 pub to X25519 u-coordinate
96
+ const x25519Pub = ed25519.utils.toMontgomery(pubKey);
97
+
98
+ // Convert Ed25519 private key to X25519 scalar
99
+ const x25519Priv = ed25519.utils.toMontgomerySecret(privKey);
100
+
101
+ return x25519.getSharedSecret(x25519Priv, x25519Pub);
102
+ }
103
+
104
+ /**
105
+ * Convert an Ed25519 private key to its X25519 (Montgomery) scalar.
106
+ *
107
+ * Uses the same clamped-scalar derivation as `x25519Ecdh` internally.
108
+ * The returned 32 bytes are a valid X25519 private key suitable for
109
+ * `x25519.getSharedSecret` or `x25519.getPublicKey`.
110
+ */
111
+ export function ed25519PrivToX25519(edPriv: Uint8Array): Uint8Array {
112
+ return ed25519.utils.toMontgomerySecret(edPriv);
113
+ }
114
+
115
+ /**
116
+ * Derive an X25519 public key from a raw X25519 private scalar.
117
+ */
118
+ export function x25519PubFromPriv(x25519Priv: Uint8Array): Uint8Array {
119
+ return x25519.getPublicKey(x25519Priv);
120
+ }
121
+
122
+ /**
123
+ * Perform X25519 ECDH with raw X25519 keys (no Ed25519 conversion).
124
+ *
125
+ * @param x25519Priv - 32-byte X25519 private scalar (from `ed25519PrivToX25519` or `x25519PubFromPriv`)
126
+ * @param x25519Pub - 32-byte X25519 public key (u-coordinate)
127
+ * @returns 32-byte shared secret
128
+ */
129
+ export function x25519EcdhRaw(x25519Priv: Uint8Array, x25519Pub: Uint8Array): Uint8Array {
130
+ return x25519.getSharedSecret(x25519Priv, x25519Pub);
131
+ }
132
+
133
+ // =============================================================================
134
+ // Amount Encryption/Decryption
135
+ // =============================================================================
136
+
137
+ /**
138
+ * Derive an 8-byte encryption key from X25519 shared secret
139
+ *
140
+ * @param sharedSecret - 32-byte X25519 shared secret
141
+ * @returns 8-byte encryption key
142
+ */
143
+ export function deriveAmountKey(sharedSecret: Uint8Array): Uint8Array {
144
+ const hash = sha256(sharedSecret);
145
+ return hash.slice(0, 8);
146
+ }
147
+
148
+ /**
149
+ * Encrypt amount with XOR using shared secret
150
+ *
151
+ * @param amount - Amount in satoshis
152
+ * @param sharedSecret - 32-byte X25519 shared secret
153
+ * @returns 8-byte encrypted amount
154
+ */
155
+ export function encryptAmountEd25519(amount: bigint, sharedSecret: Uint8Array): Uint8Array {
156
+ const key = deriveAmountKey(sharedSecret);
157
+ const amountBytes = new Uint8Array(8);
158
+
159
+ let temp = amount;
160
+ for (let i = 0; i < 8; i++) {
161
+ amountBytes[i] = Number(temp & 0xffn);
162
+ temp >>= 8n;
163
+ }
164
+
165
+ const encrypted = new Uint8Array(8);
166
+ for (let i = 0; i < 8; i++) {
167
+ encrypted[i] = amountBytes[i] ^ key[i];
168
+ }
169
+
170
+ return encrypted;
171
+ }
172
+
173
+ /**
174
+ * Decrypt amount with XOR using shared secret
175
+ *
176
+ * @param encryptedAmount - 8-byte encrypted amount
177
+ * @param sharedSecret - 32-byte X25519 shared secret
178
+ * @returns Decrypted amount in satoshis
179
+ */
180
+ export function decryptAmountEd25519(encryptedAmount: Uint8Array, sharedSecret: Uint8Array): bigint {
181
+ const key = deriveAmountKey(sharedSecret);
182
+
183
+ const decrypted = new Uint8Array(8);
184
+ for (let i = 0; i < 8; i++) {
185
+ decrypted[i] = encryptedAmount[i] ^ key[i];
186
+ }
187
+
188
+ let amount = 0n;
189
+ for (let i = 7; i >= 0; i--) {
190
+ amount = (amount << 8n) | BigInt(decrypted[i]);
191
+ }
192
+
193
+ return amount;
194
+ }
195
+
196
+ // ============================================================================
197
+ // Combined Note Data Encryption (token_id + amount)
198
+ // ============================================================================
199
+
200
+ /**
201
+ * Derive a 40-byte keystream for encrypting token_id(32) + amount(8).
202
+ * Uses two SHA-256 hashes with domain separation to get enough key material.
203
+ *
204
+ * keystream[0..32] = sha256(sharedSecret || 0x00) — for token_id
205
+ * keystream[32..40] = sha256(sharedSecret || 0x01)[0..8] — for amount
206
+ */
207
+ function deriveNoteDataKeystream(sharedSecret: Uint8Array): Uint8Array {
208
+ const buf0 = new Uint8Array(sharedSecret.length + 1);
209
+ buf0.set(sharedSecret);
210
+ buf0[sharedSecret.length] = 0x00;
211
+
212
+ const buf1 = new Uint8Array(sharedSecret.length + 1);
213
+ buf1.set(sharedSecret);
214
+ buf1[sharedSecret.length] = 0x01;
215
+
216
+ const key0 = sha256(buf0); // 32 bytes for token_id
217
+ const key1 = sha256(buf1); // 32 bytes, take first 8 for amount
218
+
219
+ const keystream = new Uint8Array(40);
220
+ keystream.set(key0, 0);
221
+ keystream.set(key1.slice(0, 8), 32);
222
+ return keystream;
223
+ }
224
+
225
+ /**
226
+ * Encrypt note data: token_id(32 bytes, big-endian) || amount(8 bytes, LE)
227
+ *
228
+ * @param tokenId - Token identifier as bigint
229
+ * @param amount - Amount in token's native units
230
+ * @param sharedSecret - 32-byte X25519 shared secret
231
+ * @returns 40-byte encrypted blob
232
+ */
233
+ export function encryptNoteData(
234
+ tokenId: bigint,
235
+ amount: bigint,
236
+ sharedSecret: Uint8Array,
237
+ ): Uint8Array {
238
+ const keystream = deriveNoteDataKeystream(sharedSecret);
239
+ const plaintext = new Uint8Array(40);
240
+
241
+ // token_id: 32 bytes big-endian
242
+ let t = tokenId;
243
+ for (let i = 31; i >= 0; i--) {
244
+ plaintext[i] = Number(t & 0xffn);
245
+ t >>= 8n;
246
+ }
247
+
248
+ // amount: 8 bytes little-endian
249
+ let a = amount;
250
+ for (let i = 0; i < 8; i++) {
251
+ plaintext[32 + i] = Number(a & 0xffn);
252
+ a >>= 8n;
253
+ }
254
+
255
+ // XOR
256
+ const encrypted = new Uint8Array(40);
257
+ for (let i = 0; i < 40; i++) {
258
+ encrypted[i] = plaintext[i] ^ keystream[i];
259
+ }
260
+ return encrypted;
261
+ }
262
+
263
+ /**
264
+ * Decrypt note data: extracts token_id and amount from 40-byte encrypted blob.
265
+ *
266
+ * @param encryptedData - 40-byte encrypted blob
267
+ * @param sharedSecret - 32-byte X25519 shared secret
268
+ * @returns { tokenId, amount }
269
+ */
270
+ export function decryptNoteData(
271
+ encryptedData: Uint8Array,
272
+ sharedSecret: Uint8Array,
273
+ ): { tokenId: bigint; amount: bigint } {
274
+ if (encryptedData.length < 40) {
275
+ throw new Error(`Expected 40-byte encrypted note data, got ${encryptedData.length}`);
276
+ }
277
+
278
+ const keystream = deriveNoteDataKeystream(sharedSecret);
279
+ const plaintext = new Uint8Array(40);
280
+ for (let i = 0; i < 40; i++) {
281
+ plaintext[i] = encryptedData[i] ^ keystream[i];
282
+ }
283
+
284
+ // token_id: 32 bytes big-endian → bigint
285
+ let tokenId = 0n;
286
+ for (let i = 0; i < 32; i++) {
287
+ tokenId = (tokenId << 8n) | BigInt(plaintext[i]);
288
+ }
289
+
290
+ // amount: 8 bytes little-endian → bigint
291
+ let amount = 0n;
292
+ for (let i = 7; i >= 0; i--) {
293
+ amount = (amount << 8n) | BigInt(plaintext[32 + i]);
294
+ }
295
+
296
+ return { tokenId, amount };
297
+ }