@hula-privacy/mixer 0.2.0 → 0.3.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/dist/index.mjs ADDED
@@ -0,0 +1,2480 @@
1
+ // src/wallet.ts
2
+ import { PublicKey as PublicKey4, Connection, ComputeBudgetProgram } from "@solana/web3.js";
3
+ import { AnchorProvider, Program, Wallet } from "@coral-xyz/anchor";
4
+
5
+ // src/idl.ts
6
+ var HulaPrivacyIdl = {
7
+ "address": "tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA",
8
+ "metadata": {
9
+ "name": "hula_privacy",
10
+ "version": "0.1.0",
11
+ "spec": "0.1.0",
12
+ "description": "Privacy wallet for Solana using ZK proofs"
13
+ },
14
+ "instructions": [
15
+ {
16
+ "name": "initialize_new_tree",
17
+ "docs": [
18
+ "Initialize a new merkle tree when the current one is full"
19
+ ],
20
+ "discriminator": [
21
+ 42,
22
+ 154,
23
+ 29,
24
+ 105,
25
+ 242,
26
+ 162,
27
+ 191,
28
+ 224
29
+ ],
30
+ "accounts": [
31
+ {
32
+ "name": "payer",
33
+ "docs": [
34
+ "The payer for account creation"
35
+ ],
36
+ "writable": true,
37
+ "signer": true
38
+ },
39
+ {
40
+ "name": "pool",
41
+ "docs": [
42
+ "Global privacy pool account"
43
+ ],
44
+ "writable": true,
45
+ "pda": {
46
+ "seeds": [
47
+ {
48
+ "kind": "const",
49
+ "value": [
50
+ 104,
51
+ 117,
52
+ 108,
53
+ 97,
54
+ 95,
55
+ 112,
56
+ 111,
57
+ 111,
58
+ 108
59
+ ]
60
+ }
61
+ ]
62
+ }
63
+ },
64
+ {
65
+ "name": "current_tree",
66
+ "docs": [
67
+ "The current (full) merkle tree"
68
+ ],
69
+ "pda": {
70
+ "seeds": [
71
+ {
72
+ "kind": "const",
73
+ "value": [
74
+ 109,
75
+ 101,
76
+ 114,
77
+ 107,
78
+ 108,
79
+ 101,
80
+ 95,
81
+ 116,
82
+ 114,
83
+ 101,
84
+ 101
85
+ ]
86
+ },
87
+ {
88
+ "kind": "account",
89
+ "path": "pool.current_tree_index",
90
+ "account": "PrivacyPool"
91
+ }
92
+ ]
93
+ }
94
+ },
95
+ {
96
+ "name": "new_tree",
97
+ "docs": [
98
+ "New merkle tree account (PDA with tree_index)"
99
+ ],
100
+ "writable": true,
101
+ "pda": {
102
+ "seeds": [
103
+ {
104
+ "kind": "const",
105
+ "value": [
106
+ 109,
107
+ 101,
108
+ 114,
109
+ 107,
110
+ 108,
111
+ 101,
112
+ 95,
113
+ 116,
114
+ 114,
115
+ 101,
116
+ 101
117
+ ]
118
+ },
119
+ {
120
+ "kind": "arg",
121
+ "path": "tree_index"
122
+ }
123
+ ]
124
+ }
125
+ },
126
+ {
127
+ "name": "system_program",
128
+ "docs": [
129
+ "System program"
130
+ ],
131
+ "address": "11111111111111111111111111111111"
132
+ }
133
+ ],
134
+ "args": [
135
+ {
136
+ "name": "tree_index",
137
+ "type": "u32"
138
+ }
139
+ ]
140
+ },
141
+ {
142
+ "name": "initialize_pool",
143
+ "docs": [
144
+ "Initialize the global privacy pool"
145
+ ],
146
+ "discriminator": [
147
+ 95,
148
+ 180,
149
+ 10,
150
+ 172,
151
+ 84,
152
+ 174,
153
+ 232,
154
+ 40
155
+ ],
156
+ "accounts": [
157
+ {
158
+ "name": "authority",
159
+ "docs": [
160
+ "The authority creating the pool (pays for account creation)"
161
+ ],
162
+ "writable": true,
163
+ "signer": true
164
+ },
165
+ {
166
+ "name": "pool",
167
+ "docs": [
168
+ "Global privacy pool account (PDA)"
169
+ ],
170
+ "writable": true,
171
+ "pda": {
172
+ "seeds": [
173
+ {
174
+ "kind": "const",
175
+ "value": [
176
+ 104,
177
+ 117,
178
+ 108,
179
+ 97,
180
+ 95,
181
+ 112,
182
+ 111,
183
+ 111,
184
+ 108
185
+ ]
186
+ }
187
+ ]
188
+ }
189
+ },
190
+ {
191
+ "name": "merkle_tree",
192
+ "docs": [
193
+ "Merkle tree account (PDA with tree index 0)"
194
+ ],
195
+ "writable": true,
196
+ "pda": {
197
+ "seeds": [
198
+ {
199
+ "kind": "const",
200
+ "value": [
201
+ 109,
202
+ 101,
203
+ 114,
204
+ 107,
205
+ 108,
206
+ 101,
207
+ 95,
208
+ 116,
209
+ 114,
210
+ 101,
211
+ 101
212
+ ]
213
+ },
214
+ {
215
+ "kind": "const",
216
+ "value": [
217
+ 0,
218
+ 0,
219
+ 0,
220
+ 0
221
+ ]
222
+ }
223
+ ]
224
+ }
225
+ },
226
+ {
227
+ "name": "system_program",
228
+ "docs": [
229
+ "System program"
230
+ ],
231
+ "address": "11111111111111111111111111111111"
232
+ }
233
+ ],
234
+ "args": []
235
+ },
236
+ {
237
+ "name": "transact",
238
+ "docs": [
239
+ "Execute a private transaction"
240
+ ],
241
+ "discriminator": [
242
+ 217,
243
+ 149,
244
+ 130,
245
+ 143,
246
+ 221,
247
+ 52,
248
+ 252,
249
+ 119
250
+ ],
251
+ "accounts": [
252
+ {
253
+ "name": "payer",
254
+ "writable": true,
255
+ "signer": true
256
+ },
257
+ {
258
+ "name": "mint"
259
+ },
260
+ {
261
+ "name": "pool",
262
+ "writable": true,
263
+ "pda": {
264
+ "seeds": [
265
+ {
266
+ "kind": "const",
267
+ "value": [
268
+ 104,
269
+ 117,
270
+ 108,
271
+ 97,
272
+ 95,
273
+ 112,
274
+ 111,
275
+ 111,
276
+ 108
277
+ ]
278
+ }
279
+ ]
280
+ }
281
+ },
282
+ {
283
+ "name": "input_tree",
284
+ "docs": [
285
+ "Input tree - where the input UTXOs come from (for merkle root verification)",
286
+ "If None, defaults to current tree"
287
+ ],
288
+ "pda": {
289
+ "seeds": [
290
+ {
291
+ "kind": "const",
292
+ "value": [
293
+ 109,
294
+ 101,
295
+ 114,
296
+ 107,
297
+ 108,
298
+ 101,
299
+ 95,
300
+ 116,
301
+ 114,
302
+ 101,
303
+ 101
304
+ ]
305
+ },
306
+ {
307
+ "kind": "arg",
308
+ "path": "input_tree_index.unwrap_or(pool.current_tree_index)"
309
+ }
310
+ ]
311
+ }
312
+ },
313
+ {
314
+ "name": "merkle_tree",
315
+ "docs": [
316
+ "Output tree - where new commitments will be inserted (current active tree)"
317
+ ],
318
+ "writable": true,
319
+ "pda": {
320
+ "seeds": [
321
+ {
322
+ "kind": "const",
323
+ "value": [
324
+ 109,
325
+ 101,
326
+ 114,
327
+ 107,
328
+ 108,
329
+ 101,
330
+ 95,
331
+ 116,
332
+ 114,
333
+ 101,
334
+ 101
335
+ ]
336
+ },
337
+ {
338
+ "kind": "account",
339
+ "path": "pool.current_tree_index",
340
+ "account": "PrivacyPool"
341
+ }
342
+ ]
343
+ }
344
+ },
345
+ {
346
+ "name": "vault",
347
+ "docs": [
348
+ "Vault for this mint (created if needed)"
349
+ ],
350
+ "writable": true,
351
+ "pda": {
352
+ "seeds": [
353
+ {
354
+ "kind": "const",
355
+ "value": [
356
+ 118,
357
+ 97,
358
+ 117,
359
+ 108,
360
+ 116
361
+ ]
362
+ },
363
+ {
364
+ "kind": "account",
365
+ "path": "mint"
366
+ }
367
+ ]
368
+ }
369
+ },
370
+ {
371
+ "name": "depositor_token_account",
372
+ "docs": [
373
+ "Depositor's token account (optional)"
374
+ ],
375
+ "writable": true,
376
+ "optional": true
377
+ },
378
+ {
379
+ "name": "depositor",
380
+ "signer": true,
381
+ "optional": true
382
+ },
383
+ {
384
+ "name": "recipient_token_account",
385
+ "docs": [
386
+ "Recipient's token account (optional)"
387
+ ],
388
+ "writable": true,
389
+ "optional": true
390
+ },
391
+ {
392
+ "name": "fee_recipient_token_account",
393
+ "docs": [
394
+ "Fee recipient's token account (optional)"
395
+ ],
396
+ "writable": true,
397
+ "optional": true
398
+ },
399
+ {
400
+ "name": "nullifier_account_0",
401
+ "docs": [
402
+ "Nullifier account 0 (optional - for first input UTXO)"
403
+ ],
404
+ "writable": true,
405
+ "optional": true
406
+ },
407
+ {
408
+ "name": "nullifier_account_1",
409
+ "docs": [
410
+ "Nullifier account 1 (optional - for second input UTXO)"
411
+ ],
412
+ "writable": true,
413
+ "optional": true
414
+ },
415
+ {
416
+ "name": "token_program",
417
+ "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
418
+ },
419
+ {
420
+ "name": "system_program",
421
+ "address": "11111111111111111111111111111111"
422
+ }
423
+ ],
424
+ "args": [
425
+ {
426
+ "name": "input_tree_index",
427
+ "type": {
428
+ "option": "u32"
429
+ }
430
+ },
431
+ {
432
+ "name": "proof",
433
+ "type": {
434
+ "array": [
435
+ "u8",
436
+ 256
437
+ ]
438
+ }
439
+ },
440
+ {
441
+ "name": "public_inputs",
442
+ "type": {
443
+ "defined": {
444
+ "name": "PublicInputs"
445
+ }
446
+ }
447
+ },
448
+ {
449
+ "name": "encrypted_notes",
450
+ "type": {
451
+ "vec": "bytes"
452
+ }
453
+ }
454
+ ]
455
+ }
456
+ ],
457
+ "accounts": [
458
+ {
459
+ "name": "MerkleTreeAccount",
460
+ "discriminator": [
461
+ 147,
462
+ 200,
463
+ 34,
464
+ 248,
465
+ 131,
466
+ 187,
467
+ 248,
468
+ 253
469
+ ]
470
+ },
471
+ {
472
+ "name": "PrivacyPool",
473
+ "discriminator": [
474
+ 133,
475
+ 184,
476
+ 191,
477
+ 79,
478
+ 252,
479
+ 142,
480
+ 190,
481
+ 150
482
+ ]
483
+ }
484
+ ],
485
+ "errors": [
486
+ {
487
+ "code": 6e3,
488
+ "name": "Groth16MulFailed",
489
+ "msg": "Groth16 multiplication failed"
490
+ },
491
+ {
492
+ "code": 6001,
493
+ "name": "Groth16AddFailed",
494
+ "msg": "Groth16 addition failed"
495
+ },
496
+ {
497
+ "code": 6002,
498
+ "name": "Groth16PairingFailed",
499
+ "msg": "Groth16 pairing failed"
500
+ },
501
+ {
502
+ "code": 6003,
503
+ "name": "InvalidPublicInputs",
504
+ "msg": "Invalid public inputs"
505
+ },
506
+ {
507
+ "code": 6004,
508
+ "name": "ProofVerificationFailed",
509
+ "msg": "Proof verification failed"
510
+ },
511
+ {
512
+ "code": 6005,
513
+ "name": "PoolPaused",
514
+ "msg": "Pool is paused"
515
+ },
516
+ {
517
+ "code": 6006,
518
+ "name": "InvalidMint",
519
+ "msg": "Invalid mint"
520
+ },
521
+ {
522
+ "code": 6007,
523
+ "name": "Unauthorized",
524
+ "msg": "Unauthorized"
525
+ },
526
+ {
527
+ "code": 6008,
528
+ "name": "TreeFull",
529
+ "msg": "Merkle tree is full"
530
+ },
531
+ {
532
+ "code": 6009,
533
+ "name": "InvalidRoot",
534
+ "msg": "Invalid merkle root"
535
+ },
536
+ {
537
+ "code": 6010,
538
+ "name": "NullifierAlreadySpent",
539
+ "msg": "Nullifier already spent"
540
+ },
541
+ {
542
+ "code": 6011,
543
+ "name": "NullifierAlreadyUsed",
544
+ "msg": "Nullifier already used - double spend attempt"
545
+ },
546
+ {
547
+ "code": 6012,
548
+ "name": "MissingNullifierAccount",
549
+ "msg": "Missing nullifier account"
550
+ },
551
+ {
552
+ "code": 6013,
553
+ "name": "InvalidNullifierAccount",
554
+ "msg": "Invalid nullifier account"
555
+ },
556
+ {
557
+ "code": 6014,
558
+ "name": "InvalidPublicAmount",
559
+ "msg": "Invalid public amount"
560
+ },
561
+ {
562
+ "code": 6015,
563
+ "name": "ArithmeticOverflow",
564
+ "msg": "Arithmetic overflow"
565
+ },
566
+ {
567
+ "code": 6016,
568
+ "name": "InvalidRecipient",
569
+ "msg": "Invalid recipient"
570
+ },
571
+ {
572
+ "code": 6017,
573
+ "name": "InvalidFee",
574
+ "msg": "Invalid fee"
575
+ },
576
+ {
577
+ "code": 6018,
578
+ "name": "TreeNotFull",
579
+ "msg": "Tree is not full yet"
580
+ },
581
+ {
582
+ "code": 6019,
583
+ "name": "InvalidTreeIndex",
584
+ "msg": "Invalid tree index"
585
+ }
586
+ ],
587
+ "types": [
588
+ {
589
+ "name": "MerkleTreeAccount",
590
+ "docs": [
591
+ "Incremental Merkle Tree for UTXO commitments",
592
+ "Uses a sparse representation - only stores filled subtrees"
593
+ ],
594
+ "type": {
595
+ "kind": "struct",
596
+ "fields": [
597
+ {
598
+ "name": "pool",
599
+ "docs": [
600
+ "The privacy pool this tree belongs to"
601
+ ],
602
+ "type": "pubkey"
603
+ },
604
+ {
605
+ "name": "tree_index",
606
+ "docs": [
607
+ "Tree index (which tree in the sequence)"
608
+ ],
609
+ "type": "u32"
610
+ },
611
+ {
612
+ "name": "next_index",
613
+ "docs": [
614
+ "Current number of leaves inserted"
615
+ ],
616
+ "type": "u32"
617
+ },
618
+ {
619
+ "name": "root",
620
+ "docs": [
621
+ "Current root of the tree"
622
+ ],
623
+ "type": {
624
+ "array": [
625
+ "u8",
626
+ 32
627
+ ]
628
+ }
629
+ },
630
+ {
631
+ "name": "filled_subtrees",
632
+ "docs": [
633
+ "Filled subtrees at each level (for incremental insertion)",
634
+ "filled_subtrees[i] = the rightmost filled node at level i"
635
+ ],
636
+ "type": {
637
+ "array": [
638
+ {
639
+ "array": [
640
+ "u8",
641
+ 32
642
+ ]
643
+ },
644
+ 10
645
+ ]
646
+ }
647
+ },
648
+ {
649
+ "name": "root_history",
650
+ "docs": [
651
+ "Historical roots for verification (ring buffer)",
652
+ "Allows verifying proofs against recent roots"
653
+ ],
654
+ "type": {
655
+ "array": [
656
+ {
657
+ "array": [
658
+ "u8",
659
+ 32
660
+ ]
661
+ },
662
+ 32
663
+ ]
664
+ }
665
+ },
666
+ {
667
+ "name": "root_history_index",
668
+ "docs": [
669
+ "Current position in root history ring buffer"
670
+ ],
671
+ "type": "u8"
672
+ },
673
+ {
674
+ "name": "bump",
675
+ "docs": [
676
+ "Bump seed for PDA"
677
+ ],
678
+ "type": "u8"
679
+ }
680
+ ]
681
+ }
682
+ },
683
+ {
684
+ "name": "PrivacyPool",
685
+ "docs": [
686
+ "Global Privacy Pool Account",
687
+ "Single pool for all tokens - vaults are created per mint lazily"
688
+ ],
689
+ "type": {
690
+ "kind": "struct",
691
+ "fields": [
692
+ {
693
+ "name": "authority",
694
+ "docs": [
695
+ "Authority that can update pool settings"
696
+ ],
697
+ "type": "pubkey"
698
+ },
699
+ {
700
+ "name": "commitment_count",
701
+ "docs": [
702
+ "Total number of commitments across all trees"
703
+ ],
704
+ "type": "u64"
705
+ },
706
+ {
707
+ "name": "merkle_root",
708
+ "docs": [
709
+ "Current merkle root (of the active tree)"
710
+ ],
711
+ "type": {
712
+ "array": [
713
+ "u8",
714
+ 32
715
+ ]
716
+ }
717
+ },
718
+ {
719
+ "name": "bump",
720
+ "docs": [
721
+ "Pool bump seed"
722
+ ],
723
+ "type": "u8"
724
+ },
725
+ {
726
+ "name": "paused",
727
+ "docs": [
728
+ "Whether the pool is paused"
729
+ ],
730
+ "type": "bool"
731
+ },
732
+ {
733
+ "name": "current_tree_index",
734
+ "docs": [
735
+ "Current active tree index (0-based)"
736
+ ],
737
+ "type": "u32"
738
+ },
739
+ {
740
+ "name": "_reserved",
741
+ "docs": [
742
+ "Reserved for future use"
743
+ ],
744
+ "type": "bytes"
745
+ }
746
+ ]
747
+ }
748
+ },
749
+ {
750
+ "name": "PublicInputs",
751
+ "docs": [
752
+ "Public inputs for the transaction circuit"
753
+ ],
754
+ "type": {
755
+ "kind": "struct",
756
+ "fields": [
757
+ {
758
+ "name": "merkle_root",
759
+ "type": {
760
+ "array": [
761
+ "u8",
762
+ 32
763
+ ]
764
+ }
765
+ },
766
+ {
767
+ "name": "nullifiers",
768
+ "type": {
769
+ "array": [
770
+ {
771
+ "array": [
772
+ "u8",
773
+ 32
774
+ ]
775
+ },
776
+ 2
777
+ ]
778
+ }
779
+ },
780
+ {
781
+ "name": "output_commitments",
782
+ "type": {
783
+ "array": [
784
+ {
785
+ "array": [
786
+ "u8",
787
+ 32
788
+ ]
789
+ },
790
+ 2
791
+ ]
792
+ }
793
+ },
794
+ {
795
+ "name": "public_deposit",
796
+ "type": "u64"
797
+ },
798
+ {
799
+ "name": "public_withdraw",
800
+ "type": "u64"
801
+ },
802
+ {
803
+ "name": "recipient",
804
+ "type": "pubkey"
805
+ },
806
+ {
807
+ "name": "mint_token_address",
808
+ "type": "pubkey"
809
+ },
810
+ {
811
+ "name": "fee",
812
+ "type": "u64"
813
+ }
814
+ ]
815
+ }
816
+ }
817
+ ],
818
+ "constants": [
819
+ {
820
+ "name": "MERKLE_TREE_SEED",
821
+ "type": "bytes",
822
+ "value": "[109, 101, 114, 107, 108, 101, 95, 116, 114, 101, 101]"
823
+ },
824
+ {
825
+ "name": "NULLIFIER_SEED",
826
+ "type": "bytes",
827
+ "value": "[110, 117, 108, 108, 105, 102, 105, 101, 114]"
828
+ },
829
+ {
830
+ "name": "POOL_SEED",
831
+ "type": "bytes",
832
+ "value": "[104, 117, 108, 97, 95, 112, 111, 111, 108]"
833
+ },
834
+ {
835
+ "name": "VAULT_SEED",
836
+ "type": "bytes",
837
+ "value": "[118, 97, 117, 108, 116]"
838
+ }
839
+ ]
840
+ };
841
+ var idl_default = HulaPrivacyIdl;
842
+
843
+ // src/crypto.ts
844
+ import { buildPoseidon } from "circomlibjs";
845
+ import * as nacl from "tweetnacl";
846
+ import { sha256 } from "@noble/hashes/sha256";
847
+
848
+ // src/constants.ts
849
+ import { PublicKey } from "@solana/web3.js";
850
+ var PROGRAM_ID = new PublicKey("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
851
+ var TOKEN_2022_PROGRAM_ID = new PublicKey(
852
+ "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
853
+ );
854
+ var POOL_SEED = Buffer.from("hula_pool");
855
+ var VAULT_SEED = Buffer.from("vault");
856
+ var MERKLE_TREE_SEED = Buffer.from("merkle_tree");
857
+ var NULLIFIER_SEED = Buffer.from("nullifier");
858
+ var NUM_INPUT_UTXOS = 2;
859
+ var NUM_OUTPUT_UTXOS = 2;
860
+ var MERKLE_TREE_DEPTH = 10;
861
+ var MAX_LEAVES = 1 << MERKLE_TREE_DEPTH;
862
+ var PROOF_SIZE = 256;
863
+ var DOMAIN_OWNER = 0n;
864
+ var DOMAIN_VIEWING = 1n;
865
+ var DOMAIN_NULLIFIER = 2n;
866
+ var DOMAIN_ENCRYPTION = 3n;
867
+ var FIELD_PRIME = BigInt(
868
+ "21888242871839275222246405745257275088548364400416034343698204186575808495617"
869
+ );
870
+ function getPoolPDA() {
871
+ return PublicKey.findProgramAddressSync([POOL_SEED], PROGRAM_ID);
872
+ }
873
+ function getMerkleTreePDA(treeIndex = 0) {
874
+ const treeIndexBuffer = Buffer.alloc(4);
875
+ treeIndexBuffer.writeUInt32LE(treeIndex, 0);
876
+ return PublicKey.findProgramAddressSync(
877
+ [MERKLE_TREE_SEED, treeIndexBuffer],
878
+ PROGRAM_ID
879
+ );
880
+ }
881
+ function getVaultPDA(mint) {
882
+ return PublicKey.findProgramAddressSync(
883
+ [VAULT_SEED, mint.toBytes()],
884
+ PROGRAM_ID
885
+ );
886
+ }
887
+ function getNullifierPDA(nullifier) {
888
+ return PublicKey.findProgramAddressSync(
889
+ [NULLIFIER_SEED, nullifier],
890
+ PROGRAM_ID
891
+ );
892
+ }
893
+
894
+ // src/crypto.ts
895
+ var poseidonInstance = null;
896
+ async function initPoseidon() {
897
+ if (!poseidonInstance) {
898
+ poseidonInstance = await buildPoseidon();
899
+ }
900
+ }
901
+ function isPoseidonInitialized() {
902
+ return poseidonInstance !== null;
903
+ }
904
+ function getPoseidon() {
905
+ if (!poseidonInstance) {
906
+ throw new Error("Poseidon not initialized. Call initPoseidon() first.");
907
+ }
908
+ return poseidonInstance;
909
+ }
910
+ function poseidonHash(inputs) {
911
+ const poseidon = getPoseidon();
912
+ const hash = poseidon(inputs.map((x) => poseidon.F.e(x)));
913
+ return poseidon.F.toObject(hash);
914
+ }
915
+ function bytesToBigInt(bytes) {
916
+ let result = 0n;
917
+ for (let i = 0; i < bytes.length; i++) {
918
+ result = (result << 8n) + BigInt(bytes[i]);
919
+ }
920
+ return result;
921
+ }
922
+ function bigIntToBytes(value, length) {
923
+ const bytes = new Uint8Array(length);
924
+ let v = value;
925
+ for (let i = length - 1; i >= 0; i--) {
926
+ bytes[i] = Number(v & 0xffn);
927
+ v >>= 8n;
928
+ }
929
+ return bytes;
930
+ }
931
+ function bigIntToBytes32(value) {
932
+ return bigIntToBytes(value, 32);
933
+ }
934
+ function hexToBytes(hex) {
935
+ const cleaned = hex.startsWith("0x") ? hex.slice(2) : hex;
936
+ const bytes = new Uint8Array(cleaned.length / 2);
937
+ for (let i = 0; i < bytes.length; i++) {
938
+ bytes[i] = parseInt(cleaned.substr(i * 2, 2), 16);
939
+ }
940
+ return bytes;
941
+ }
942
+ function bytesToHex(bytes) {
943
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
944
+ }
945
+ function pubkeyToBigInt(pubkey) {
946
+ return bytesToBigInt(pubkey.toBytes());
947
+ }
948
+ function generateSpendingKey() {
949
+ const randomBytes2 = nacl.randomBytes(32);
950
+ return bytesToBigInt(randomBytes2) % 2n ** 253n;
951
+ }
952
+ function deriveKeys(spendingKey) {
953
+ const owner = poseidonHash([spendingKey, DOMAIN_OWNER]);
954
+ const viewingKey = poseidonHash([spendingKey, DOMAIN_VIEWING]);
955
+ const nullifierKey = poseidonHash([spendingKey, DOMAIN_NULLIFIER]);
956
+ const encryptionSeed = poseidonHash([spendingKey, DOMAIN_ENCRYPTION]);
957
+ const seedBytes = bigIntToBytes(encryptionSeed, 32);
958
+ const hashedSeed = sha256(seedBytes);
959
+ const encryptionKeyPair = nacl.box.keyPair.fromSecretKey(hashedSeed);
960
+ return {
961
+ spendingKey,
962
+ owner,
963
+ viewingKey,
964
+ nullifierKey,
965
+ encryptionKeyPair
966
+ };
967
+ }
968
+ function deriveSpendingKeyFromSignature(signature) {
969
+ if (signature.length < 64) {
970
+ throw new Error("Signature must be at least 64 bytes");
971
+ }
972
+ const hash = sha256(signature);
973
+ return bytesToBigInt(hash) % FIELD_PRIME;
974
+ }
975
+ function encryptNote(noteData, recipientEncryptionPubKey) {
976
+ const mintPrefix = noteData.mintTokenAddress.toString(16).padStart(64, "0").slice(0, 16);
977
+ const payload = {
978
+ v: noteData.value.toString(),
979
+ // value
980
+ m: mintPrefix,
981
+ // mint (first 8 bytes as hex)
982
+ s: noteData.secret.toString()
983
+ // secret
984
+ // leafIndex omitted - recipient queries relayer with computed commitment
985
+ };
986
+ const message = new TextEncoder().encode(JSON.stringify(payload));
987
+ const nonce = nacl.randomBytes(24);
988
+ const ephemeralKeyPair = nacl.box.keyPair();
989
+ const ciphertext = nacl.box(
990
+ message,
991
+ nonce,
992
+ recipientEncryptionPubKey,
993
+ ephemeralKeyPair.secretKey
994
+ );
995
+ return {
996
+ ephemeralPubKey: ephemeralKeyPair.publicKey,
997
+ ciphertext,
998
+ nonce
999
+ };
1000
+ }
1001
+ function decryptNote(encryptedNote, encryptionSecretKey) {
1002
+ try {
1003
+ const decrypted = nacl.box.open(
1004
+ encryptedNote.ciphertext,
1005
+ encryptedNote.nonce,
1006
+ encryptedNote.ephemeralPubKey,
1007
+ encryptionSecretKey
1008
+ );
1009
+ if (!decrypted) return null;
1010
+ const noteData = JSON.parse(new TextDecoder().decode(decrypted));
1011
+ if (noteData.v !== void 0) {
1012
+ return {
1013
+ value: BigInt(noteData.v),
1014
+ mintPrefix: noteData.m,
1015
+ // First 8 bytes as hex string
1016
+ secret: BigInt(noteData.s)
1017
+ };
1018
+ } else {
1019
+ return {
1020
+ value: BigInt(noteData.value),
1021
+ mintPrefix: BigInt(noteData.mintTokenAddress).toString(16).padStart(64, "0").slice(0, 16),
1022
+ secret: BigInt(noteData.secret)
1023
+ };
1024
+ }
1025
+ } catch {
1026
+ return null;
1027
+ }
1028
+ }
1029
+ function serializeEncryptedNote(note) {
1030
+ const totalLength = 32 + 24 + 2 + note.ciphertext.length;
1031
+ const buffer = new Uint8Array(totalLength);
1032
+ buffer.set(note.ephemeralPubKey, 0);
1033
+ buffer.set(note.nonce, 32);
1034
+ buffer[56] = note.ciphertext.length >> 8 & 255;
1035
+ buffer[57] = note.ciphertext.length & 255;
1036
+ buffer.set(note.ciphertext, 58);
1037
+ return buffer;
1038
+ }
1039
+ function deserializeEncryptedNote(data) {
1040
+ const ephemeralPubKey = data.slice(0, 32);
1041
+ const nonce = data.slice(32, 56);
1042
+ const ciphertextLength = data[56] << 8 | data[57];
1043
+ const ciphertext = data.slice(58, 58 + ciphertextLength);
1044
+ return { ephemeralPubKey, nonce, ciphertext };
1045
+ }
1046
+ function formatCommitment(value, length = 16) {
1047
+ if (value instanceof Uint8Array) {
1048
+ return `0x${bytesToHex(value).slice(0, length)}...`;
1049
+ }
1050
+ return `0x${value.toString(16).padStart(64, "0").slice(0, length)}...`;
1051
+ }
1052
+
1053
+ // src/api.ts
1054
+ var DEFAULT_API_URL = "http://localhost:3001";
1055
+ var RelayerClient = class {
1056
+ baseUrl;
1057
+ constructor(baseUrl = DEFAULT_API_URL) {
1058
+ this.baseUrl = baseUrl.replace(/\/$/, "");
1059
+ }
1060
+ async fetch(path2, init) {
1061
+ const response = await fetch(`${this.baseUrl}${path2}`, {
1062
+ ...init,
1063
+ headers: {
1064
+ "Content-Type": "application/json",
1065
+ ...init?.headers
1066
+ }
1067
+ });
1068
+ if (!response.ok) {
1069
+ const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
1070
+ throw new Error(errorData.error || `HTTP ${response.status}`);
1071
+ }
1072
+ return response.json();
1073
+ }
1074
+ // ============================================================================
1075
+ // Health & Stats
1076
+ // ============================================================================
1077
+ /**
1078
+ * Health check
1079
+ */
1080
+ async health() {
1081
+ return this.fetch("/health");
1082
+ }
1083
+ /**
1084
+ * Get protocol stats
1085
+ */
1086
+ async getStats() {
1087
+ return this.fetch("/api/stats");
1088
+ }
1089
+ // ============================================================================
1090
+ // Pool
1091
+ // ============================================================================
1092
+ /**
1093
+ * Get pool data
1094
+ */
1095
+ async getPool() {
1096
+ return this.fetch("/api/pool");
1097
+ }
1098
+ // ============================================================================
1099
+ // Trees
1100
+ // ============================================================================
1101
+ /**
1102
+ * Get all merkle trees
1103
+ */
1104
+ async getTrees() {
1105
+ return this.fetch("/api/trees");
1106
+ }
1107
+ /**
1108
+ * Get tree by index
1109
+ */
1110
+ async getTree(treeIndex) {
1111
+ return this.fetch(`/api/trees/${treeIndex}`);
1112
+ }
1113
+ // ============================================================================
1114
+ // Leaves / Commitments
1115
+ // ============================================================================
1116
+ /**
1117
+ * Get leaves with cursor pagination
1118
+ */
1119
+ async getLeaves(cursor, limit, treeIndex) {
1120
+ const params = new URLSearchParams();
1121
+ if (cursor) params.set("cursor", cursor);
1122
+ if (limit) params.set("limit", String(limit));
1123
+ if (treeIndex !== void 0) params.set("treeIndex", String(treeIndex));
1124
+ return this.fetch(`/api/leaves?${params}`);
1125
+ }
1126
+ /**
1127
+ * Get leaves after a specific index (for syncing)
1128
+ */
1129
+ async getLeavesAfter(treeIndex, afterLeafIndex, limit) {
1130
+ const params = new URLSearchParams();
1131
+ params.set("treeIndex", String(treeIndex));
1132
+ params.set("afterLeafIndex", String(afterLeafIndex));
1133
+ if (limit) params.set("limit", String(limit));
1134
+ return this.fetch(`/api/leaves/after?${params}`);
1135
+ }
1136
+ /**
1137
+ * Get all leaves for a tree (auto-paginated)
1138
+ */
1139
+ async getAllLeavesForTree(treeIndex) {
1140
+ const allLeaves = [];
1141
+ let cursor;
1142
+ while (true) {
1143
+ const response = await this.getLeaves(cursor, 100, treeIndex);
1144
+ allLeaves.push(...response.items);
1145
+ if (!response.hasMore || !response.nextCursor) break;
1146
+ cursor = response.nextCursor;
1147
+ }
1148
+ return allLeaves;
1149
+ }
1150
+ /**
1151
+ * Get all commitment values for a tree as bigints
1152
+ */
1153
+ async getCommitmentsForTree(treeIndex) {
1154
+ const leaves = await this.getAllLeavesForTree(treeIndex);
1155
+ leaves.sort((a, b) => a.leafIndex - b.leafIndex);
1156
+ return leaves.map((l) => {
1157
+ const commitment = l.commitment.startsWith("0x") ? l.commitment : `0x${l.commitment}`;
1158
+ return BigInt(commitment);
1159
+ });
1160
+ }
1161
+ // ============================================================================
1162
+ // Transactions
1163
+ // ============================================================================
1164
+ /**
1165
+ * Get transactions with cursor pagination
1166
+ */
1167
+ async getTransactions(cursor, limit, mint) {
1168
+ const params = new URLSearchParams();
1169
+ if (cursor) params.set("cursor", cursor);
1170
+ if (limit) params.set("limit", String(limit));
1171
+ if (mint) params.set("mint", mint);
1172
+ return this.fetch(`/api/transactions?${params}`);
1173
+ }
1174
+ /**
1175
+ * Get transaction by signature
1176
+ */
1177
+ async getTransaction(signature) {
1178
+ return this.fetch(`/api/transactions/${signature}`);
1179
+ }
1180
+ // ============================================================================
1181
+ // Nullifiers
1182
+ // ============================================================================
1183
+ /**
1184
+ * Check if nullifier is spent
1185
+ */
1186
+ async checkNullifier(nullifier) {
1187
+ return this.fetch(`/api/nullifiers/check?nullifier=${encodeURIComponent(nullifier)}`);
1188
+ }
1189
+ /**
1190
+ * Check multiple nullifiers at once
1191
+ */
1192
+ async checkNullifiersBatch(nullifiers) {
1193
+ return this.fetch("/api/nullifiers/check-batch", {
1194
+ method: "POST",
1195
+ body: JSON.stringify({ nullifiers })
1196
+ });
1197
+ }
1198
+ // ============================================================================
1199
+ // Encrypted Notes
1200
+ // ============================================================================
1201
+ /**
1202
+ * Get encrypted notes with cursor pagination
1203
+ */
1204
+ async getNotes(cursor, limit, afterSlot) {
1205
+ const params = new URLSearchParams();
1206
+ if (cursor) params.set("cursor", cursor);
1207
+ if (limit) params.set("limit", String(limit));
1208
+ if (afterSlot) params.set("afterSlot", afterSlot);
1209
+ return this.fetch(`/api/notes?${params}`);
1210
+ }
1211
+ /**
1212
+ * Get all notes (auto-paginated)
1213
+ */
1214
+ async getAllNotes(afterSlot) {
1215
+ const allNotes = [];
1216
+ let cursor;
1217
+ while (true) {
1218
+ const response = await this.getNotes(cursor, 100, afterSlot);
1219
+ allNotes.push(...response.items);
1220
+ if (!response.hasMore || !response.nextCursor) break;
1221
+ cursor = response.nextCursor;
1222
+ }
1223
+ return allNotes;
1224
+ }
1225
+ };
1226
+ var defaultClient = null;
1227
+ function getRelayerClient(url) {
1228
+ if (url) {
1229
+ return new RelayerClient(url);
1230
+ }
1231
+ if (!defaultClient) {
1232
+ defaultClient = new RelayerClient();
1233
+ }
1234
+ return defaultClient;
1235
+ }
1236
+ function setDefaultRelayerUrl(url) {
1237
+ defaultClient = new RelayerClient(url);
1238
+ }
1239
+
1240
+ // src/utxo.ts
1241
+ import { PublicKey as PublicKey2 } from "@solana/web3.js";
1242
+ function computeCommitment(value, mintTokenAddress, owner, secret) {
1243
+ return poseidonHash([value, mintTokenAddress, owner, secret]);
1244
+ }
1245
+ function computeNullifier(spendingKey, commitment, leafIndex) {
1246
+ const nullifierKey = poseidonHash([spendingKey, DOMAIN_NULLIFIER]);
1247
+ return poseidonHash([nullifierKey, commitment, BigInt(leafIndex)]);
1248
+ }
1249
+ function computeNullifierFromKeys(keys, commitment, leafIndex) {
1250
+ return poseidonHash([keys.nullifierKey, commitment, BigInt(leafIndex)]);
1251
+ }
1252
+ function createUTXO(value, mintTokenAddress, owner, leafIndex, treeIndex = 0) {
1253
+ const secret = generateSpendingKey();
1254
+ const commitment = computeCommitment(value, mintTokenAddress, owner, secret);
1255
+ return {
1256
+ value,
1257
+ mintTokenAddress,
1258
+ owner,
1259
+ secret,
1260
+ commitment,
1261
+ leafIndex,
1262
+ treeIndex
1263
+ };
1264
+ }
1265
+ function createDummyUTXO() {
1266
+ return {
1267
+ value: 0n,
1268
+ mintTokenAddress: 0n,
1269
+ owner: 0n,
1270
+ secret: 0n,
1271
+ commitment: 0n,
1272
+ leafIndex: 0,
1273
+ treeIndex: 0
1274
+ };
1275
+ }
1276
+ function serializeUTXO(utxo, mint, createdTx, spent = false, spentTx) {
1277
+ return {
1278
+ value: utxo.value.toString(),
1279
+ mintTokenAddress: utxo.mintTokenAddress.toString(),
1280
+ owner: utxo.owner.toString(),
1281
+ secret: utxo.secret.toString(),
1282
+ commitment: utxo.commitment.toString(),
1283
+ leafIndex: utxo.leafIndex,
1284
+ treeIndex: utxo.treeIndex,
1285
+ mint,
1286
+ spent,
1287
+ spentTx,
1288
+ createdTx,
1289
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1290
+ };
1291
+ }
1292
+ function deserializeUTXO(data) {
1293
+ return {
1294
+ value: BigInt(data.value),
1295
+ mintTokenAddress: BigInt(data.mintTokenAddress),
1296
+ owner: BigInt(data.owner),
1297
+ secret: BigInt(data.secret),
1298
+ commitment: BigInt(data.commitment),
1299
+ leafIndex: data.leafIndex,
1300
+ treeIndex: data.treeIndex
1301
+ };
1302
+ }
1303
+ function scanNotesForUTXOs(notes, walletKeys, commitments) {
1304
+ const foundUTXOs = [];
1305
+ for (const note of notes) {
1306
+ try {
1307
+ const noteBytes = hexToBytes(note.encryptedData);
1308
+ const encryptedNote = deserializeEncryptedNote(noteBytes);
1309
+ const decrypted = decryptNote(encryptedNote, walletKeys.encryptionKeyPair.secretKey);
1310
+ if (!decrypted) continue;
1311
+ const mintAddress = note.mint;
1312
+ const mintTokenAddress = pubkeyToBigInt(new PublicKey2(mintAddress));
1313
+ const commitment = computeCommitment(
1314
+ decrypted.value,
1315
+ mintTokenAddress,
1316
+ walletKeys.owner,
1317
+ decrypted.secret
1318
+ );
1319
+ let foundLeafIndex;
1320
+ const treeCommitments = commitments.get(note.treeIndex);
1321
+ if (treeCommitments) {
1322
+ for (const [leafIndex, onChainCommitment] of treeCommitments.entries()) {
1323
+ if (onChainCommitment === commitment) {
1324
+ foundLeafIndex = leafIndex;
1325
+ break;
1326
+ }
1327
+ }
1328
+ }
1329
+ if (foundLeafIndex === void 0) {
1330
+ continue;
1331
+ }
1332
+ foundUTXOs.push({
1333
+ value: decrypted.value.toString(),
1334
+ mintTokenAddress: mintTokenAddress.toString(),
1335
+ owner: walletKeys.owner.toString(),
1336
+ secret: decrypted.secret.toString(),
1337
+ commitment: commitment.toString(),
1338
+ leafIndex: foundLeafIndex,
1339
+ treeIndex: note.treeIndex,
1340
+ mint: note.mint,
1341
+ spent: false,
1342
+ createdTx: note.txSignature,
1343
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1344
+ });
1345
+ } catch (err) {
1346
+ }
1347
+ }
1348
+ return foundUTXOs;
1349
+ }
1350
+ async function syncUTXOs(walletKeys, existingUTXOs, relayerUrl, afterSlot) {
1351
+ const client = getRelayerClient(relayerUrl);
1352
+ const notes = await client.getAllNotes(afterSlot);
1353
+ const pool = await client.getPool();
1354
+ const commitments = /* @__PURE__ */ new Map();
1355
+ for (let treeIndex = 0; treeIndex <= pool.currentTreeIndex; treeIndex++) {
1356
+ const leaves = await client.getCommitmentsForTree(treeIndex);
1357
+ const treeMap = /* @__PURE__ */ new Map();
1358
+ leaves.forEach((commitment, index) => {
1359
+ treeMap.set(index, commitment);
1360
+ });
1361
+ commitments.set(treeIndex, treeMap);
1362
+ }
1363
+ const newUTXOs = scanNotesForUTXOs(
1364
+ notes.map((n) => ({
1365
+ encryptedData: n.encryptedData,
1366
+ treeIndex: n.treeIndex,
1367
+ mint: n.mint,
1368
+ txSignature: n.txSignature
1369
+ })),
1370
+ walletKeys,
1371
+ commitments
1372
+ );
1373
+ const allUTXOs = [...newUTXOs, ...existingUTXOs];
1374
+ const unspentUTXOs = allUTXOs.filter((u) => !u.spent);
1375
+ const nullifiersToCheck = [];
1376
+ const utxoByNullifier = /* @__PURE__ */ new Map();
1377
+ for (const utxo of unspentUTXOs) {
1378
+ const commitment = BigInt(utxo.commitment);
1379
+ const nullifier = computeNullifier(
1380
+ walletKeys.spendingKey,
1381
+ commitment,
1382
+ utxo.leafIndex
1383
+ );
1384
+ const nullifierStr = nullifier.toString(16).padStart(64, "0");
1385
+ nullifiersToCheck.push(nullifierStr);
1386
+ utxoByNullifier.set(nullifierStr, utxo.commitment);
1387
+ }
1388
+ const spentUTXOs = [];
1389
+ if (nullifiersToCheck.length > 0) {
1390
+ const batchSize = 50;
1391
+ for (let i = 0; i < nullifiersToCheck.length; i += batchSize) {
1392
+ const batch = nullifiersToCheck.slice(i, i + batchSize);
1393
+ const result = await client.checkNullifiersBatch(batch);
1394
+ for (const r of result.results) {
1395
+ if (r.spent) {
1396
+ const utxoId = utxoByNullifier.get(r.nullifier);
1397
+ if (utxoId) {
1398
+ spentUTXOs.push(utxoId);
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1403
+ }
1404
+ return { newUTXOs, spentUTXOs };
1405
+ }
1406
+ function selectUTXOs(utxos, targetAmount, mint) {
1407
+ const filtered = mint !== void 0 ? utxos.filter((u) => u.mintTokenAddress === mint) : utxos;
1408
+ const sorted = [...filtered].sort((a, b) => {
1409
+ if (a.value > b.value) return -1;
1410
+ if (a.value < b.value) return 1;
1411
+ return 0;
1412
+ });
1413
+ const selected = [];
1414
+ let total = 0n;
1415
+ for (const utxo of sorted) {
1416
+ if (total >= targetAmount) break;
1417
+ selected.push(utxo);
1418
+ total += utxo.value;
1419
+ }
1420
+ if (total < targetAmount) {
1421
+ throw new Error(
1422
+ `Insufficient balance. Need ${targetAmount}, have ${total}`
1423
+ );
1424
+ }
1425
+ return selected;
1426
+ }
1427
+ function calculateBalance(utxos, mint) {
1428
+ const filtered = mint !== void 0 ? utxos.filter((u) => u.mintTokenAddress === mint) : utxos;
1429
+ return filtered.reduce((sum, u) => sum + u.value, 0n);
1430
+ }
1431
+
1432
+ // src/transaction.ts
1433
+ import { PublicKey as PublicKey3, SystemProgram } from "@solana/web3.js";
1434
+ import {
1435
+ getAssociatedTokenAddressSync,
1436
+ createAssociatedTokenAccountIdempotentInstruction
1437
+ } from "@solana/spl-token";
1438
+ import { BN } from "@coral-xyz/anchor";
1439
+
1440
+ // src/merkle.ts
1441
+ var ZEROS = [];
1442
+ function computeZeros() {
1443
+ if (!isPoseidonInitialized()) {
1444
+ throw new Error("Poseidon not initialized. Call initPoseidon() first.");
1445
+ }
1446
+ if (ZEROS.length > 0) {
1447
+ return ZEROS;
1448
+ }
1449
+ const zeros = [0n];
1450
+ const poseidon = getPoseidon();
1451
+ for (let i = 0; i < MERKLE_TREE_DEPTH; i++) {
1452
+ const hash = poseidon([zeros[i], zeros[i]]);
1453
+ zeros.push(poseidon.F.toObject(hash));
1454
+ }
1455
+ ZEROS = zeros;
1456
+ return zeros;
1457
+ }
1458
+ function getZeroAtLevel(level) {
1459
+ if (ZEROS.length === 0) {
1460
+ computeZeros();
1461
+ }
1462
+ return ZEROS[level];
1463
+ }
1464
+ function getEmptyTreeRoot() {
1465
+ if (ZEROS.length === 0) {
1466
+ computeZeros();
1467
+ }
1468
+ return ZEROS[MERKLE_TREE_DEPTH];
1469
+ }
1470
+ function computeMerklePathFromLeaves(leafIndex, leaves) {
1471
+ if (!isPoseidonInitialized()) {
1472
+ throw new Error("Poseidon not initialized. Call initPoseidon() first.");
1473
+ }
1474
+ const zeros = computeZeros();
1475
+ const pathElements = [];
1476
+ const pathIndices = [];
1477
+ const poseidon = getPoseidon();
1478
+ let currentLevel = [...leaves];
1479
+ const treeSize = 1 << MERKLE_TREE_DEPTH;
1480
+ while (currentLevel.length < treeSize) {
1481
+ currentLevel.push(zeros[0]);
1482
+ }
1483
+ let targetIndex = leafIndex;
1484
+ for (let level = 0; level < MERKLE_TREE_DEPTH; level++) {
1485
+ const isLeft = (targetIndex & 1) === 0;
1486
+ pathIndices.push(isLeft ? 0 : 1);
1487
+ const siblingIndex = isLeft ? targetIndex + 1 : targetIndex - 1;
1488
+ pathElements.push(currentLevel[siblingIndex]);
1489
+ const nextLevel = [];
1490
+ for (let i = 0; i < currentLevel.length; i += 2) {
1491
+ const left = currentLevel[i];
1492
+ const right = currentLevel[i + 1] ?? zeros[level];
1493
+ const hash = poseidon([left, right]);
1494
+ nextLevel.push(poseidon.F.toObject(hash));
1495
+ }
1496
+ currentLevel = nextLevel;
1497
+ targetIndex = Math.floor(targetIndex / 2);
1498
+ }
1499
+ return { pathElements, pathIndices, root: currentLevel[0] };
1500
+ }
1501
+ function computeMerkleRoot(leaves) {
1502
+ if (leaves.length === 0) {
1503
+ return getEmptyTreeRoot();
1504
+ }
1505
+ const { root } = computeMerklePathFromLeaves(0, leaves);
1506
+ return root;
1507
+ }
1508
+ function verifyMerklePath(leafValue, path2) {
1509
+ if (!isPoseidonInitialized()) {
1510
+ throw new Error("Poseidon not initialized. Call initPoseidon() first.");
1511
+ }
1512
+ let current = leafValue;
1513
+ const poseidon = getPoseidon();
1514
+ for (let i = 0; i < path2.pathElements.length; i++) {
1515
+ const sibling = path2.pathElements[i];
1516
+ const isLeft = path2.pathIndices[i] === 0;
1517
+ const left = isLeft ? current : sibling;
1518
+ const right = isLeft ? sibling : current;
1519
+ const hash = poseidon([left, right]);
1520
+ current = poseidon.F.toObject(hash);
1521
+ }
1522
+ return current === path2.root;
1523
+ }
1524
+ async function fetchMerklePath(treeIndex, leafIndex, relayerUrl) {
1525
+ const client = getRelayerClient(relayerUrl);
1526
+ const leaves = await client.getCommitmentsForTree(treeIndex);
1527
+ if (leafIndex >= leaves.length) {
1528
+ throw new Error(`Leaf index ${leafIndex} not found in tree ${treeIndex} (has ${leaves.length} leaves)`);
1529
+ }
1530
+ return computeMerklePathFromLeaves(leafIndex, leaves);
1531
+ }
1532
+ async function fetchMerkleRoot(treeIndex, relayerUrl) {
1533
+ const client = getRelayerClient(relayerUrl);
1534
+ const tree = await client.getTree(treeIndex);
1535
+ const rootStr = tree.root.startsWith("0x") ? tree.root : `0x${tree.root}`;
1536
+ return BigInt(rootStr);
1537
+ }
1538
+ async function getNextLeafIndex(treeIndex, relayerUrl) {
1539
+ const client = getRelayerClient(relayerUrl);
1540
+ const tree = await client.getTree(treeIndex);
1541
+ return tree.nextIndex;
1542
+ }
1543
+ async function getCurrentTreeIndex(relayerUrl) {
1544
+ const client = getRelayerClient(relayerUrl);
1545
+ const pool = await client.getPool();
1546
+ return pool.currentTreeIndex;
1547
+ }
1548
+
1549
+ // src/proof.ts
1550
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
1551
+ import { execSync } from "child_process";
1552
+ import path from "path";
1553
+ var circuitWasmPath = null;
1554
+ var circuitZkeyPath = null;
1555
+ function setCircuitPaths(wasmPath, zkeyPath) {
1556
+ circuitWasmPath = wasmPath;
1557
+ circuitZkeyPath = zkeyPath;
1558
+ }
1559
+ function getCircuitPaths() {
1560
+ if (circuitWasmPath && circuitZkeyPath) {
1561
+ return { wasmPath: circuitWasmPath, zkeyPath: circuitZkeyPath };
1562
+ }
1563
+ const possibleBasePaths = [
1564
+ path.join(process.cwd(), "circuits", "build"),
1565
+ path.join(process.cwd(), "..", "circuits", "build"),
1566
+ path.join(process.cwd(), "assets")
1567
+ ];
1568
+ for (const basePath of possibleBasePaths) {
1569
+ const wasmPath = path.join(basePath, "transaction_js", "transaction.wasm");
1570
+ const zkeyPath = path.join(basePath, "keys", "transaction_final.zkey");
1571
+ if (existsSync(wasmPath) && existsSync(zkeyPath)) {
1572
+ return { wasmPath, zkeyPath };
1573
+ }
1574
+ const altWasmPath = path.join(basePath, "transaction.wasm");
1575
+ const altZkeyPath = path.join(basePath, "transaction_final.zkey");
1576
+ if (existsSync(altWasmPath) && existsSync(altZkeyPath)) {
1577
+ return { wasmPath: altWasmPath, zkeyPath: altZkeyPath };
1578
+ }
1579
+ }
1580
+ throw new Error(
1581
+ "Circuit files not found. Either:\n1. Run 'make' in circuits/ directory to build them, or\n2. Call setCircuitPaths() with the correct paths"
1582
+ );
1583
+ }
1584
+ function verifyCircuitFiles() {
1585
+ const { wasmPath, zkeyPath } = getCircuitPaths();
1586
+ if (!existsSync(wasmPath)) {
1587
+ throw new Error(`Circuit WASM not found: ${wasmPath}`);
1588
+ }
1589
+ if (!existsSync(zkeyPath)) {
1590
+ throw new Error(`Circuit zkey not found: ${zkeyPath}`);
1591
+ }
1592
+ }
1593
+ function toBigEndianBytes(decimalStr) {
1594
+ let hex = BigInt(decimalStr).toString(16);
1595
+ hex = hex.padStart(64, "0");
1596
+ const bytes = new Uint8Array(32);
1597
+ for (let i = 0; i < 32; i++) {
1598
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1599
+ }
1600
+ return bytes;
1601
+ }
1602
+ function parseProof(proofJson) {
1603
+ const proof = new Uint8Array(PROOF_SIZE);
1604
+ const pi_a_x = toBigEndianBytes(proofJson.pi_a[0]);
1605
+ const pi_a_y = toBigEndianBytes(proofJson.pi_a[1]);
1606
+ proof.set(pi_a_x, 0);
1607
+ proof.set(pi_a_y, 32);
1608
+ const pi_b_x0 = toBigEndianBytes(proofJson.pi_b[0][1]);
1609
+ const pi_b_x1 = toBigEndianBytes(proofJson.pi_b[0][0]);
1610
+ const pi_b_y0 = toBigEndianBytes(proofJson.pi_b[1][1]);
1611
+ const pi_b_y1 = toBigEndianBytes(proofJson.pi_b[1][0]);
1612
+ proof.set(pi_b_x0, 64);
1613
+ proof.set(pi_b_x1, 96);
1614
+ proof.set(pi_b_y0, 128);
1615
+ proof.set(pi_b_y1, 160);
1616
+ const pi_c_x = toBigEndianBytes(proofJson.pi_c[0]);
1617
+ const pi_c_y = toBigEndianBytes(proofJson.pi_c[1]);
1618
+ proof.set(pi_c_x, 192);
1619
+ proof.set(pi_c_y, 224);
1620
+ return proof;
1621
+ }
1622
+ async function generateProof(circuitInputs) {
1623
+ verifyCircuitFiles();
1624
+ const { wasmPath, zkeyPath } = getCircuitPaths();
1625
+ const tempDir = path.dirname(zkeyPath);
1626
+ const timestamp = Date.now();
1627
+ const inputPath = path.join(tempDir, `temp_input_${timestamp}.json`);
1628
+ const witnessPath = path.join(tempDir, `temp_witness_${timestamp}.wtns`);
1629
+ const proofPath = path.join(tempDir, `temp_proof_${timestamp}.json`);
1630
+ const publicPath = path.join(tempDir, `temp_public_${timestamp}.json`);
1631
+ try {
1632
+ writeFileSync(inputPath, JSON.stringify(circuitInputs));
1633
+ const witnessGenScript = path.join(
1634
+ path.dirname(wasmPath),
1635
+ "generate_witness.cjs"
1636
+ );
1637
+ if (!existsSync(witnessGenScript)) {
1638
+ throw new Error(`Witness generation script not found: ${witnessGenScript}`);
1639
+ }
1640
+ execSync(`node ${witnessGenScript} ${wasmPath} ${inputPath} ${witnessPath}`, {
1641
+ stdio: "pipe",
1642
+ cwd: tempDir
1643
+ });
1644
+ execSync(
1645
+ `npx snarkjs groth16 prove ${zkeyPath} ${witnessPath} ${proofPath} ${publicPath}`,
1646
+ {
1647
+ stdio: "pipe",
1648
+ cwd: tempDir
1649
+ }
1650
+ );
1651
+ const proofJson = JSON.parse(readFileSync(proofPath, "utf-8"));
1652
+ const publicSignals = JSON.parse(readFileSync(publicPath, "utf-8"));
1653
+ const proof = parseProof(proofJson);
1654
+ return { proof, publicSignals };
1655
+ } finally {
1656
+ const cleanup = (filePath) => {
1657
+ try {
1658
+ if (existsSync(filePath)) unlinkSync(filePath);
1659
+ } catch {
1660
+ }
1661
+ };
1662
+ cleanup(inputPath);
1663
+ cleanup(witnessPath);
1664
+ cleanup(proofPath);
1665
+ cleanup(publicPath);
1666
+ }
1667
+ }
1668
+ async function generateProofInMemory(circuitInputs) {
1669
+ verifyCircuitFiles();
1670
+ const { wasmPath, zkeyPath } = getCircuitPaths();
1671
+ const snarkjs = await import("snarkjs");
1672
+ const { proof: proofData, publicSignals } = await snarkjs.groth16.fullProve(
1673
+ circuitInputs,
1674
+ wasmPath,
1675
+ zkeyPath
1676
+ );
1677
+ const proof = parseProof(proofData);
1678
+ return { proof, publicSignals };
1679
+ }
1680
+
1681
+ // src/transaction.ts
1682
+ async function buildTransaction(request, walletKeys, relayerUrl) {
1683
+ const client = getRelayerClient(relayerUrl);
1684
+ const pool = await client.getPool();
1685
+ const currentTreeIndex = pool.currentTreeIndex;
1686
+ const depositAmount = request.depositAmount ?? 0n;
1687
+ const withdrawAmount = request.withdrawAmount ?? 0n;
1688
+ const fee = request.fee ?? 0n;
1689
+ const inputUtxos = request.inputUtxos ?? [];
1690
+ const outputs = request.outputs ?? [];
1691
+ if (depositAmount === 0n && inputUtxos.length === 0) {
1692
+ throw new Error("Either depositAmount or inputUtxos is required");
1693
+ }
1694
+ if (withdrawAmount > 0n && !request.recipient) {
1695
+ throw new Error("recipient is required when withdrawing");
1696
+ }
1697
+ let inputTreeIndex = currentTreeIndex;
1698
+ if (inputUtxos.length > 0) {
1699
+ inputTreeIndex = inputUtxos[0].treeIndex;
1700
+ for (const utxo of inputUtxos) {
1701
+ if (utxo.treeIndex !== inputTreeIndex) {
1702
+ throw new Error("All input UTXOs must be from the same tree");
1703
+ }
1704
+ }
1705
+ }
1706
+ const leaves = await client.getCommitmentsForTree(inputTreeIndex);
1707
+ let merkleRoot;
1708
+ if (inputUtxos.length > 0) {
1709
+ merkleRoot = await fetchMerkleRoot(inputTreeIndex, relayerUrl);
1710
+ } else {
1711
+ merkleRoot = await fetchMerkleRoot(currentTreeIndex, relayerUrl);
1712
+ }
1713
+ const nextLeafIndex = await getNextLeafIndex(currentTreeIndex, relayerUrl);
1714
+ const totalInputValue = inputUtxos.reduce((sum, u) => sum + u.value, 0n);
1715
+ const totalAvailable = totalInputValue + depositAmount;
1716
+ const mintBigInt = pubkeyToBigInt(request.mint);
1717
+ const outputUtxos = [];
1718
+ let totalOutputValue = 0n;
1719
+ let outputIndex = 0;
1720
+ for (const output of outputs) {
1721
+ const owner = output.owner === "self" ? walletKeys.owner : output.owner;
1722
+ const utxo = createUTXO(
1723
+ output.amount,
1724
+ mintBigInt,
1725
+ owner,
1726
+ nextLeafIndex + outputIndex,
1727
+ currentTreeIndex
1728
+ );
1729
+ outputUtxos.push(utxo);
1730
+ totalOutputValue += output.amount;
1731
+ outputIndex++;
1732
+ }
1733
+ const totalOut = withdrawAmount + fee + totalOutputValue;
1734
+ const changeAmount = totalAvailable - totalOut;
1735
+ if (changeAmount < 0n) {
1736
+ throw new Error(
1737
+ `Insufficient value. Have ${totalAvailable} (inputs: ${totalInputValue}, deposit: ${depositAmount}), need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount}, fee: ${fee})`
1738
+ );
1739
+ }
1740
+ if (changeAmount > 0n) {
1741
+ if (outputUtxos.length >= NUM_OUTPUT_UTXOS) {
1742
+ throw new Error("No output slot available for change");
1743
+ }
1744
+ const changeUtxo = createUTXO(
1745
+ changeAmount,
1746
+ mintBigInt,
1747
+ walletKeys.owner,
1748
+ nextLeafIndex + outputIndex,
1749
+ currentTreeIndex
1750
+ );
1751
+ outputUtxos.push(changeUtxo);
1752
+ totalOutputValue += changeAmount;
1753
+ }
1754
+ while (outputUtxos.length < NUM_OUTPUT_UTXOS) {
1755
+ outputUtxos.push(createDummyUTXO());
1756
+ }
1757
+ const circuitInputs = buildCircuitInputs(
1758
+ merkleRoot,
1759
+ inputUtxos,
1760
+ outputUtxos,
1761
+ walletKeys,
1762
+ leaves,
1763
+ depositAmount,
1764
+ withdrawAmount,
1765
+ request.recipient ?? PublicKey3.default,
1766
+ request.mint,
1767
+ fee
1768
+ );
1769
+ const { proof } = await generateProof(circuitInputs);
1770
+ const nullifiers = circuitInputs.nullifiers.map((n) => BigInt(n));
1771
+ const publicInputs = {
1772
+ merkleRoot: Array.from(bigIntToBytes32(merkleRoot)),
1773
+ nullifiers: nullifiers.map((n) => Array.from(bigIntToBytes32(n))),
1774
+ outputCommitments: outputUtxos.map((u) => Array.from(bigIntToBytes32(u.commitment))),
1775
+ publicDeposit: depositAmount,
1776
+ publicWithdraw: withdrawAmount,
1777
+ recipient: request.recipient ?? PublicKey3.default,
1778
+ mintTokenAddress: request.mint,
1779
+ fee
1780
+ };
1781
+ const encryptedNotes = [];
1782
+ for (let i = 0; i < outputs.length; i++) {
1783
+ const output = outputs[i];
1784
+ const utxo = outputUtxos[i];
1785
+ if (output.encryptionPubKey && utxo.value > 0n) {
1786
+ const encrypted = encryptNote(
1787
+ {
1788
+ value: utxo.value,
1789
+ mintTokenAddress: utxo.mintTokenAddress,
1790
+ secret: utxo.secret
1791
+ // leafIndex omitted - recipient queries relayer with computed commitment
1792
+ },
1793
+ output.encryptionPubKey
1794
+ );
1795
+ encryptedNotes.push(Buffer.from(serializeEncryptedNote(encrypted)));
1796
+ }
1797
+ }
1798
+ if (changeAmount > 0n) {
1799
+ const changeIndex = outputs.length;
1800
+ const changeUtxo = outputUtxos[changeIndex];
1801
+ const encrypted = encryptNote(
1802
+ {
1803
+ value: changeUtxo.value,
1804
+ mintTokenAddress: changeUtxo.mintTokenAddress,
1805
+ secret: changeUtxo.secret
1806
+ // leafIndex omitted - we already know it locally
1807
+ },
1808
+ walletKeys.encryptionKeyPair.publicKey
1809
+ );
1810
+ encryptedNotes.push(Buffer.from(serializeEncryptedNote(encrypted)));
1811
+ }
1812
+ return {
1813
+ proof,
1814
+ publicInputs,
1815
+ encryptedNotes,
1816
+ outputUtxos: outputUtxos.filter((u) => u.value > 0n),
1817
+ nullifiers,
1818
+ inputTreeIndex,
1819
+ outputTreeIndex: currentTreeIndex
1820
+ };
1821
+ }
1822
+ function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, leaves, depositAmount, withdrawAmount, recipient, mint, fee) {
1823
+ const inputSpendingKeys = [];
1824
+ const inputValues = [];
1825
+ const inputSecrets = [];
1826
+ const inputLeafIndices = [];
1827
+ const inputPathElements = [];
1828
+ const inputPathIndices = [];
1829
+ const nullifiers = [];
1830
+ for (let i = 0; i < NUM_INPUT_UTXOS; i++) {
1831
+ if (i < inputUtxos.length) {
1832
+ const utxo = inputUtxos[i];
1833
+ inputSpendingKeys.push(walletKeys.spendingKey.toString());
1834
+ inputValues.push(utxo.value.toString());
1835
+ inputSecrets.push(utxo.secret.toString());
1836
+ inputLeafIndices.push(utxo.leafIndex.toString());
1837
+ const { pathElements, pathIndices } = computeMerklePathFromLeaves(
1838
+ utxo.leafIndex,
1839
+ leaves
1840
+ );
1841
+ inputPathElements.push(pathElements.map((e) => e.toString()));
1842
+ inputPathIndices.push(pathIndices);
1843
+ const nullifier = computeNullifier(
1844
+ walletKeys.spendingKey,
1845
+ utxo.commitment,
1846
+ utxo.leafIndex
1847
+ );
1848
+ nullifiers.push(nullifier.toString());
1849
+ } else {
1850
+ inputSpendingKeys.push("0");
1851
+ inputValues.push("0");
1852
+ inputSecrets.push("0");
1853
+ inputLeafIndices.push("0");
1854
+ inputPathElements.push(Array(MERKLE_TREE_DEPTH).fill("0"));
1855
+ inputPathIndices.push(Array(MERKLE_TREE_DEPTH).fill(0));
1856
+ nullifiers.push("0");
1857
+ }
1858
+ }
1859
+ return {
1860
+ merkleRoot: merkleRoot.toString(),
1861
+ nullifiers,
1862
+ outputCommitments: outputUtxos.map((u) => u.commitment.toString()),
1863
+ publicDeposit: depositAmount.toString(),
1864
+ publicWithdraw: withdrawAmount.toString(),
1865
+ recipient: pubkeyToBigInt(recipient).toString(),
1866
+ mintTokenAddress: pubkeyToBigInt(mint).toString(),
1867
+ fee: fee.toString(),
1868
+ inputSpendingKeys,
1869
+ inputValues,
1870
+ inputSecrets,
1871
+ inputLeafIndices,
1872
+ inputPathElements,
1873
+ inputPathIndices,
1874
+ outputValues: outputUtxos.map((u) => u.value.toString()),
1875
+ outputOwners: outputUtxos.map((u) => u.owner.toString()),
1876
+ outputSecrets: outputUtxos.map((u) => u.secret.toString())
1877
+ };
1878
+ }
1879
+ function buildTransactionAccounts(builtTx, payer, mint, depositAmount, withdrawAmount, recipient) {
1880
+ const [poolPda] = getPoolPDA();
1881
+ const [vaultPda] = getVaultPDA(mint);
1882
+ const [inputTreePda] = getMerkleTreePDA(builtTx.inputTreeIndex);
1883
+ const [outputTreePda] = getMerkleTreePDA(builtTx.outputTreeIndex);
1884
+ const preInstructions = [];
1885
+ let depositorTokenAccount = null;
1886
+ let depositor = null;
1887
+ if (depositAmount > 0n) {
1888
+ depositorTokenAccount = getAssociatedTokenAddressSync(
1889
+ mint,
1890
+ payer,
1891
+ false,
1892
+ TOKEN_2022_PROGRAM_ID
1893
+ );
1894
+ depositor = payer;
1895
+ }
1896
+ let recipientTokenAccount = null;
1897
+ if (withdrawAmount > 0n && recipient) {
1898
+ recipientTokenAccount = getAssociatedTokenAddressSync(
1899
+ mint,
1900
+ recipient,
1901
+ false,
1902
+ TOKEN_2022_PROGRAM_ID
1903
+ );
1904
+ const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
1905
+ payer,
1906
+ recipientTokenAccount,
1907
+ recipient,
1908
+ mint,
1909
+ TOKEN_2022_PROGRAM_ID
1910
+ );
1911
+ preInstructions.push(createAtaIx);
1912
+ }
1913
+ const nullifierPdas = [];
1914
+ for (const nullifier of builtTx.nullifiers) {
1915
+ if (nullifier === 0n) {
1916
+ nullifierPdas.push(null);
1917
+ } else {
1918
+ const nullifierBytes = bigIntToBytes32(nullifier);
1919
+ const [nullifierPda] = getNullifierPDA(nullifierBytes);
1920
+ nullifierPdas.push(nullifierPda);
1921
+ }
1922
+ }
1923
+ const inputTreeIndex = builtTx.inputTreeIndex !== builtTx.outputTreeIndex ? builtTx.inputTreeIndex : null;
1924
+ return {
1925
+ accounts: {
1926
+ payer,
1927
+ mint,
1928
+ pool: poolPda,
1929
+ inputTree: inputTreePda,
1930
+ merkleTree: outputTreePda,
1931
+ vault: vaultPda,
1932
+ depositorTokenAccount,
1933
+ depositor,
1934
+ recipientTokenAccount,
1935
+ feeRecipientTokenAccount: null,
1936
+ // TODO: Add fee recipient support
1937
+ nullifierAccount0: nullifierPdas[0] ?? null,
1938
+ nullifierAccount1: nullifierPdas[1] ?? null,
1939
+ tokenProgram: TOKEN_2022_PROGRAM_ID,
1940
+ systemProgram: SystemProgram.programId
1941
+ },
1942
+ preInstructions,
1943
+ inputTreeIndex
1944
+ };
1945
+ }
1946
+ function toAnchorPublicInputs(builtTx) {
1947
+ return {
1948
+ merkleRoot: builtTx.publicInputs.merkleRoot,
1949
+ nullifiers: builtTx.publicInputs.nullifiers,
1950
+ outputCommitments: builtTx.publicInputs.outputCommitments,
1951
+ publicDeposit: new BN(builtTx.publicInputs.publicDeposit.toString()),
1952
+ publicWithdraw: new BN(builtTx.publicInputs.publicWithdraw.toString()),
1953
+ recipient: builtTx.publicInputs.recipient,
1954
+ mintTokenAddress: builtTx.publicInputs.mintTokenAddress,
1955
+ fee: new BN(builtTx.publicInputs.fee.toString())
1956
+ };
1957
+ }
1958
+
1959
+ // src/wallet.ts
1960
+ var COMPUTE_UNITS = 4e5;
1961
+ var COMPUTE_UNIT_PRICE = 1;
1962
+ var HulaWallet = class _HulaWallet {
1963
+ connection;
1964
+ relayerUrl;
1965
+ programId;
1966
+ keys;
1967
+ signer;
1968
+ utxos = [];
1969
+ lastSyncedSlot = "0";
1970
+ initialized = false;
1971
+ constructor(connection, relayerUrl, programId, keys, signer) {
1972
+ this.connection = connection;
1973
+ this.relayerUrl = relayerUrl;
1974
+ this.programId = programId;
1975
+ this.keys = keys;
1976
+ this.signer = signer;
1977
+ }
1978
+ /**
1979
+ * Create a new wallet with a random spending key
1980
+ */
1981
+ static async create(config) {
1982
+ await initPoseidon();
1983
+ const connection = new Connection(config.rpcUrl, "confirmed");
1984
+ const relayerUrl = config.relayerUrl;
1985
+ const programId = config.programId ?? new PublicKey4("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
1986
+ setDefaultRelayerUrl(relayerUrl);
1987
+ const spendingKey = generateSpendingKey();
1988
+ const keys = deriveKeys(spendingKey);
1989
+ const wallet = new _HulaWallet(connection, relayerUrl, programId, keys);
1990
+ wallet.initialized = true;
1991
+ return wallet;
1992
+ }
1993
+ /**
1994
+ * Create a wallet from a Solana keypair
1995
+ *
1996
+ * Derives the spending key deterministically from the keypair's secret key.
1997
+ */
1998
+ static async fromKeypair(keypair, config) {
1999
+ await initPoseidon();
2000
+ const connection = new Connection(config.rpcUrl, "confirmed");
2001
+ const relayerUrl = config.relayerUrl;
2002
+ const programId = config.programId ?? new PublicKey4("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
2003
+ setDefaultRelayerUrl(relayerUrl);
2004
+ const spendingKey = deriveSpendingKeyFromSignature(keypair.secretKey);
2005
+ const keys = deriveKeys(spendingKey);
2006
+ const wallet = new _HulaWallet(connection, relayerUrl, programId, keys, keypair);
2007
+ wallet.initialized = true;
2008
+ return wallet;
2009
+ }
2010
+ /**
2011
+ * Create a wallet from an existing spending key
2012
+ */
2013
+ static async fromSpendingKey(spendingKey, config) {
2014
+ await initPoseidon();
2015
+ const connection = new Connection(config.rpcUrl, "confirmed");
2016
+ const relayerUrl = config.relayerUrl;
2017
+ const programId = config.programId ?? new PublicKey4("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
2018
+ setDefaultRelayerUrl(relayerUrl);
2019
+ const keys = deriveKeys(spendingKey);
2020
+ const wallet = new _HulaWallet(connection, relayerUrl, programId, keys);
2021
+ wallet.initialized = true;
2022
+ return wallet;
2023
+ }
2024
+ /**
2025
+ * Create a wallet from a Solana signature (deterministic derivation)
2026
+ *
2027
+ * This allows recovery as long as the user can sign with their Solana wallet.
2028
+ */
2029
+ static async fromSignature(signature, config) {
2030
+ await initPoseidon();
2031
+ const connection = new Connection(config.rpcUrl, "confirmed");
2032
+ const relayerUrl = config.relayerUrl;
2033
+ const programId = config.programId ?? new PublicKey4("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
2034
+ setDefaultRelayerUrl(relayerUrl);
2035
+ const spendingKey = deriveSpendingKeyFromSignature(signature);
2036
+ const keys = deriveKeys(spendingKey);
2037
+ const wallet = new _HulaWallet(connection, relayerUrl, programId, keys);
2038
+ wallet.initialized = true;
2039
+ return wallet;
2040
+ }
2041
+ // ============================================================================
2042
+ // Getters
2043
+ // ============================================================================
2044
+ /**
2045
+ * Get wallet's owner hash (public identifier)
2046
+ */
2047
+ get owner() {
2048
+ return this.keys.owner;
2049
+ }
2050
+ /**
2051
+ * Get wallet's owner hash as hex string
2052
+ */
2053
+ getOwnerHash() {
2054
+ return "0x" + this.keys.owner.toString(16).padStart(64, "0");
2055
+ }
2056
+ /**
2057
+ * Get wallet's encryption public key as hex string
2058
+ */
2059
+ getEncryptionPublicKey() {
2060
+ return "0x" + Buffer.from(this.keys.encryptionKeyPair.publicKey).toString("hex");
2061
+ }
2062
+ /**
2063
+ * Get wallet's encryption public key as bytes
2064
+ */
2065
+ get encryptionPublicKey() {
2066
+ return this.keys.encryptionKeyPair.publicKey;
2067
+ }
2068
+ /**
2069
+ * Get spending key (CAREFUL - this is sensitive!)
2070
+ */
2071
+ get spendingKey() {
2072
+ return this.keys.spendingKey;
2073
+ }
2074
+ /**
2075
+ * Get all wallet keys
2076
+ */
2077
+ get walletKeys() {
2078
+ return this.keys;
2079
+ }
2080
+ /**
2081
+ * Get the connection
2082
+ */
2083
+ getConnection() {
2084
+ return this.connection;
2085
+ }
2086
+ /**
2087
+ * Get the signer keypair (if available)
2088
+ */
2089
+ getSigner() {
2090
+ return this.signer;
2091
+ }
2092
+ /**
2093
+ * Set the signer keypair
2094
+ */
2095
+ setSigner(signer) {
2096
+ this.signer = signer;
2097
+ }
2098
+ // ============================================================================
2099
+ // UTXO Management
2100
+ // ============================================================================
2101
+ /**
2102
+ * Sync wallet with relayer
2103
+ *
2104
+ * Fetches new encrypted notes and checks for spent UTXOs.
2105
+ */
2106
+ async sync(onProgress) {
2107
+ if (!this.initialized) {
2108
+ throw new Error("Wallet not initialized");
2109
+ }
2110
+ onProgress?.({ stage: "notes", current: 0, total: 100 });
2111
+ try {
2112
+ const { newUTXOs, spentUTXOs } = await syncUTXOs(
2113
+ this.keys,
2114
+ this.utxos,
2115
+ this.relayerUrl,
2116
+ this.lastSyncedSlot !== "0" ? this.lastSyncedSlot : void 0
2117
+ );
2118
+ this.utxos.push(...newUTXOs);
2119
+ for (const spentId of spentUTXOs) {
2120
+ const utxo = this.utxos.find((u) => u.commitment === spentId);
2121
+ if (utxo) {
2122
+ utxo.spent = true;
2123
+ }
2124
+ }
2125
+ const client = getRelayerClient(this.relayerUrl);
2126
+ const stats = await client.getStats();
2127
+ this.lastSyncedSlot = Date.now().toString();
2128
+ onProgress?.({ stage: "complete", current: 100, total: 100 });
2129
+ return {
2130
+ newUtxos: newUTXOs.length,
2131
+ spentUtxos: spentUTXOs.length,
2132
+ errors: []
2133
+ };
2134
+ } catch (err) {
2135
+ const errorMsg = err instanceof Error ? err.message : "Sync failed";
2136
+ onProgress?.({ stage: "complete", current: 100, total: 100 });
2137
+ return {
2138
+ newUtxos: 0,
2139
+ spentUtxos: 0,
2140
+ errors: [errorMsg]
2141
+ };
2142
+ }
2143
+ }
2144
+ /**
2145
+ * Get all unspent UTXOs
2146
+ */
2147
+ getUnspentUTXOs() {
2148
+ return this.utxos.filter((u) => !u.spent).map(deserializeUTXO);
2149
+ }
2150
+ /**
2151
+ * Get all UTXOs (including spent)
2152
+ */
2153
+ getAllUTXOs() {
2154
+ return [...this.utxos];
2155
+ }
2156
+ /**
2157
+ * Get balance for a specific mint
2158
+ */
2159
+ getBalance(mint) {
2160
+ const mintBigInt = pubkeyToBigInt(mint);
2161
+ const unspent = this.getUnspentUTXOs();
2162
+ return calculateBalance(unspent, mintBigInt);
2163
+ }
2164
+ /**
2165
+ * Import UTXOs (for wallet recovery or manual import)
2166
+ */
2167
+ importUTXOs(utxos) {
2168
+ this.utxos.push(...utxos);
2169
+ }
2170
+ /**
2171
+ * Export UTXOs for backup
2172
+ */
2173
+ exportUTXOs() {
2174
+ return [...this.utxos];
2175
+ }
2176
+ // ============================================================================
2177
+ // Transaction Building
2178
+ // ============================================================================
2179
+ /**
2180
+ * Deposit tokens into the privacy pool
2181
+ *
2182
+ * @param mint - Token mint address
2183
+ * @param amount - Amount in raw token units
2184
+ * @returns Transaction signature
2185
+ */
2186
+ async deposit(mint, amount) {
2187
+ if (!this.signer) {
2188
+ throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
2189
+ }
2190
+ const builtTx = await buildTransaction(
2191
+ {
2192
+ mint,
2193
+ depositAmount: amount,
2194
+ outputs: []
2195
+ // Change will be created automatically
2196
+ },
2197
+ this.keys,
2198
+ this.relayerUrl
2199
+ );
2200
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
2201
+ builtTx,
2202
+ this.signer.publicKey,
2203
+ mint,
2204
+ amount,
2205
+ 0n
2206
+ );
2207
+ const allPreInstructions = [
2208
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
2209
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
2210
+ ...preInstructions
2211
+ ];
2212
+ const wallet = new Wallet(this.signer);
2213
+ const provider = new AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
2214
+ const program = new Program(idl_default, provider);
2215
+ const publicInputs = toAnchorPublicInputs(builtTx);
2216
+ const signature = await program.methods.transact(
2217
+ inputTreeIndex,
2218
+ Array.from(builtTx.proof),
2219
+ publicInputs,
2220
+ builtTx.encryptedNotes
2221
+ ).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
2222
+ return signature;
2223
+ }
2224
+ /**
2225
+ * Transfer tokens privately to another user
2226
+ *
2227
+ * @param mint - Token mint address
2228
+ * @param amount - Amount in raw token units
2229
+ * @param recipientOwner - Recipient's owner hash (bigint)
2230
+ * @param recipientEncryptionPubKey - Recipient's encryption public key
2231
+ * @returns Transaction signature
2232
+ */
2233
+ async transfer(mint, amount, recipientOwner, recipientEncryptionPubKey) {
2234
+ if (!this.signer) {
2235
+ throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
2236
+ }
2237
+ const mintBigInt = pubkeyToBigInt(mint);
2238
+ const unspent = this.getUnspentUTXOs().filter(
2239
+ (u) => u.mintTokenAddress === mintBigInt
2240
+ );
2241
+ const inputUtxos = selectUTXOs(unspent, amount, mintBigInt);
2242
+ const builtTx = await buildTransaction(
2243
+ {
2244
+ mint,
2245
+ inputUtxos,
2246
+ outputs: [
2247
+ {
2248
+ owner: recipientOwner,
2249
+ amount,
2250
+ encryptionPubKey: recipientEncryptionPubKey
2251
+ }
2252
+ ]
2253
+ },
2254
+ this.keys,
2255
+ this.relayerUrl
2256
+ );
2257
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
2258
+ builtTx,
2259
+ this.signer.publicKey,
2260
+ mint,
2261
+ 0n,
2262
+ 0n
2263
+ );
2264
+ const allPreInstructions = [
2265
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
2266
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
2267
+ ...preInstructions
2268
+ ];
2269
+ const wallet = new Wallet(this.signer);
2270
+ const provider = new AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
2271
+ const program = new Program(idl_default, provider);
2272
+ const publicInputs = toAnchorPublicInputs(builtTx);
2273
+ const signature = await program.methods.transact(
2274
+ inputTreeIndex,
2275
+ Array.from(builtTx.proof),
2276
+ publicInputs,
2277
+ builtTx.encryptedNotes
2278
+ ).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
2279
+ for (const utxo of inputUtxos) {
2280
+ const serialized = this.utxos.find((u) => u.commitment === utxo.commitment.toString());
2281
+ if (serialized) {
2282
+ serialized.spent = true;
2283
+ serialized.spentTx = signature;
2284
+ }
2285
+ }
2286
+ return signature;
2287
+ }
2288
+ /**
2289
+ * Withdraw tokens from the privacy pool to a public address
2290
+ *
2291
+ * @param mint - Token mint address
2292
+ * @param amount - Amount in raw token units
2293
+ * @param recipient - Public key to receive the tokens
2294
+ * @returns Transaction signature
2295
+ */
2296
+ async withdraw(mint, amount, recipient) {
2297
+ if (!this.signer) {
2298
+ throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
2299
+ }
2300
+ const mintBigInt = pubkeyToBigInt(mint);
2301
+ const unspent = this.getUnspentUTXOs().filter(
2302
+ (u) => u.mintTokenAddress === mintBigInt
2303
+ );
2304
+ const inputUtxos = selectUTXOs(unspent, amount, mintBigInt);
2305
+ const builtTx = await buildTransaction(
2306
+ {
2307
+ mint,
2308
+ inputUtxos,
2309
+ withdrawAmount: amount,
2310
+ recipient
2311
+ },
2312
+ this.keys,
2313
+ this.relayerUrl
2314
+ );
2315
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
2316
+ builtTx,
2317
+ this.signer.publicKey,
2318
+ mint,
2319
+ 0n,
2320
+ amount,
2321
+ recipient
2322
+ );
2323
+ const allPreInstructions = [
2324
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
2325
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
2326
+ ...preInstructions
2327
+ ];
2328
+ const wallet = new Wallet(this.signer);
2329
+ const provider = new AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
2330
+ const program = new Program(idl_default, provider);
2331
+ const publicInputs = toAnchorPublicInputs(builtTx);
2332
+ const signature = await program.methods.transact(
2333
+ inputTreeIndex,
2334
+ Array.from(builtTx.proof),
2335
+ publicInputs,
2336
+ builtTx.encryptedNotes
2337
+ ).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
2338
+ for (const utxo of inputUtxos) {
2339
+ const serialized = this.utxos.find((u) => u.commitment === utxo.commitment.toString());
2340
+ if (serialized) {
2341
+ serialized.spent = true;
2342
+ serialized.spentTx = signature;
2343
+ }
2344
+ }
2345
+ return signature;
2346
+ }
2347
+ /**
2348
+ * Build a custom transaction
2349
+ */
2350
+ async buildTransaction(request) {
2351
+ const tx = await buildTransaction(request, this.keys, this.relayerUrl);
2352
+ const { preInstructions } = buildTransactionAccounts(
2353
+ tx,
2354
+ PublicKey4.default,
2355
+ request.mint,
2356
+ request.depositAmount ?? 0n,
2357
+ request.withdrawAmount ?? 0n,
2358
+ request.recipient
2359
+ );
2360
+ return { transaction: tx, instructions: preInstructions };
2361
+ }
2362
+ // ============================================================================
2363
+ // Transaction Submission
2364
+ // ============================================================================
2365
+ /**
2366
+ * Submit a transaction using an Anchor program
2367
+ *
2368
+ * @param program - Anchor program instance (any type to avoid IDL complexity)
2369
+ * @param payer - Keypair for signing
2370
+ * @param builtTx - Built transaction from buildTransaction()
2371
+ * @param mint - Token mint
2372
+ * @param depositAmount - Amount being deposited (if any)
2373
+ * @param withdrawAmount - Amount being withdrawn (if any)
2374
+ * @param recipient - Recipient for withdrawal (if any)
2375
+ */
2376
+ async submitTransaction(program, payer, builtTx, mint, depositAmount = 0n, withdrawAmount = 0n, recipient) {
2377
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
2378
+ builtTx,
2379
+ payer.publicKey,
2380
+ mint,
2381
+ depositAmount,
2382
+ withdrawAmount,
2383
+ recipient
2384
+ );
2385
+ const publicInputs = toAnchorPublicInputs(builtTx);
2386
+ const tx = await program.methods.transact(
2387
+ inputTreeIndex,
2388
+ Array.from(builtTx.proof),
2389
+ publicInputs,
2390
+ builtTx.encryptedNotes
2391
+ ).accounts(accounts).preInstructions(preInstructions).signers([payer]).rpc();
2392
+ for (const _utxo of builtTx.outputUtxos) {
2393
+ }
2394
+ return tx;
2395
+ }
2396
+ };
2397
+ function getKeyDerivationMessage() {
2398
+ return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1");
2399
+ }
2400
+ async function initHulaSDK() {
2401
+ if (!isPoseidonInitialized()) {
2402
+ await initPoseidon();
2403
+ }
2404
+ }
2405
+ export {
2406
+ DOMAIN_ENCRYPTION,
2407
+ DOMAIN_NULLIFIER,
2408
+ DOMAIN_OWNER,
2409
+ DOMAIN_VIEWING,
2410
+ FIELD_PRIME,
2411
+ HulaWallet,
2412
+ MAX_LEAVES,
2413
+ MERKLE_TREE_DEPTH,
2414
+ MERKLE_TREE_SEED,
2415
+ NULLIFIER_SEED,
2416
+ NUM_INPUT_UTXOS,
2417
+ NUM_OUTPUT_UTXOS,
2418
+ POOL_SEED,
2419
+ PROGRAM_ID,
2420
+ PROOF_SIZE,
2421
+ RelayerClient,
2422
+ TOKEN_2022_PROGRAM_ID,
2423
+ VAULT_SEED,
2424
+ bigIntToBytes,
2425
+ bigIntToBytes32,
2426
+ buildTransaction,
2427
+ buildTransactionAccounts,
2428
+ bytesToBigInt,
2429
+ bytesToHex,
2430
+ calculateBalance,
2431
+ computeCommitment,
2432
+ computeMerklePathFromLeaves,
2433
+ computeMerkleRoot,
2434
+ computeNullifier,
2435
+ computeNullifierFromKeys,
2436
+ computeZeros,
2437
+ createDummyUTXO,
2438
+ createUTXO,
2439
+ decryptNote,
2440
+ deriveKeys,
2441
+ deriveSpendingKeyFromSignature,
2442
+ deserializeEncryptedNote,
2443
+ deserializeUTXO,
2444
+ encryptNote,
2445
+ fetchMerklePath,
2446
+ fetchMerkleRoot,
2447
+ formatCommitment,
2448
+ generateProof,
2449
+ generateProofInMemory,
2450
+ generateSpendingKey,
2451
+ getCircuitPaths,
2452
+ getCurrentTreeIndex,
2453
+ getEmptyTreeRoot,
2454
+ getKeyDerivationMessage,
2455
+ getMerkleTreePDA,
2456
+ getNextLeafIndex,
2457
+ getNullifierPDA,
2458
+ getPoolPDA,
2459
+ getPoseidon,
2460
+ getRelayerClient,
2461
+ getVaultPDA,
2462
+ getZeroAtLevel,
2463
+ hexToBytes,
2464
+ initHulaSDK,
2465
+ initPoseidon,
2466
+ isPoseidonInitialized,
2467
+ parseProof,
2468
+ poseidonHash,
2469
+ pubkeyToBigInt,
2470
+ scanNotesForUTXOs,
2471
+ selectUTXOs,
2472
+ serializeEncryptedNote,
2473
+ serializeUTXO,
2474
+ setCircuitPaths,
2475
+ setDefaultRelayerUrl,
2476
+ syncUTXOs,
2477
+ toAnchorPublicInputs,
2478
+ verifyCircuitFiles,
2479
+ verifyMerklePath
2480
+ };