nova-privacy-sdk 1.0.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.
Files changed (69) hide show
  1. package/.github/workflows/npm-publish.yml +55 -0
  2. package/PUBLISH.md +122 -0
  3. package/README.md +177 -0
  4. package/__tests__/e2e.test.ts +56 -0
  5. package/__tests__/e2espl.test.ts +73 -0
  6. package/__tests__/encryption.test.ts +1635 -0
  7. package/circuit2/transaction2.wasm +0 -0
  8. package/circuit2/transaction2.zkey +0 -0
  9. package/dist/config.d.ts +9 -0
  10. package/dist/config.js +12 -0
  11. package/dist/deposit.d.ts +18 -0
  12. package/dist/deposit.js +392 -0
  13. package/dist/depositSPL.d.ts +20 -0
  14. package/dist/depositSPL.js +448 -0
  15. package/dist/exportUtils.d.ts +11 -0
  16. package/dist/exportUtils.js +11 -0
  17. package/dist/getUtxos.d.ts +29 -0
  18. package/dist/getUtxos.js +294 -0
  19. package/dist/getUtxosSPL.d.ts +33 -0
  20. package/dist/getUtxosSPL.js +395 -0
  21. package/dist/index.d.ts +125 -0
  22. package/dist/index.js +302 -0
  23. package/dist/models/keypair.d.ts +26 -0
  24. package/dist/models/keypair.js +43 -0
  25. package/dist/models/utxo.d.ts +49 -0
  26. package/dist/models/utxo.js +85 -0
  27. package/dist/utils/address_lookup_table.d.ts +9 -0
  28. package/dist/utils/address_lookup_table.js +45 -0
  29. package/dist/utils/constants.d.ts +31 -0
  30. package/dist/utils/constants.js +62 -0
  31. package/dist/utils/encryption.d.ts +107 -0
  32. package/dist/utils/encryption.js +376 -0
  33. package/dist/utils/logger.d.ts +9 -0
  34. package/dist/utils/logger.js +35 -0
  35. package/dist/utils/merkle_tree.d.ts +92 -0
  36. package/dist/utils/merkle_tree.js +186 -0
  37. package/dist/utils/node-shim.d.ts +5 -0
  38. package/dist/utils/node-shim.js +5 -0
  39. package/dist/utils/prover.d.ts +36 -0
  40. package/dist/utils/prover.js +147 -0
  41. package/dist/utils/utils.d.ts +69 -0
  42. package/dist/utils/utils.js +182 -0
  43. package/dist/withdraw.d.ts +21 -0
  44. package/dist/withdraw.js +270 -0
  45. package/dist/withdrawSPL.d.ts +23 -0
  46. package/dist/withdrawSPL.js +306 -0
  47. package/package.json +77 -0
  48. package/setup-git.sh +51 -0
  49. package/setup-github.sh +36 -0
  50. package/src/config.ts +22 -0
  51. package/src/deposit.ts +487 -0
  52. package/src/depositSPL.ts +567 -0
  53. package/src/exportUtils.ts +13 -0
  54. package/src/getUtxos.ts +396 -0
  55. package/src/getUtxosSPL.ts +528 -0
  56. package/src/index.ts +350 -0
  57. package/src/models/keypair.ts +52 -0
  58. package/src/models/utxo.ts +106 -0
  59. package/src/utils/address_lookup_table.ts +78 -0
  60. package/src/utils/constants.ts +84 -0
  61. package/src/utils/encryption.ts +464 -0
  62. package/src/utils/logger.ts +42 -0
  63. package/src/utils/merkle_tree.ts +207 -0
  64. package/src/utils/node-shim.ts +6 -0
  65. package/src/utils/prover.ts +222 -0
  66. package/src/utils/utils.ts +242 -0
  67. package/src/withdraw.ts +332 -0
  68. package/src/withdrawSPL.ts +394 -0
  69. package/tsconfig.json +28 -0
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "nova-privacy-sdk",
3
+ "version": "1.0.0",
4
+ "description": "SDK for Nova Privacy - Private cryptocurrency transactions on Solana. Fork of Privacy Cash SDK.",
5
+ "main": "dist/index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/NovaShieldWallet/nova-privacy-shield.git"
9
+ },
10
+ "homepage": "https://github.com/NovaShieldWallet/nova-privacy-shield#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/NovaShieldWallet/nova-privacy-shield/issues"
13
+ },
14
+ "exports": {
15
+ ".": "./dist/index.js",
16
+ "./utils": "./dist/exportUtils.js"
17
+ },
18
+ "types": "dist/index.d.ts",
19
+ "type": "module",
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build",
23
+ "test": "vitest run --exclude \"**/__tests__/e2e.test.ts,**/__tests__/e2espl.test.ts\"",
24
+ "teste2e": "vitest run e2e.test.ts",
25
+ "teste2espl": "vitest run e2espl.test.ts"
26
+ },
27
+ "keywords": [
28
+ "solana",
29
+ "privacy",
30
+ "zk",
31
+ "zero-knowledge",
32
+ "private-transactions",
33
+ "cryptocurrency",
34
+ "blockchain",
35
+ "sdk",
36
+ "nova",
37
+ "privacy-cash",
38
+ "private-withdraw",
39
+ "private-send",
40
+ "solana-privacy"
41
+ ],
42
+ "author": {
43
+ "name": "Nova Shield Wallet",
44
+ "email": "hi@nshield.org",
45
+ "url": "https://nshield.org"
46
+ },
47
+ "license": "ISC",
48
+ "dependencies": {
49
+ "@coral-xyz/anchor": "^0.31.1",
50
+ "@ethersproject/keccak256": "^5.8.0",
51
+ "@ethersproject/sha2": "^5.8.0",
52
+ "@lightprotocol/hasher.rs": "^0.2.1",
53
+ "@solana/spl-token": "^0.4.14",
54
+ "@solana/web3.js": "^1.98.4",
55
+ "@types/bn.js": "^5.2.0",
56
+ "@types/snarkjs": "^0.7.9",
57
+ "bn.js": "^5.2.2",
58
+ "borsh": "^2.0.0",
59
+ "bs58": "^6.0.0",
60
+ "dotenv": "^17.2.2",
61
+ "ethers": "^6.15.0",
62
+ "ffjavascript": "^0.3.1",
63
+ "node-localstorage": "^3.0.5",
64
+ "snarkjs": "^0.7.5",
65
+ "tmp-promise": "^3.0.3",
66
+ "tweetnacl": "^1.0.3"
67
+ },
68
+ "devDependencies": {
69
+ "@types/bn.js": "^5.1.5",
70
+ "@types/node": "^24.3.0",
71
+ "@types/node-localstorage": "^1.3.3",
72
+ "@types/snarkjs": "^0.7.9",
73
+ "@types/tmp": "^0.2.6",
74
+ "typescript": "^5.9.2",
75
+ "vitest": "^3.2.4"
76
+ }
77
+ }
package/setup-git.sh ADDED
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+
3
+ # Script to set up git repository and prepare for GitHub push
4
+ # Usage: ./setup-git.sh YOUR_GITHUB_USERNAME
5
+
6
+ if [ -z "$1" ]; then
7
+ echo "Usage: ./setup-git.sh YOUR_GITHUB_USERNAME"
8
+ echo "Example: ./setup-git.sh myusername"
9
+ exit 1
10
+ fi
11
+
12
+ GITHUB_USERNAME=$1
13
+ REPO_NAME="nova-privacy-sdk"
14
+
15
+ echo "🚀 Setting up Nova Privacy SDK for GitHub and npm..."
16
+
17
+ # Update package.json with GitHub username
18
+ echo "📝 Updating package.json with GitHub username..."
19
+ sed -i '' "s/YOUR_USERNAME/$GITHUB_USERNAME/g" package.json
20
+ sed -i '' "s/Your Name <your.email@example.com>/$(git config user.name) <$(git config user.email)>/g" package.json 2>/dev/null || echo "⚠️ Update author in package.json manually"
21
+
22
+ # Update README.md with GitHub username
23
+ echo "📝 Updating README.md with GitHub username..."
24
+ sed -i '' "s/YOUR_USERNAME/$GITHUB_USERNAME/g" README.md
25
+
26
+ # Check if git is initialized
27
+ if [ ! -d ".git" ]; then
28
+ echo "🔧 Initializing git repository..."
29
+ git init
30
+ fi
31
+
32
+ # Build the project
33
+ echo "🔨 Building project..."
34
+ npm run build
35
+
36
+ echo ""
37
+ echo "✅ Setup complete!"
38
+ echo ""
39
+ echo "Next steps:"
40
+ echo "1. Create a new repository on GitHub named: $REPO_NAME"
41
+ echo "2. Run these commands:"
42
+ echo " git add ."
43
+ echo " git commit -m 'Initial commit: Nova Privacy SDK fork'"
44
+ echo " git branch -M main"
45
+ echo " git remote add origin https://github.com/$GITHUB_USERNAME/$REPO_NAME.git"
46
+ echo " git push -u origin main"
47
+ echo ""
48
+ echo "3. After pushing to GitHub, publish to npm:"
49
+ echo " npm login"
50
+ echo " npm publish"
51
+ echo ""
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+
3
+ # Script to set up git repository for nova-privacy-shield
4
+ # This will update the git remote to point to NovaShieldWallet/nova-privacy-shield
5
+
6
+ echo "🔧 Setting up GitHub repository for nova-privacy-shield..."
7
+
8
+ # Check if git is initialized
9
+ if [ ! -d ".git" ]; then
10
+ echo "Initializing git repository..."
11
+ git init
12
+ git branch -M main
13
+ fi
14
+
15
+ # Remove existing remote if it exists
16
+ if git remote get-url origin &>/dev/null; then
17
+ echo "Removing existing remote..."
18
+ git remote remove origin
19
+ fi
20
+
21
+ # Add new remote
22
+ echo "Adding new GitHub remote..."
23
+ git remote add origin https://github.com/NovaShieldWallet/nova-privacy-shield.git
24
+
25
+ echo ""
26
+ echo "✅ Git remote configured!"
27
+ echo ""
28
+ echo "Current remote:"
29
+ git remote -v
30
+ echo ""
31
+ echo "Next steps:"
32
+ echo "1. Repository already exists at: https://github.com/NovaShieldWallet/nova-privacy-shield"
33
+ echo "2. Add all files: git add ."
34
+ echo "3. Commit: git commit -m 'Initial commit: Nova Privacy SDK fork'"
35
+ echo "4. Push: git push -u origin main"
36
+ echo ""
package/src/config.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { RELAYER_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
+ usdc_withdraw_rent_fee: number
8
+ rent_fees: any
9
+ }
10
+
11
+ let config: Config | undefined
12
+
13
+ export async function getConfig<K extends keyof Config>(key: K): Promise<Config[K]> {
14
+ if (!config) {
15
+ const res = await fetch(RELAYER_API_URL + '/config')
16
+ config = await res.json()
17
+ }
18
+ if (typeof config![key] == 'undefined') {
19
+ throw new Error(`can not get ${key} from ${RELAYER_API_URL}/config`)
20
+ }
21
+ return config![key]
22
+ }
package/src/deposit.ts ADDED
@@ -0,0 +1,487 @@
1
+ import { Connection, Keypair, PublicKey, TransactionInstruction, SystemProgram, ComputeBudgetProgram, VersionedTransaction, TransactionMessage, LAMPORTS_PER_SOL } from '@solana/web3.js';
2
+ import BN from 'bn.js';
3
+ import { Utxo } from './models/utxo.js';
4
+ import { fetchMerkleProof, findNullifierPDAs, getExtDataHash, getProgramAccounts, queryRemoteTreeState, findCrossCheckNullifierPDAs } from './utils/utils.js';
5
+ import { prove, parseProofToBytesArray, parseToBytesArray } from './utils/prover.js';
6
+ import * as hasher from '@lightprotocol/hasher.rs';
7
+ import { MerkleTree } from './utils/merkle_tree.js';
8
+ import { EncryptionService, serializeProofAndExtData } from './utils/encryption.js';
9
+ import { Keypair as UtxoKeypair } from './models/keypair.js';
10
+ import { getUtxos, isUtxoSpent } from './getUtxos.js';
11
+ import { FIELD_SIZE, FEE_RECIPIENT, MERKLE_TREE_DEPTH, RELAYER_API_URL, PROGRAM_ID, ALT_ADDRESS } from './utils/constants.js';
12
+ import { useExistingALT } from './utils/address_lookup_table.js';
13
+ import { logger } from './utils/logger.js';
14
+
15
+
16
+ // Function to relay pre-signed deposit transaction to indexer backend
17
+ async function relayDepositToIndexer(signedTransaction: string, publicKey: PublicKey, referrer?: string): Promise<string> {
18
+ try {
19
+ logger.debug('Relaying pre-signed deposit transaction to indexer backend...');
20
+
21
+ const params: any = {
22
+ signedTransaction,
23
+ senderAddress: publicKey.toString()
24
+ };
25
+
26
+ if (referrer) {
27
+ params.referralWalletAddress = referrer
28
+ }
29
+
30
+ const response = await fetch(`${RELAYER_API_URL}/deposit`, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ body: JSON.stringify(params)
36
+ });
37
+
38
+ if (!response.ok) {
39
+ logger.error('res text:', await response.text())
40
+ throw new Error('response not ok')
41
+ // const errorData = await response.json() as { error?: string };
42
+ // throw new Error(`Deposit relay failed: ${response.status} ${response.statusText} - ${errorData.error || 'Unknown error'}`);
43
+ }
44
+
45
+ const result = await response.json() as { signature: string, success: boolean };
46
+ logger.debug('Pre-signed deposit transaction relayed successfully!');
47
+ logger.debug('Response:', result);
48
+
49
+ return result.signature;
50
+ } catch (error) {
51
+ console.error('Failed to relay deposit transaction to indexer:', error);
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ type DepositParams = {
57
+ publicKey: PublicKey,
58
+ connection: Connection,
59
+ amount_in_lamports: number,
60
+ storage: Storage,
61
+ encryptionService: EncryptionService,
62
+ keyBasePath: string,
63
+ lightWasm: hasher.LightWasm,
64
+ referrer?: string,
65
+ transactionSigner: (tx: VersionedTransaction) => Promise<VersionedTransaction>
66
+ }
67
+ export async function deposit({ lightWasm, storage, keyBasePath, publicKey, connection, amount_in_lamports, encryptionService, transactionSigner, referrer }: DepositParams) {
68
+ // check limit
69
+ let limitAmount = await checkDepositLimit(connection)
70
+
71
+ if (limitAmount && amount_in_lamports > limitAmount * LAMPORTS_PER_SOL) {
72
+ throw new Error(`Don't deposit more than ${limitAmount} SOL`)
73
+ }
74
+
75
+ // const amount_in_lamports = amount_in_sol * LAMPORTS_PER_SOL
76
+ const fee_amount_in_lamports = 0
77
+ logger.debug('Encryption key generated from user keypair');
78
+ logger.debug(`User wallet: ${publicKey.toString()}`);
79
+ logger.debug(`Deposit amount: ${amount_in_lamports} lamports (${amount_in_lamports / LAMPORTS_PER_SOL} SOL)`);
80
+ logger.debug(`Calculated fee: ${fee_amount_in_lamports} lamports (${fee_amount_in_lamports / LAMPORTS_PER_SOL} SOL)`);
81
+
82
+ // Check wallet balance
83
+ const balance = await connection.getBalance(publicKey);
84
+ logger.debug(`Wallet balance: ${balance / 1e9} SOL`);
85
+
86
+ if (balance < amount_in_lamports + fee_amount_in_lamports) {
87
+ throw new Error(`Insufficient balance: ${balance / 1e9} SOL. Need at least ${(amount_in_lamports + fee_amount_in_lamports) / LAMPORTS_PER_SOL} SOL.`);
88
+ }
89
+
90
+ const { treeAccount, treeTokenAccount, globalConfigAccount } = getProgramAccounts()
91
+
92
+ // Create the merkle tree with the pre-initialized poseidon hash
93
+ const tree = new MerkleTree(MERKLE_TREE_DEPTH, lightWasm);
94
+
95
+ // Initialize root and nextIndex variables
96
+ const { root, nextIndex: currentNextIndex } = await queryRemoteTreeState();
97
+
98
+ logger.debug(`Using tree root: ${root}`);
99
+ logger.debug(`New UTXOs will be inserted at indices: ${currentNextIndex} and ${currentNextIndex + 1}`);
100
+
101
+ // Generate a deterministic private key derived from the wallet keypair
102
+ // const utxoPrivateKey = encryptionService.deriveUtxoPrivateKey();
103
+ const utxoPrivateKey = encryptionService.getUtxoPrivateKeyV2();
104
+
105
+ // Create a UTXO keypair that will be used for all inputs and outputs
106
+ const utxoKeypair = new UtxoKeypair(utxoPrivateKey, lightWasm);
107
+ logger.debug('Using wallet-derived UTXO keypair for deposit');
108
+
109
+ // Fetch existing UTXOs for this user
110
+ logger.debug('\nFetching existing UTXOs...');
111
+ const existingUnspentUtxos = await getUtxos({ connection, publicKey, encryptionService, storage });
112
+
113
+ // Calculate output amounts and external amount based on scenario
114
+ let extAmount: number;
115
+ let outputAmount: string;
116
+
117
+ // Create inputs based on whether we have existing UTXOs
118
+ let inputs: Utxo[];
119
+ let inputMerklePathIndices: number[];
120
+ let inputMerklePathElements: string[][];
121
+
122
+ if (existingUnspentUtxos.length === 0) {
123
+ // Scenario 1: Fresh deposit with dummy inputs - add new funds to the system
124
+ extAmount = amount_in_lamports;
125
+ outputAmount = new BN(amount_in_lamports).sub(new BN(fee_amount_in_lamports)).toString();
126
+
127
+ logger.debug(`Fresh deposit scenario (no existing UTXOs):`);
128
+ logger.debug(`External amount (deposit): ${extAmount}`);
129
+ logger.debug(`Fee amount: ${fee_amount_in_lamports}`);
130
+ logger.debug(`Output amount: ${outputAmount}`);
131
+
132
+ // Use two dummy UTXOs as inputs
133
+ inputs = [
134
+ new Utxo({
135
+ lightWasm,
136
+ keypair: utxoKeypair
137
+ }),
138
+ new Utxo({
139
+ lightWasm,
140
+ keypair: utxoKeypair
141
+ })
142
+ ];
143
+
144
+ // Both inputs are dummy, so use mock indices and zero-filled Merkle paths
145
+ inputMerklePathIndices = inputs.map((input) => input.index || 0);
146
+ inputMerklePathElements = inputs.map(() => {
147
+ return [...new Array(tree.levels).fill("0")];
148
+ });
149
+ } else {
150
+ // Scenario 2: Deposit that consolidates with existing UTXO(s)
151
+ const firstUtxo = existingUnspentUtxos[0];
152
+ const firstUtxoAmount = firstUtxo.amount;
153
+ const secondUtxoAmount = existingUnspentUtxos.length > 1 ? existingUnspentUtxos[1].amount : new BN(0);
154
+ extAmount = amount_in_lamports; // Still depositing new funds
155
+
156
+ // Output combines existing UTXO amounts + new deposit amount - fee
157
+ outputAmount = firstUtxoAmount.add(secondUtxoAmount).add(new BN(amount_in_lamports)).sub(new BN(fee_amount_in_lamports)).toString();
158
+
159
+ logger.debug(`Deposit with consolidation scenario:`);
160
+ logger.debug(`First existing UTXO amount: ${firstUtxoAmount.toString()}`);
161
+ if (secondUtxoAmount.gt(new BN(0))) {
162
+ logger.debug(`Second existing UTXO amount: ${secondUtxoAmount.toString()}`);
163
+ }
164
+ logger.debug(`New deposit amount: ${amount_in_lamports}`);
165
+ logger.debug(`Fee amount: ${fee_amount_in_lamports}`);
166
+ logger.debug(`Output amount (existing UTXOs + deposit - fee): ${outputAmount}`);
167
+ logger.debug(`External amount (deposit): ${extAmount}`);
168
+
169
+ logger.debug('\nFirst UTXO to be consolidated:');
170
+ await firstUtxo.log();
171
+
172
+ // Use first existing UTXO as first input, and either second UTXO or dummy UTXO as second input
173
+ const secondUtxo = existingUnspentUtxos.length > 1 ? existingUnspentUtxos[1] : new Utxo({
174
+ lightWasm,
175
+ keypair: utxoKeypair,
176
+ amount: '0'
177
+ });
178
+
179
+ inputs = [
180
+ firstUtxo, // Use the first existing UTXO
181
+ secondUtxo // Use second UTXO if available, otherwise dummy
182
+ ];
183
+
184
+ // Fetch Merkle proofs for real UTXOs
185
+ const firstUtxoCommitment = await firstUtxo.getCommitment();
186
+ const firstUtxoMerkleProof = await fetchMerkleProof(firstUtxoCommitment);
187
+
188
+ let secondUtxoMerkleProof;
189
+ if (secondUtxo.amount.gt(new BN(0))) {
190
+ // Second UTXO is real, fetch its proof
191
+ const secondUtxoCommitment = await secondUtxo.getCommitment();
192
+ secondUtxoMerkleProof = await fetchMerkleProof(secondUtxoCommitment);
193
+ logger.debug('\nSecond UTXO to be consolidated:');
194
+ await secondUtxo.log();
195
+ }
196
+
197
+ // Use the real pathIndices from API for real inputs, mock index for dummy input
198
+ inputMerklePathIndices = [
199
+ firstUtxo.index || 0, // Use the real UTXO's index
200
+ secondUtxo.amount.gt(new BN(0)) ? (secondUtxo.index || 0) : 0 // Real UTXO index or dummy
201
+ ];
202
+
203
+ // Create Merkle path elements: real proof for real inputs, zeros for dummy input
204
+ inputMerklePathElements = [
205
+ firstUtxoMerkleProof.pathElements, // Real Merkle proof for first existing UTXO
206
+ secondUtxo.amount.gt(new BN(0)) ? secondUtxoMerkleProof!.pathElements : [...new Array(tree.levels).fill("0")] // Real proof or zero-filled for dummy
207
+ ];
208
+
209
+ logger.debug(`Using first UTXO with amount: ${firstUtxo.amount.toString()} and index: ${firstUtxo.index}`);
210
+ logger.debug(`Using second ${secondUtxo.amount.gt(new BN(0)) ? 'UTXO' : 'dummy UTXO'} with amount: ${secondUtxo.amount.toString()}${secondUtxo.amount.gt(new BN(0)) ? ` and index: ${secondUtxo.index}` : ''}`);
211
+ logger.debug(`First UTXO Merkle proof path indices from API: [${firstUtxoMerkleProof.pathIndices.join(', ')}]`);
212
+ if (secondUtxo.amount.gt(new BN(0))) {
213
+ logger.debug(`Second UTXO Merkle proof path indices from API: [${secondUtxoMerkleProof!.pathIndices.join(', ')}]`);
214
+ }
215
+ }
216
+
217
+ const publicAmountForCircuit = new BN(extAmount).sub(new BN(fee_amount_in_lamports)).add(FIELD_SIZE).mod(FIELD_SIZE);
218
+ logger.debug(`Public amount calculation: (${extAmount} - ${fee_amount_in_lamports} + FIELD_SIZE) % FIELD_SIZE = ${publicAmountForCircuit.toString()}`);
219
+
220
+ // Create outputs for the transaction with the same shared keypair
221
+ const outputs = [
222
+ new Utxo({
223
+ lightWasm,
224
+ amount: outputAmount,
225
+ keypair: utxoKeypair,
226
+ index: currentNextIndex // This UTXO will be inserted at currentNextIndex
227
+ }), // Output with value (either deposit amount minus fee, or input amount minus fee)
228
+ new Utxo({
229
+ lightWasm,
230
+ amount: '0',
231
+ keypair: utxoKeypair,
232
+ index: currentNextIndex + 1 // This UTXO will be inserted at currentNextIndex + 1
233
+ }) // Empty UTXO
234
+ ];
235
+
236
+ // Verify this matches the circuit balance equation: sumIns + publicAmount = sumOuts
237
+ const sumIns = inputs.reduce((sum, input) => sum.add(input.amount), new BN(0));
238
+ const sumOuts = outputs.reduce((sum, output) => sum.add(output.amount), new BN(0));
239
+ logger.debug(`Circuit balance check: sumIns(${sumIns.toString()}) + publicAmount(${publicAmountForCircuit.toString()}) should equal sumOuts(${sumOuts.toString()})`);
240
+
241
+ // Convert to circuit-compatible format
242
+ const publicAmountCircuitResult = sumIns.add(publicAmountForCircuit).mod(FIELD_SIZE);
243
+ logger.debug(`Balance verification: ${sumIns.toString()} + ${publicAmountForCircuit.toString()} (mod FIELD_SIZE) = ${publicAmountCircuitResult.toString()}`);
244
+ logger.debug(`Expected sum of outputs: ${sumOuts.toString()}`);
245
+ logger.debug(`Balance equation satisfied: ${publicAmountCircuitResult.eq(sumOuts)}`);
246
+
247
+ // Generate nullifiers and commitments
248
+ const inputNullifiers = await Promise.all(inputs.map(x => x.getNullifier()));
249
+ const outputCommitments = await Promise.all(outputs.map(x => x.getCommitment()));
250
+
251
+ // Save original commitment and nullifier values for verification
252
+ logger.debug('\n=== UTXO VALIDATION ===');
253
+ logger.debug('Output 0 Commitment:', outputCommitments[0]);
254
+ logger.debug('Output 1 Commitment:', outputCommitments[1]);
255
+
256
+ // Encrypt the UTXO data using a compact format that includes the keypair
257
+ logger.debug('\nEncrypting UTXOs with keypair data...');
258
+ const encryptedOutput1 = encryptionService.encryptUtxo(outputs[0]);
259
+ const encryptedOutput2 = encryptionService.encryptUtxo(outputs[1]);
260
+
261
+ logger.debug(`\nOutput[0] (with value):`);
262
+ await outputs[0].log();
263
+ logger.debug(`\nOutput[1] (empty):`);
264
+ await outputs[1].log();
265
+
266
+ logger.debug(`\nEncrypted output 1 size: ${encryptedOutput1.length} bytes`);
267
+ logger.debug(`Encrypted output 2 size: ${encryptedOutput2.length} bytes`);
268
+ logger.debug(`Total encrypted outputs size: ${encryptedOutput1.length + encryptedOutput2.length} bytes`);
269
+
270
+ // Test decryption to verify commitment values match
271
+ logger.debug('\n=== TESTING DECRYPTION ===');
272
+ logger.debug('Decrypting output 1 to verify commitment matches...');
273
+ const decryptedUtxo1 = await encryptionService.decryptUtxo(encryptedOutput1, lightWasm);
274
+ const decryptedCommitment1 = await decryptedUtxo1.getCommitment();
275
+ logger.debug('Original commitment:', outputCommitments[0]);
276
+ logger.debug('Decrypted commitment:', decryptedCommitment1);
277
+ logger.debug('Commitment matches:', outputCommitments[0] === decryptedCommitment1);
278
+
279
+ // Create the deposit ExtData with real encrypted outputs
280
+ const extData = {
281
+ // recipient - just a placeholder, not actually used for deposits.
282
+ recipient: new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM'),
283
+ extAmount: new BN(extAmount),
284
+ encryptedOutput1: encryptedOutput1,
285
+ encryptedOutput2: encryptedOutput2,
286
+ fee: new BN(fee_amount_in_lamports),
287
+ feeRecipient: FEE_RECIPIENT,
288
+ mintAddress: inputs[0].mintAddress
289
+ };
290
+
291
+ // Calculate the extDataHash with the encrypted outputs (now includes mintAddress for security)
292
+ const calculatedExtDataHash = getExtDataHash(extData);
293
+
294
+ // Create the input for the proof generation (must match circuit input order exactly)
295
+ const input = {
296
+ // Common transaction data
297
+ root: root,
298
+ inputNullifier: inputNullifiers, // Use resolved values instead of Promise objects
299
+ outputCommitment: outputCommitments, // Use resolved values instead of Promise objects
300
+ publicAmount: publicAmountForCircuit.toString(), // Use proper field arithmetic result
301
+ extDataHash: calculatedExtDataHash,
302
+
303
+ // Input UTXO data (UTXOs being spent) - ensure all values are in decimal format
304
+ inAmount: inputs.map(x => x.amount.toString(10)),
305
+ inPrivateKey: inputs.map(x => x.keypair.privkey),
306
+ inBlinding: inputs.map(x => x.blinding.toString(10)),
307
+ inPathIndices: inputMerklePathIndices,
308
+ inPathElements: inputMerklePathElements,
309
+
310
+ // Output UTXO data (UTXOs being created) - ensure all values are in decimal format
311
+ outAmount: outputs.map(x => x.amount.toString(10)),
312
+ outBlinding: outputs.map(x => x.blinding.toString(10)),
313
+ outPubkey: outputs.map(x => x.keypair.pubkey),
314
+
315
+ // new mint address
316
+ mintAddress: inputs[0].mintAddress
317
+ };
318
+
319
+ logger.info('generating ZK proof...');
320
+
321
+ // Generate the zero-knowledge proof
322
+ const { proof, publicSignals } = await prove(input, keyBasePath);
323
+ // Parse the proof and public signals into byte arrays
324
+ const proofInBytes = parseProofToBytesArray(proof);
325
+ const inputsInBytes = parseToBytesArray(publicSignals);
326
+
327
+ // Create the proof object to submit to the program
328
+ const proofToSubmit = {
329
+ proofA: proofInBytes.proofA,
330
+ proofB: proofInBytes.proofB.flat(),
331
+ proofC: proofInBytes.proofC,
332
+ root: inputsInBytes[0],
333
+ publicAmount: inputsInBytes[1],
334
+ extDataHash: inputsInBytes[2],
335
+ inputNullifiers: [
336
+ inputsInBytes[3],
337
+ inputsInBytes[4]
338
+ ],
339
+ outputCommitments: [
340
+ inputsInBytes[5],
341
+ inputsInBytes[6]
342
+ ],
343
+ };
344
+
345
+ // Find PDAs for nullifiers and commitments
346
+ const { nullifier0PDA, nullifier1PDA } = findNullifierPDAs(proofToSubmit);
347
+ const { nullifier2PDA, nullifier3PDA } = findCrossCheckNullifierPDAs(proofToSubmit);
348
+
349
+ // Address Lookup Table for transaction size optimization
350
+ logger.debug('Setting up Address Lookup Table...');
351
+
352
+ const lookupTableAccount = await useExistingALT(connection, ALT_ADDRESS);
353
+
354
+ if (!lookupTableAccount?.value) {
355
+ throw new Error(`ALT not found at address ${ALT_ADDRESS.toString()} `);
356
+ }
357
+
358
+ // Serialize the proof and extData
359
+ const serializedProof = serializeProofAndExtData(proofToSubmit, extData);
360
+ logger.debug(`Total instruction data size: ${serializedProof.length} bytes`);
361
+
362
+ // Create the deposit instruction (user signs, not relayer)
363
+ const depositInstruction = new TransactionInstruction({
364
+ keys: [
365
+ { pubkey: treeAccount, isSigner: false, isWritable: true },
366
+ { pubkey: nullifier0PDA, isSigner: false, isWritable: true },
367
+ { pubkey: nullifier1PDA, isSigner: false, isWritable: true },
368
+ { pubkey: nullifier2PDA, isSigner: false, isWritable: false },
369
+ { pubkey: nullifier3PDA, isSigner: false, isWritable: false },
370
+ { pubkey: treeTokenAccount, isSigner: false, isWritable: true },
371
+ { pubkey: globalConfigAccount, isSigner: false, isWritable: false },
372
+ // recipient - just a placeholder, not actually used for deposits. using an ALT address to save bytes
373
+ { pubkey: new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM'), isSigner: false, isWritable: true },
374
+ // fee recipient
375
+ { pubkey: FEE_RECIPIENT, isSigner: false, isWritable: true },
376
+ // signer
377
+ { pubkey: publicKey, isSigner: true, isWritable: true },
378
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
379
+ ],
380
+ programId: PROGRAM_ID,
381
+ data: serializedProof,
382
+ });
383
+
384
+ // Set compute budget for the transaction
385
+ const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
386
+ units: 1_000_000
387
+ });
388
+
389
+ // Create versioned transaction with Address Lookup Table
390
+ const recentBlockhash = await connection.getLatestBlockhash();
391
+
392
+ const messageV0 = new TransactionMessage({
393
+ payerKey: publicKey, // User pays for their own deposit
394
+ recentBlockhash: recentBlockhash.blockhash,
395
+ instructions: [modifyComputeUnits, depositInstruction],
396
+ }).compileToV0Message([lookupTableAccount.value]);
397
+
398
+ let versionedTransaction = new VersionedTransaction(messageV0);
399
+
400
+ // sign tx
401
+ versionedTransaction = await transactionSigner(versionedTransaction)
402
+
403
+ logger.debug('Transaction signed by user');
404
+
405
+ // Serialize the signed transaction for relay
406
+ const serializedTransaction = Buffer.from(versionedTransaction.serialize()).toString('base64');
407
+
408
+ logger.debug('Prepared signed transaction for relay to indexer backend');
409
+
410
+ // Relay the pre-signed transaction to indexer backend
411
+ logger.info('submitting transaction to relayer...')
412
+ const signature = await relayDepositToIndexer(serializedTransaction, publicKey, referrer);
413
+ logger.debug('Transaction signature:', signature);
414
+ logger.debug(`Transaction link: https://explorer.solana.com/tx/${signature}`);
415
+
416
+ logger.info('Waiting for transaction confirmation...')
417
+
418
+ let retryTimes = 0
419
+ let itv = 2
420
+ const encryptedOutputStr = Buffer.from(encryptedOutput1).toString('hex')
421
+ let start = Date.now()
422
+ while (true) {
423
+ logger.debug(`retryTimes: ${retryTimes}`)
424
+ await new Promise(resolve => setTimeout(resolve, itv * 1000));
425
+ logger.debug('Fetching updated tree state...');
426
+ let res = await fetch(RELAYER_API_URL + '/utxos/check/' + encryptedOutputStr)
427
+ let resJson = await res.json()
428
+ if (resJson.exists) {
429
+ logger.debug(`Top up successfully in ${((Date.now() - start) / 1000).toFixed(2)} seconds!`);
430
+ return { tx: signature }
431
+ }
432
+ if (retryTimes >= 10) {
433
+ throw new Error('Refresh the page to see latest balance.')
434
+ }
435
+ retryTimes++
436
+ }
437
+
438
+ }
439
+
440
+
441
+ async function checkDepositLimit(connection: Connection) {
442
+ try {
443
+
444
+ // Derive the tree account PDA
445
+ const [treeAccount] = PublicKey.findProgramAddressSync(
446
+ [Buffer.from('merkle_tree')],
447
+ PROGRAM_ID
448
+ );
449
+
450
+
451
+ // Fetch the account data
452
+ const accountInfo = await connection.getAccountInfo(treeAccount);
453
+
454
+ if (!accountInfo) {
455
+ console.error('❌ Tree account not found. Make sure the program is initialized.' + PROGRAM_ID);
456
+ return;
457
+ }
458
+
459
+ logger.debug(`Account data size: ${accountInfo.data.length} bytes`);
460
+ const authority = new PublicKey(accountInfo.data.slice(8, 40));
461
+ const nextIndex = new BN(accountInfo.data.slice(40, 48), 'le');
462
+ const rootIndex = new BN(accountInfo.data.slice(4112, 4120), 'le');
463
+ const maxDepositAmount = new BN(accountInfo.data.slice(4120, 4128), 'le');
464
+ const bump = accountInfo.data[4128];
465
+
466
+
467
+ // Convert to SOL using BN division to handle large numbers
468
+ const lamportsPerSol = new BN(1_000_000_000);
469
+ const maxDepositSol = maxDepositAmount.div(lamportsPerSol);
470
+ const remainder = maxDepositAmount.mod(lamportsPerSol);
471
+
472
+ // Format the SOL amount with decimals
473
+ let solFormatted = '1';
474
+ if (remainder.eq(new BN(0))) {
475
+ solFormatted = maxDepositSol.toString();
476
+ } else {
477
+ // Handle fractional SOL by converting remainder to decimal
478
+ const fractional = remainder.toNumber() / 1e9;
479
+ solFormatted = `${maxDepositSol.toString()}${fractional.toFixed(9).substring(1)}`;
480
+ }
481
+ return Number(solFormatted)
482
+
483
+ } catch (error) {
484
+ console.log('❌ Error reading deposit limit:', error);
485
+ throw error
486
+ }
487
+ }