privacycash 1.0.6

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.
Files changed (56) hide show
  1. package/.github/workflows/npm-publish.yml +67 -0
  2. package/README.md +22 -0
  3. package/__tests__/e2e.test.ts +52 -0
  4. package/__tests__/encryption.test.ts +1635 -0
  5. package/circuit2/transaction2.wasm +0 -0
  6. package/circuit2/transaction2.zkey +0 -0
  7. package/dist/config.d.ts +7 -0
  8. package/dist/config.js +16 -0
  9. package/dist/deposit.d.ts +18 -0
  10. package/dist/deposit.js +402 -0
  11. package/dist/exportUtils.d.ts +6 -0
  12. package/dist/exportUtils.js +6 -0
  13. package/dist/getUtxos.d.ts +27 -0
  14. package/dist/getUtxos.js +352 -0
  15. package/dist/index.d.ts +61 -0
  16. package/dist/index.js +169 -0
  17. package/dist/models/keypair.d.ts +26 -0
  18. package/dist/models/keypair.js +43 -0
  19. package/dist/models/utxo.d.ts +49 -0
  20. package/dist/models/utxo.js +76 -0
  21. package/dist/utils/address_lookup_table.d.ts +8 -0
  22. package/dist/utils/address_lookup_table.js +21 -0
  23. package/dist/utils/constants.d.ts +14 -0
  24. package/dist/utils/constants.js +15 -0
  25. package/dist/utils/encryption.d.ts +107 -0
  26. package/dist/utils/encryption.js +374 -0
  27. package/dist/utils/logger.d.ts +9 -0
  28. package/dist/utils/logger.js +35 -0
  29. package/dist/utils/merkle_tree.d.ts +92 -0
  30. package/dist/utils/merkle_tree.js +186 -0
  31. package/dist/utils/node-shim.d.ts +5 -0
  32. package/dist/utils/node-shim.js +5 -0
  33. package/dist/utils/prover.d.ts +33 -0
  34. package/dist/utils/prover.js +123 -0
  35. package/dist/utils/utils.d.ts +67 -0
  36. package/dist/utils/utils.js +151 -0
  37. package/dist/withdraw.d.ts +21 -0
  38. package/dist/withdraw.js +270 -0
  39. package/package.json +48 -0
  40. package/src/config.ts +28 -0
  41. package/src/deposit.ts +496 -0
  42. package/src/exportUtils.ts +6 -0
  43. package/src/getUtxos.ts +466 -0
  44. package/src/index.ts +191 -0
  45. package/src/models/keypair.ts +52 -0
  46. package/src/models/utxo.ts +97 -0
  47. package/src/utils/address_lookup_table.ts +29 -0
  48. package/src/utils/constants.ts +26 -0
  49. package/src/utils/encryption.ts +461 -0
  50. package/src/utils/logger.ts +42 -0
  51. package/src/utils/merkle_tree.ts +207 -0
  52. package/src/utils/node-shim.ts +6 -0
  53. package/src/utils/prover.ts +189 -0
  54. package/src/utils/utils.ts +213 -0
  55. package/src/withdraw.ts +334 -0
  56. package/tsconfig.json +28 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Utility functions for ZK Cash
3
+ *
4
+ * Provides common utility functions for the ZK Cash system
5
+ * Based on: https://github.com/tornadocash/tornado-nova
6
+ */
7
+ import BN from 'bn.js';
8
+ import * as borsh from 'borsh';
9
+ import { sha256 } from '@ethersproject/sha2';
10
+ import { PublicKey } from '@solana/web3.js';
11
+ import { INDEXER_API_URL, PROGRAM_ID } from './constants.js';
12
+ import { logger } from './logger.js';
13
+ import { getConfig } from '../config.js';
14
+ /**
15
+ * Calculate deposit fee based on deposit amount and fee rate
16
+ * @param depositAmount Amount being deposited in lamports
17
+ * @returns Fee amount in lamports
18
+ */
19
+ export async function calculateDepositFee(depositAmount) {
20
+ return Math.floor(depositAmount * (await getConfig('deposit_fee_rate')) / 10000);
21
+ }
22
+ /**
23
+ * Calculate withdrawal fee based on withdrawal amount and fee rate
24
+ * @param withdrawalAmount Amount being withdrawn in lamports
25
+ * @returns Fee amount in lamports
26
+ */
27
+ export async function calculateWithdrawalFee(withdrawalAmount) {
28
+ return Math.floor(withdrawalAmount * (await getConfig('withdraw_fee_rate')) / 10000);
29
+ }
30
+ /**
31
+ * Mock encryption function - in real implementation this would be proper encryption
32
+ * For testing, we just return a fixed prefix to ensure consistent extDataHash
33
+ * @param value Value to encrypt
34
+ * @returns Encrypted string representation
35
+ */
36
+ export function mockEncrypt(value) {
37
+ return JSON.stringify(value);
38
+ }
39
+ /**
40
+ * Calculates the hash of ext data using Borsh serialization
41
+ * @param extData External data object containing recipient, amount, encrypted outputs, fee, fee recipient, and mint address
42
+ * @returns The hash as a Uint8Array (32 bytes)
43
+ */
44
+ export function getExtDataHash(extData) {
45
+ // Convert all inputs to their appropriate types
46
+ const recipient = extData.recipient instanceof PublicKey
47
+ ? extData.recipient
48
+ : new PublicKey(extData.recipient);
49
+ const feeRecipient = extData.feeRecipient instanceof PublicKey
50
+ ? extData.feeRecipient
51
+ : new PublicKey(extData.feeRecipient);
52
+ const mintAddress = extData.mintAddress instanceof PublicKey
53
+ ? extData.mintAddress
54
+ : new PublicKey(extData.mintAddress);
55
+ // Convert to BN for proper i64/u64 handling
56
+ const extAmount = new BN(extData.extAmount.toString());
57
+ const fee = new BN(extData.fee.toString());
58
+ // Handle encrypted outputs - they might not be present in Account Data Separation approach
59
+ const encryptedOutput1 = extData.encryptedOutput1
60
+ ? Buffer.from(extData.encryptedOutput1)
61
+ : Buffer.alloc(0); // Empty buffer if not provided
62
+ const encryptedOutput2 = extData.encryptedOutput2
63
+ ? Buffer.from(extData.encryptedOutput2)
64
+ : Buffer.alloc(0); // Empty buffer if not provided
65
+ // Define the borsh schema matching the Rust struct
66
+ const schema = {
67
+ struct: {
68
+ recipient: { array: { type: 'u8', len: 32 } },
69
+ extAmount: 'i64',
70
+ encryptedOutput1: { array: { type: 'u8' } },
71
+ encryptedOutput2: { array: { type: 'u8' } },
72
+ fee: 'u64',
73
+ feeRecipient: { array: { type: 'u8', len: 32 } },
74
+ mintAddress: { array: { type: 'u8', len: 32 } },
75
+ }
76
+ };
77
+ const value = {
78
+ recipient: recipient.toBytes(),
79
+ extAmount: extAmount, // BN instance - Borsh handles it correctly with i64 type
80
+ encryptedOutput1: encryptedOutput1,
81
+ encryptedOutput2: encryptedOutput2,
82
+ fee: fee, // BN instance - Borsh handles it correctly with u64 type
83
+ feeRecipient: feeRecipient.toBytes(),
84
+ mintAddress: mintAddress.toBytes(),
85
+ };
86
+ // Serialize with Borsh
87
+ const serializedData = borsh.serialize(schema, value);
88
+ // Calculate the SHA-256 hash
89
+ const hashHex = sha256(serializedData);
90
+ // Convert from hex string to Uint8Array
91
+ return Buffer.from(hashHex.slice(2), 'hex');
92
+ }
93
+ // Function to fetch Merkle proof from API for a given commitment
94
+ export async function fetchMerkleProof(commitment) {
95
+ try {
96
+ logger.debug(`Fetching Merkle proof for commitment: ${commitment}`);
97
+ const response = await fetch(`${INDEXER_API_URL}/merkle/proof/${commitment}`);
98
+ if (!response.ok) {
99
+ throw new Error(`Failed to fetch Merkle proof: ${response.status} ${response.statusText}`);
100
+ }
101
+ const data = await response.json();
102
+ logger.debug(`✓ Fetched Merkle proof with ${data.pathElements.length} elements`);
103
+ return data;
104
+ }
105
+ catch (error) {
106
+ console.error(`Failed to fetch Merkle proof for commitment ${commitment}:`, error);
107
+ throw error;
108
+ }
109
+ }
110
+ // Find nullifier PDAs for the given proof
111
+ export function findNullifierPDAs(proof) {
112
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier0"), Buffer.from(proof.inputNullifiers[0])], PROGRAM_ID);
113
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier1"), Buffer.from(proof.inputNullifiers[1])], PROGRAM_ID);
114
+ return { nullifier0PDA, nullifier1PDA };
115
+ }
116
+ // Find commitment PDAs for the given proof
117
+ export function findCommitmentPDAs(proof) {
118
+ const [commitment0PDA] = PublicKey.findProgramAddressSync([Buffer.from("commitment0"), Buffer.from(proof.outputCommitments[0])], PROGRAM_ID);
119
+ const [commitment1PDA] = PublicKey.findProgramAddressSync([Buffer.from("commitment1"), Buffer.from(proof.outputCommitments[1])], PROGRAM_ID);
120
+ return { commitment0PDA, commitment1PDA };
121
+ }
122
+ // Function to query remote tree state from indexer API
123
+ export async function queryRemoteTreeState() {
124
+ try {
125
+ logger.debug('Fetching Merkle root and nextIndex from API...');
126
+ const response = await fetch(`${INDEXER_API_URL}/merkle/root`);
127
+ if (!response.ok) {
128
+ throw new Error(`Failed to fetch Merkle root and nextIndex: ${response.status} ${response.statusText}`);
129
+ }
130
+ const data = await response.json();
131
+ logger.debug(`Fetched root from API: ${data.root}`);
132
+ logger.debug(`Fetched nextIndex from API: ${data.nextIndex}`);
133
+ return data;
134
+ }
135
+ catch (error) {
136
+ console.error('Failed to fetch root and nextIndex from API:', error);
137
+ throw error;
138
+ }
139
+ }
140
+ export function getProgramAccounts() {
141
+ // Derive PDA (Program Derived Addresses) for the tree account and other required accounts
142
+ const [treeAccount] = PublicKey.findProgramAddressSync([Buffer.from('merkle_tree')], PROGRAM_ID);
143
+ const [treeTokenAccount] = PublicKey.findProgramAddressSync([Buffer.from('tree_token')], PROGRAM_ID);
144
+ const [globalConfigAccount] = PublicKey.findProgramAddressSync([Buffer.from('global_config')], PROGRAM_ID);
145
+ return { treeAccount, treeTokenAccount, globalConfigAccount };
146
+ }
147
+ export function findCrossCheckNullifierPDAs(proof) {
148
+ const [nullifier2PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier0"), Buffer.from(proof.inputNullifiers[1])], PROGRAM_ID);
149
+ const [nullifier3PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier1"), Buffer.from(proof.inputNullifiers[0])], PROGRAM_ID);
150
+ return { nullifier2PDA, nullifier3PDA };
151
+ }
@@ -0,0 +1,21 @@
1
+ import { Connection, PublicKey } from '@solana/web3.js';
2
+ import * as hasher from '@lightprotocol/hasher.rs';
3
+ import { EncryptionService } from './utils/encryption.js';
4
+ type WithdrawParams = {
5
+ publicKey: PublicKey;
6
+ connection: Connection;
7
+ amount_in_lamports: number;
8
+ keyBasePath: string;
9
+ encryptionService: EncryptionService;
10
+ lightWasm: hasher.LightWasm;
11
+ recipient: PublicKey;
12
+ storage: Storage;
13
+ };
14
+ export declare function withdraw({ recipient, lightWasm, storage, publicKey, connection, amount_in_lamports, encryptionService, keyBasePath }: WithdrawParams): Promise<{
15
+ isPartial: boolean;
16
+ tx: string;
17
+ recipient: string;
18
+ amount_in_lamports: number;
19
+ fee_in_lamports: number;
20
+ }>;
21
+ export {};
@@ -0,0 +1,270 @@
1
+ import { LAMPORTS_PER_SOL } from '@solana/web3.js';
2
+ import BN from 'bn.js';
3
+ import { Buffer } from 'buffer';
4
+ import { Keypair as UtxoKeypair } from './models/keypair.js';
5
+ import { Utxo } from './models/utxo.js';
6
+ import { parseProofToBytesArray, parseToBytesArray, prove } from './utils/prover.js';
7
+ import { ALT_ADDRESS, DEPLOYER_ID, FEE_RECIPIENT, FIELD_SIZE, INDEXER_API_URL, MERKLE_TREE_DEPTH } from './utils/constants.js';
8
+ import { serializeProofAndExtData } from './utils/encryption.js';
9
+ import { fetchMerkleProof, findCommitmentPDAs, findNullifierPDAs, getExtDataHash, getProgramAccounts, queryRemoteTreeState, findCrossCheckNullifierPDAs } from './utils/utils.js';
10
+ import { getUtxos } from './getUtxos.js';
11
+ import { logger } from './utils/logger.js';
12
+ import { getConfig } from './config.js';
13
+ // Indexer API endpoint
14
+ // Function to submit withdraw request to indexer backend
15
+ async function submitWithdrawToIndexer(params) {
16
+ try {
17
+ const response = await fetch(`${INDEXER_API_URL}/withdraw`, {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ body: JSON.stringify(params)
23
+ });
24
+ if (!response.ok) {
25
+ const errorData = await response.json();
26
+ throw new Error(errorData.error);
27
+ }
28
+ const result = await response.json();
29
+ logger.debug('Withdraw request submitted successfully!');
30
+ logger.debug('Response:', result);
31
+ return result.signature;
32
+ }
33
+ catch (error) {
34
+ logger.debug('Failed to submit withdraw request to indexer:', typeof error, error);
35
+ throw error;
36
+ }
37
+ }
38
+ export async function withdraw({ recipient, lightWasm, storage, publicKey, connection, amount_in_lamports, encryptionService, keyBasePath }) {
39
+ let fee_in_lamports = amount_in_lamports * (await getConfig('withdraw_fee_rate')) + LAMPORTS_PER_SOL * (await getConfig('withdraw_rent_fee'));
40
+ amount_in_lamports -= fee_in_lamports;
41
+ let isPartial = false;
42
+ logger.debug('Encryption key generated from user keypair');
43
+ logger.debug(`Deployer wallet: ${DEPLOYER_ID.toString()}`);
44
+ const { treeAccount, treeTokenAccount, globalConfigAccount } = getProgramAccounts();
45
+ // Get current tree state
46
+ const { root, nextIndex: currentNextIndex } = await queryRemoteTreeState();
47
+ logger.debug(`Using tree root: ${root}`);
48
+ logger.debug(`New UTXOs will be inserted at indices: ${currentNextIndex} and ${currentNextIndex + 1}`);
49
+ // Generate a deterministic private key derived from the wallet keypair
50
+ const utxoPrivateKey = encryptionService.deriveUtxoPrivateKey();
51
+ // Create a UTXO keypair that will be used for all inputs and outputs
52
+ const utxoKeypair = new UtxoKeypair(utxoPrivateKey, lightWasm);
53
+ logger.debug('Using wallet-derived UTXO keypair for withdrawal');
54
+ // Generate a deterministic private key derived from the wallet keypair (V2)
55
+ const utxoPrivateKeyV2 = encryptionService.getUtxoPrivateKeyV2();
56
+ const utxoKeypairV2 = new UtxoKeypair(utxoPrivateKeyV2, lightWasm);
57
+ // Fetch existing UTXOs for this user
58
+ logger.debug('\nFetching existing UTXOs...');
59
+ const unspentUtxos = await getUtxos({ connection, publicKey, encryptionService, storage });
60
+ logger.debug(`Found ${unspentUtxos.length} total UTXOs`);
61
+ // Calculate and log total unspent UTXO balance
62
+ const totalUnspentBalance = unspentUtxos.reduce((sum, utxo) => sum.add(utxo.amount), new BN(0));
63
+ logger.debug(`Total unspent UTXO balance before: ${totalUnspentBalance.toString()} lamports (${totalUnspentBalance.toNumber() / 1e9} SOL)`);
64
+ if (unspentUtxos.length < 1) {
65
+ throw new Error('Need at least 1 unspent UTXO to perform a withdrawal');
66
+ }
67
+ // Sort UTXOs by amount in descending order to use the largest ones first
68
+ unspentUtxos.sort((a, b) => b.amount.cmp(a.amount));
69
+ // Use the largest UTXO as first input, and either second largest UTXO or dummy UTXO as second input
70
+ const firstInput = unspentUtxos[0];
71
+ const secondInput = unspentUtxos.length > 1 ? unspentUtxos[1] : new Utxo({
72
+ lightWasm,
73
+ keypair: utxoKeypair,
74
+ amount: '0'
75
+ });
76
+ const inputs = [firstInput, secondInput];
77
+ const totalInputAmount = firstInput.amount.add(secondInput.amount);
78
+ logger.debug(`Using UTXO with amount: ${firstInput.amount.toString()} and ${secondInput.amount.gt(new BN(0)) ? 'second UTXO with amount: ' + secondInput.amount.toString() : 'dummy UTXO'}`);
79
+ if (totalInputAmount.toNumber() === 0) {
80
+ throw new Error('no balance');
81
+ }
82
+ if (totalInputAmount.lt(new BN(amount_in_lamports + fee_in_lamports))) {
83
+ isPartial = true;
84
+ amount_in_lamports = totalInputAmount.toNumber();
85
+ amount_in_lamports -= fee_in_lamports;
86
+ }
87
+ // Calculate the change amount (what's left after withdrawal and fee)
88
+ const changeAmount = totalInputAmount.sub(new BN(amount_in_lamports)).sub(new BN(fee_in_lamports));
89
+ logger.debug(`Withdrawing ${amount_in_lamports} lamports with ${fee_in_lamports} fee, ${changeAmount.toString()} as change`);
90
+ // Get Merkle proofs for both input UTXOs
91
+ const inputMerkleProofs = await Promise.all(inputs.map(async (utxo, index) => {
92
+ // For dummy UTXO (amount is 0), use a zero-filled proof
93
+ if (utxo.amount.eq(new BN(0))) {
94
+ return {
95
+ pathElements: [...new Array(MERKLE_TREE_DEPTH).fill("0")],
96
+ pathIndices: Array(MERKLE_TREE_DEPTH).fill(0)
97
+ };
98
+ }
99
+ // For real UTXOs, fetch the proof from API
100
+ const commitment = await utxo.getCommitment();
101
+ return fetchMerkleProof(commitment);
102
+ }));
103
+ // Extract path elements and indices
104
+ const inputMerklePathElements = inputMerkleProofs.map(proof => proof.pathElements);
105
+ const inputMerklePathIndices = inputs.map(utxo => utxo.index || 0);
106
+ // Create outputs: first output is change, second is dummy (required by protocol)
107
+ const outputs = [
108
+ new Utxo({
109
+ lightWasm,
110
+ amount: changeAmount.toString(),
111
+ keypair: utxoKeypairV2,
112
+ index: currentNextIndex
113
+ }), // Change output
114
+ new Utxo({
115
+ lightWasm,
116
+ amount: '0',
117
+ keypair: utxoKeypairV2,
118
+ index: currentNextIndex + 1
119
+ }) // Empty UTXO
120
+ ];
121
+ // For withdrawals, extAmount is negative (funds leaving the system)
122
+ const extAmount = -amount_in_lamports;
123
+ const publicAmountForCircuit = new BN(extAmount).sub(new BN(fee_in_lamports)).add(FIELD_SIZE).mod(FIELD_SIZE);
124
+ logger.debug(`Public amount calculation: (${extAmount} - ${fee_in_lamports} + FIELD_SIZE) % FIELD_SIZE = ${publicAmountForCircuit.toString()}`);
125
+ // Verify this matches the circuit balance equation: sumIns + publicAmount = sumOuts
126
+ const sumIns = inputs.reduce((sum, input) => sum.add(input.amount), new BN(0));
127
+ const sumOuts = outputs.reduce((sum, output) => sum.add(output.amount), new BN(0));
128
+ logger.debug(`Circuit balance check: sumIns(${sumIns.toString()}) + publicAmount(${publicAmountForCircuit.toString()}) should equal sumOuts(${sumOuts.toString()})`);
129
+ // Convert to circuit-compatible format
130
+ const publicAmountCircuitResult = sumIns.add(publicAmountForCircuit).mod(FIELD_SIZE);
131
+ logger.debug(`Balance verification: ${sumIns.toString()} + ${publicAmountForCircuit.toString()} (mod FIELD_SIZE) = ${publicAmountCircuitResult.toString()}`);
132
+ logger.debug(`Expected sum of outputs: ${sumOuts.toString()}`);
133
+ logger.debug(`Balance equation satisfied: ${publicAmountCircuitResult.eq(sumOuts)}`);
134
+ // Generate nullifiers and commitments
135
+ const inputNullifiers = await Promise.all(inputs.map(x => x.getNullifier()));
136
+ const outputCommitments = await Promise.all(outputs.map(x => x.getCommitment()));
137
+ // Save original commitment and nullifier values for verification
138
+ logger.debug('\n=== UTXO VALIDATION ===');
139
+ logger.debug('Output 0 Commitment:', outputCommitments[0]);
140
+ logger.debug('Output 1 Commitment:', outputCommitments[1]);
141
+ // Encrypt the UTXO data using a compact format that includes the keypair
142
+ logger.debug('\nEncrypting UTXOs with keypair data...');
143
+ const encryptedOutput1 = encryptionService.encryptUtxo(outputs[0]);
144
+ const encryptedOutput2 = encryptionService.encryptUtxo(outputs[1]);
145
+ logger.debug(`\nOutput[0] (change):`);
146
+ await outputs[0].log();
147
+ logger.debug(`\nOutput[1] (empty):`);
148
+ await outputs[1].log();
149
+ logger.debug(`\nEncrypted output 1 size: ${encryptedOutput1.length} bytes`);
150
+ logger.debug(`Encrypted output 2 size: ${encryptedOutput2.length} bytes`);
151
+ logger.debug(`Total encrypted outputs size: ${encryptedOutput1.length + encryptedOutput2.length} bytes`);
152
+ // Test decryption to verify commitment values match
153
+ logger.debug('\n=== TESTING DECRYPTION ===');
154
+ logger.debug('Decrypting output 1 to verify commitment matches...');
155
+ const decryptedUtxo1 = await encryptionService.decryptUtxo(encryptedOutput1, lightWasm);
156
+ const decryptedCommitment1 = await decryptedUtxo1.getCommitment();
157
+ logger.debug('Original commitment:', outputCommitments[0]);
158
+ logger.debug('Decrypted commitment:', decryptedCommitment1);
159
+ logger.debug('Commitment matches:', outputCommitments[0] === decryptedCommitment1);
160
+ // Create the withdrawal ExtData with real encrypted outputs
161
+ const extData = {
162
+ // it can be any address
163
+ recipient,
164
+ extAmount: new BN(extAmount),
165
+ encryptedOutput1: encryptedOutput1,
166
+ encryptedOutput2: encryptedOutput2,
167
+ fee: new BN(fee_in_lamports),
168
+ feeRecipient: FEE_RECIPIENT,
169
+ mintAddress: inputs[0].mintAddress
170
+ };
171
+ // Calculate the extDataHash with the encrypted outputs
172
+ const calculatedExtDataHash = getExtDataHash(extData);
173
+ // Create the input for the proof generation
174
+ const input = {
175
+ // Common transaction data
176
+ root: root,
177
+ inputNullifier: inputNullifiers,
178
+ outputCommitment: outputCommitments,
179
+ publicAmount: publicAmountForCircuit.toString(),
180
+ extDataHash: calculatedExtDataHash,
181
+ // Input UTXO data (UTXOs being spent)
182
+ inAmount: inputs.map(x => x.amount.toString(10)),
183
+ inPrivateKey: inputs.map(x => x.keypair.privkey),
184
+ inBlinding: inputs.map(x => x.blinding.toString(10)),
185
+ inPathIndices: inputMerklePathIndices,
186
+ inPathElements: inputMerklePathElements,
187
+ // Output UTXO data (UTXOs being created)
188
+ outAmount: outputs.map(x => x.amount.toString(10)),
189
+ outBlinding: outputs.map(x => x.blinding.toString(10)),
190
+ outPubkey: outputs.map(x => x.keypair.pubkey),
191
+ // new mint address
192
+ mintAddress: inputs[0].mintAddress
193
+ };
194
+ logger.info('generating ZK proof...');
195
+ // Generate the zero-knowledge proof
196
+ const { proof, publicSignals } = await prove(input, keyBasePath);
197
+ // Parse the proof and public signals into byte arrays
198
+ const proofInBytes = parseProofToBytesArray(proof);
199
+ const inputsInBytes = parseToBytesArray(publicSignals);
200
+ // Create the proof object to submit to the program
201
+ const proofToSubmit = {
202
+ proofA: proofInBytes.proofA,
203
+ proofB: proofInBytes.proofB.flat(),
204
+ proofC: proofInBytes.proofC,
205
+ root: inputsInBytes[0],
206
+ publicAmount: inputsInBytes[1],
207
+ extDataHash: inputsInBytes[2],
208
+ inputNullifiers: [
209
+ inputsInBytes[3],
210
+ inputsInBytes[4]
211
+ ],
212
+ outputCommitments: [
213
+ inputsInBytes[5],
214
+ inputsInBytes[6]
215
+ ],
216
+ };
217
+ // Find PDAs for nullifiers and commitments
218
+ const { nullifier0PDA, nullifier1PDA } = findNullifierPDAs(proofToSubmit);
219
+ const { nullifier2PDA, nullifier3PDA } = findCrossCheckNullifierPDAs(proofToSubmit);
220
+ const { commitment0PDA, commitment1PDA } = findCommitmentPDAs(proofToSubmit);
221
+ // Serialize the proof and extData
222
+ const serializedProof = serializeProofAndExtData(proofToSubmit, extData);
223
+ logger.debug(`Total instruction data size: ${serializedProof.length} bytes`);
224
+ // Prepare withdraw parameters for indexer backend
225
+ const withdrawParams = {
226
+ serializedProof: serializedProof.toString('base64'),
227
+ treeAccount: treeAccount.toString(),
228
+ nullifier0PDA: nullifier0PDA.toString(),
229
+ nullifier1PDA: nullifier1PDA.toString(),
230
+ nullifier2PDA: nullifier2PDA.toString(),
231
+ nullifier3PDA: nullifier3PDA.toString(),
232
+ commitment0PDA: commitment0PDA.toString(),
233
+ commitment1PDA: commitment1PDA.toString(),
234
+ treeTokenAccount: treeTokenAccount.toString(),
235
+ globalConfigAccount: globalConfigAccount.toString(),
236
+ recipient: recipient.toString(),
237
+ feeRecipientAccount: FEE_RECIPIENT.toString(),
238
+ extAmount: extAmount,
239
+ encryptedOutput1: encryptedOutput1.toString('base64'),
240
+ encryptedOutput2: encryptedOutput2.toString('base64'),
241
+ fee: fee_in_lamports,
242
+ lookupTableAddress: ALT_ADDRESS.toString(),
243
+ senderAddress: publicKey.toString()
244
+ };
245
+ logger.debug('Prepared withdraw parameters for indexer backend');
246
+ // Submit to indexer backend instead of directly to Solana
247
+ logger.info('submitting transaction to relayer...');
248
+ const signature = await submitWithdrawToIndexer(withdrawParams);
249
+ // Wait a moment for the transaction to be confirmed
250
+ logger.info('waiting for transaction confirmation...');
251
+ let retryTimes = 0;
252
+ let itv = 2;
253
+ const encryptedOutputStr = Buffer.from(encryptedOutput1).toString('hex');
254
+ let start = Date.now();
255
+ while (true) {
256
+ console.log(`retryTimes: ${retryTimes}`);
257
+ await new Promise(resolve => setTimeout(resolve, itv * 1000));
258
+ console.log('Fetching updated tree state...');
259
+ let res = await fetch(INDEXER_API_URL + '/utxos/check/' + encryptedOutputStr);
260
+ let resJson = await res.json();
261
+ console.log('resJson:', resJson);
262
+ if (resJson.exists) {
263
+ return { isPartial, tx: signature, recipient: recipient.toString(), amount_in_lamports, fee_in_lamports };
264
+ }
265
+ if (retryTimes >= 10) {
266
+ throw new Error('Refresh the page to see latest balance.');
267
+ }
268
+ retryTimes++;
269
+ }
270
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "privacycash",
3
+ "version": "1.0.6",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": "./dist/index.js",
8
+ "./utils": "./dist/exportUtils.js"
9
+ },
10
+ "types": "dist/index.d.ts",
11
+ "type": "module",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "test": "vitest run --exclude \"**/__tests__/e2e.test.ts\"",
15
+ "teste2e": "vitest run e2e.test.ts"
16
+ },
17
+ "keywords": [],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "@coral-xyz/anchor": "^0.31.1",
22
+ "@ethersproject/keccak256": "^5.8.0",
23
+ "@ethersproject/sha2": "^5.8.0",
24
+ "@lightprotocol/hasher.rs": "^0.2.1",
25
+ "@solana/web3.js": "^1.98.4",
26
+ "@types/bn.js": "^5.2.0",
27
+ "@types/snarkjs": "^0.7.9",
28
+ "bn.js": "^5.2.2",
29
+ "borsh": "^2.0.0",
30
+ "bs58": "^6.0.0",
31
+ "dotenv": "^17.2.2",
32
+ "ethers": "^6.15.0",
33
+ "ffjavascript": "^0.3.1",
34
+ "node-localstorage": "^3.0.5",
35
+ "snarkjs": "^0.7.5",
36
+ "tmp-promise": "^3.0.3",
37
+ "tweetnacl": "^1.0.3"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bn.js": "^5.1.5",
41
+ "@types/node": "^24.3.0",
42
+ "@types/node-localstorage": "^1.3.3",
43
+ "@types/snarkjs": "^0.7.9",
44
+ "@types/tmp": "^0.2.6",
45
+ "typescript": "^5.9.2",
46
+ "vitest": "^3.2.4"
47
+ }
48
+ }
package/src/config.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { INDEXER_API_URL } from "./utils/constants.js";
2
+
3
+ type Config = {
4
+ withdraw_fee_rate: number
5
+ withdraw_rent_fee: number
6
+ deposit_fee_rate: number
7
+ }
8
+
9
+ let config: Config | undefined
10
+
11
+ export async function getConfig<K extends keyof Config>(key: K): Promise<Config[K]> {
12
+ if (!config) {
13
+ const res = await fetch(INDEXER_API_URL + '/config')
14
+ const data = await res.json()
15
+
16
+ // check types
17
+ if (
18
+ typeof data.withdraw_fee_rate !== 'number' ||
19
+ typeof data.withdraw_rent_fee !== 'number' ||
20
+ typeof data.deposit_fee_rate !== 'number'
21
+ ) {
22
+ throw new Error("Invalid config received from server")
23
+ }
24
+
25
+ config = data
26
+ }
27
+ return config![key]
28
+ }