@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/README.md +290 -0
- package/package.json +52 -0
- package/src/api.ts +276 -0
- package/src/constants.ts +108 -0
- package/src/crypto.ts +293 -0
- package/src/index.ts +185 -0
- package/src/merkle.ts +220 -0
- package/src/proof.ts +251 -0
- package/src/transaction.ts +464 -0
- package/src/types.ts +331 -0
- package/src/utxo.ts +358 -0
- package/src/wallet.ts +475 -0
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
|
+
|