@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/wallet.ts ADDED
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Wallet management for Hula Privacy Protocol
3
+ *
4
+ * High-level wallet abstraction that manages keys, UTXOs, and syncing
5
+ */
6
+
7
+ import { PublicKey, Connection, Keypair, type TransactionInstruction } from "@solana/web3.js";
8
+ import type { Program } from "@coral-xyz/anchor";
9
+
10
+ import {
11
+ initPoseidon,
12
+ isPoseidonInitialized,
13
+ deriveKeys,
14
+ generateSpendingKey,
15
+ deriveSpendingKeyFromSignature,
16
+ pubkeyToBigInt,
17
+ } from "./crypto";
18
+ import { syncUTXOs, deserializeUTXO, selectUTXOs, calculateBalance } from "./utxo";
19
+ import { buildTransaction, buildTransactionAccounts, toAnchorPublicInputs } from "./transaction";
20
+ import { getRelayerClient, setDefaultRelayerUrl } from "./api";
21
+ import type {
22
+ WalletKeys,
23
+ UTXO,
24
+ SerializableUTXO,
25
+ HulaSDKConfig,
26
+ TransactionRequest,
27
+ SyncProgress,
28
+ SyncResult,
29
+ } from "./types";
30
+
31
+ // ============================================================================
32
+ // HulaWallet Class
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Hula Privacy Wallet
37
+ *
38
+ * High-level abstraction for managing privacy transactions
39
+ */
40
+ export class HulaWallet {
41
+ private connection: Connection;
42
+ private relayerUrl: string;
43
+ private keys: WalletKeys;
44
+ private utxos: SerializableUTXO[] = [];
45
+ private lastSyncedSlot: string = "0";
46
+ private initialized: boolean = false;
47
+
48
+ private constructor(
49
+ connection: Connection,
50
+ relayerUrl: string,
51
+ keys: WalletKeys
52
+ ) {
53
+ this.connection = connection;
54
+ this.relayerUrl = relayerUrl;
55
+ this.keys = keys;
56
+ }
57
+
58
+ /**
59
+ * Create a new wallet with a random spending key
60
+ */
61
+ static async create(config: HulaSDKConfig): Promise<HulaWallet> {
62
+ await initPoseidon();
63
+
64
+ const connection = new Connection(config.rpcUrl, "confirmed");
65
+ const relayerUrl = config.relayerUrl;
66
+ setDefaultRelayerUrl(relayerUrl);
67
+
68
+ const spendingKey = generateSpendingKey();
69
+ const keys = deriveKeys(spendingKey);
70
+
71
+ const wallet = new HulaWallet(connection, relayerUrl, keys);
72
+ wallet.initialized = true;
73
+ return wallet;
74
+ }
75
+
76
+ /**
77
+ * Create a wallet from an existing spending key
78
+ */
79
+ static async fromSpendingKey(
80
+ spendingKey: bigint,
81
+ config: HulaSDKConfig
82
+ ): Promise<HulaWallet> {
83
+ await initPoseidon();
84
+
85
+ const connection = new Connection(config.rpcUrl, "confirmed");
86
+ const relayerUrl = config.relayerUrl;
87
+ setDefaultRelayerUrl(relayerUrl);
88
+
89
+ const keys = deriveKeys(spendingKey);
90
+
91
+ const wallet = new HulaWallet(connection, relayerUrl, keys);
92
+ wallet.initialized = true;
93
+ return wallet;
94
+ }
95
+
96
+ /**
97
+ * Create a wallet from a Solana signature (deterministic derivation)
98
+ *
99
+ * This allows recovery as long as the user can sign with their Solana wallet.
100
+ */
101
+ static async fromSignature(
102
+ signature: Uint8Array,
103
+ config: HulaSDKConfig
104
+ ): Promise<HulaWallet> {
105
+ await initPoseidon();
106
+
107
+ const connection = new Connection(config.rpcUrl, "confirmed");
108
+ const relayerUrl = config.relayerUrl;
109
+ setDefaultRelayerUrl(relayerUrl);
110
+
111
+ const spendingKey = deriveSpendingKeyFromSignature(signature);
112
+ const keys = deriveKeys(spendingKey);
113
+
114
+ const wallet = new HulaWallet(connection, relayerUrl, keys);
115
+ wallet.initialized = true;
116
+ return wallet;
117
+ }
118
+
119
+ // ============================================================================
120
+ // Getters
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Get wallet's owner hash (public identifier)
125
+ */
126
+ get owner(): bigint {
127
+ return this.keys.owner;
128
+ }
129
+
130
+ /**
131
+ * Get wallet's owner hash as hex string
132
+ */
133
+ get ownerHex(): string {
134
+ return this.keys.owner.toString(16).padStart(64, "0");
135
+ }
136
+
137
+ /**
138
+ * Get wallet's encryption public key
139
+ */
140
+ get encryptionPublicKey(): Uint8Array {
141
+ return this.keys.encryptionKeyPair.publicKey;
142
+ }
143
+
144
+ /**
145
+ * Get spending key (CAREFUL - this is sensitive!)
146
+ */
147
+ get spendingKey(): bigint {
148
+ return this.keys.spendingKey;
149
+ }
150
+
151
+ /**
152
+ * Get all wallet keys
153
+ */
154
+ get walletKeys(): WalletKeys {
155
+ return this.keys;
156
+ }
157
+
158
+ // ============================================================================
159
+ // UTXO Management
160
+ // ============================================================================
161
+
162
+ /**
163
+ * Sync wallet with relayer
164
+ *
165
+ * Fetches new encrypted notes and checks for spent UTXOs.
166
+ */
167
+ async sync(onProgress?: (progress: SyncProgress) => void): Promise<SyncResult> {
168
+ if (!this.initialized) {
169
+ throw new Error("Wallet not initialized");
170
+ }
171
+
172
+ onProgress?.({ stage: "notes", current: 0, total: 100 });
173
+
174
+ try {
175
+ const { newUTXOs, spentUTXOs } = await syncUTXOs(
176
+ this.keys,
177
+ this.utxos,
178
+ this.relayerUrl,
179
+ this.lastSyncedSlot !== "0" ? this.lastSyncedSlot : undefined
180
+ );
181
+
182
+ // Add new UTXOs
183
+ this.utxos.push(...newUTXOs);
184
+
185
+ // Mark spent UTXOs
186
+ for (const spentId of spentUTXOs) {
187
+ const utxo = this.utxos.find(u => u.commitment === spentId);
188
+ if (utxo) {
189
+ utxo.spent = true;
190
+ }
191
+ }
192
+
193
+ // Update last synced slot
194
+ const client = getRelayerClient(this.relayerUrl);
195
+ const stats = await client.getStats();
196
+ // Use current state as sync point
197
+ this.lastSyncedSlot = Date.now().toString(); // Simplified
198
+
199
+ onProgress?.({ stage: "complete", current: 100, total: 100 });
200
+
201
+ return {
202
+ newUtxos: newUTXOs.length,
203
+ spentUtxos: spentUTXOs.length,
204
+ errors: [],
205
+ };
206
+ } catch (err) {
207
+ const errorMsg = err instanceof Error ? err.message : "Sync failed";
208
+ onProgress?.({ stage: "complete", current: 100, total: 100 });
209
+ return {
210
+ newUtxos: 0,
211
+ spentUtxos: 0,
212
+ errors: [errorMsg],
213
+ };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get all unspent UTXOs
219
+ */
220
+ getUnspentUTXOs(): UTXO[] {
221
+ return this.utxos
222
+ .filter(u => !u.spent)
223
+ .map(deserializeUTXO);
224
+ }
225
+
226
+ /**
227
+ * Get all UTXOs (including spent)
228
+ */
229
+ getAllUTXOs(): SerializableUTXO[] {
230
+ return [...this.utxos];
231
+ }
232
+
233
+ /**
234
+ * Get balance for a specific mint
235
+ */
236
+ getBalance(mint: PublicKey): bigint {
237
+ const mintBigInt = pubkeyToBigInt(mint);
238
+ const unspent = this.getUnspentUTXOs();
239
+ return calculateBalance(unspent, mintBigInt);
240
+ }
241
+
242
+ /**
243
+ * Import UTXOs (for wallet recovery or manual import)
244
+ */
245
+ importUTXOs(utxos: SerializableUTXO[]): void {
246
+ this.utxos.push(...utxos);
247
+ }
248
+
249
+ /**
250
+ * Export UTXOs for backup
251
+ */
252
+ exportUTXOs(): SerializableUTXO[] {
253
+ return [...this.utxos];
254
+ }
255
+
256
+ // ============================================================================
257
+ // Transaction Building
258
+ // ============================================================================
259
+
260
+ /**
261
+ * Deposit tokens into the privacy pool
262
+ */
263
+ async deposit(
264
+ mint: PublicKey,
265
+ amount: bigint
266
+ ): Promise<{
267
+ transaction: ReturnType<typeof buildTransaction> extends Promise<infer T> ? T : never;
268
+ instructions: TransactionInstruction[];
269
+ }> {
270
+ const tx = await buildTransaction(
271
+ {
272
+ mint,
273
+ depositAmount: amount,
274
+ outputs: [], // Change will be created automatically
275
+ },
276
+ this.keys,
277
+ this.relayerUrl
278
+ );
279
+
280
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
281
+ tx,
282
+ PublicKey.default, // Payer will be set by caller
283
+ mint,
284
+ amount,
285
+ 0n
286
+ );
287
+
288
+ return { transaction: tx, instructions: preInstructions };
289
+ }
290
+
291
+ /**
292
+ * Transfer tokens privately
293
+ */
294
+ async transfer(
295
+ mint: PublicKey,
296
+ amount: bigint,
297
+ recipientOwner: bigint,
298
+ recipientEncryptionPubKey: Uint8Array
299
+ ): Promise<{
300
+ transaction: ReturnType<typeof buildTransaction> extends Promise<infer T> ? T : never;
301
+ instructions: TransactionInstruction[];
302
+ }> {
303
+ const mintBigInt = pubkeyToBigInt(mint);
304
+ const unspent = this.getUnspentUTXOs().filter(
305
+ u => u.mintTokenAddress === mintBigInt
306
+ );
307
+ const inputUtxos = selectUTXOs(unspent, amount, mintBigInt);
308
+
309
+ const tx = await buildTransaction(
310
+ {
311
+ mint,
312
+ inputUtxos,
313
+ outputs: [
314
+ {
315
+ owner: recipientOwner,
316
+ amount,
317
+ encryptionPubKey: recipientEncryptionPubKey,
318
+ },
319
+ ],
320
+ },
321
+ this.keys,
322
+ this.relayerUrl
323
+ );
324
+
325
+ const { preInstructions } = buildTransactionAccounts(
326
+ tx,
327
+ PublicKey.default,
328
+ mint,
329
+ 0n,
330
+ 0n
331
+ );
332
+
333
+ return { transaction: tx, instructions: preInstructions };
334
+ }
335
+
336
+ /**
337
+ * Withdraw tokens from the privacy pool to a public address
338
+ */
339
+ async withdraw(
340
+ mint: PublicKey,
341
+ amount: bigint,
342
+ recipient: PublicKey
343
+ ): Promise<{
344
+ transaction: ReturnType<typeof buildTransaction> extends Promise<infer T> ? T : never;
345
+ instructions: TransactionInstruction[];
346
+ }> {
347
+ const mintBigInt = pubkeyToBigInt(mint);
348
+ const unspent = this.getUnspentUTXOs().filter(
349
+ u => u.mintTokenAddress === mintBigInt
350
+ );
351
+ const inputUtxos = selectUTXOs(unspent, amount, mintBigInt);
352
+
353
+ const tx = await buildTransaction(
354
+ {
355
+ mint,
356
+ inputUtxos,
357
+ withdrawAmount: amount,
358
+ recipient,
359
+ },
360
+ this.keys,
361
+ this.relayerUrl
362
+ );
363
+
364
+ const { preInstructions } = buildTransactionAccounts(
365
+ tx,
366
+ PublicKey.default,
367
+ mint,
368
+ 0n,
369
+ amount,
370
+ recipient
371
+ );
372
+
373
+ return { transaction: tx, instructions: preInstructions };
374
+ }
375
+
376
+ /**
377
+ * Build a custom transaction
378
+ */
379
+ async buildTransaction(request: Omit<TransactionRequest, "mint"> & { mint: PublicKey }): Promise<{
380
+ transaction: ReturnType<typeof buildTransaction> extends Promise<infer T> ? T : never;
381
+ instructions: TransactionInstruction[];
382
+ }> {
383
+ const tx = await buildTransaction(request, this.keys, this.relayerUrl);
384
+
385
+ const { preInstructions } = buildTransactionAccounts(
386
+ tx,
387
+ PublicKey.default,
388
+ request.mint,
389
+ request.depositAmount ?? 0n,
390
+ request.withdrawAmount ?? 0n,
391
+ request.recipient
392
+ );
393
+
394
+ return { transaction: tx, instructions: preInstructions };
395
+ }
396
+
397
+ // ============================================================================
398
+ // Transaction Submission
399
+ // ============================================================================
400
+
401
+ /**
402
+ * Submit a transaction using an Anchor program
403
+ *
404
+ * @param program - Anchor program instance (any type to avoid IDL complexity)
405
+ * @param payer - Keypair for signing
406
+ * @param builtTx - Built transaction from buildTransaction()
407
+ * @param mint - Token mint
408
+ * @param depositAmount - Amount being deposited (if any)
409
+ * @param withdrawAmount - Amount being withdrawn (if any)
410
+ * @param recipient - Recipient for withdrawal (if any)
411
+ */
412
+ async submitTransaction(
413
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
414
+ program: Program<any>,
415
+ payer: Keypair,
416
+ builtTx: Awaited<ReturnType<typeof buildTransaction>>,
417
+ mint: PublicKey,
418
+ depositAmount: bigint = 0n,
419
+ withdrawAmount: bigint = 0n,
420
+ recipient?: PublicKey
421
+ ): Promise<string> {
422
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
423
+ builtTx,
424
+ payer.publicKey,
425
+ mint,
426
+ depositAmount,
427
+ withdrawAmount,
428
+ recipient
429
+ );
430
+
431
+ const publicInputs = toAnchorPublicInputs(builtTx);
432
+
433
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
434
+ const tx = await (program.methods as any)
435
+ .transact(
436
+ inputTreeIndex,
437
+ Array.from(builtTx.proof),
438
+ publicInputs,
439
+ builtTx.encryptedNotes
440
+ )
441
+ .accounts(accounts)
442
+ .preInstructions(preInstructions)
443
+ .signers([payer])
444
+ .rpc();
445
+
446
+ // Mark input UTXOs as spent locally
447
+ for (const _utxo of builtTx.outputUtxos) {
448
+ // Find corresponding input UTXOs and mark spent
449
+ // (In practice, track by commitment)
450
+ }
451
+
452
+ return tx;
453
+ }
454
+ }
455
+
456
+ // ============================================================================
457
+ // Utility Functions
458
+ // ============================================================================
459
+
460
+ /**
461
+ * Create a deterministic message for wallet key derivation
462
+ */
463
+ export function getKeyDerivationMessage(): Uint8Array {
464
+ return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1");
465
+ }
466
+
467
+ /**
468
+ * Quick initialization for SDK usage
469
+ */
470
+ export async function initHulaSDK(): Promise<void> {
471
+ if (!isPoseidonInitialized()) {
472
+ await initPoseidon();
473
+ }
474
+ }
475
+