@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
package/dist/errors.js ADDED
@@ -0,0 +1,127 @@
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 class PrivacyCashError extends Error {
13
+ code;
14
+ recoverable;
15
+ cause;
16
+ constructor(message, code, recoverable = false, cause) {
17
+ super(message);
18
+ this.code = code;
19
+ this.recoverable = recoverable;
20
+ this.cause = cause;
21
+ this.name = "PrivacyCashError";
22
+ // Maintains proper stack trace in V8 environments
23
+ if (Error.captureStackTrace) {
24
+ Error.captureStackTrace(this, this.constructor);
25
+ }
26
+ }
27
+ }
28
+ /**
29
+ * ZK proof generation errors
30
+ * These are generally not recoverable without fixing inputs
31
+ */
32
+ export class ZKProofError extends PrivacyCashError {
33
+ constructor(message, code = "ZK_GENERIC", cause) {
34
+ super(message, code, false, cause);
35
+ this.name = "ZKProofError";
36
+ }
37
+ }
38
+ /**
39
+ * Network/RPC related errors
40
+ * These are often recoverable with retry
41
+ */
42
+ export class NetworkError extends PrivacyCashError {
43
+ constructor(message, code = "NETWORK_GENERIC", cause) {
44
+ super(message, code, true, cause);
45
+ this.name = "NetworkError";
46
+ }
47
+ }
48
+ /**
49
+ * Insufficient balance for operation
50
+ */
51
+ export class InsufficientBalanceError extends PrivacyCashError {
52
+ required;
53
+ available;
54
+ token;
55
+ constructor(required, available, token = "SOL") {
56
+ super(`Insufficient ${token} balance: need ${required} lamports, have ${available}`, "INSUFFICIENT_BALANCE", false);
57
+ this.required = required;
58
+ this.available = available;
59
+ this.token = token;
60
+ this.name = "InsufficientBalanceError";
61
+ }
62
+ }
63
+ /**
64
+ * Deposit limit exceeded
65
+ */
66
+ export class DepositLimitError extends PrivacyCashError {
67
+ limit;
68
+ attempted;
69
+ constructor(limit, attempted) {
70
+ super(`Deposit limit exceeded: max ${limit} lamports, attempted ${attempted}`, "DEPOSIT_LIMIT_EXCEEDED", false);
71
+ this.limit = limit;
72
+ this.attempted = attempted;
73
+ this.name = "DepositLimitError";
74
+ }
75
+ }
76
+ /**
77
+ * Transaction confirmation timeout
78
+ */
79
+ export class TransactionTimeoutError extends PrivacyCashError {
80
+ signature;
81
+ constructor(message, signature) {
82
+ super(message, "TRANSACTION_TIMEOUT", true);
83
+ this.signature = signature;
84
+ this.name = "TransactionTimeoutError";
85
+ }
86
+ }
87
+ /**
88
+ * UTXO related errors
89
+ */
90
+ export class UTXOError extends PrivacyCashError {
91
+ constructor(message, code = "UTXO_GENERIC", cause) {
92
+ super(message, code, false, cause);
93
+ this.name = "UTXOError";
94
+ }
95
+ }
96
+ /**
97
+ * Encryption/decryption errors
98
+ */
99
+ export class EncryptionError extends PrivacyCashError {
100
+ constructor(message, code = "ENCRYPTION_GENERIC", cause) {
101
+ super(message, code, false, cause);
102
+ this.name = "EncryptionError";
103
+ }
104
+ }
105
+ /**
106
+ * Relayer API errors
107
+ */
108
+ export class RelayerError extends PrivacyCashError {
109
+ statusCode;
110
+ constructor(message, statusCode, cause) {
111
+ super(message, "RELAYER_ERROR", true, cause);
112
+ this.statusCode = statusCode;
113
+ this.name = "RelayerError";
114
+ }
115
+ }
116
+ /**
117
+ * Helper to wrap unknown errors
118
+ */
119
+ export function wrapError(error, fallbackMessage) {
120
+ if (error instanceof PrivacyCashError) {
121
+ return error;
122
+ }
123
+ if (error instanceof Error) {
124
+ return new PrivacyCashError(error.message || fallbackMessage, "UNKNOWN_ERROR", false, error);
125
+ }
126
+ return new PrivacyCashError(typeof error === "string" ? error : fallbackMessage, "UNKNOWN_ERROR", false);
127
+ }
@@ -0,0 +1,10 @@
1
+ export { getConfig } from './config.js';
2
+ export { deposit } from './deposit.js';
3
+ export { withdraw } from './withdraw.js';
4
+ export { EncryptionService } from './utils/encryption.js';
5
+ export { setLogger } from './utils/logger.js';
6
+ export { getBalanceFromUtxos, getUtxos, localstorageKey } from './getUtxos.js';
7
+ export { depositSPL } from './depositSPL.js';
8
+ export { withdrawSPL } from './withdrawSPL.js';
9
+ export { getBalanceFromUtxosSPL, getUtxosSPL } from './getUtxosSPL.js';
10
+ export { type TokenList, type SplList, tokens } from './utils/constants.js';
@@ -0,0 +1,10 @@
1
+ export { getConfig } from './config.js';
2
+ export { deposit } from './deposit.js';
3
+ export { withdraw } from './withdraw.js';
4
+ export { EncryptionService } from './utils/encryption.js';
5
+ export { setLogger } from './utils/logger.js';
6
+ export { getBalanceFromUtxos, getUtxos, localstorageKey } from './getUtxos.js';
7
+ export { depositSPL } from './depositSPL.js';
8
+ export { withdrawSPL } from './withdrawSPL.js';
9
+ export { getBalanceFromUtxosSPL, getUtxosSPL } from './getUtxosSPL.js';
10
+ export { tokens } from './utils/constants.js';
@@ -0,0 +1,30 @@
1
+ import { Connection, PublicKey } from "@solana/web3.js";
2
+ import { Utxo } from "./models/utxo.js";
3
+ import { EncryptionService } from "./utils/encryption.js";
4
+ export declare function localstorageKey(key: PublicKey): string;
5
+ /**
6
+ * Fetch and decrypt all UTXOs for a user
7
+ * @param signed The user's signature
8
+ * @param connection Solana connection to fetch on-chain commitment accounts
9
+ * @param setStatus A global state updator. Set live status message showing on webpage
10
+ * @returns Array of decrypted UTXOs that belong to the user
11
+ */
12
+ export declare function getUtxos({ publicKey, connection, encryptionService, storage, abortSignal, offset, }: {
13
+ publicKey: PublicKey;
14
+ connection: Connection;
15
+ encryptionService: EncryptionService;
16
+ storage: Storage;
17
+ abortSignal?: AbortSignal;
18
+ offset?: number;
19
+ }): Promise<Utxo[]>;
20
+ /**
21
+ * Check if a UTXO has been spent
22
+ * @param connection Solana connection
23
+ * @param utxo The UTXO to check
24
+ * @param retries Number of retries remaining (default 3)
25
+ * @returns Promise<boolean> true if spent, false if unspent
26
+ */
27
+ export declare function isUtxoSpent(connection: Connection, utxo: Utxo, retries?: number): Promise<boolean>;
28
+ export declare function getBalanceFromUtxos(utxos: Utxo[]): {
29
+ lamports: number;
30
+ };
@@ -0,0 +1,335 @@
1
+ import { PublicKey, } from "@solana/web3.js";
2
+ import BN from "bn.js";
3
+ import { Keypair as UtxoKeypair } from "./models/keypair.js";
4
+ import { WasmFactory } from "@lightprotocol/hasher.rs";
5
+ //@ts-ignore
6
+ import * as ffjavascript from "ffjavascript";
7
+ import { FETCH_UTXOS_GROUP_SIZE, RELAYER_API_URL, LSK_ENCRYPTED_OUTPUTS, LSK_FETCH_OFFSET, PROGRAM_ID, } from "./utils/constants.js";
8
+ import { logger } from "./utils/logger.js";
9
+ import { debugLogger } from "./utils/debug-logger.js";
10
+ // Use type assertion for the utility functions (same pattern as in get_verification_keys.ts)
11
+ const utils = ffjavascript.utils;
12
+ const { unstringifyBigInts, leInt2Buff } = utils;
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(() => {
15
+ resolve("ok");
16
+ }, ms));
17
+ }
18
+ export function localstorageKey(key) {
19
+ return PROGRAM_ID.toString().substring(0, 6) + key.toString();
20
+ }
21
+ let roundStartIndex = 0;
22
+ let decryptionTaskFinished = 0;
23
+ /**
24
+ * Fetch and decrypt all UTXOs for a user
25
+ * @param signed The user's signature
26
+ * @param connection Solana connection to fetch on-chain commitment accounts
27
+ * @param setStatus A global state updator. Set live status message showing on webpage
28
+ * @returns Array of decrypted UTXOs that belong to the user
29
+ */
30
+ export async function getUtxos({ publicKey, connection, encryptionService, storage, abortSignal, offset, }) {
31
+ let valid_utxos = [];
32
+ let valid_strings = [];
33
+ let history_indexes = [];
34
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey));
35
+ if (offsetStr) {
36
+ roundStartIndex = Number(offsetStr);
37
+ }
38
+ else {
39
+ roundStartIndex = 0;
40
+ }
41
+ decryptionTaskFinished = 0;
42
+ if (!offset) {
43
+ offset = 0;
44
+ }
45
+ roundStartIndex = Math.max(offset, roundStartIndex);
46
+ while (true) {
47
+ if (abortSignal?.aborted) {
48
+ throw new Error("aborted");
49
+ }
50
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey));
51
+ let fetch_utxo_offset = offsetStr ? Number(offsetStr) : 0;
52
+ if (offset) {
53
+ fetch_utxo_offset = Math.max(offset, fetch_utxo_offset);
54
+ }
55
+ let fetch_utxo_end = fetch_utxo_offset + FETCH_UTXOS_GROUP_SIZE;
56
+ let fetch_utxo_url = `${RELAYER_API_URL}/utxos/range?start=${fetch_utxo_offset}&end=${fetch_utxo_end}`;
57
+ let fetched = await fetchUserUtxos({
58
+ publicKey,
59
+ connection,
60
+ url: fetch_utxo_url,
61
+ encryptionService,
62
+ storage,
63
+ initOffset: offset,
64
+ });
65
+ let am = 0;
66
+ const nonZeroUtxos = [];
67
+ const nonZeroEncrypted = [];
68
+ for (let [k, utxo] of fetched.utxos.entries()) {
69
+ history_indexes.push(utxo.index);
70
+ if (utxo.amount.toNumber() > 0) {
71
+ nonZeroUtxos.push(utxo);
72
+ nonZeroEncrypted.push(fetched.encryptedOutputs[k]);
73
+ }
74
+ }
75
+ if (nonZeroUtxos.length > 0) {
76
+ const spentFlags = await areUtxosSpent(connection, nonZeroUtxos);
77
+ for (let i = 0; i < nonZeroUtxos.length; i++) {
78
+ if (!spentFlags[i]) {
79
+ logger.debug(`found unspent encrypted_output ${nonZeroEncrypted[i]}`);
80
+ am += nonZeroUtxos[i].amount.toNumber();
81
+ valid_utxos.push(nonZeroUtxos[i]);
82
+ valid_strings.push(nonZeroEncrypted[i]);
83
+ }
84
+ }
85
+ }
86
+ storage.setItem(LSK_FETCH_OFFSET + localstorageKey(publicKey), (fetch_utxo_offset + fetched.len).toString());
87
+ if (!fetched.hasMore) {
88
+ break;
89
+ }
90
+ await sleep(20);
91
+ }
92
+ // get history index
93
+ let historyKey = "tradeHistory" + localstorageKey(publicKey);
94
+ let rec = storage.getItem(historyKey);
95
+ let recIndexes = [];
96
+ if (rec?.length) {
97
+ recIndexes = rec.split(",").map((n) => Number(n));
98
+ }
99
+ if (recIndexes.length) {
100
+ history_indexes = [...history_indexes, ...recIndexes];
101
+ }
102
+ let unique_history_indexes = Array.from(new Set(history_indexes));
103
+ let top20 = unique_history_indexes.sort((a, b) => b - a).slice(0, 20);
104
+ if (top20.length) {
105
+ storage.setItem(historyKey, top20.join(","));
106
+ }
107
+ // store valid strings
108
+ logger.debug(`valid_strings len before set: ${valid_strings.length}`);
109
+ valid_strings = [...new Set(valid_strings)];
110
+ logger.debug(`valid_strings len after set: ${valid_strings.length}`);
111
+ storage.setItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey), JSON.stringify(valid_strings));
112
+ return valid_utxos;
113
+ }
114
+ async function fetchUserUtxos({ publicKey, connection, url, storage, encryptionService, initOffset, }) {
115
+ const lightWasm = await WasmFactory.getInstance();
116
+ // Derive the UTXO keypair from the wallet keypair
117
+ const utxoPrivateKey = encryptionService.deriveUtxoPrivateKey();
118
+ const utxoKeypair = new UtxoKeypair(utxoPrivateKey, lightWasm);
119
+ // Fetch all UTXOs from the API
120
+ let encryptedOutputs = [];
121
+ logger.debug("fetching utxo data", url);
122
+ let res = await fetch(url);
123
+ if (!res.ok)
124
+ throw new Error(`HTTP error! status: ${res.status}`);
125
+ const data = await res.json();
126
+ logger.debug("got utxo data");
127
+ if (!data) {
128
+ throw new Error("API returned empty data");
129
+ }
130
+ else if (Array.isArray(data)) {
131
+ // Handle the case where the API returns an array of UTXOs
132
+ const utxos = data;
133
+ // Extract encrypted outputs from the array of UTXOs
134
+ encryptedOutputs = utxos
135
+ .filter((utxo) => utxo.encrypted_output)
136
+ .map((utxo) => utxo.encrypted_output);
137
+ }
138
+ else if (typeof data === "object" && data.encrypted_outputs) {
139
+ // Handle the case where the API returns an object with encrypted_outputs array
140
+ const apiResponse = data;
141
+ encryptedOutputs = apiResponse.encrypted_outputs;
142
+ }
143
+ else {
144
+ throw new Error(`API returned unexpected data format: ${JSON.stringify(data).substring(0, 100)}...`);
145
+ }
146
+ // Try to decrypt each encrypted output
147
+ const myUtxos = [];
148
+ const myEncryptedOutputs = [];
149
+ let decryptionAttempts = 0;
150
+ let successfulDecryptions = 0;
151
+ let cachedStringNum = 0;
152
+ let cachedString = storage.getItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey));
153
+ if (cachedString) {
154
+ cachedStringNum = JSON.parse(cachedString).length;
155
+ }
156
+ let decryptionTaskTotal = data.total + cachedStringNum - roundStartIndex;
157
+ let batchRes = await decrypt_outputs(encryptedOutputs, encryptionService, utxoKeypair, lightWasm);
158
+ decryptionTaskFinished += encryptedOutputs.length;
159
+ logger.debug("batchReslen", batchRes.length);
160
+ for (let i = 0; i < batchRes.length; i++) {
161
+ let dres = batchRes[i];
162
+ if (dres.status == "decrypted" && dres.utxo) {
163
+ myUtxos.push(dres.utxo);
164
+ myEncryptedOutputs.push(dres.encryptedOutput);
165
+ }
166
+ }
167
+ logger.info(`(decrypting cached utxo: ${decryptionTaskFinished + 1}/${decryptionTaskTotal}...)`);
168
+ // check cached string when no more fetching tasks
169
+ if (!data.hasMore) {
170
+ if (cachedString) {
171
+ let cachedEncryptedOutputs = JSON.parse(cachedString);
172
+ if (decryptionTaskFinished % 100 == 0) {
173
+ logger.info(`(decrypting cached utxo: ${decryptionTaskFinished + 1}/${decryptionTaskTotal}...)`);
174
+ }
175
+ let batchRes = await decrypt_outputs(cachedEncryptedOutputs, encryptionService, utxoKeypair, lightWasm);
176
+ decryptionTaskFinished += cachedEncryptedOutputs.length;
177
+ logger.debug("cachedbatchReslen", batchRes.length, " source", cachedEncryptedOutputs.length);
178
+ for (let i = 0; i < batchRes.length; i++) {
179
+ let dres = batchRes[i];
180
+ if (dres.status == "decrypted" && dres.utxo) {
181
+ myUtxos.push(dres.utxo);
182
+ myEncryptedOutputs.push(dres.encryptedOutput);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return {
188
+ encryptedOutputs: myEncryptedOutputs,
189
+ utxos: myUtxos,
190
+ hasMore: data.hasMore,
191
+ len: encryptedOutputs.length,
192
+ };
193
+ }
194
+ /**
195
+ * Check if a UTXO has been spent
196
+ * @param connection Solana connection
197
+ * @param utxo The UTXO to check
198
+ * @param retries Number of retries remaining (default 3)
199
+ * @returns Promise<boolean> true if spent, false if unspent
200
+ */
201
+ export async function isUtxoSpent(connection, utxo, retries = 3) {
202
+ try {
203
+ // Get the nullifier for this UTXO
204
+ const nullifier = await utxo.getNullifier();
205
+ logger.debug(`Checking if UTXO with nullifier ${nullifier} is spent`);
206
+ // Convert decimal nullifier string to byte array (same format as in proofs)
207
+ // This matches how commitments are handled and how the Rust code expects the seeds
208
+ const nullifierBytes = Array.from(leInt2Buff(unstringifyBigInts(nullifier), 32)).reverse();
209
+ // Try nullifier0 seed
210
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier0"), Buffer.from(nullifierBytes)], PROGRAM_ID);
211
+ logger.debug(`Derived nullifier0 PDA: ${nullifier0PDA.toBase58()}`);
212
+ const nullifier0Account = await connection.getAccountInfo(nullifier0PDA);
213
+ if (nullifier0Account !== null) {
214
+ logger.debug(`UTXO is spent (nullifier0 account exists)`);
215
+ return true;
216
+ }
217
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier1"), Buffer.from(nullifierBytes)], PROGRAM_ID);
218
+ logger.debug(`Derived nullifier1 PDA: ${nullifier1PDA.toBase58()}`);
219
+ const nullifier1Account = await connection.getAccountInfo(nullifier1PDA);
220
+ if (nullifier1Account !== null) {
221
+ logger.debug(`UTXO is spent (nullifier1 account exists)`);
222
+ return true;
223
+ }
224
+ return false;
225
+ }
226
+ catch (error) {
227
+ console.error("Error checking if UTXO is spent:", error);
228
+ if (retries <= 0) {
229
+ console.error("Max retries reached for isUtxoSpent, returning false");
230
+ return false; // Assume unspent if we can't verify
231
+ }
232
+ await new Promise((resolve) => setTimeout(resolve, 3000));
233
+ return await isUtxoSpent(connection, utxo, retries - 1);
234
+ }
235
+ }
236
+ async function areUtxosSpent(connection, utxos) {
237
+ try {
238
+ const allPDAs = [];
239
+ for (let i = 0; i < utxos.length; i++) {
240
+ const utxo = utxos[i];
241
+ const nullifier = await utxo.getNullifier();
242
+ const nullifierBytes = Array.from(leInt2Buff(unstringifyBigInts(nullifier), 32)).reverse();
243
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier0"), Buffer.from(nullifierBytes)], PROGRAM_ID);
244
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier1"), Buffer.from(nullifierBytes)], PROGRAM_ID);
245
+ allPDAs.push({ utxoIndex: i, pda: nullifier0PDA });
246
+ allPDAs.push({ utxoIndex: i, pda: nullifier1PDA });
247
+ }
248
+ const results = await connection.getMultipleAccountsInfo(allPDAs.map((x) => x.pda));
249
+ const spentFlags = new Array(utxos.length).fill(false);
250
+ for (let i = 0; i < allPDAs.length; i++) {
251
+ if (results[i] !== null) {
252
+ spentFlags[allPDAs[i].utxoIndex] = true;
253
+ }
254
+ }
255
+ return spentFlags;
256
+ }
257
+ catch (error) {
258
+ console.error("Error checking if UTXOs are spent:", error);
259
+ await new Promise((resolve) => setTimeout(resolve, 3000));
260
+ return await areUtxosSpent(connection, utxos);
261
+ }
262
+ }
263
+ // Calculate total balance
264
+ export function getBalanceFromUtxos(utxos) {
265
+ const totalBalance = utxos.reduce((sum, utxo) => sum.add(utxo.amount), new BN(0));
266
+ // const LAMPORTS_PER_SOL = new BN(1_000_000_000);
267
+ // const balanceInSol = totalBalance.div(LAMPORTS_PER_SOL);
268
+ // const remainderLamports = totalBalance.mod(LAMPORTS_PER_SOL);
269
+ return { lamports: totalBalance.toNumber() };
270
+ }
271
+ async function decrypt_outputs(encryptedOutputs, encryptionService, utxoKeypair, lightWasm) {
272
+ let results = [];
273
+ let skippedCount = 0;
274
+ let failedCount = 0;
275
+ let successCount = 0;
276
+ // decrypt all UTXO
277
+ for (const encryptedOutput of encryptedOutputs) {
278
+ if (!encryptedOutput) {
279
+ results.push({ status: "skipped" });
280
+ skippedCount++;
281
+ debugLogger.recordDecryptionSkipped();
282
+ continue;
283
+ }
284
+ debugLogger.recordDecryptionAttempt();
285
+ try {
286
+ const utxo = await encryptionService.decryptUtxo(encryptedOutput, lightWasm);
287
+ // decryptUtxo returns null for schema version mismatch (early termination)
288
+ if (utxo === null) {
289
+ results.push({ status: "skipped" });
290
+ skippedCount++;
291
+ debugLogger.recordDecryptionSkipped();
292
+ continue;
293
+ }
294
+ results.push({ status: "decrypted", utxo, encryptedOutput });
295
+ successCount++;
296
+ debugLogger.recordDecryptionSuccess();
297
+ }
298
+ catch (error) {
299
+ // Record the failure with full context instead of silently swallowing
300
+ debugLogger.recordDecryptionFailure(error, encryptedOutput);
301
+ results.push({ status: "unDecrypted" });
302
+ failedCount++;
303
+ }
304
+ }
305
+ // Log batch summary
306
+ debugLogger.utxoBatchSummary(encryptedOutputs.length, successCount, skippedCount, failedCount);
307
+ results = results.filter((r) => r.status == "decrypted");
308
+ if (!results.length) {
309
+ return [];
310
+ }
311
+ // update utxo index
312
+ if (results.length > 0) {
313
+ let encrypted_outputs = results.map((r) => r.encryptedOutput);
314
+ let url = RELAYER_API_URL + `/utxos/indices`;
315
+ let res = await fetch(url, {
316
+ method: "POST",
317
+ headers: { "Content-Type": "application/json" },
318
+ body: JSON.stringify({ encrypted_outputs }),
319
+ });
320
+ let j = await res.json();
321
+ if (!j.indices ||
322
+ !Array.isArray(j.indices) ||
323
+ j.indices.length != encrypted_outputs.length) {
324
+ throw new Error("failed fetching /utxos/indices");
325
+ }
326
+ for (let i = 0; i < results.length; i++) {
327
+ let utxo = results[i].utxo;
328
+ if (utxo.index !== j.indices[i] && typeof j.indices[i] == "number") {
329
+ logger.debug(`Updated UTXO index from ${utxo.index} to ${j.indices[i]}`);
330
+ utxo.index = j.indices[i];
331
+ }
332
+ }
333
+ }
334
+ return results;
335
+ }
@@ -0,0 +1,34 @@
1
+ import { Connection, PublicKey } from "@solana/web3.js";
2
+ import { Utxo } from "./models/utxo.js";
3
+ import { EncryptionService } from "./utils/encryption.js";
4
+ export declare function localstorageKey(key: PublicKey): string;
5
+ /**
6
+ * Fetch and decrypt all UTXOs for a user
7
+ * @param signed The user's signature
8
+ * @param connection Solana connection to fetch on-chain commitment accounts
9
+ * @param setStatus A global state updator. Set live status message showing on webpage
10
+ * @returns Array of decrypted UTXOs that belong to the user
11
+ */
12
+ export declare function getUtxosSPL({ publicKey, connection, encryptionService, storage, abortSignal, offset, mintAddress, }: {
13
+ publicKey: PublicKey;
14
+ connection: Connection;
15
+ encryptionService: EncryptionService;
16
+ storage: Storage;
17
+ mintAddress: PublicKey | string;
18
+ abortSignal?: AbortSignal;
19
+ offset?: number;
20
+ }): Promise<Utxo[]>;
21
+ /**
22
+ * Check if a UTXO has been spent
23
+ * @param connection Solana connection
24
+ * @param utxo The UTXO to check
25
+ * @param retries Number of retries remaining (default 3)
26
+ * @returns Promise<boolean> true if spent, false if unspent
27
+ */
28
+ export declare function isUtxoSpent(connection: Connection, utxo: Utxo, retries?: number): Promise<boolean>;
29
+ export declare function getBalanceFromUtxosSPL(utxos: Utxo[]): {
30
+ base_units: number;
31
+ amount: number;
32
+ /** @deprecated use base_units instead */
33
+ lamports: number;
34
+ };