@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 +12 -14
- package/dist/index.d.ts +12 -14
- package/dist/index.js +91 -66
- package/dist/index.mjs +91 -66
- package/package.json +1 -1
- package/src/crypto.ts +19 -37
- package/src/idl.ts +1 -18
- package/src/transaction.ts +2 -15
- package/src/types.ts +0 -5
- package/src/utxo.ts +7 -5
- package/src/wallet.ts +92 -2
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
|
-
*
|
|
680
|
-
|
|
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
|
-
*
|
|
680
|
-
|
|
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":
|
|
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 **
|
|
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
|
|
1086
|
-
const
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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 +
|
|
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}
|
|
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
|
|
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":
|
|
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 **
|
|
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
|
|
977
|
-
const
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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 +
|
|
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}
|
|
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
|
|
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
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 **
|
|
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
|
-
*
|
|
183
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
}
|
package/src/transaction.ts
CHANGED
|
@@ -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 +
|
|
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}
|
|
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
|
|
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
|
/**
|