@hula-privacy/mixer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Type definitions for Hula Privacy SDK
3
+ */
4
+
5
+ import type { PublicKey, Keypair, Connection } from "@solana/web3.js";
6
+
7
+ // ============================================================================
8
+ // Cryptographic Types
9
+ // ============================================================================
10
+
11
+ /**
12
+ * UTXO (Unspent Transaction Output) in the privacy pool
13
+ */
14
+ export interface UTXO {
15
+ /** Amount in raw token units */
16
+ value: bigint;
17
+ /** Token mint address as bigint */
18
+ mintTokenAddress: bigint;
19
+ /** Owner hash (derived from spending key) */
20
+ owner: bigint;
21
+ /** Random blinding factor */
22
+ secret: bigint;
23
+ /** Poseidon hash commitment */
24
+ commitment: bigint;
25
+ /** Index in the merkle tree */
26
+ leafIndex: number;
27
+ /** Which tree this UTXO is in */
28
+ treeIndex: number;
29
+ }
30
+
31
+ /**
32
+ * Serializable UTXO for storage/transmission
33
+ */
34
+ export interface SerializableUTXO {
35
+ value: string;
36
+ mintTokenAddress: string;
37
+ owner: string;
38
+ secret: string;
39
+ commitment: string;
40
+ leafIndex: number;
41
+ treeIndex: number;
42
+ /** Token mint public key */
43
+ mint: string;
44
+ /** Whether this UTXO has been spent */
45
+ spent: boolean;
46
+ /** Transaction that spent this UTXO */
47
+ spentTx?: string;
48
+ /** Transaction that created this UTXO */
49
+ createdTx: string;
50
+ /** Timestamp when created */
51
+ createdAt: string;
52
+ }
53
+
54
+ /**
55
+ * Wallet keys derived from spending key
56
+ */
57
+ export interface WalletKeys {
58
+ /** Master spending key - KEEP SECRET */
59
+ spendingKey: bigint;
60
+ /** Owner hash - public identifier for receiving funds */
61
+ owner: bigint;
62
+ /** Viewing key - for decrypting notes */
63
+ viewingKey: bigint;
64
+ /** Nullifier key - for computing nullifiers */
65
+ nullifierKey: bigint;
66
+ /** X25519 keypair for note encryption */
67
+ encryptionKeyPair: {
68
+ publicKey: Uint8Array;
69
+ secretKey: Uint8Array;
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Encrypted note attached to transactions
75
+ */
76
+ export interface EncryptedNote {
77
+ /** Ephemeral public key for ECDH */
78
+ ephemeralPubKey: Uint8Array;
79
+ /** Encrypted ciphertext */
80
+ ciphertext: Uint8Array;
81
+ /** Nonce for decryption */
82
+ nonce: Uint8Array;
83
+ }
84
+
85
+ // ============================================================================
86
+ // Merkle Tree Types
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Merkle path for proving inclusion
91
+ */
92
+ export interface MerklePath {
93
+ /** Sibling hashes at each level */
94
+ pathElements: bigint[];
95
+ /** Direction at each level (0 = left, 1 = right) */
96
+ pathIndices: number[];
97
+ /** Root of the tree */
98
+ root: bigint;
99
+ }
100
+
101
+ /**
102
+ * Leaf data from the relayer
103
+ */
104
+ export interface LeafData {
105
+ id: string;
106
+ treeIndex: number;
107
+ leafIndex: number;
108
+ commitment: string;
109
+ slot: string;
110
+ createdAt: string;
111
+ }
112
+
113
+ // ============================================================================
114
+ // Transaction Types
115
+ // ============================================================================
116
+
117
+ /**
118
+ * Circuit inputs for ZK proof generation
119
+ */
120
+ export interface CircuitInputs {
121
+ // Public inputs
122
+ merkleRoot: string;
123
+ nullifiers: string[];
124
+ outputCommitments: string[];
125
+ publicDeposit: string;
126
+ publicWithdraw: string;
127
+ recipient: string;
128
+ mintTokenAddress: string;
129
+ fee: string;
130
+
131
+ // Private inputs for input UTXOs
132
+ inputSpendingKeys: string[];
133
+ inputValues: string[];
134
+ inputSecrets: string[];
135
+ inputLeafIndices: string[];
136
+ inputPathElements: string[][];
137
+ inputPathIndices: number[][];
138
+
139
+ // Private inputs for output UTXOs
140
+ outputValues: string[];
141
+ outputOwners: string[];
142
+ outputSecrets: string[];
143
+ }
144
+
145
+ /**
146
+ * Public inputs for the on-chain verifier
147
+ */
148
+ export interface PublicInputs {
149
+ merkleRoot: number[];
150
+ nullifiers: number[][];
151
+ outputCommitments: number[][];
152
+ publicDeposit: bigint;
153
+ publicWithdraw: bigint;
154
+ recipient: PublicKey;
155
+ mintTokenAddress: PublicKey;
156
+ fee: bigint;
157
+ }
158
+
159
+ /**
160
+ * Output UTXO specification
161
+ */
162
+ export interface OutputSpec {
163
+ /** Owner hash (bigint) or "self" for own wallet */
164
+ owner: bigint | "self";
165
+ /** Amount in raw token units */
166
+ amount: bigint;
167
+ /** Encryption public key for encrypted note (optional) */
168
+ encryptionPubKey?: Uint8Array;
169
+ }
170
+
171
+ /**
172
+ * Transaction request for building a privacy transaction
173
+ */
174
+ export interface TransactionRequest {
175
+ /** Amount to deposit from public wallet */
176
+ depositAmount?: bigint;
177
+ /** Input UTXOs to spend */
178
+ inputUtxos?: UTXO[];
179
+ /** Output UTXOs to create */
180
+ outputs?: OutputSpec[];
181
+ /** Amount to withdraw to public wallet */
182
+ withdrawAmount?: bigint;
183
+ /** Recipient public key for withdrawal */
184
+ recipient?: PublicKey;
185
+ /** Relayer fee */
186
+ fee?: bigint;
187
+ /** Token mint address */
188
+ mint: PublicKey;
189
+ }
190
+
191
+ /**
192
+ * Built transaction ready for submission
193
+ */
194
+ export interface BuiltTransaction {
195
+ /** Proof bytes (256 bytes) */
196
+ proof: Uint8Array;
197
+ /** Public inputs for the circuit */
198
+ publicInputs: PublicInputs;
199
+ /** Encrypted notes for outputs */
200
+ encryptedNotes: Buffer[];
201
+ /** Created output UTXOs (for tracking) */
202
+ outputUtxos: UTXO[];
203
+ /** Nullifiers being spent */
204
+ nullifiers: bigint[];
205
+ /** Input tree index */
206
+ inputTreeIndex: number;
207
+ /** Output tree index (current tree) */
208
+ outputTreeIndex: number;
209
+ }
210
+
211
+ // ============================================================================
212
+ // Relayer API Types
213
+ // ============================================================================
214
+
215
+ /**
216
+ * Paginated response from relayer
217
+ */
218
+ export interface PaginatedResponse<T> {
219
+ items: T[];
220
+ nextCursor: string | null;
221
+ hasMore: boolean;
222
+ }
223
+
224
+ /**
225
+ * Pool data from relayer
226
+ */
227
+ export interface PoolData {
228
+ authority: string;
229
+ commitmentCount: string;
230
+ merkleRoot: string;
231
+ paused: boolean;
232
+ currentTreeIndex: number;
233
+ lastSlot: string;
234
+ }
235
+
236
+ /**
237
+ * Tree data from relayer
238
+ */
239
+ export interface TreeData {
240
+ id: string;
241
+ treeIndex: number;
242
+ nextIndex: number;
243
+ root: string;
244
+ }
245
+
246
+ /**
247
+ * Transaction data from relayer
248
+ */
249
+ export interface TransactionData {
250
+ id: string;
251
+ signature: string;
252
+ slot: string;
253
+ blockTime?: string;
254
+ payer: string;
255
+ mint: string;
256
+ outputTreeIndex: number;
257
+ publicDeposit: string;
258
+ publicWithdraw: string;
259
+ fee: string;
260
+ success: boolean;
261
+ nullifiers: string[];
262
+ encryptedNotes: { noteIndex: number; data: string }[];
263
+ }
264
+
265
+ /**
266
+ * Encrypted note data from relayer
267
+ */
268
+ export interface NoteData {
269
+ id: string;
270
+ noteIndex: number;
271
+ encryptedData: string;
272
+ txSignature: string;
273
+ slot: string;
274
+ mint: string;
275
+ treeIndex: number;
276
+ }
277
+
278
+ /**
279
+ * Stats from relayer
280
+ */
281
+ export interface StatsData {
282
+ pool: {
283
+ commitmentCount: string;
284
+ currentTreeIndex: number;
285
+ paused: boolean;
286
+ } | null;
287
+ treeCount: number;
288
+ leafCount: number;
289
+ transactionCount: number;
290
+ nullifierCount: number;
291
+ }
292
+
293
+ // ============================================================================
294
+ // SDK Config Types
295
+ // ============================================================================
296
+
297
+ /**
298
+ * SDK configuration
299
+ */
300
+ export interface HulaSDKConfig {
301
+ /** Solana RPC endpoint */
302
+ rpcUrl: string;
303
+ /** Relayer API endpoint */
304
+ relayerUrl: string;
305
+ /** Path to circuit WASM file */
306
+ circuitWasmPath?: string;
307
+ /** Path to circuit zkey file */
308
+ circuitZkeyPath?: string;
309
+ /** Network: localnet | devnet | mainnet */
310
+ network?: "localnet" | "devnet" | "mainnet";
311
+ }
312
+
313
+ /**
314
+ * Wallet sync progress
315
+ */
316
+ export interface SyncProgress {
317
+ stage: "notes" | "nullifiers" | "complete";
318
+ current: number;
319
+ total: number;
320
+ }
321
+
322
+ /**
323
+ * Wallet sync result
324
+ */
325
+ export interface SyncResult {
326
+ newUtxos: number;
327
+ spentUtxos: number;
328
+ errors: string[];
329
+ }
330
+
331
+
package/src/utxo.ts ADDED
@@ -0,0 +1,358 @@
1
+ /**
2
+ * UTXO (Unspent Transaction Output) management
3
+ *
4
+ * Provides functions for creating, scanning, and managing UTXOs
5
+ */
6
+
7
+ import { getRelayerClient } from "./api";
8
+ import {
9
+ poseidonHash,
10
+ decryptNote,
11
+ deserializeEncryptedNote,
12
+ generateSpendingKey,
13
+ hexToBytes,
14
+ } from "./crypto";
15
+ import { DOMAIN_NULLIFIER } from "./constants";
16
+ import type { UTXO, WalletKeys, SerializableUTXO, NoteData } from "./types";
17
+
18
+ // ============================================================================
19
+ // Commitment & Nullifier Computation
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Compute UTXO commitment
24
+ * commitment = Poseidon(value, mintTokenAddress, owner, secret)
25
+ */
26
+ export function computeCommitment(
27
+ value: bigint,
28
+ mintTokenAddress: bigint,
29
+ owner: bigint,
30
+ secret: bigint
31
+ ): bigint {
32
+ return poseidonHash([value, mintTokenAddress, owner, secret]);
33
+ }
34
+
35
+ /**
36
+ * Compute nullifier for spending a UTXO
37
+ * nullifier = Poseidon(nullifierKey, commitment, leafIndex)
38
+ */
39
+ export function computeNullifier(
40
+ spendingKey: bigint,
41
+ commitment: bigint,
42
+ leafIndex: number
43
+ ): bigint {
44
+ const nullifierKey = poseidonHash([spendingKey, DOMAIN_NULLIFIER]);
45
+ return poseidonHash([nullifierKey, commitment, BigInt(leafIndex)]);
46
+ }
47
+
48
+ /**
49
+ * Compute nullifier from wallet keys
50
+ */
51
+ export function computeNullifierFromKeys(
52
+ keys: WalletKeys,
53
+ commitment: bigint,
54
+ leafIndex: number
55
+ ): bigint {
56
+ return poseidonHash([keys.nullifierKey, commitment, BigInt(leafIndex)]);
57
+ }
58
+
59
+ // ============================================================================
60
+ // UTXO Creation
61
+ // ============================================================================
62
+
63
+ /**
64
+ * Create a new UTXO with random secret
65
+ */
66
+ export function createUTXO(
67
+ value: bigint,
68
+ mintTokenAddress: bigint,
69
+ owner: bigint,
70
+ leafIndex: number,
71
+ treeIndex: number = 0
72
+ ): UTXO {
73
+ const secret = generateSpendingKey(); // Random blinding factor
74
+ const commitment = computeCommitment(value, mintTokenAddress, owner, secret);
75
+
76
+ return {
77
+ value,
78
+ mintTokenAddress,
79
+ owner,
80
+ secret,
81
+ commitment,
82
+ leafIndex,
83
+ treeIndex,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Create a dummy (zero) UTXO for padding
89
+ */
90
+ export function createDummyUTXO(): UTXO {
91
+ return {
92
+ value: 0n,
93
+ mintTokenAddress: 0n,
94
+ owner: 0n,
95
+ secret: 0n,
96
+ commitment: 0n,
97
+ leafIndex: 0,
98
+ treeIndex: 0,
99
+ };
100
+ }
101
+
102
+ // ============================================================================
103
+ // UTXO Serialization
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Serialize UTXO for storage
108
+ */
109
+ export function serializeUTXO(
110
+ utxo: UTXO,
111
+ mint: string,
112
+ createdTx: string,
113
+ spent: boolean = false,
114
+ spentTx?: string
115
+ ): SerializableUTXO {
116
+ return {
117
+ value: utxo.value.toString(),
118
+ mintTokenAddress: utxo.mintTokenAddress.toString(),
119
+ owner: utxo.owner.toString(),
120
+ secret: utxo.secret.toString(),
121
+ commitment: utxo.commitment.toString(),
122
+ leafIndex: utxo.leafIndex,
123
+ treeIndex: utxo.treeIndex,
124
+ mint,
125
+ spent,
126
+ spentTx,
127
+ createdTx,
128
+ createdAt: new Date().toISOString(),
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Deserialize UTXO from storage
134
+ */
135
+ export function deserializeUTXO(data: SerializableUTXO): UTXO {
136
+ return {
137
+ value: BigInt(data.value),
138
+ mintTokenAddress: BigInt(data.mintTokenAddress),
139
+ owner: BigInt(data.owner),
140
+ secret: BigInt(data.secret),
141
+ commitment: BigInt(data.commitment),
142
+ leafIndex: data.leafIndex,
143
+ treeIndex: data.treeIndex,
144
+ };
145
+ }
146
+
147
+ // ============================================================================
148
+ // UTXO Scanning
149
+ // ============================================================================
150
+
151
+ /**
152
+ * Scan encrypted notes for UTXOs belonging to this wallet
153
+ *
154
+ * This attempts to decrypt each note with the wallet's encryption key.
155
+ * If successful, the note belongs to this wallet.
156
+ */
157
+ export function scanNotesForUTXOs(
158
+ notes: Array<{
159
+ encryptedData: string;
160
+ treeIndex: number;
161
+ mint: string;
162
+ txSignature: string;
163
+ }>,
164
+ walletKeys: WalletKeys,
165
+ commitments: Map<number, Map<number, bigint>> // treeIndex -> leafIndex -> commitment
166
+ ): SerializableUTXO[] {
167
+ const foundUTXOs: SerializableUTXO[] = [];
168
+
169
+ for (const note of notes) {
170
+ try {
171
+ // Parse encrypted note from hex
172
+ const noteBytes = hexToBytes(note.encryptedData);
173
+ const encryptedNote = deserializeEncryptedNote(noteBytes);
174
+
175
+ // Try to decrypt
176
+ const decrypted = decryptNote(encryptedNote, walletKeys.encryptionKeyPair.secretKey);
177
+ if (!decrypted) continue;
178
+
179
+ // Successfully decrypted - this is our UTXO
180
+ const commitment = computeCommitment(
181
+ decrypted.value,
182
+ decrypted.mintTokenAddress,
183
+ walletKeys.owner,
184
+ decrypted.secret
185
+ );
186
+
187
+ // Verify commitment exists on-chain (optional but recommended)
188
+ const treeCommitments = commitments.get(note.treeIndex);
189
+ if (treeCommitments) {
190
+ const onChainCommitment = treeCommitments.get(decrypted.leafIndex);
191
+ if (onChainCommitment !== undefined && onChainCommitment !== commitment) {
192
+ console.warn(`Commitment mismatch for leaf ${decrypted.leafIndex} in tree ${note.treeIndex}`);
193
+ continue;
194
+ }
195
+ }
196
+
197
+ foundUTXOs.push({
198
+ value: decrypted.value.toString(),
199
+ mintTokenAddress: decrypted.mintTokenAddress.toString(),
200
+ owner: walletKeys.owner.toString(),
201
+ secret: decrypted.secret.toString(),
202
+ commitment: commitment.toString(),
203
+ leafIndex: decrypted.leafIndex,
204
+ treeIndex: note.treeIndex,
205
+ mint: note.mint,
206
+ spent: false,
207
+ createdTx: note.txSignature,
208
+ createdAt: new Date().toISOString(),
209
+ });
210
+ } catch (err) {
211
+ // Failed to decrypt - not our note, skip
212
+ }
213
+ }
214
+
215
+ return foundUTXOs;
216
+ }
217
+
218
+ /**
219
+ * Sync wallet UTXOs from relayer
220
+ *
221
+ * This fetches all encrypted notes and attempts to decrypt them.
222
+ * Also checks which UTXOs have been spent.
223
+ */
224
+ export async function syncUTXOs(
225
+ walletKeys: WalletKeys,
226
+ existingUTXOs: SerializableUTXO[],
227
+ relayerUrl?: string,
228
+ afterSlot?: string
229
+ ): Promise<{
230
+ newUTXOs: SerializableUTXO[];
231
+ spentUTXOs: string[]; // IDs of UTXOs that are now spent
232
+ }> {
233
+ const client = getRelayerClient(relayerUrl);
234
+
235
+ // Fetch all notes since last sync
236
+ const notes = await client.getAllNotes(afterSlot);
237
+
238
+ // Build commitment map for verification (optional)
239
+ const pool = await client.getPool();
240
+ const commitments = new Map<number, Map<number, bigint>>();
241
+
242
+ // For each tree, fetch commitments
243
+ for (let treeIndex = 0; treeIndex <= pool.currentTreeIndex; treeIndex++) {
244
+ const leaves = await client.getCommitmentsForTree(treeIndex);
245
+ const treeMap = new Map<number, bigint>();
246
+ leaves.forEach((commitment, index) => {
247
+ treeMap.set(index, commitment);
248
+ });
249
+ commitments.set(treeIndex, treeMap);
250
+ }
251
+
252
+ // Scan notes for new UTXOs
253
+ const newUTXOs = scanNotesForUTXOs(
254
+ notes.map(n => ({
255
+ encryptedData: n.encryptedData,
256
+ treeIndex: n.treeIndex,
257
+ mint: n.mint,
258
+ txSignature: n.txSignature,
259
+ })),
260
+ walletKeys,
261
+ commitments
262
+ );
263
+
264
+ // Check which existing UTXOs have been spent
265
+ const unspentUTXOs = existingUTXOs.filter(u => !u.spent);
266
+ const nullifiersToCheck: string[] = [];
267
+ const utxoByNullifier = new Map<string, string>();
268
+
269
+ for (const utxo of unspentUTXOs) {
270
+ const commitment = BigInt(utxo.commitment);
271
+ const nullifier = computeNullifier(
272
+ walletKeys.spendingKey,
273
+ commitment,
274
+ utxo.leafIndex
275
+ );
276
+ const nullifierStr = nullifier.toString();
277
+ nullifiersToCheck.push(nullifierStr);
278
+ utxoByNullifier.set(nullifierStr, utxo.commitment); // Use commitment as ID
279
+ }
280
+
281
+ const spentUTXOs: string[] = [];
282
+
283
+ if (nullifiersToCheck.length > 0) {
284
+ const batchSize = 50;
285
+ for (let i = 0; i < nullifiersToCheck.length; i += batchSize) {
286
+ const batch = nullifiersToCheck.slice(i, i + batchSize);
287
+ const result = await client.checkNullifiersBatch(batch);
288
+
289
+ for (const r of result.results) {
290
+ if (r.spent) {
291
+ const utxoId = utxoByNullifier.get(r.nullifier);
292
+ if (utxoId) {
293
+ spentUTXOs.push(utxoId);
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ return { newUTXOs, spentUTXOs };
301
+ }
302
+
303
+ // ============================================================================
304
+ // UTXO Selection
305
+ // ============================================================================
306
+
307
+ /**
308
+ * Select UTXOs for spending (greedy algorithm)
309
+ *
310
+ * Selects the largest UTXOs first until the target amount is reached.
311
+ */
312
+ export function selectUTXOs(
313
+ utxos: UTXO[],
314
+ targetAmount: bigint,
315
+ mint?: bigint
316
+ ): UTXO[] {
317
+ // Filter by mint if specified
318
+ const filtered = mint !== undefined
319
+ ? utxos.filter(u => u.mintTokenAddress === mint)
320
+ : utxos;
321
+
322
+ // Sort by value descending
323
+ const sorted = [...filtered].sort((a, b) => {
324
+ if (a.value > b.value) return -1;
325
+ if (a.value < b.value) return 1;
326
+ return 0;
327
+ });
328
+
329
+ const selected: UTXO[] = [];
330
+ let total = 0n;
331
+
332
+ for (const utxo of sorted) {
333
+ if (total >= targetAmount) break;
334
+ selected.push(utxo);
335
+ total += utxo.value;
336
+ }
337
+
338
+ if (total < targetAmount) {
339
+ throw new Error(
340
+ `Insufficient balance. Need ${targetAmount}, have ${total}`
341
+ );
342
+ }
343
+
344
+ return selected;
345
+ }
346
+
347
+ /**
348
+ * Calculate total balance of UTXOs
349
+ */
350
+ export function calculateBalance(utxos: UTXO[], mint?: bigint): bigint {
351
+ const filtered = mint !== undefined
352
+ ? utxos.filter(u => u.mintTokenAddress === mint)
353
+ : utxos;
354
+
355
+ return filtered.reduce((sum, u) => sum + u.value, 0n);
356
+ }
357
+
358
+