@velumdotcash/sdk 2.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/dist/__tests__/paylink.test.d.ts +9 -0
  4. package/dist/__tests__/paylink.test.js +254 -0
  5. package/dist/config.d.ts +9 -0
  6. package/dist/config.js +12 -0
  7. package/dist/deposit.d.ts +22 -0
  8. package/dist/deposit.js +445 -0
  9. package/dist/depositSPL.d.ts +24 -0
  10. package/dist/depositSPL.js +499 -0
  11. package/dist/errors.d.ts +78 -0
  12. package/dist/errors.js +127 -0
  13. package/dist/exportUtils.d.ts +10 -0
  14. package/dist/exportUtils.js +10 -0
  15. package/dist/getUtxos.d.ts +30 -0
  16. package/dist/getUtxos.js +335 -0
  17. package/dist/getUtxosSPL.d.ts +34 -0
  18. package/dist/getUtxosSPL.js +442 -0
  19. package/dist/index.d.ts +183 -0
  20. package/dist/index.js +436 -0
  21. package/dist/models/keypair.d.ts +26 -0
  22. package/dist/models/keypair.js +43 -0
  23. package/dist/models/utxo.d.ts +51 -0
  24. package/dist/models/utxo.js +99 -0
  25. package/dist/test_paylink_logic.test.d.ts +1 -0
  26. package/dist/test_paylink_logic.test.js +114 -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 +27 -0
  30. package/dist/utils/constants.js +56 -0
  31. package/dist/utils/debug-logger.d.ts +250 -0
  32. package/dist/utils/debug-logger.js +688 -0
  33. package/dist/utils/encryption.d.ts +152 -0
  34. package/dist/utils/encryption.js +700 -0
  35. package/dist/utils/logger.d.ts +9 -0
  36. package/dist/utils/logger.js +35 -0
  37. package/dist/utils/merkle_tree.d.ts +92 -0
  38. package/dist/utils/merkle_tree.js +186 -0
  39. package/dist/utils/node-shim.d.ts +14 -0
  40. package/dist/utils/node-shim.js +21 -0
  41. package/dist/utils/prover.d.ts +36 -0
  42. package/dist/utils/prover.js +169 -0
  43. package/dist/utils/utils.d.ts +64 -0
  44. package/dist/utils/utils.js +165 -0
  45. package/dist/withdraw.d.ts +22 -0
  46. package/dist/withdraw.js +290 -0
  47. package/dist/withdrawSPL.d.ts +24 -0
  48. package/dist/withdrawSPL.js +329 -0
  49. package/package.json +59 -0
@@ -0,0 +1,499 @@
1
+ import { PublicKey, TransactionInstruction, SystemProgram, ComputeBudgetProgram, VersionedTransaction, TransactionMessage, } from "@solana/web3.js";
2
+ import BN from "bn.js";
3
+ import { Utxo } from "./models/utxo.js";
4
+ import { fetchMerkleProof, findNullifierPDAs, getProgramAccounts, queryRemoteTreeState, findCrossCheckNullifierPDAs, getExtDataHash, getMintAddressField, } from "./utils/utils.js";
5
+ import { prove, parseProofToBytesArray, parseToBytesArray, } from "./utils/prover.js";
6
+ import { MerkleTree } from "./utils/merkle_tree.js";
7
+ import { serializeProofAndExtData, } from "./utils/encryption.js";
8
+ import { Keypair as UtxoKeypair } from "./models/keypair.js";
9
+ import { getUtxosSPL } from "./getUtxosSPL.js";
10
+ import { FIELD_SIZE, FEE_RECIPIENT, MERKLE_TREE_DEPTH, RELAYER_API_URL, PROGRAM_ID, ALT_ADDRESS, tokens, } from "./utils/constants.js";
11
+ import { useExistingALT, } from "./utils/address_lookup_table.js";
12
+ import { logger } from "./utils/logger.js";
13
+ import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, getAccount, } from "@solana/spl-token";
14
+ // Function to relay pre-signed deposit transaction to indexer backend
15
+ async function relayDepositToIndexer({ signedTransaction, publicKey, referrer, mintAddress, }) {
16
+ try {
17
+ logger.debug("Relaying pre-signed deposit transaction to indexer backend...");
18
+ const params = {
19
+ signedTransaction,
20
+ senderAddress: publicKey.toString(),
21
+ };
22
+ if (referrer) {
23
+ params.referralWalletAddress = referrer;
24
+ }
25
+ params.mintAddress = mintAddress;
26
+ const response = await fetch(`${RELAYER_API_URL}/deposit/spl`, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ },
31
+ body: JSON.stringify(params),
32
+ });
33
+ if (!response.ok) {
34
+ logger.debug("res text:", await response.text());
35
+ throw new Error("response not ok");
36
+ // const errorData = await response.json() as { error?: string };
37
+ // throw new Error(`Deposit relay failed: ${response.status} ${response.statusText} - ${errorData.error || 'Unknown error'}`);
38
+ }
39
+ let result;
40
+ try {
41
+ result = await response.json();
42
+ logger.debug("Pre-signed deposit transaction relayed successfully!");
43
+ logger.debug("Response:", result);
44
+ }
45
+ catch (e) {
46
+ console.log("response.text", await response.text());
47
+ throw new Error("failed to parse json");
48
+ }
49
+ return result.signature;
50
+ }
51
+ catch (error) {
52
+ console.error("Failed to relay deposit transaction to indexer:", error.message);
53
+ throw error;
54
+ }
55
+ }
56
+ export async function depositSPL({ lightWasm, storage, keyBasePath, publicKey, connection, base_units, amount, encryptionService, transactionSigner, referrer, mintAddress, signer, recipientUtxoPublicKey, recipientEncryptionKey, }) {
57
+ if (typeof mintAddress == "string") {
58
+ mintAddress = new PublicKey(mintAddress);
59
+ }
60
+ let token = tokens.find((t) => t.pubkey.toString() == mintAddress.toString());
61
+ if (!token) {
62
+ throw new Error("token not found: " + mintAddress.toString());
63
+ }
64
+ if (amount) {
65
+ base_units = amount * token.units_per_token;
66
+ }
67
+ if (!base_units) {
68
+ throw new Error('You must input at least one of "base_units" or "amount"');
69
+ }
70
+ if (!signer) {
71
+ signer = publicKey;
72
+ }
73
+ // let mintInfo = await getMint(connection, token.pubkey)
74
+ // let units_per_token = 10 ** mintInfo.decimals
75
+ let recipient = new PublicKey("AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM");
76
+ let recipient_ata = getAssociatedTokenAddressSync(token.pubkey, recipient, true);
77
+ let feeRecipientTokenAccount = getAssociatedTokenAddressSync(token.pubkey, FEE_RECIPIENT, true);
78
+ let signerTokenAccount = getAssociatedTokenAddressSync(token.pubkey, signer);
79
+ // Derive tree account PDA with mint address for SPL
80
+ const [treeAccount] = PublicKey.findProgramAddressSync([Buffer.from("merkle_tree"), token.pubkey.toBuffer()], PROGRAM_ID);
81
+ let limitAmount = await checkDepositLimit(connection, treeAccount, token);
82
+ if (limitAmount && base_units > limitAmount * token.units_per_token) {
83
+ throw new Error(`Don't deposit more than ${limitAmount} ${token.name.toUpperCase()}`);
84
+ }
85
+ // const base_units = amount_in_sol * units_per_token
86
+ const fee_base_units = 0;
87
+ logger.debug("Encryption key generated from user keypair");
88
+ logger.debug(`User wallet: ${signer.toString()}`);
89
+ logger.debug(`Deposit amount: ${base_units} base_units (${base_units / token.units_per_token} ${token.name.toUpperCase()})`);
90
+ logger.debug(`Calculated fee: ${fee_base_units} base_units (${fee_base_units / token.units_per_token} ${token.name.toUpperCase()})`);
91
+ // Check SPL balance
92
+ const accountInfo = await getAccount(connection, signerTokenAccount);
93
+ let balance = Number(accountInfo.amount);
94
+ logger.debug(`wallet balance: ${balance / token.units_per_token} ${token.name.toUpperCase()}`);
95
+ logger.debug("balance", balance);
96
+ logger.debug("base_units + fee_base_units", base_units + fee_base_units);
97
+ if (balance < base_units + fee_base_units) {
98
+ throw new Error(`Insufficient balance. Need at least ${(base_units + fee_base_units) / token.units_per_token} ${token.name.toUpperCase()}.`);
99
+ }
100
+ // Check SOL balance for account rent + transaction fees
101
+ // The program creates a rent-exempt account (~953,520 lamports) during deposit
102
+ const solBalance = await connection.getBalance(signer);
103
+ logger.debug(`SOL Wallet balance: ${solBalance / 1e9} SOL`);
104
+ if (solBalance < 1_100_000) {
105
+ throw new Error(`Need at least 0.0011 SOL for account rent and transaction fees. You have ${(solBalance / 1e9).toFixed(6)} SOL.`);
106
+ }
107
+ const { globalConfigAccount } = getProgramAccounts();
108
+ // Create the merkle tree with the pre-initialized poseidon hash
109
+ const tree = new MerkleTree(MERKLE_TREE_DEPTH, lightWasm);
110
+ // Initialize root and nextIndex variables
111
+ const { root, nextIndex: currentNextIndex } = await queryRemoteTreeState(token.name);
112
+ logger.debug(`Using tree root: ${root}`);
113
+ logger.debug(`New UTXOs will be inserted at indices: ${currentNextIndex} and ${currentNextIndex + 1}`);
114
+ // Generate a deterministic private key derived from the wallet keypair
115
+ // const utxoPrivateKey = encryptionService.deriveUtxoPrivateKey();
116
+ const utxoPrivateKey = encryptionService.getUtxoPrivateKeyV2();
117
+ // Create a UTXO keypair that will be used for all inputs and outputs
118
+ const utxoKeypair = new UtxoKeypair(utxoPrivateKey, lightWasm);
119
+ logger.debug("Using wallet-derived UTXO keypair for deposit");
120
+ // Fetch existing UTXOs for this user
121
+ logger.debug("\nFetching existing UTXOs...");
122
+ let mintUtxos = [];
123
+ if (!recipientUtxoPublicKey) {
124
+ mintUtxos = await getUtxosSPL({
125
+ connection,
126
+ publicKey,
127
+ encryptionService,
128
+ storage,
129
+ mintAddress,
130
+ });
131
+ }
132
+ else {
133
+ logger.debug("\nThird-party deposit detected. Skipping UTXO fetch (Fresh Deposit forced).");
134
+ }
135
+ // Calculate output amounts and external amount based on scenario
136
+ let extAmount;
137
+ let outputAmount;
138
+ // Create inputs based on whether we have existing UTXOs
139
+ let inputs;
140
+ let inputMerklePathIndices;
141
+ let inputMerklePathElements;
142
+ if (mintUtxos.length === 0) {
143
+ // Scenario 1: Fresh deposit with dummy inputs - add new funds to the system
144
+ extAmount = base_units;
145
+ outputAmount = new BN(base_units).sub(new BN(fee_base_units)).toString();
146
+ logger.debug(`Fresh deposit scenario (no existing UTXOs):`);
147
+ logger.debug(`External amount (deposit): ${extAmount}`);
148
+ logger.debug(`Fee amount: ${fee_base_units}`);
149
+ logger.debug(`Output amount: ${outputAmount}`);
150
+ // Use two dummy UTXOs as inputs
151
+ inputs = [
152
+ new Utxo({
153
+ lightWasm,
154
+ keypair: utxoKeypair,
155
+ mintAddress: token.pubkey.toString(),
156
+ }),
157
+ new Utxo({
158
+ lightWasm,
159
+ keypair: utxoKeypair,
160
+ mintAddress: token.pubkey.toString(),
161
+ }),
162
+ ];
163
+ // Both inputs are dummy, so use mock indices and zero-filled Merkle paths
164
+ inputMerklePathIndices = inputs.map((input) => input.index || 0);
165
+ inputMerklePathElements = inputs.map(() => {
166
+ return [...new Array(tree.levels).fill("0")];
167
+ });
168
+ }
169
+ else {
170
+ // Scenario 2: Deposit that consolidates with existing UTXO(s)
171
+ const firstUtxo = mintUtxos[0];
172
+ const firstUtxoAmount = firstUtxo.amount;
173
+ const secondUtxoAmount = mintUtxos.length > 1 ? mintUtxos[1].amount : new BN(0);
174
+ extAmount = base_units; // Still depositing new funds
175
+ // Output combines existing UTXO amounts + new deposit amount - fee
176
+ outputAmount = firstUtxoAmount
177
+ .add(secondUtxoAmount)
178
+ .add(new BN(base_units))
179
+ .sub(new BN(fee_base_units))
180
+ .toString();
181
+ logger.debug(`Deposit with consolidation scenario:`);
182
+ logger.debug(`First existing UTXO amount: ${firstUtxoAmount.toString()}`);
183
+ if (secondUtxoAmount.gt(new BN(0))) {
184
+ logger.debug(`Second existing UTXO amount: ${secondUtxoAmount.toString()}`);
185
+ }
186
+ logger.debug(`New deposit amount: ${base_units}`);
187
+ logger.debug(`Fee amount: ${fee_base_units}`);
188
+ logger.debug(`Output amount (existing UTXOs + deposit - fee): ${outputAmount}`);
189
+ logger.debug(`External amount (deposit): ${extAmount}`);
190
+ logger.debug("\nFirst UTXO to be consolidated:");
191
+ // Use first existing UTXO as first input, and either second UTXO or dummy UTXO as second input
192
+ const secondUtxo = mintUtxos.length > 1
193
+ ? mintUtxos[1]
194
+ : new Utxo({
195
+ lightWasm,
196
+ keypair: utxoKeypair,
197
+ amount: "0", // This UTXO will be inserted at currentNextIndex
198
+ mintAddress: token.pubkey.toString(),
199
+ });
200
+ inputs = [
201
+ firstUtxo, // Use the first existing UTXO
202
+ secondUtxo, // Use second UTXO if available, otherwise dummy
203
+ ];
204
+ // Fetch Merkle proofs for real UTXOs
205
+ const firstUtxoCommitment = await firstUtxo.getCommitment();
206
+ const firstUtxoMerkleProof = await fetchMerkleProof(firstUtxoCommitment, token.name);
207
+ let secondUtxoMerkleProof;
208
+ if (secondUtxo.amount.gt(new BN(0))) {
209
+ // Second UTXO is real, fetch its proof
210
+ const secondUtxoCommitment = await secondUtxo.getCommitment();
211
+ secondUtxoMerkleProof = await fetchMerkleProof(secondUtxoCommitment, token.name);
212
+ logger.debug("\nSecond UTXO to be consolidated:");
213
+ await secondUtxo.log();
214
+ }
215
+ // Use the real pathIndices from API for real inputs, mock index for dummy input
216
+ inputMerklePathIndices = [
217
+ firstUtxo.index || 0, // Use the real UTXO's index
218
+ secondUtxo.amount.gt(new BN(0)) ? secondUtxo.index || 0 : 0, // Real UTXO index or dummy
219
+ ];
220
+ // Create Merkle path elements: real proof for real inputs, zeros for dummy input
221
+ inputMerklePathElements = [
222
+ firstUtxoMerkleProof.pathElements, // Real Merkle proof for first existing UTXO
223
+ secondUtxo.amount.gt(new BN(0))
224
+ ? secondUtxoMerkleProof.pathElements
225
+ : [...new Array(tree.levels).fill("0")], // Real proof or zero-filled for dummy
226
+ ];
227
+ logger.debug(`Using first UTXO with amount: ${firstUtxo.amount.toString()} and index: ${firstUtxo.index}`);
228
+ 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}` : ""}`);
229
+ logger.debug(`First UTXO Merkle proof path indices from API: [${firstUtxoMerkleProof.pathIndices.join(", ")}]`);
230
+ if (secondUtxo.amount.gt(new BN(0))) {
231
+ logger.debug(`Second UTXO Merkle proof path indices from API: [${secondUtxoMerkleProof.pathIndices.join(", ")}]`);
232
+ }
233
+ }
234
+ const publicAmountForCircuit = new BN(extAmount)
235
+ .sub(new BN(fee_base_units))
236
+ .add(FIELD_SIZE)
237
+ .mod(FIELD_SIZE);
238
+ logger.debug(`Public amount calculation: (${extAmount} - ${fee_base_units} + FIELD_SIZE) % FIELD_SIZE = ${publicAmountForCircuit.toString()}`);
239
+ // Create outputs for the transaction with the same shared keypair
240
+ let output1Config = {
241
+ lightWasm,
242
+ amount: outputAmount,
243
+ index: currentNextIndex, // This UTXO will be inserted at currentNextIndex
244
+ mintAddress: token.pubkey.toString(),
245
+ };
246
+ if (recipientUtxoPublicKey) {
247
+ // Third-party deposit: Use Recipient's Public Key
248
+ output1Config.publicKey = recipientUtxoPublicKey;
249
+ }
250
+ else {
251
+ // Self-deposit: Use Sender's Keypair
252
+ output1Config.keypair = utxoKeypair;
253
+ }
254
+ const outputs = [
255
+ new Utxo(output1Config), // Output with value (either deposit amount minus fee, or input amount minus fee)
256
+ new Utxo({
257
+ lightWasm,
258
+ amount: "0",
259
+ keypair: utxoKeypair,
260
+ index: currentNextIndex + 1, // This UTXO will be inserted at currentNextIndex
261
+ mintAddress: token.pubkey.toString(),
262
+ }), // Empty UTXO
263
+ ];
264
+ // Verify this matches the circuit balance equation: sumIns + publicAmount = sumOuts
265
+ const sumIns = inputs.reduce((sum, input) => sum.add(input.amount), new BN(0));
266
+ const sumOuts = outputs.reduce((sum, output) => sum.add(output.amount), new BN(0));
267
+ logger.debug(`Circuit balance check: sumIns(${sumIns.toString()}) + publicAmount(${publicAmountForCircuit.toString()}) should equal sumOuts(${sumOuts.toString()})`);
268
+ // Convert to circuit-compatible format
269
+ const publicAmountCircuitResult = sumIns
270
+ .add(publicAmountForCircuit)
271
+ .mod(FIELD_SIZE);
272
+ logger.debug(`Balance verification: ${sumIns.toString()} + ${publicAmountForCircuit.toString()} (mod FIELD_SIZE) = ${publicAmountCircuitResult.toString()}`);
273
+ logger.debug(`Expected sum of outputs: ${sumOuts.toString()}`);
274
+ logger.debug(`Balance equation satisfied: ${publicAmountCircuitResult.eq(sumOuts)}`);
275
+ // Generate nullifiers and commitments
276
+ const inputNullifiers = await Promise.all(inputs.map((x) => x.getNullifier()));
277
+ const outputCommitments = await Promise.all(outputs.map((x) => x.getCommitment()));
278
+ // Save original commitment and nullifier values for verification
279
+ logger.debug("\n=== UTXO VALIDATION ===");
280
+ logger.debug("Output 0 Commitment:", outputCommitments[0]);
281
+ logger.debug("Output 1 Commitment:", outputCommitments[1]);
282
+ // Encrypt the UTXO data using a compact format that includes the keypair
283
+ logger.debug("\nEncrypting UTXOs with keypair data...");
284
+ let encryptedOutput1;
285
+ if (recipientEncryptionKey) {
286
+ logger.debug("Encrypting Output 1 for Recipient (Asymmetric)...");
287
+ encryptedOutput1 = encryptionService.encryptUtxo(outputs[0], recipientEncryptionKey);
288
+ }
289
+ else {
290
+ encryptedOutput1 = encryptionService.encryptUtxo(outputs[0]);
291
+ }
292
+ const encryptedOutput2 = encryptionService.encryptUtxo(outputs[1]);
293
+ // logger.debug(`\nOutput[0] (with value):`);
294
+ // await outputs[0].log();
295
+ // logger.debug(`\nOutput[1] (empty):`);
296
+ // await outputs[1].log();
297
+ logger.debug(`\nEncrypted output 1 size: ${encryptedOutput1.length} bytes`);
298
+ logger.debug(`Encrypted output 2 size: ${encryptedOutput2.length} bytes`);
299
+ logger.debug(`Total encrypted outputs size: ${encryptedOutput1.length + encryptedOutput2.length} bytes`);
300
+ // Test decryption to verify commitment values match
301
+ if (!recipientEncryptionKey) {
302
+ logger.debug("\n=== TESTING DECRYPTION ===");
303
+ logger.debug("Decrypting output 1 to verify commitment matches...");
304
+ const decryptedUtxo1 = await encryptionService.decryptUtxo(encryptedOutput1, lightWasm);
305
+ if (decryptedUtxo1) {
306
+ const decryptedCommitment1 = await decryptedUtxo1.getCommitment();
307
+ logger.debug("Original commitment:", outputCommitments[0]);
308
+ logger.debug("Decrypted commitment:", decryptedCommitment1);
309
+ logger.debug("Commitment matches:", outputCommitments[0] === decryptedCommitment1);
310
+ }
311
+ }
312
+ // Create the deposit ExtData with real encrypted outputs
313
+ const extData = {
314
+ // recipient - just a placeholder, not actually used for deposits.
315
+ recipient: recipient_ata,
316
+ extAmount: new BN(extAmount),
317
+ encryptedOutput1: encryptedOutput1,
318
+ encryptedOutput2: encryptedOutput2,
319
+ fee: new BN(fee_base_units),
320
+ feeRecipient: feeRecipientTokenAccount,
321
+ mintAddress: token.pubkey.toString(),
322
+ };
323
+ // Calculate the extDataHash with the encrypted outputs (now includes mintAddress for security)
324
+ const calculatedExtDataHash = getExtDataHash(extData);
325
+ // Create the input for the proof generation (must match circuit input order exactly)
326
+ const input = {
327
+ // Common transaction data
328
+ root: root,
329
+ mintAddress: getMintAddressField(token.pubkey), // new mint address
330
+ publicAmount: publicAmountForCircuit.toString(), // Use proper field arithmetic result
331
+ extDataHash: calculatedExtDataHash,
332
+ // Input UTXO data (UTXOs being spent) - ensure all values are in decimal format
333
+ inAmount: inputs.map((x) => x.amount.toString(10)),
334
+ inPrivateKey: inputs.map((x) => x.keypair.privkey),
335
+ inBlinding: inputs.map((x) => x.blinding.toString(10)),
336
+ inPathIndices: inputMerklePathIndices,
337
+ inPathElements: inputMerklePathElements,
338
+ inputNullifier: inputNullifiers, // Use resolved values instead of Promise objects
339
+ // Output UTXO data (UTXOs being created) - ensure all values are in decimal format
340
+ outAmount: outputs.map((x) => x.amount.toString(10)),
341
+ outBlinding: outputs.map((x) => x.blinding.toString(10)),
342
+ outPubkey: outputs.map((x) => x.pubkey),
343
+ outputCommitment: outputCommitments,
344
+ };
345
+ logger.info("generating ZK proof...");
346
+ // Generate the zero-knowledge proof
347
+ const { proof, publicSignals } = await prove(input, keyBasePath);
348
+ // Parse the proof and public signals into byte arrays
349
+ const proofInBytes = parseProofToBytesArray(proof);
350
+ const inputsInBytes = parseToBytesArray(publicSignals);
351
+ // Create the proof object to submit to the program
352
+ const proofToSubmit = {
353
+ proofA: proofInBytes.proofA,
354
+ proofB: proofInBytes.proofB.flat(),
355
+ proofC: proofInBytes.proofC,
356
+ root: inputsInBytes[0],
357
+ publicAmount: inputsInBytes[1],
358
+ extDataHash: inputsInBytes[2],
359
+ inputNullifiers: [inputsInBytes[3], inputsInBytes[4]],
360
+ outputCommitments: [inputsInBytes[5], inputsInBytes[6]],
361
+ };
362
+ // Find PDAs for nullifiers and commitments
363
+ const { nullifier0PDA, nullifier1PDA } = findNullifierPDAs(proofToSubmit);
364
+ const { nullifier2PDA, nullifier3PDA } = findCrossCheckNullifierPDAs(proofToSubmit);
365
+ const [globalConfigPda, globalConfigPdaBump] = await PublicKey.findProgramAddressSync([Buffer.from("global_config")], PROGRAM_ID);
366
+ const treeAta = getAssociatedTokenAddressSync(token.pubkey, globalConfigPda, true);
367
+ const lookupTableAccount = await useExistingALT(connection, ALT_ADDRESS);
368
+ if (!lookupTableAccount?.value) {
369
+ throw new Error(`ALT not found at address ${ALT_ADDRESS.toString()} `);
370
+ }
371
+ // Serialize the proof and extData with SPL discriminator
372
+ const serializedProof = serializeProofAndExtData(proofToSubmit, extData, true);
373
+ logger.debug(`Total instruction data size: ${serializedProof.length} bytes`);
374
+ // Create the deposit instruction (user signs, not relayer)
375
+ const depositInstruction = new TransactionInstruction({
376
+ keys: [
377
+ { pubkey: treeAccount, isSigner: false, isWritable: true },
378
+ { pubkey: nullifier0PDA, isSigner: false, isWritable: true },
379
+ { pubkey: nullifier1PDA, isSigner: false, isWritable: true },
380
+ { pubkey: nullifier2PDA, isSigner: false, isWritable: false },
381
+ { pubkey: nullifier3PDA, isSigner: false, isWritable: false },
382
+ { pubkey: globalConfigAccount, isSigner: false, isWritable: false },
383
+ // signer
384
+ { pubkey: signer, isSigner: true, isWritable: true },
385
+ // SPL token mint
386
+ { pubkey: token.pubkey, isSigner: false, isWritable: false },
387
+ // signer's token account
388
+ { pubkey: signerTokenAccount, isSigner: false, isWritable: true },
389
+ // recipient (placeholder)
390
+ { pubkey: recipient, isSigner: false, isWritable: true },
391
+ // recipient's token account (placeholder)
392
+ { pubkey: recipient_ata, isSigner: false, isWritable: true },
393
+ // tree ATA
394
+ { pubkey: treeAta, isSigner: false, isWritable: true },
395
+ // fee recipient token account
396
+ { pubkey: feeRecipientTokenAccount, isSigner: false, isWritable: true },
397
+ // token program id
398
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
399
+ // ATA program
400
+ {
401
+ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
402
+ isSigner: false,
403
+ isWritable: false,
404
+ },
405
+ // system protgram
406
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
407
+ ],
408
+ programId: PROGRAM_ID,
409
+ data: serializedProof,
410
+ });
411
+ // Set compute budget for the transaction
412
+ const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
413
+ units: 1_000_000,
414
+ });
415
+ // Create versioned transaction with Address Lookup Table
416
+ const recentBlockhash = await connection.getLatestBlockhash();
417
+ const messageV0 = new TransactionMessage({
418
+ payerKey: signer, // User pays for their own deposit
419
+ recentBlockhash: recentBlockhash.blockhash,
420
+ instructions: [modifyComputeUnits, depositInstruction],
421
+ }).compileToV0Message([lookupTableAccount.value]);
422
+ let versionedTransaction = new VersionedTransaction(messageV0);
423
+ // sign tx
424
+ versionedTransaction = await transactionSigner(versionedTransaction);
425
+ logger.debug("Transaction signed by user");
426
+ // Serialize the signed transaction for relay
427
+ const serializedTransaction = Buffer.from(versionedTransaction.serialize()).toString("base64");
428
+ logger.debug("Prepared signed transaction for relay to indexer backend");
429
+ // Relay the pre-signed transaction to indexer backend
430
+ logger.info("submitting transaction to relayer...");
431
+ const signature = await relayDepositToIndexer({
432
+ mintAddress: token.pubkey.toString(),
433
+ publicKey: signer,
434
+ signedTransaction: serializedTransaction,
435
+ referrer,
436
+ });
437
+ logger.debug("Transaction signature:", signature);
438
+ logger.debug(`Transaction link: https://orbmarkets.io/tx/${signature}`);
439
+ logger.info("Waiting for transaction confirmation...");
440
+ let retryTimes = 0;
441
+ let itv = 2;
442
+ const encryptedOutputStr = Buffer.from(encryptedOutput1).toString("hex");
443
+ let start = Date.now();
444
+ while (true) {
445
+ logger.info("Confirming transaction..");
446
+ logger.debug(`retryTimes: ${retryTimes}`);
447
+ await new Promise((resolve) => setTimeout(resolve, itv * 1000));
448
+ logger.debug("Fetching updated tree state...");
449
+ let url = RELAYER_API_URL +
450
+ "/utxos/check/" +
451
+ encryptedOutputStr +
452
+ "?token=" +
453
+ token.name;
454
+ let res = await fetch(url);
455
+ let resJson = await res.json();
456
+ if (resJson.exists) {
457
+ logger.debug(`Top up successfully in ${((Date.now() - start) / 1000).toFixed(2)} seconds!`);
458
+ return { tx: signature };
459
+ }
460
+ if (retryTimes >= 10) {
461
+ throw new Error("Refresh the page to see latest balance.");
462
+ }
463
+ retryTimes++;
464
+ }
465
+ }
466
+ async function checkDepositLimit(connection, treeAccount, token) {
467
+ try {
468
+ // Fetch the account data
469
+ const accountInfo = await connection.getAccountInfo(treeAccount);
470
+ if (!accountInfo) {
471
+ console.error("❌ Tree account not found. Make sure the program is initialized.");
472
+ return;
473
+ }
474
+ const authority = new PublicKey(accountInfo.data.slice(8, 40));
475
+ const nextIndex = new BN(accountInfo.data.slice(40, 48), "le");
476
+ const rootIndex = new BN(accountInfo.data.slice(4112, 4120), "le");
477
+ const maxDepositAmount = new BN(accountInfo.data.slice(4120, 4128), "le");
478
+ const bump = accountInfo.data[4128];
479
+ // Convert to SPL using BN division to handle large numbers
480
+ const unitesPerToken = new BN(token.units_per_token);
481
+ const maxDepositSpl = maxDepositAmount.div(unitesPerToken);
482
+ const remainder = maxDepositAmount.mod(unitesPerToken);
483
+ // Format the SPL amount with decimals
484
+ let amountFormatted = "1";
485
+ if (remainder.eq(new BN(0))) {
486
+ amountFormatted = maxDepositSpl.toString();
487
+ }
488
+ else {
489
+ // Handle fractional SPL by converting remainder to decimal
490
+ const fractional = remainder.toNumber() / token.units_per_token;
491
+ amountFormatted = `${maxDepositSpl.toString()}${fractional.toFixed(Math.log10(token.units_per_token)).substring(1)}`;
492
+ }
493
+ return Number(amountFormatted);
494
+ }
495
+ catch (error) {
496
+ console.log("❌ Error reading deposit limit:", error);
497
+ throw error;
498
+ }
499
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Custom error classes for Privacy Cash SDK
3
+ *
4
+ * These provide typed errors with:
5
+ * - Error codes for logging/analytics
6
+ * - Recoverable flag to indicate if retry is possible
7
+ * - Cause preservation for debugging
8
+ */
9
+ /**
10
+ * Base error class for all Privacy Cash errors
11
+ */
12
+ export declare class PrivacyCashError extends Error {
13
+ readonly code: string;
14
+ readonly recoverable: boolean;
15
+ readonly cause?: Error | undefined;
16
+ constructor(message: string, code: string, recoverable?: boolean, cause?: Error | undefined);
17
+ }
18
+ /**
19
+ * ZK proof generation errors
20
+ * These are generally not recoverable without fixing inputs
21
+ */
22
+ export declare class ZKProofError extends PrivacyCashError {
23
+ constructor(message: string, code?: string, cause?: Error);
24
+ }
25
+ /**
26
+ * Network/RPC related errors
27
+ * These are often recoverable with retry
28
+ */
29
+ export declare class NetworkError extends PrivacyCashError {
30
+ constructor(message: string, code?: string, cause?: Error);
31
+ }
32
+ /**
33
+ * Insufficient balance for operation
34
+ */
35
+ export declare class InsufficientBalanceError extends PrivacyCashError {
36
+ readonly required: number;
37
+ readonly available: number;
38
+ readonly token: string;
39
+ constructor(required: number, available: number, token?: string);
40
+ }
41
+ /**
42
+ * Deposit limit exceeded
43
+ */
44
+ export declare class DepositLimitError extends PrivacyCashError {
45
+ readonly limit: number;
46
+ readonly attempted: number;
47
+ constructor(limit: number, attempted: number);
48
+ }
49
+ /**
50
+ * Transaction confirmation timeout
51
+ */
52
+ export declare class TransactionTimeoutError extends PrivacyCashError {
53
+ readonly signature?: string | undefined;
54
+ constructor(message: string, signature?: string | undefined);
55
+ }
56
+ /**
57
+ * UTXO related errors
58
+ */
59
+ export declare class UTXOError extends PrivacyCashError {
60
+ constructor(message: string, code?: string, cause?: Error);
61
+ }
62
+ /**
63
+ * Encryption/decryption errors
64
+ */
65
+ export declare class EncryptionError extends PrivacyCashError {
66
+ constructor(message: string, code?: string, cause?: Error);
67
+ }
68
+ /**
69
+ * Relayer API errors
70
+ */
71
+ export declare class RelayerError extends PrivacyCashError {
72
+ readonly statusCode?: number | undefined;
73
+ constructor(message: string, statusCode?: number | undefined, cause?: Error);
74
+ }
75
+ /**
76
+ * Helper to wrap unknown errors
77
+ */
78
+ export declare function wrapError(error: unknown, fallbackMessage: string): PrivacyCashError;