@hula-privacy/mixer 0.3.0 → 0.4.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.d.mts CHANGED
@@ -109,7 +109,6 @@ interface CircuitInputs {
109
109
  publicWithdraw: string;
110
110
  recipient: string;
111
111
  mintTokenAddress: string;
112
- fee: string;
113
112
  inputSpendingKeys: string[];
114
113
  inputValues: string[];
115
114
  inputSecrets: string[];
@@ -131,7 +130,6 @@ interface PublicInputs {
131
130
  publicWithdraw: bigint;
132
131
  recipient: PublicKey;
133
132
  mintTokenAddress: PublicKey;
134
- fee: bigint;
135
133
  }
136
134
  /**
137
135
  * Output UTXO specification
@@ -158,8 +156,6 @@ interface TransactionRequest {
158
156
  withdrawAmount?: bigint;
159
157
  /** Recipient public key for withdrawal */
160
158
  recipient?: PublicKey;
161
- /** Relayer fee */
162
- fee?: bigint;
163
159
  /** Token mint address */
164
160
  mint: PublicKey;
165
161
  }
@@ -223,7 +219,6 @@ interface TransactionData {
223
219
  outputTreeIndex: number;
224
220
  publicDeposit: string;
225
221
  publicWithdraw: string;
226
- fee: string;
227
222
  success: boolean;
228
223
  nullifiers: string[];
229
224
  encryptedNotes: {
@@ -319,7 +314,6 @@ declare function buildTransactionAccounts(builtTx: BuiltTransaction, payer: Publ
319
314
  depositorTokenAccount: PublicKey | null;
320
315
  depositor: PublicKey | null;
321
316
  recipientTokenAccount: PublicKey | null;
322
- feeRecipientTokenAccount: PublicKey | null;
323
317
  nullifierAccount0: PublicKey | null;
324
318
  nullifierAccount1: PublicKey | null;
325
319
  tokenProgram: PublicKey;
@@ -339,7 +333,6 @@ declare function toAnchorPublicInputs(builtTx: BuiltTransaction): {
339
333
  publicWithdraw: BN;
340
334
  recipient: PublicKey;
341
335
  mintTokenAddress: PublicKey;
342
- fee: BN;
343
336
  };
344
337
 
345
338
  /**
@@ -472,6 +465,15 @@ declare class HulaWallet {
472
465
  * @returns Transaction signature
473
466
  */
474
467
  withdraw(mint: PublicKey, amount: bigint, recipient: PublicKey): Promise<string>;
468
+ /**
469
+ * Merge UTXOs
470
+ *
471
+ * @param utxo1 - First UTXO to merge
472
+ * @param utxo2 - Second UTXO to merge
473
+ * @returns Transaction signature
474
+ *
475
+ */
476
+ mergeUTXOs(utxo1: UTXO, utxo2: UTXO): Promise<string>;
475
477
  /**
476
478
  * Build a custom transaction
477
479
  */
@@ -660,7 +662,7 @@ declare function pubkeyToBigInt(pubkey: {
660
662
  /**
661
663
  * Generate a new random spending key
662
664
  */
663
- declare function generateSpendingKey(): bigint;
665
+ declare function generateSpendingKey(numberOfBits?: bigint): bigint;
664
666
  /**
665
667
  * Derive all wallet keys from spending key
666
668
  */
@@ -676,13 +678,10 @@ declare function deriveSpendingKeyFromSignature(signature: Uint8Array): bigint;
676
678
  * Encrypt a note for a recipient
677
679
  *
678
680
  * The note contains the UTXO data needed for the recipient to claim it.
679
- * Optimized for size:
680
- * - mintTokenAddress: only first 8 bytes (can match on-chain by prefix)
681
- * - leafIndex: omitted (recipient can query relayer with commitment)
682
- */
681
+ *
682
+ */
683
683
  declare function encryptNote(noteData: {
684
684
  value: bigint;
685
- mintTokenAddress: bigint;
686
685
  secret: bigint;
687
686
  leafIndex?: number;
688
687
  }, recipientEncryptionPubKey: Uint8Array): EncryptedNote;
@@ -695,7 +694,6 @@ declare function encryptNote(noteData: {
695
694
  */
696
695
  declare function decryptNote(encryptedNote: EncryptedNote, encryptionSecretKey: Uint8Array): {
697
696
  value: bigint;
698
- mintPrefix: string;
699
697
  secret: bigint;
700
698
  } | null;
701
699
  /**
package/dist/index.d.ts CHANGED
@@ -109,7 +109,6 @@ interface CircuitInputs {
109
109
  publicWithdraw: string;
110
110
  recipient: string;
111
111
  mintTokenAddress: string;
112
- fee: string;
113
112
  inputSpendingKeys: string[];
114
113
  inputValues: string[];
115
114
  inputSecrets: string[];
@@ -131,7 +130,6 @@ interface PublicInputs {
131
130
  publicWithdraw: bigint;
132
131
  recipient: PublicKey;
133
132
  mintTokenAddress: PublicKey;
134
- fee: bigint;
135
133
  }
136
134
  /**
137
135
  * Output UTXO specification
@@ -158,8 +156,6 @@ interface TransactionRequest {
158
156
  withdrawAmount?: bigint;
159
157
  /** Recipient public key for withdrawal */
160
158
  recipient?: PublicKey;
161
- /** Relayer fee */
162
- fee?: bigint;
163
159
  /** Token mint address */
164
160
  mint: PublicKey;
165
161
  }
@@ -223,7 +219,6 @@ interface TransactionData {
223
219
  outputTreeIndex: number;
224
220
  publicDeposit: string;
225
221
  publicWithdraw: string;
226
- fee: string;
227
222
  success: boolean;
228
223
  nullifiers: string[];
229
224
  encryptedNotes: {
@@ -319,7 +314,6 @@ declare function buildTransactionAccounts(builtTx: BuiltTransaction, payer: Publ
319
314
  depositorTokenAccount: PublicKey | null;
320
315
  depositor: PublicKey | null;
321
316
  recipientTokenAccount: PublicKey | null;
322
- feeRecipientTokenAccount: PublicKey | null;
323
317
  nullifierAccount0: PublicKey | null;
324
318
  nullifierAccount1: PublicKey | null;
325
319
  tokenProgram: PublicKey;
@@ -339,7 +333,6 @@ declare function toAnchorPublicInputs(builtTx: BuiltTransaction): {
339
333
  publicWithdraw: BN;
340
334
  recipient: PublicKey;
341
335
  mintTokenAddress: PublicKey;
342
- fee: BN;
343
336
  };
344
337
 
345
338
  /**
@@ -472,6 +465,15 @@ declare class HulaWallet {
472
465
  * @returns Transaction signature
473
466
  */
474
467
  withdraw(mint: PublicKey, amount: bigint, recipient: PublicKey): Promise<string>;
468
+ /**
469
+ * Merge UTXOs
470
+ *
471
+ * @param utxo1 - First UTXO to merge
472
+ * @param utxo2 - Second UTXO to merge
473
+ * @returns Transaction signature
474
+ *
475
+ */
476
+ mergeUTXOs(utxo1: UTXO, utxo2: UTXO): Promise<string>;
475
477
  /**
476
478
  * Build a custom transaction
477
479
  */
@@ -660,7 +662,7 @@ declare function pubkeyToBigInt(pubkey: {
660
662
  /**
661
663
  * Generate a new random spending key
662
664
  */
663
- declare function generateSpendingKey(): bigint;
665
+ declare function generateSpendingKey(numberOfBits?: bigint): bigint;
664
666
  /**
665
667
  * Derive all wallet keys from spending key
666
668
  */
@@ -676,13 +678,10 @@ declare function deriveSpendingKeyFromSignature(signature: Uint8Array): bigint;
676
678
  * Encrypt a note for a recipient
677
679
  *
678
680
  * The note contains the UTXO data needed for the recipient to claim it.
679
- * Optimized for size:
680
- * - mintTokenAddress: only first 8 bytes (can match on-chain by prefix)
681
- * - leafIndex: omitted (recipient can query relayer with commitment)
682
- */
681
+ *
682
+ */
683
683
  declare function encryptNote(noteData: {
684
684
  value: bigint;
685
- mintTokenAddress: bigint;
686
685
  secret: bigint;
687
686
  leafIndex?: number;
688
687
  }, recipientEncryptionPubKey: Uint8Array): EncryptedNote;
@@ -695,7 +694,6 @@ declare function encryptNote(noteData: {
695
694
  */
696
695
  declare function decryptNote(encryptedNote: EncryptedNote, encryptionSecretKey: Uint8Array): {
697
696
  value: bigint;
698
- mintPrefix: string;
699
697
  secret: bigint;
700
698
  } | null;
701
699
  /**
package/dist/index.js CHANGED
@@ -497,14 +497,6 @@ var HulaPrivacyIdl = {
497
497
  "writable": true,
498
498
  "optional": true
499
499
  },
500
- {
501
- "name": "fee_recipient_token_account",
502
- "docs": [
503
- "Fee recipient's token account (optional)"
504
- ],
505
- "writable": true,
506
- "optional": true
507
- },
508
500
  {
509
501
  "name": "nullifier_account_0",
510
502
  "docs": [
@@ -679,16 +671,11 @@ var HulaPrivacyIdl = {
679
671
  },
680
672
  {
681
673
  "code": 6017,
682
- "name": "InvalidFee",
683
- "msg": "Invalid fee"
684
- },
685
- {
686
- "code": 6018,
687
674
  "name": "TreeNotFull",
688
675
  "msg": "Tree is not full yet"
689
676
  },
690
677
  {
691
- "code": 6019,
678
+ "code": 6018,
692
679
  "name": "InvalidTreeIndex",
693
680
  "msg": "Invalid tree index"
694
681
  }
@@ -915,10 +902,6 @@ var HulaPrivacyIdl = {
915
902
  {
916
903
  "name": "mint_token_address",
917
904
  "type": "pubkey"
918
- },
919
- {
920
- "name": "fee",
921
- "type": "u64"
922
905
  }
923
906
  ]
924
907
  }
@@ -1054,9 +1037,9 @@ function bytesToHex(bytes) {
1054
1037
  function pubkeyToBigInt(pubkey) {
1055
1038
  return bytesToBigInt(pubkey.toBytes());
1056
1039
  }
1057
- function generateSpendingKey() {
1040
+ function generateSpendingKey(numberOfBits = 253n) {
1058
1041
  const randomBytes2 = nacl.randomBytes(32);
1059
- return bytesToBigInt(randomBytes2) % 2n ** 253n;
1042
+ return bytesToBigInt(randomBytes2) % 2n ** numberOfBits;
1060
1043
  }
1061
1044
  function deriveKeys(spendingKey) {
1062
1045
  const owner = poseidonHash([spendingKey, DOMAIN_OWNER]);
@@ -1082,17 +1065,8 @@ function deriveSpendingKeyFromSignature(signature) {
1082
1065
  return bytesToBigInt(hash) % FIELD_PRIME;
1083
1066
  }
1084
1067
  function encryptNote(noteData, recipientEncryptionPubKey) {
1085
- const mintPrefix = noteData.mintTokenAddress.toString(16).padStart(64, "0").slice(0, 16);
1086
- const payload = {
1087
- v: noteData.value.toString(),
1088
- // value
1089
- m: mintPrefix,
1090
- // mint (first 8 bytes as hex)
1091
- s: noteData.secret.toString()
1092
- // secret
1093
- // leafIndex omitted - recipient queries relayer with computed commitment
1094
- };
1095
- const message = new TextEncoder().encode(JSON.stringify(payload));
1068
+ const payload = `${noteData.value.toString()}|${noteData.secret.toString()}`;
1069
+ const message = new TextEncoder().encode(payload);
1096
1070
  const nonce = nacl.randomBytes(24);
1097
1071
  const ephemeralKeyPair = nacl.box.keyPair();
1098
1072
  const ciphertext = nacl.box(
@@ -1116,22 +1090,18 @@ function decryptNote(encryptedNote, encryptionSecretKey) {
1116
1090
  encryptionSecretKey
1117
1091
  );
1118
1092
  if (!decrypted) return null;
1119
- const noteData = JSON.parse(new TextDecoder().decode(decrypted));
1120
- if (noteData.v !== void 0) {
1121
- return {
1122
- value: BigInt(noteData.v),
1123
- mintPrefix: noteData.m,
1124
- // First 8 bytes as hex string
1125
- secret: BigInt(noteData.s)
1126
- };
1127
- } else {
1128
- return {
1129
- value: BigInt(noteData.value),
1130
- mintPrefix: BigInt(noteData.mintTokenAddress).toString(16).padStart(64, "0").slice(0, 16),
1131
- secret: BigInt(noteData.secret)
1132
- };
1133
- }
1134
- } catch {
1093
+ let noteData = null;
1094
+ const data = new TextDecoder().decode(decrypted).split("|");
1095
+ noteData = {
1096
+ v: BigInt(data[0]),
1097
+ s: BigInt(data[1])
1098
+ };
1099
+ if (!noteData) return null;
1100
+ return {
1101
+ value: BigInt(noteData.v),
1102
+ secret: BigInt(noteData.s)
1103
+ };
1104
+ } catch (error) {
1135
1105
  return null;
1136
1106
  }
1137
1107
  }
@@ -1359,7 +1329,7 @@ function computeNullifierFromKeys(keys, commitment, leafIndex) {
1359
1329
  return poseidonHash([keys.nullifierKey, commitment, BigInt(leafIndex)]);
1360
1330
  }
1361
1331
  function createUTXO(value, mintTokenAddress, owner, leafIndex, treeIndex = 0) {
1362
- const secret = generateSpendingKey();
1332
+ const secret = generateSpendingKey(127n);
1363
1333
  const commitment = computeCommitment(value, mintTokenAddress, owner, secret);
1364
1334
  return {
1365
1335
  value,
@@ -1526,6 +1496,9 @@ function selectUTXOs(utxos, targetAmount, mint) {
1526
1496
  selected.push(utxo);
1527
1497
  total += utxo.value;
1528
1498
  }
1499
+ if (selected.length > NUM_INPUT_UTXOS) {
1500
+ throw new Error(`Selected ${selected.length} UTXOs, but only ${NUM_INPUT_UTXOS} are allowed. You can merge your UTXOs to reach the target amount.`);
1501
+ }
1529
1502
  if (total < targetAmount) {
1530
1503
  throw new Error(
1531
1504
  `Insufficient balance. Need ${targetAmount}, have ${total}`
@@ -1791,7 +1764,6 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1791
1764
  const currentTreeIndex = pool.currentTreeIndex;
1792
1765
  const depositAmount = request.depositAmount ?? 0n;
1793
1766
  const withdrawAmount = request.withdrawAmount ?? 0n;
1794
- const fee = request.fee ?? 0n;
1795
1767
  const inputUtxos = request.inputUtxos ?? [];
1796
1768
  const outputs = request.outputs ?? [];
1797
1769
  if (depositAmount === 0n && inputUtxos.length === 0) {
@@ -1836,11 +1808,11 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1836
1808
  totalOutputValue += output.amount;
1837
1809
  outputIndex++;
1838
1810
  }
1839
- const totalOut = withdrawAmount + fee + totalOutputValue;
1811
+ const totalOut = withdrawAmount + totalOutputValue;
1840
1812
  const changeAmount = totalAvailable - totalOut;
1841
1813
  if (changeAmount < 0n) {
1842
1814
  throw new Error(
1843
- `Insufficient value. Have ${totalAvailable} (inputs: ${totalInputValue}, deposit: ${depositAmount}), need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount}, fee: ${fee})`
1815
+ `Insufficient value. Have ${totalAvailable} (inputs: ${totalInputValue}, deposit: ${depositAmount}), need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount})`
1844
1816
  );
1845
1817
  }
1846
1818
  if (changeAmount > 0n) {
@@ -1869,8 +1841,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1869
1841
  depositAmount,
1870
1842
  withdrawAmount,
1871
1843
  request.recipient ?? import_web33.PublicKey.default,
1872
- request.mint,
1873
- fee
1844
+ request.mint
1874
1845
  );
1875
1846
  const { proof } = await generateProof(circuitInputs);
1876
1847
  const nullifiers = circuitInputs.nullifiers.map((n) => BigInt(n));
@@ -1881,8 +1852,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1881
1852
  publicDeposit: depositAmount,
1882
1853
  publicWithdraw: withdrawAmount,
1883
1854
  recipient: request.recipient ?? import_web33.PublicKey.default,
1884
- mintTokenAddress: request.mint,
1885
- fee
1855
+ mintTokenAddress: request.mint
1886
1856
  };
1887
1857
  const encryptedNotes = [];
1888
1858
  for (let i = 0; i < outputs.length; i++) {
@@ -1892,9 +1862,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1892
1862
  const encrypted = encryptNote(
1893
1863
  {
1894
1864
  value: utxo.value,
1895
- mintTokenAddress: utxo.mintTokenAddress,
1896
1865
  secret: utxo.secret
1897
- // leafIndex omitted - recipient queries relayer with computed commitment
1898
1866
  },
1899
1867
  output.encryptionPubKey
1900
1868
  );
@@ -1907,9 +1875,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1907
1875
  const encrypted = encryptNote(
1908
1876
  {
1909
1877
  value: changeUtxo.value,
1910
- mintTokenAddress: changeUtxo.mintTokenAddress,
1911
1878
  secret: changeUtxo.secret
1912
- // leafIndex omitted - we already know it locally
1913
1879
  },
1914
1880
  walletKeys.encryptionKeyPair.publicKey
1915
1881
  );
@@ -1925,7 +1891,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1925
1891
  outputTreeIndex: currentTreeIndex
1926
1892
  };
1927
1893
  }
1928
- function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, leaves, depositAmount, withdrawAmount, recipient, mint, fee) {
1894
+ function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, leaves, depositAmount, withdrawAmount, recipient, mint) {
1929
1895
  const inputSpendingKeys = [];
1930
1896
  const inputValues = [];
1931
1897
  const inputSecrets = [];
@@ -1970,7 +1936,6 @@ function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, lea
1970
1936
  publicWithdraw: withdrawAmount.toString(),
1971
1937
  recipient: pubkeyToBigInt(recipient).toString(),
1972
1938
  mintTokenAddress: pubkeyToBigInt(mint).toString(),
1973
- fee: fee.toString(),
1974
1939
  inputSpendingKeys,
1975
1940
  inputValues,
1976
1941
  inputSecrets,
@@ -2038,8 +2003,6 @@ function buildTransactionAccounts(builtTx, payer, mint, depositAmount, withdrawA
2038
2003
  depositorTokenAccount,
2039
2004
  depositor,
2040
2005
  recipientTokenAccount,
2041
- feeRecipientTokenAccount: null,
2042
- // TODO: Add fee recipient support
2043
2006
  nullifierAccount0: nullifierPdas[0] ?? null,
2044
2007
  nullifierAccount1: nullifierPdas[1] ?? null,
2045
2008
  tokenProgram: TOKEN_2022_PROGRAM_ID,
@@ -2057,8 +2020,7 @@ function toAnchorPublicInputs(builtTx) {
2057
2020
  publicDeposit: new import_anchor.BN(builtTx.publicInputs.publicDeposit.toString()),
2058
2021
  publicWithdraw: new import_anchor.BN(builtTx.publicInputs.publicWithdraw.toString()),
2059
2022
  recipient: builtTx.publicInputs.recipient,
2060
- mintTokenAddress: builtTx.publicInputs.mintTokenAddress,
2061
- fee: new import_anchor.BN(builtTx.publicInputs.fee.toString())
2023
+ mintTokenAddress: builtTx.publicInputs.mintTokenAddress
2062
2024
  };
2063
2025
  }
2064
2026
 
@@ -2450,6 +2412,69 @@ var HulaWallet = class _HulaWallet {
2450
2412
  }
2451
2413
  return signature;
2452
2414
  }
2415
+ /**
2416
+ * Merge UTXOs
2417
+ *
2418
+ * @param utxo1 - First UTXO to merge
2419
+ * @param utxo2 - Second UTXO to merge
2420
+ * @returns Transaction signature
2421
+ *
2422
+ */
2423
+ async mergeUTXOs(utxo1, utxo2) {
2424
+ if (!this.signer) {
2425
+ throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
2426
+ }
2427
+ if (utxo1.mintTokenAddress !== utxo2.mintTokenAddress) {
2428
+ throw new Error("UTXOs have different mint token addresses");
2429
+ }
2430
+ const mint = new import_web34.PublicKey(
2431
+ bigIntToBytes(utxo1.mintTokenAddress, 32)
2432
+ );
2433
+ const mergedUtxo = createUTXO(utxo1.value + utxo2.value, utxo1.mintTokenAddress, utxo1.owner, utxo1.leafIndex, utxo1.treeIndex);
2434
+ const builtTx = await buildTransaction(
2435
+ {
2436
+ mint,
2437
+ inputUtxos: [utxo1, utxo2],
2438
+ outputs: [{
2439
+ owner: this.keys.owner,
2440
+ amount: mergedUtxo.value,
2441
+ encryptionPubKey: this.keys.encryptionKeyPair.publicKey
2442
+ }]
2443
+ },
2444
+ this.keys,
2445
+ this.relayerUrl
2446
+ );
2447
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
2448
+ builtTx,
2449
+ this.signer.publicKey,
2450
+ mint,
2451
+ 0n,
2452
+ 0n
2453
+ );
2454
+ const allPreInstructions = [
2455
+ import_web34.ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
2456
+ import_web34.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
2457
+ ...preInstructions
2458
+ ];
2459
+ const wallet = new import_anchor2.Wallet(this.signer);
2460
+ const provider = new import_anchor2.AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
2461
+ const program = new import_anchor2.Program(idl_default, provider);
2462
+ const publicInputs = toAnchorPublicInputs(builtTx);
2463
+ const signature = await program.methods.transact(
2464
+ inputTreeIndex,
2465
+ Array.from(builtTx.proof),
2466
+ publicInputs,
2467
+ builtTx.encryptedNotes
2468
+ ).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
2469
+ for (const utxo of [utxo1, utxo2]) {
2470
+ const serialized = this.utxos.find((u) => u.commitment === utxo.commitment.toString());
2471
+ if (serialized) {
2472
+ serialized.spent = true;
2473
+ serialized.spentTx = signature;
2474
+ }
2475
+ }
2476
+ return signature;
2477
+ }
2453
2478
  /**
2454
2479
  * Build a custom transaction
2455
2480
  */
@@ -2501,7 +2526,7 @@ var HulaWallet = class _HulaWallet {
2501
2526
  }
2502
2527
  };
2503
2528
  function getKeyDerivationMessage() {
2504
- return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1");
2529
+ return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1. Don't share this message with anyone and keep it secret. It's private key for your privacy wallet.");
2505
2530
  }
2506
2531
  async function initHulaSDK() {
2507
2532
  if (!isPoseidonInitialized()) {
package/dist/index.mjs CHANGED
@@ -388,14 +388,6 @@ var HulaPrivacyIdl = {
388
388
  "writable": true,
389
389
  "optional": true
390
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
391
  {
400
392
  "name": "nullifier_account_0",
401
393
  "docs": [
@@ -570,16 +562,11 @@ var HulaPrivacyIdl = {
570
562
  },
571
563
  {
572
564
  "code": 6017,
573
- "name": "InvalidFee",
574
- "msg": "Invalid fee"
575
- },
576
- {
577
- "code": 6018,
578
565
  "name": "TreeNotFull",
579
566
  "msg": "Tree is not full yet"
580
567
  },
581
568
  {
582
- "code": 6019,
569
+ "code": 6018,
583
570
  "name": "InvalidTreeIndex",
584
571
  "msg": "Invalid tree index"
585
572
  }
@@ -806,10 +793,6 @@ var HulaPrivacyIdl = {
806
793
  {
807
794
  "name": "mint_token_address",
808
795
  "type": "pubkey"
809
- },
810
- {
811
- "name": "fee",
812
- "type": "u64"
813
796
  }
814
797
  ]
815
798
  }
@@ -945,9 +928,9 @@ function bytesToHex(bytes) {
945
928
  function pubkeyToBigInt(pubkey) {
946
929
  return bytesToBigInt(pubkey.toBytes());
947
930
  }
948
- function generateSpendingKey() {
931
+ function generateSpendingKey(numberOfBits = 253n) {
949
932
  const randomBytes2 = nacl.randomBytes(32);
950
- return bytesToBigInt(randomBytes2) % 2n ** 253n;
933
+ return bytesToBigInt(randomBytes2) % 2n ** numberOfBits;
951
934
  }
952
935
  function deriveKeys(spendingKey) {
953
936
  const owner = poseidonHash([spendingKey, DOMAIN_OWNER]);
@@ -973,17 +956,8 @@ function deriveSpendingKeyFromSignature(signature) {
973
956
  return bytesToBigInt(hash) % FIELD_PRIME;
974
957
  }
975
958
  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));
959
+ const payload = `${noteData.value.toString()}|${noteData.secret.toString()}`;
960
+ const message = new TextEncoder().encode(payload);
987
961
  const nonce = nacl.randomBytes(24);
988
962
  const ephemeralKeyPair = nacl.box.keyPair();
989
963
  const ciphertext = nacl.box(
@@ -1007,22 +981,18 @@ function decryptNote(encryptedNote, encryptionSecretKey) {
1007
981
  encryptionSecretKey
1008
982
  );
1009
983
  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 {
984
+ let noteData = null;
985
+ const data = new TextDecoder().decode(decrypted).split("|");
986
+ noteData = {
987
+ v: BigInt(data[0]),
988
+ s: BigInt(data[1])
989
+ };
990
+ if (!noteData) return null;
991
+ return {
992
+ value: BigInt(noteData.v),
993
+ secret: BigInt(noteData.s)
994
+ };
995
+ } catch (error) {
1026
996
  return null;
1027
997
  }
1028
998
  }
@@ -1250,7 +1220,7 @@ function computeNullifierFromKeys(keys, commitment, leafIndex) {
1250
1220
  return poseidonHash([keys.nullifierKey, commitment, BigInt(leafIndex)]);
1251
1221
  }
1252
1222
  function createUTXO(value, mintTokenAddress, owner, leafIndex, treeIndex = 0) {
1253
- const secret = generateSpendingKey();
1223
+ const secret = generateSpendingKey(127n);
1254
1224
  const commitment = computeCommitment(value, mintTokenAddress, owner, secret);
1255
1225
  return {
1256
1226
  value,
@@ -1417,6 +1387,9 @@ function selectUTXOs(utxos, targetAmount, mint) {
1417
1387
  selected.push(utxo);
1418
1388
  total += utxo.value;
1419
1389
  }
1390
+ if (selected.length > NUM_INPUT_UTXOS) {
1391
+ throw new Error(`Selected ${selected.length} UTXOs, but only ${NUM_INPUT_UTXOS} are allowed. You can merge your UTXOs to reach the target amount.`);
1392
+ }
1420
1393
  if (total < targetAmount) {
1421
1394
  throw new Error(
1422
1395
  `Insufficient balance. Need ${targetAmount}, have ${total}`
@@ -1685,7 +1658,6 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1685
1658
  const currentTreeIndex = pool.currentTreeIndex;
1686
1659
  const depositAmount = request.depositAmount ?? 0n;
1687
1660
  const withdrawAmount = request.withdrawAmount ?? 0n;
1688
- const fee = request.fee ?? 0n;
1689
1661
  const inputUtxos = request.inputUtxos ?? [];
1690
1662
  const outputs = request.outputs ?? [];
1691
1663
  if (depositAmount === 0n && inputUtxos.length === 0) {
@@ -1730,11 +1702,11 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1730
1702
  totalOutputValue += output.amount;
1731
1703
  outputIndex++;
1732
1704
  }
1733
- const totalOut = withdrawAmount + fee + totalOutputValue;
1705
+ const totalOut = withdrawAmount + totalOutputValue;
1734
1706
  const changeAmount = totalAvailable - totalOut;
1735
1707
  if (changeAmount < 0n) {
1736
1708
  throw new Error(
1737
- `Insufficient value. Have ${totalAvailable} (inputs: ${totalInputValue}, deposit: ${depositAmount}), need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount}, fee: ${fee})`
1709
+ `Insufficient value. Have ${totalAvailable} (inputs: ${totalInputValue}, deposit: ${depositAmount}), need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount})`
1738
1710
  );
1739
1711
  }
1740
1712
  if (changeAmount > 0n) {
@@ -1763,8 +1735,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1763
1735
  depositAmount,
1764
1736
  withdrawAmount,
1765
1737
  request.recipient ?? PublicKey3.default,
1766
- request.mint,
1767
- fee
1738
+ request.mint
1768
1739
  );
1769
1740
  const { proof } = await generateProof(circuitInputs);
1770
1741
  const nullifiers = circuitInputs.nullifiers.map((n) => BigInt(n));
@@ -1775,8 +1746,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1775
1746
  publicDeposit: depositAmount,
1776
1747
  publicWithdraw: withdrawAmount,
1777
1748
  recipient: request.recipient ?? PublicKey3.default,
1778
- mintTokenAddress: request.mint,
1779
- fee
1749
+ mintTokenAddress: request.mint
1780
1750
  };
1781
1751
  const encryptedNotes = [];
1782
1752
  for (let i = 0; i < outputs.length; i++) {
@@ -1786,9 +1756,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1786
1756
  const encrypted = encryptNote(
1787
1757
  {
1788
1758
  value: utxo.value,
1789
- mintTokenAddress: utxo.mintTokenAddress,
1790
1759
  secret: utxo.secret
1791
- // leafIndex omitted - recipient queries relayer with computed commitment
1792
1760
  },
1793
1761
  output.encryptionPubKey
1794
1762
  );
@@ -1801,9 +1769,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1801
1769
  const encrypted = encryptNote(
1802
1770
  {
1803
1771
  value: changeUtxo.value,
1804
- mintTokenAddress: changeUtxo.mintTokenAddress,
1805
1772
  secret: changeUtxo.secret
1806
- // leafIndex omitted - we already know it locally
1807
1773
  },
1808
1774
  walletKeys.encryptionKeyPair.publicKey
1809
1775
  );
@@ -1819,7 +1785,7 @@ async function buildTransaction(request, walletKeys, relayerUrl) {
1819
1785
  outputTreeIndex: currentTreeIndex
1820
1786
  };
1821
1787
  }
1822
- function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, leaves, depositAmount, withdrawAmount, recipient, mint, fee) {
1788
+ function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, leaves, depositAmount, withdrawAmount, recipient, mint) {
1823
1789
  const inputSpendingKeys = [];
1824
1790
  const inputValues = [];
1825
1791
  const inputSecrets = [];
@@ -1864,7 +1830,6 @@ function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, lea
1864
1830
  publicWithdraw: withdrawAmount.toString(),
1865
1831
  recipient: pubkeyToBigInt(recipient).toString(),
1866
1832
  mintTokenAddress: pubkeyToBigInt(mint).toString(),
1867
- fee: fee.toString(),
1868
1833
  inputSpendingKeys,
1869
1834
  inputValues,
1870
1835
  inputSecrets,
@@ -1932,8 +1897,6 @@ function buildTransactionAccounts(builtTx, payer, mint, depositAmount, withdrawA
1932
1897
  depositorTokenAccount,
1933
1898
  depositor,
1934
1899
  recipientTokenAccount,
1935
- feeRecipientTokenAccount: null,
1936
- // TODO: Add fee recipient support
1937
1900
  nullifierAccount0: nullifierPdas[0] ?? null,
1938
1901
  nullifierAccount1: nullifierPdas[1] ?? null,
1939
1902
  tokenProgram: TOKEN_2022_PROGRAM_ID,
@@ -1951,8 +1914,7 @@ function toAnchorPublicInputs(builtTx) {
1951
1914
  publicDeposit: new BN(builtTx.publicInputs.publicDeposit.toString()),
1952
1915
  publicWithdraw: new BN(builtTx.publicInputs.publicWithdraw.toString()),
1953
1916
  recipient: builtTx.publicInputs.recipient,
1954
- mintTokenAddress: builtTx.publicInputs.mintTokenAddress,
1955
- fee: new BN(builtTx.publicInputs.fee.toString())
1917
+ mintTokenAddress: builtTx.publicInputs.mintTokenAddress
1956
1918
  };
1957
1919
  }
1958
1920
 
@@ -2344,6 +2306,69 @@ var HulaWallet = class _HulaWallet {
2344
2306
  }
2345
2307
  return signature;
2346
2308
  }
2309
+ /**
2310
+ * Merge UTXOs
2311
+ *
2312
+ * @param utxo1 - First UTXO to merge
2313
+ * @param utxo2 - Second UTXO to merge
2314
+ * @returns Transaction signature
2315
+ *
2316
+ */
2317
+ async mergeUTXOs(utxo1, utxo2) {
2318
+ if (!this.signer) {
2319
+ throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
2320
+ }
2321
+ if (utxo1.mintTokenAddress !== utxo2.mintTokenAddress) {
2322
+ throw new Error("UTXOs have different mint token addresses");
2323
+ }
2324
+ const mint = new PublicKey4(
2325
+ bigIntToBytes(utxo1.mintTokenAddress, 32)
2326
+ );
2327
+ const mergedUtxo = createUTXO(utxo1.value + utxo2.value, utxo1.mintTokenAddress, utxo1.owner, utxo1.leafIndex, utxo1.treeIndex);
2328
+ const builtTx = await buildTransaction(
2329
+ {
2330
+ mint,
2331
+ inputUtxos: [utxo1, utxo2],
2332
+ outputs: [{
2333
+ owner: this.keys.owner,
2334
+ amount: mergedUtxo.value,
2335
+ encryptionPubKey: this.keys.encryptionKeyPair.publicKey
2336
+ }]
2337
+ },
2338
+ this.keys,
2339
+ this.relayerUrl
2340
+ );
2341
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
2342
+ builtTx,
2343
+ this.signer.publicKey,
2344
+ mint,
2345
+ 0n,
2346
+ 0n
2347
+ );
2348
+ const allPreInstructions = [
2349
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
2350
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
2351
+ ...preInstructions
2352
+ ];
2353
+ const wallet = new Wallet(this.signer);
2354
+ const provider = new AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
2355
+ const program = new Program(idl_default, provider);
2356
+ const publicInputs = toAnchorPublicInputs(builtTx);
2357
+ const signature = await program.methods.transact(
2358
+ inputTreeIndex,
2359
+ Array.from(builtTx.proof),
2360
+ publicInputs,
2361
+ builtTx.encryptedNotes
2362
+ ).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
2363
+ for (const utxo of [utxo1, utxo2]) {
2364
+ const serialized = this.utxos.find((u) => u.commitment === utxo.commitment.toString());
2365
+ if (serialized) {
2366
+ serialized.spent = true;
2367
+ serialized.spentTx = signature;
2368
+ }
2369
+ }
2370
+ return signature;
2371
+ }
2347
2372
  /**
2348
2373
  * Build a custom transaction
2349
2374
  */
@@ -2395,7 +2420,7 @@ var HulaWallet = class _HulaWallet {
2395
2420
  }
2396
2421
  };
2397
2422
  function getKeyDerivationMessage() {
2398
- return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1");
2423
+ return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1. Don't share this message with anyone and keep it secret. It's private key for your privacy wallet.");
2399
2424
  }
2400
2425
  async function initHulaSDK() {
2401
2426
  if (!isPoseidonInitialized()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hula-privacy/mixer",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Hula Privacy Protocol SDK - Complete toolkit for private transactions on Solana",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/crypto.ts CHANGED
@@ -127,9 +127,9 @@ export function pubkeyToBigInt(pubkey: { toBytes(): Uint8Array }): bigint {
127
127
  /**
128
128
  * Generate a new random spending key
129
129
  */
130
- export function generateSpendingKey(): bigint {
130
+ export function generateSpendingKey(numberOfBits: bigint = 253n): bigint {
131
131
  const randomBytes = nacl.randomBytes(32);
132
- return bytesToBigInt(randomBytes) % (2n ** 253n);
132
+ return bytesToBigInt(randomBytes) % (2n ** numberOfBits);
133
133
  }
134
134
 
135
135
  /**
@@ -179,31 +179,19 @@ export function deriveSpendingKeyFromSignature(signature: Uint8Array): bigint {
179
179
  * Encrypt a note for a recipient
180
180
  *
181
181
  * The note contains the UTXO data needed for the recipient to claim it.
182
- * Optimized for size:
183
- * - mintTokenAddress: only first 8 bytes (can match on-chain by prefix)
184
- * - leafIndex: omitted (recipient can query relayer with commitment)
185
- */
182
+ *
183
+ */
186
184
  export function encryptNote(
187
185
  noteData: {
188
186
  value: bigint;
189
- mintTokenAddress: bigint;
190
187
  secret: bigint;
191
188
  leafIndex?: number; // Optional, omitted to save space
192
189
  },
193
190
  recipientEncryptionPubKey: Uint8Array
194
191
  ): EncryptedNote {
195
- // Use only first 8 bytes of mintTokenAddress to save space
196
- // Recipient can match this prefix against known mints
197
- const mintPrefix = noteData.mintTokenAddress.toString(16).padStart(64, "0").slice(0, 16);
198
-
199
- const payload = {
200
- v: noteData.value.toString(), // value
201
- m: mintPrefix, // mint (first 8 bytes as hex)
202
- s: noteData.secret.toString(), // secret
203
- // leafIndex omitted - recipient queries relayer with computed commitment
204
- };
192
+ const payload = `${noteData.value.toString()}|${noteData.secret.toString()}`;
205
193
 
206
- const message = new TextEncoder().encode(JSON.stringify(payload));
194
+ const message = new TextEncoder().encode(payload);
207
195
  const nonce = nacl.randomBytes(24);
208
196
  const ephemeralKeyPair = nacl.box.keyPair();
209
197
 
@@ -231,7 +219,7 @@ export function encryptNote(
231
219
  export function decryptNote(
232
220
  encryptedNote: EncryptedNote,
233
221
  encryptionSecretKey: Uint8Array
234
- ): { value: bigint; mintPrefix: string; secret: bigint } | null {
222
+ ): { value: bigint; secret: bigint } | null {
235
223
  try {
236
224
  const decrypted = nacl.box.open(
237
225
  encryptedNote.ciphertext,
@@ -242,25 +230,19 @@ export function decryptNote(
242
230
 
243
231
  if (!decrypted) return null;
244
232
 
245
- const noteData = JSON.parse(new TextDecoder().decode(decrypted));
246
-
247
- // Handle both old format (full data) and new format (compact)
248
- if (noteData.v !== undefined) {
249
- // New compact format
250
- return {
251
- value: BigInt(noteData.v),
252
- mintPrefix: noteData.m, // First 8 bytes as hex string
253
- secret: BigInt(noteData.s),
254
- };
255
- } else {
256
- // Legacy format (full mintTokenAddress)
257
- return {
258
- value: BigInt(noteData.value),
259
- mintPrefix: BigInt(noteData.mintTokenAddress).toString(16).padStart(64, "0").slice(0, 16),
260
- secret: BigInt(noteData.secret),
233
+ let noteData: { v: bigint; s: bigint } | null = null;
234
+ const data = new TextDecoder().decode(decrypted).split("|");
235
+ noteData = {
236
+ v: BigInt(data[0]),
237
+ s: BigInt(data[1]),
261
238
  };
262
- }
263
- } catch {
239
+ if (!noteData) return null;
240
+
241
+ return {
242
+ value: BigInt(noteData.v),
243
+ secret: BigInt(noteData.s),
244
+ };
245
+ } catch (error) {
264
246
  return null;
265
247
  }
266
248
  }
package/src/idl.ts CHANGED
@@ -383,14 +383,6 @@ const HulaPrivacyIdl = {
383
383
  "writable": true,
384
384
  "optional": true
385
385
  },
386
- {
387
- "name": "fee_recipient_token_account",
388
- "docs": [
389
- "Fee recipient's token account (optional)"
390
- ],
391
- "writable": true,
392
- "optional": true
393
- },
394
386
  {
395
387
  "name": "nullifier_account_0",
396
388
  "docs": [
@@ -565,16 +557,11 @@ const HulaPrivacyIdl = {
565
557
  },
566
558
  {
567
559
  "code": 6017,
568
- "name": "InvalidFee",
569
- "msg": "Invalid fee"
570
- },
571
- {
572
- "code": 6018,
573
560
  "name": "TreeNotFull",
574
561
  "msg": "Tree is not full yet"
575
562
  },
576
563
  {
577
- "code": 6019,
564
+ "code": 6018,
578
565
  "name": "InvalidTreeIndex",
579
566
  "msg": "Invalid tree index"
580
567
  }
@@ -801,10 +788,6 @@ const HulaPrivacyIdl = {
801
788
  {
802
789
  "name": "mint_token_address",
803
790
  "type": "pubkey"
804
- },
805
- {
806
- "name": "fee",
807
- "type": "u64"
808
791
  }
809
792
  ]
810
793
  }
@@ -64,7 +64,6 @@ export async function buildTransaction(
64
64
  // Validate inputs
65
65
  const depositAmount = request.depositAmount ?? 0n;
66
66
  const withdrawAmount = request.withdrawAmount ?? 0n;
67
- const fee = request.fee ?? 0n;
68
67
  const inputUtxos = request.inputUtxos ?? [];
69
68
  const outputs = request.outputs ?? [];
70
69
 
@@ -127,13 +126,13 @@ export async function buildTransaction(
127
126
  }
128
127
 
129
128
  // Calculate change
130
- const totalOut = withdrawAmount + fee + totalOutputValue;
129
+ const totalOut = withdrawAmount + totalOutputValue;
131
130
  const changeAmount = totalAvailable - totalOut;
132
131
 
133
132
  if (changeAmount < 0n) {
134
133
  throw new Error(
135
134
  `Insufficient value. Have ${totalAvailable} (inputs: ${totalInputValue}, deposit: ${depositAmount}), ` +
136
- `need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount}, fee: ${fee})`
135
+ `need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount})`
137
136
  );
138
137
  }
139
138
 
@@ -169,7 +168,6 @@ export async function buildTransaction(
169
168
  withdrawAmount,
170
169
  request.recipient ?? PublicKey.default,
171
170
  request.mint,
172
- fee
173
171
  );
174
172
 
175
173
  // Generate ZK proof
@@ -185,7 +183,6 @@ export async function buildTransaction(
185
183
  publicWithdraw: withdrawAmount,
186
184
  recipient: request.recipient ?? PublicKey.default,
187
185
  mintTokenAddress: request.mint,
188
- fee,
189
186
  };
190
187
 
191
188
  // Create encrypted notes for outputs with encryption keys
@@ -199,9 +196,7 @@ export async function buildTransaction(
199
196
  const encrypted = encryptNote(
200
197
  {
201
198
  value: utxo.value,
202
- mintTokenAddress: utxo.mintTokenAddress,
203
199
  secret: utxo.secret,
204
- // leafIndex omitted - recipient queries relayer with computed commitment
205
200
  },
206
201
  output.encryptionPubKey
207
202
  );
@@ -216,9 +211,7 @@ export async function buildTransaction(
216
211
  const encrypted = encryptNote(
217
212
  {
218
213
  value: changeUtxo.value,
219
- mintTokenAddress: changeUtxo.mintTokenAddress,
220
214
  secret: changeUtxo.secret,
221
- // leafIndex omitted - we already know it locally
222
215
  },
223
216
  walletKeys.encryptionKeyPair.publicKey
224
217
  );
@@ -249,7 +242,6 @@ function buildCircuitInputs(
249
242
  withdrawAmount: bigint,
250
243
  recipient: PublicKey,
251
244
  mint: PublicKey,
252
- fee: bigint
253
245
  ): CircuitInputs {
254
246
  const inputSpendingKeys: string[] = [];
255
247
  const inputValues: string[] = [];
@@ -302,7 +294,6 @@ function buildCircuitInputs(
302
294
  publicWithdraw: withdrawAmount.toString(),
303
295
  recipient: pubkeyToBigInt(recipient).toString(),
304
296
  mintTokenAddress: pubkeyToBigInt(mint).toString(),
305
- fee: fee.toString(),
306
297
 
307
298
  inputSpendingKeys,
308
299
  inputValues,
@@ -344,7 +335,6 @@ export function buildTransactionAccounts(
344
335
  depositorTokenAccount: PublicKey | null;
345
336
  depositor: PublicKey | null;
346
337
  recipientTokenAccount: PublicKey | null;
347
- feeRecipientTokenAccount: PublicKey | null;
348
338
  nullifierAccount0: PublicKey | null;
349
339
  nullifierAccount1: PublicKey | null;
350
340
  tokenProgram: PublicKey;
@@ -422,7 +412,6 @@ export function buildTransactionAccounts(
422
412
  depositorTokenAccount,
423
413
  depositor,
424
414
  recipientTokenAccount,
425
- feeRecipientTokenAccount: null, // TODO: Add fee recipient support
426
415
  nullifierAccount0: nullifierPdas[0] ?? null,
427
416
  nullifierAccount1: nullifierPdas[1] ?? null,
428
417
  tokenProgram: TOKEN_2022_PROGRAM_ID,
@@ -448,7 +437,6 @@ export function toAnchorPublicInputs(builtTx: BuiltTransaction): {
448
437
  publicWithdraw: BN;
449
438
  recipient: PublicKey;
450
439
  mintTokenAddress: PublicKey;
451
- fee: BN;
452
440
  } {
453
441
  return {
454
442
  merkleRoot: builtTx.publicInputs.merkleRoot,
@@ -458,6 +446,5 @@ export function toAnchorPublicInputs(builtTx: BuiltTransaction): {
458
446
  publicWithdraw: new BN(builtTx.publicInputs.publicWithdraw.toString()),
459
447
  recipient: builtTx.publicInputs.recipient,
460
448
  mintTokenAddress: builtTx.publicInputs.mintTokenAddress,
461
- fee: new BN(builtTx.publicInputs.fee.toString()),
462
449
  };
463
450
  }
package/src/types.ts CHANGED
@@ -126,7 +126,6 @@ export interface CircuitInputs {
126
126
  publicWithdraw: string;
127
127
  recipient: string;
128
128
  mintTokenAddress: string;
129
- fee: string;
130
129
 
131
130
  // Private inputs for input UTXOs
132
131
  inputSpendingKeys: string[];
@@ -153,7 +152,6 @@ export interface PublicInputs {
153
152
  publicWithdraw: bigint;
154
153
  recipient: PublicKey;
155
154
  mintTokenAddress: PublicKey;
156
- fee: bigint;
157
155
  }
158
156
 
159
157
  /**
@@ -182,8 +180,6 @@ export interface TransactionRequest {
182
180
  withdrawAmount?: bigint;
183
181
  /** Recipient public key for withdrawal */
184
182
  recipient?: PublicKey;
185
- /** Relayer fee */
186
- fee?: bigint;
187
183
  /** Token mint address */
188
184
  mint: PublicKey;
189
185
  }
@@ -256,7 +252,6 @@ export interface TransactionData {
256
252
  outputTreeIndex: number;
257
253
  publicDeposit: string;
258
254
  publicWithdraw: string;
259
- fee: string;
260
255
  success: boolean;
261
256
  nullifiers: string[];
262
257
  encryptedNotes: { noteIndex: number; data: string }[];
package/src/utxo.ts CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  hexToBytes,
14
14
  pubkeyToBigInt,
15
15
  } from "./crypto";
16
- import { DOMAIN_NULLIFIER } from "./constants";
17
- import type { UTXO, WalletKeys, SerializableUTXO, NoteData } from "./types";
16
+ import { DOMAIN_NULLIFIER, NUM_INPUT_UTXOS } from "./constants";
17
+ import type { UTXO, WalletKeys, SerializableUTXO } from "./types";
18
18
  import { PublicKey } from "@solana/web3.js";
19
19
 
20
20
  // ============================================================================
@@ -72,7 +72,7 @@ export function createUTXO(
72
72
  leafIndex: number,
73
73
  treeIndex: number = 0
74
74
  ): UTXO {
75
- const secret = generateSpendingKey(); // Random blinding factor
75
+ const secret = generateSpendingKey(127n); // Random blinding factor
76
76
  const commitment = computeCommitment(value, mintTokenAddress, owner, secret);
77
77
 
78
78
  return {
@@ -350,6 +350,10 @@ export function selectUTXOs(
350
350
  total += utxo.value;
351
351
  }
352
352
 
353
+ if (selected.length > NUM_INPUT_UTXOS) {
354
+ throw new Error(`Selected ${selected.length} UTXOs, but only ${NUM_INPUT_UTXOS} are allowed. You can merge your UTXOs to reach the target amount.`);
355
+ }
356
+
353
357
  if (total < targetAmount) {
354
358
  throw new Error(
355
359
  `Insufficient balance. Need ${targetAmount}, have ${total}`
@@ -369,5 +373,3 @@ export function calculateBalance(utxos: UTXO[], mint?: bigint): bigint {
369
373
 
370
374
  return filtered.reduce((sum, u) => sum + u.value, 0n);
371
375
  }
372
-
373
-
package/src/wallet.ts CHANGED
@@ -19,8 +19,10 @@ import {
19
19
  generateSpendingKey,
20
20
  deriveSpendingKeyFromSignature,
21
21
  pubkeyToBigInt,
22
+ bigIntToBytes,
23
+ bytesToHex,
22
24
  } from "./crypto";
23
- import { syncUTXOs, deserializeUTXO, selectUTXOs, calculateBalance } from "./utxo";
25
+ import { syncUTXOs, deserializeUTXO, selectUTXOs, calculateBalance, createUTXO } from "./utxo";
24
26
  import { buildTransaction, buildTransactionAccounts, toAnchorPublicInputs } from "./transaction";
25
27
  import { getRelayerClient, setDefaultRelayerUrl } from "./api";
26
28
  import type {
@@ -575,6 +577,94 @@ export class HulaWallet {
575
577
  return signature;
576
578
  }
577
579
 
580
+
581
+ /**
582
+ * Merge UTXOs
583
+ *
584
+ * @param utxo1 - First UTXO to merge
585
+ * @param utxo2 - Second UTXO to merge
586
+ * @returns Transaction signature
587
+ *
588
+ */
589
+ async mergeUTXOs(utxo1: UTXO, utxo2: UTXO): Promise<string> {
590
+ if (!this.signer) {
591
+ throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
592
+ }
593
+ if (utxo1.mintTokenAddress !== utxo2.mintTokenAddress) {
594
+ throw new Error("UTXOs have different mint token addresses");
595
+ }
596
+
597
+ const mint = new PublicKey(
598
+ bigIntToBytes(utxo1.mintTokenAddress, 32)
599
+ );
600
+ const mergedUtxo = createUTXO(utxo1.value + utxo2.value, utxo1.mintTokenAddress, utxo1.owner, utxo1.leafIndex, utxo1.treeIndex);
601
+
602
+ // Build the transaction with ZK proof
603
+ const builtTx = await buildTransaction(
604
+ {
605
+ mint,
606
+ inputUtxos: [utxo1, utxo2],
607
+ outputs: [{
608
+ owner: this.keys.owner,
609
+ amount: mergedUtxo.value,
610
+ encryptionPubKey: this.keys.encryptionKeyPair.publicKey,
611
+ }],
612
+ },
613
+ this.keys,
614
+ this.relayerUrl
615
+ );
616
+
617
+ // Build accounts and pre-instructions
618
+ const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
619
+ builtTx,
620
+ this.signer.publicKey,
621
+ mint,
622
+ 0n,
623
+ 0n
624
+ );
625
+ // Add compute budget instructions for ZK proof verification
626
+ const allPreInstructions = [
627
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
628
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
629
+ ...preInstructions,
630
+ ];
631
+
632
+ // Setup Anchor program
633
+ const wallet = new Wallet(this.signer);
634
+ const provider = new AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
635
+
636
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
637
+ const program = new Program(HulaPrivacyIdl as any, provider);
638
+
639
+ // Convert public inputs to Anchor format
640
+ const publicInputs = toAnchorPublicInputs(builtTx);
641
+
642
+ // Submit the transaction
643
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
644
+ const signature = await (program.methods as any)
645
+ .transact(
646
+ inputTreeIndex,
647
+ Array.from(builtTx.proof),
648
+ publicInputs,
649
+ builtTx.encryptedNotes
650
+ )
651
+ .accounts(accounts)
652
+ .preInstructions(allPreInstructions)
653
+ .signers([this.signer])
654
+ .rpc();
655
+
656
+ // Mark input UTXOs as spent locally
657
+ for (const utxo of [utxo1, utxo2]) {
658
+ const serialized = this.utxos.find(u => u.commitment === utxo.commitment.toString());
659
+ if (serialized) {
660
+ serialized.spent = true;
661
+ serialized.spentTx = signature;
662
+ }
663
+ }
664
+
665
+ return signature;
666
+ }
667
+
578
668
  /**
579
669
  * Build a custom transaction
580
670
  */
@@ -663,7 +753,7 @@ export class HulaWallet {
663
753
  * Create a deterministic message for wallet key derivation
664
754
  */
665
755
  export function getKeyDerivationMessage(): Uint8Array {
666
- return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1");
756
+ return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1. Don't share this message with anyone and keep it secret. It's private key for your privacy wallet.");
667
757
  }
668
758
 
669
759
  /**