privacycash 1.0.17 → 1.0.18

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 (42) hide show
  1. package/__tests__/e2e.test.ts +5 -1
  2. package/__tests__/e2espl.test.ts +73 -0
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +6 -10
  5. package/dist/deposit.js +4 -11
  6. package/dist/depositSPL.d.ts +19 -0
  7. package/dist/depositSPL.js +433 -0
  8. package/dist/exportUtils.d.ts +3 -0
  9. package/dist/exportUtils.js +3 -0
  10. package/dist/getUtxos.d.ts +2 -1
  11. package/dist/getUtxos.js +68 -79
  12. package/dist/getUtxosSPL.d.ts +31 -0
  13. package/dist/getUtxosSPL.js +369 -0
  14. package/dist/index.d.ts +31 -1
  15. package/dist/index.js +73 -3
  16. package/dist/models/utxo.js +10 -1
  17. package/dist/utils/address_lookup_table.d.ts +1 -0
  18. package/dist/utils/address_lookup_table.js +23 -0
  19. package/dist/utils/constants.d.ts +3 -1
  20. package/dist/utils/constants.js +6 -3
  21. package/dist/utils/encryption.d.ts +1 -1
  22. package/dist/utils/encryption.js +5 -3
  23. package/dist/utils/utils.d.ts +3 -2
  24. package/dist/utils/utils.js +26 -6
  25. package/dist/withdraw.js +3 -3
  26. package/dist/withdrawSPL.d.ts +22 -0
  27. package/dist/withdrawSPL.js +290 -0
  28. package/package.json +5 -3
  29. package/src/config.ts +7 -14
  30. package/src/deposit.ts +4 -11
  31. package/src/depositSPL.ts +555 -0
  32. package/src/exportUtils.ts +5 -1
  33. package/src/getUtxos.ts +73 -78
  34. package/src/getUtxosSPL.ts +495 -0
  35. package/src/index.ts +84 -3
  36. package/src/models/utxo.ts +10 -1
  37. package/src/utils/address_lookup_table.ts +54 -6
  38. package/src/utils/constants.ts +8 -3
  39. package/src/utils/encryption.ts +6 -3
  40. package/src/utils/utils.ts +29 -6
  41. package/src/withdraw.ts +4 -4
  42. package/src/withdrawSPL.ts +379 -0
package/src/getUtxos.ts CHANGED
@@ -6,7 +6,7 @@ import { EncryptionService } from './utils/encryption.js';
6
6
  import { WasmFactory } from '@lightprotocol/hasher.rs';
7
7
  //@ts-ignore
8
8
  import * as ffjavascript from 'ffjavascript';
9
- import { FETCH_UTXOS_GROUP_SIZE, INDEXER_API_URL, LSK_ENCRYPTED_OUTPUTS, LSK_FETCH_OFFSET, PROGRAM_ID } from './utils/constants.js';
9
+ import { FETCH_UTXOS_GROUP_SIZE, RELAYER_API_URL, LSK_ENCRYPTED_OUTPUTS, LSK_FETCH_OFFSET, PROGRAM_ID } from './utils/constants.js';
10
10
  import { logger } from './utils/logger.js';
11
11
 
12
12
  // Use type assertion for the utility functions (same pattern as in get_verification_keys.ts)
@@ -41,7 +41,6 @@ export function localstorageKey(key: PublicKey) {
41
41
  return PROGRAM_ID.toString().substring(0, 6) + key.toString()
42
42
  }
43
43
 
44
- let getMyUtxosPromise: Promise<Utxo[]> | null = null
45
44
  let roundStartIndex = 0
46
45
  let decryptionTaskFinished = 0;
47
46
  /**
@@ -52,88 +51,84 @@ let decryptionTaskFinished = 0;
52
51
  * @returns Array of decrypted UTXOs that belong to the user
53
52
  */
54
53
 
55
- export async function getUtxos({ publicKey, connection, encryptionService, storage }: {
54
+ export async function getUtxos({ publicKey, connection, encryptionService, storage, abortSignal }: {
56
55
  publicKey: PublicKey,
57
56
  connection: Connection,
58
57
  encryptionService: EncryptionService,
59
- storage: Storage
58
+ storage: Storage,
59
+ abortSignal?: AbortSignal
60
60
  }): Promise<Utxo[]> {
61
- if (!getMyUtxosPromise) {
62
- getMyUtxosPromise = (async () => {
63
- let valid_utxos: Utxo[] = []
64
- let valid_strings: string[] = []
65
- let history_indexes: number[] = []
66
- try {
67
- let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey))
68
- if (offsetStr) {
69
- roundStartIndex = Number(offsetStr)
70
- } else {
71
- roundStartIndex = 0
72
- }
73
- decryptionTaskFinished = 0
74
- while (true) {
75
- let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey))
76
- let fetch_utxo_offset = offsetStr ? Number(offsetStr) : 0
77
- let fetch_utxo_end = fetch_utxo_offset + FETCH_UTXOS_GROUP_SIZE
78
- let fetch_utxo_url = `${INDEXER_API_URL}/utxos/range?start=${fetch_utxo_offset}&end=${fetch_utxo_end}`
79
- let fetched = await fetchUserUtxos({ publicKey, connection, url: fetch_utxo_url, encryptionService, storage })
80
- let am = 0
81
-
82
- const nonZeroUtxos: Utxo[] = [];
83
- const nonZeroEncrypted: any[] = [];
84
- for (let [k, utxo] of fetched.utxos.entries()) {
85
- history_indexes.push(utxo.index)
86
- if (utxo.amount.toNumber() > 0) {
87
- nonZeroUtxos.push(utxo);
88
- nonZeroEncrypted.push(fetched.encryptedOutputs[k]);
89
- }
90
- }
91
- if (nonZeroUtxos.length > 0) {
92
- const spentFlags = await areUtxosSpent(connection, nonZeroUtxos);
93
- for (let i = 0; i < nonZeroUtxos.length; i++) {
94
- if (!spentFlags[i]) {
95
- logger.debug(`found unspent encrypted_output ${nonZeroEncrypted[i]}`)
96
- am += nonZeroUtxos[i].amount.toNumber();
97
- valid_utxos.push(nonZeroUtxos[i]);
98
- valid_strings.push(nonZeroEncrypted[i]);
99
- }
100
- }
101
- }
102
- storage.setItem(LSK_FETCH_OFFSET + localstorageKey(publicKey), (fetch_utxo_offset + fetched.len).toString())
103
- if (!fetched.hasMore) {
104
- break
105
- }
106
- await sleep(20)
107
- }
108
- } catch (e: any) {
109
- throw e
110
- } finally {
111
- getMyUtxosPromise = null
112
- }
113
- // get history index
114
- let historyKey = 'tradeHistory' + localstorageKey(publicKey)
115
- let rec = storage.getItem(historyKey)
116
- let recIndexes: number[] = []
117
- if (rec?.length) {
118
- recIndexes = rec.split(',').map(n => Number(n))
119
- }
120
- if (recIndexes.length) {
121
- history_indexes = [...history_indexes, ...recIndexes]
61
+
62
+ let valid_utxos: Utxo[] = []
63
+ let valid_strings: string[] = []
64
+ let history_indexes: number[] = []
65
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey))
66
+ if (offsetStr) {
67
+ roundStartIndex = Number(offsetStr)
68
+ } else {
69
+ roundStartIndex = 0
70
+ }
71
+ decryptionTaskFinished = 0
72
+ while (true) {
73
+ if (abortSignal?.aborted) {
74
+ throw new Error('aborted')
75
+ }
76
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey))
77
+ let fetch_utxo_offset = offsetStr ? Number(offsetStr) : 0
78
+ let fetch_utxo_end = fetch_utxo_offset + FETCH_UTXOS_GROUP_SIZE
79
+ let fetch_utxo_url = `${RELAYER_API_URL}/utxos/range?start=${fetch_utxo_offset}&end=${fetch_utxo_end}`
80
+ let fetched = await fetchUserUtxos({ publicKey, connection, url: fetch_utxo_url, encryptionService, storage })
81
+ let am = 0
82
+
83
+ const nonZeroUtxos: Utxo[] = [];
84
+ const nonZeroEncrypted: any[] = [];
85
+ for (let [k, utxo] of fetched.utxos.entries()) {
86
+ history_indexes.push(utxo.index)
87
+ if (utxo.amount.toNumber() > 0) {
88
+ nonZeroUtxos.push(utxo);
89
+ nonZeroEncrypted.push(fetched.encryptedOutputs[k]);
122
90
  }
123
- let unique_history_indexes = Array.from(new Set(history_indexes));
124
- let top20 = unique_history_indexes.sort((a, b) => b - a).slice(0, 20);
125
- if (top20.length) {
126
- storage.setItem(historyKey, top20.join(','))
91
+ }
92
+ if (nonZeroUtxos.length > 0) {
93
+ const spentFlags = await areUtxosSpent(connection, nonZeroUtxos);
94
+ for (let i = 0; i < nonZeroUtxos.length; i++) {
95
+ if (!spentFlags[i]) {
96
+ logger.debug(`found unspent encrypted_output ${nonZeroEncrypted[i]}`)
97
+ am += nonZeroUtxos[i].amount.toNumber();
98
+ valid_utxos.push(nonZeroUtxos[i]);
99
+ valid_strings.push(nonZeroEncrypted[i]);
100
+ }
127
101
  }
128
- // store valid strings
129
- logger.debug(`valid_strings len before set: ${valid_strings.length}`)
130
- valid_strings = [...new Set(valid_strings)];
131
- logger.debug(`valid_strings len after set: ${valid_strings.length}`)
132
- storage.setItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey), JSON.stringify(valid_strings))
133
- return valid_utxos
134
- })()
102
+ }
103
+ storage.setItem(LSK_FETCH_OFFSET + localstorageKey(publicKey), (fetch_utxo_offset + fetched.len).toString())
104
+ if (!fetched.hasMore) {
105
+ break
106
+ }
107
+ await sleep(20)
135
108
  }
136
- return getMyUtxosPromise
109
+
110
+ // get history index
111
+ let historyKey = 'tradeHistory' + localstorageKey(publicKey)
112
+ let rec = storage.getItem(historyKey)
113
+ let recIndexes: number[] = []
114
+ if (rec?.length) {
115
+ recIndexes = rec.split(',').map(n => Number(n))
116
+ }
117
+ if (recIndexes.length) {
118
+ history_indexes = [...history_indexes, ...recIndexes]
119
+ }
120
+ let unique_history_indexes = Array.from(new Set(history_indexes));
121
+ let top20 = unique_history_indexes.sort((a, b) => b - a).slice(0, 20);
122
+ if (top20.length) {
123
+ storage.setItem(historyKey, top20.join(','))
124
+ }
125
+ // store valid strings
126
+ logger.debug(`valid_strings len before set: ${valid_strings.length}`)
127
+ valid_strings = [...new Set(valid_strings)];
128
+ logger.debug(`valid_strings len after set: ${valid_strings.length}`)
129
+ storage.setItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey), JSON.stringify(valid_strings))
130
+ return valid_utxos
131
+
137
132
  }
138
133
 
139
134
  async function fetchUserUtxos({ publicKey, connection, url, storage, encryptionService }: {
@@ -369,7 +364,7 @@ async function decrypt_outputs(
369
364
  if (results.length > 0) {
370
365
  let encrypted_outputs = results.map(r => r.encryptedOutput)
371
366
 
372
- let url = INDEXER_API_URL + `/utxos/indices`
367
+ let url = RELAYER_API_URL + `/utxos/indices`
373
368
  let res = await fetch(url, {
374
369
  method: 'POST', headers: { "Content-Type": "application/json" },
375
370
  body: JSON.stringify({ encrypted_outputs })
@@ -0,0 +1,495 @@
1
+ import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
2
+ import BN from 'bn.js';
3
+ import { Keypair as UtxoKeypair } from './models/keypair.js';
4
+ import { Utxo } from './models/utxo.js';
5
+ import { EncryptionService } from './utils/encryption.js';
6
+ import { WasmFactory } from '@lightprotocol/hasher.rs';
7
+ //@ts-ignore
8
+ import * as ffjavascript from 'ffjavascript';
9
+ import { FETCH_UTXOS_GROUP_SIZE, RELAYER_API_URL, LSK_ENCRYPTED_OUTPUTS, LSK_FETCH_OFFSET, PROGRAM_ID } from './utils/constants.js';
10
+ import { logger } from './utils/logger.js';
11
+ import { getAssociatedTokenAddress } from '@solana/spl-token';
12
+
13
+ // Use type assertion for the utility functions (same pattern as in get_verification_keys.ts)
14
+ const utils = ffjavascript.utils as any;
15
+ const { unstringifyBigInts, leInt2Buff } = utils;
16
+
17
+ /**
18
+ * Interface for the UTXO data returned from the API
19
+ */
20
+ interface ApiUtxo {
21
+ commitment: string;
22
+ encrypted_output: string; // Hex-encoded encrypted UTXO data
23
+ index: number;
24
+ nullifier?: string; // Optional, might not be present for all UTXOs
25
+ }
26
+
27
+ /**
28
+ * Interface for the API response format that includes count and encrypted_outputs
29
+ */
30
+ interface ApiResponse {
31
+ count: number;
32
+ encrypted_outputs: string[];
33
+ }
34
+
35
+ function sleep(ms: number): Promise<string> {
36
+ return new Promise(resolve => setTimeout(() => {
37
+ resolve('ok')
38
+ }, ms))
39
+ }
40
+
41
+ export function localstorageKey(key: PublicKey) {
42
+ return PROGRAM_ID.toString().substring(0, 6) + key.toString()
43
+ }
44
+
45
+ type Utxos = { [k: string]: Utxo[] }
46
+
47
+ let getMyUtxosPromise: Promise<Utxo[]> | null = null
48
+ let roundStartIndex = 0
49
+ let decryptionTaskFinished = 0;
50
+ /**
51
+ * Fetch and decrypt all UTXOs for a user
52
+ * @param signed The user's signature
53
+ * @param connection Solana connection to fetch on-chain commitment accounts
54
+ * @param setStatus A global state updator. Set live status message showing on webpage
55
+ * @returns Array of decrypted UTXOs that belong to the user
56
+ */
57
+
58
+ export async function getUtxosSPL({ publicKey, connection, encryptionService, storage, mintAddress, abortSignal }: {
59
+ publicKey: PublicKey,
60
+ connection: Connection,
61
+ encryptionService: EncryptionService,
62
+ storage: Storage,
63
+ mintAddress: PublicKey,
64
+ abortSignal?: AbortSignal
65
+ }): Promise<Utxo[]> {
66
+ let valid_utxos: Utxo[] = []
67
+ let valid_strings: string[] = []
68
+ let history_indexes: number[] = []
69
+ let publicKey_ata: PublicKey
70
+ try {
71
+ publicKey_ata = await getAssociatedTokenAddress(
72
+ mintAddress,
73
+ publicKey
74
+ );
75
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey_ata))
76
+ if (offsetStr) {
77
+ roundStartIndex = Number(offsetStr)
78
+ } else {
79
+ roundStartIndex = 0
80
+ }
81
+ decryptionTaskFinished = 0
82
+ while (true) {
83
+ if (abortSignal?.aborted) {
84
+ throw new Error('aborted')
85
+ }
86
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey_ata))
87
+ let fetch_utxo_offset = offsetStr ? Number(offsetStr) : 0
88
+ let fetch_utxo_end = fetch_utxo_offset + FETCH_UTXOS_GROUP_SIZE
89
+ let fetch_utxo_url = `${RELAYER_API_URL}/utxos/range?token=usdc&start=${fetch_utxo_offset}&end=${fetch_utxo_end}`
90
+ let fetched = await fetchUserUtxos({ publicKey, connection, url: fetch_utxo_url, encryptionService, storage, publicKey_ata })
91
+ let am = 0
92
+
93
+ const nonZeroUtxos: Utxo[] = [];
94
+ const nonZeroEncrypted: any[] = [];
95
+ for (let [k, utxo] of fetched.utxos.entries()) {
96
+ history_indexes.push(utxo.index)
97
+ if (utxo.amount.toNumber() > 0) {
98
+ nonZeroUtxos.push(utxo);
99
+ nonZeroEncrypted.push(fetched.encryptedOutputs[k]);
100
+ }
101
+ }
102
+ if (nonZeroUtxos.length > 0) {
103
+ const spentFlags = await areUtxosSpent(connection, nonZeroUtxos);
104
+ for (let i = 0; i < nonZeroUtxos.length; i++) {
105
+ if (!spentFlags[i]) {
106
+ logger.debug(`found unspent encrypted_output ${nonZeroEncrypted[i]}`)
107
+ am += nonZeroUtxos[i].amount.toNumber();
108
+ valid_utxos.push(nonZeroUtxos[i]);
109
+ valid_strings.push(nonZeroEncrypted[i]);
110
+ }
111
+ }
112
+ }
113
+ storage.setItem(LSK_FETCH_OFFSET + localstorageKey(publicKey_ata), (fetch_utxo_offset + fetched.len).toString())
114
+ if (!fetched.hasMore) {
115
+ break
116
+ }
117
+ await sleep(100)
118
+ }
119
+ } catch (e: any) {
120
+ throw e
121
+ } finally {
122
+ getMyUtxosPromise = null
123
+ }
124
+ // get history index
125
+ let historyKey = 'tradeHistory' + localstorageKey(publicKey_ata)
126
+ let rec = storage.getItem(historyKey)
127
+ let recIndexes: number[] = []
128
+ if (rec?.length) {
129
+ recIndexes = rec.split(',').map(n => Number(n))
130
+ }
131
+ if (recIndexes.length) {
132
+ history_indexes = [...history_indexes, ...recIndexes]
133
+ }
134
+ let unique_history_indexes = Array.from(new Set(history_indexes));
135
+ let top20 = unique_history_indexes.sort((a, b) => b - a).slice(0, 20);
136
+ if (top20.length) {
137
+ storage.setItem(historyKey, top20.join(','))
138
+ }
139
+ // store valid strings
140
+ logger.debug(`valid_strings len before set: ${valid_strings.length}`)
141
+ valid_strings = [...new Set(valid_strings)];
142
+ logger.debug(`valid_strings len after set: ${valid_strings.length}`)
143
+ storage.setItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey_ata), JSON.stringify(valid_strings))
144
+ // reorgnize
145
+ return valid_utxos.filter(u => u.mintAddress == mintAddress.toString())
146
+ }
147
+
148
+ async function fetchUserUtxos({ publicKey, connection, url, storage, encryptionService, publicKey_ata }: {
149
+ publicKey: PublicKey,
150
+ connection: Connection,
151
+ url: string,
152
+ encryptionService: EncryptionService,
153
+ storage: Storage,
154
+ publicKey_ata: PublicKey
155
+ }): Promise<{
156
+ encryptedOutputs: string[],
157
+ utxos: Utxo[],
158
+ hasMore: boolean,
159
+ len: number
160
+ }> {
161
+ const lightWasm = await WasmFactory.getInstance();
162
+
163
+ // Derive the UTXO keypair from the wallet keypair
164
+ const utxoPrivateKey = encryptionService.deriveUtxoPrivateKey();
165
+ const utxoKeypair = new UtxoKeypair(utxoPrivateKey, lightWasm);
166
+
167
+
168
+ // Fetch all UTXOs from the API
169
+ let encryptedOutputs: string[] = [];
170
+ logger.debug('fetching utxo data', url)
171
+ let res = await fetch(url)
172
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
173
+ const data: any = await res.json()
174
+ logger.debug('got utxo data')
175
+ if (!data) {
176
+ throw new Error('API returned empty data')
177
+ } else if (Array.isArray(data)) {
178
+ // Handle the case where the API returns an array of UTXOs
179
+ const utxos: ApiUtxo[] = data;
180
+ // Extract encrypted outputs from the array of UTXOs
181
+ encryptedOutputs = utxos
182
+ .filter(utxo => utxo.encrypted_output)
183
+ .map(utxo => utxo.encrypted_output);
184
+ } else if (typeof data === 'object' && data.encrypted_outputs) {
185
+ // Handle the case where the API returns an object with encrypted_outputs array
186
+ const apiResponse = data as ApiResponse;
187
+ encryptedOutputs = apiResponse.encrypted_outputs;
188
+ } else {
189
+ throw new Error(`API returned unexpected data format: ${JSON.stringify(data).substring(0, 100)}...`);
190
+ }
191
+
192
+ // Try to decrypt each encrypted output
193
+ const myUtxos: Utxo[] = [];
194
+ const myEncryptedOutputs: string[] = [];
195
+ let decryptionAttempts = 0;
196
+ let successfulDecryptions = 0;
197
+
198
+ let cachedStringNum = 0
199
+ let cachedString = storage.getItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey_ata))
200
+ if (cachedString) {
201
+ cachedStringNum = JSON.parse(cachedString).length
202
+ }
203
+
204
+
205
+ let decryptionTaskTotal = data.total + cachedStringNum - roundStartIndex;
206
+ let batchRes = await decrypt_outputs(encryptedOutputs, encryptionService, utxoKeypair, lightWasm)
207
+ decryptionTaskFinished += encryptedOutputs.length
208
+ logger.debug('batchReslen', batchRes.length)
209
+ for (let i = 0; i < batchRes.length; i++) {
210
+ let dres = batchRes[i]
211
+ if (dres.status == 'decrypted' && dres.utxo) {
212
+ myUtxos.push(dres.utxo)
213
+ myEncryptedOutputs.push(dres.encryptedOutput!)
214
+ }
215
+ }
216
+ logger.info(`(decrypting cached utxo: ${decryptionTaskFinished + 1}/${decryptionTaskTotal}...)`)
217
+ // check cached string when no more fetching tasks
218
+ if (!data.hasMore) {
219
+ if (cachedString) {
220
+ let cachedEncryptedOutputs = JSON.parse(cachedString)
221
+ if (decryptionTaskFinished % 100 == 0) {
222
+ logger.info(`(decrypting cached utxo: ${decryptionTaskFinished + 1}/${decryptionTaskTotal}...)`)
223
+ }
224
+ let batchRes = await decrypt_outputs(cachedEncryptedOutputs, encryptionService, utxoKeypair, lightWasm)
225
+ decryptionTaskFinished += cachedEncryptedOutputs.length
226
+ logger.debug('cachedbatchReslen', batchRes.length, ' source', cachedEncryptedOutputs.length)
227
+ for (let i = 0; i < batchRes.length; i++) {
228
+ let dres = batchRes[i]
229
+ if (dres.status == 'decrypted' && dres.utxo) {
230
+ myUtxos.push(dres.utxo)
231
+ myEncryptedOutputs.push(dres.encryptedOutput!)
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ return { encryptedOutputs: myEncryptedOutputs, utxos: myUtxos, hasMore: data.hasMore, len: encryptedOutputs.length };
238
+ }
239
+
240
+ /**
241
+ * Check if a UTXO has been spent
242
+ * @param connection Solana connection
243
+ * @param utxo The UTXO to check
244
+ * @returns Promise<boolean> true if spent, false if unspent
245
+ */
246
+ export async function isUtxoSpent(connection: Connection, utxo: Utxo): Promise<boolean> {
247
+ try {
248
+ // Get the nullifier for this UTXO
249
+ const nullifier = await utxo.getNullifier();
250
+ logger.debug(`Checking if UTXO with nullifier ${nullifier} is spent`);
251
+
252
+ // Convert decimal nullifier string to byte array (same format as in proofs)
253
+ // This matches how commitments are handled and how the Rust code expects the seeds
254
+ const nullifierBytes = Array.from(
255
+ leInt2Buff(unstringifyBigInts(nullifier), 32)
256
+ ).reverse() as number[];
257
+
258
+ // Try nullifier0 seed
259
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync(
260
+ [Buffer.from("nullifier0"), Buffer.from(nullifierBytes)],
261
+ PROGRAM_ID
262
+ );
263
+
264
+ logger.debug(`Derived nullifier0 PDA: ${nullifier0PDA.toBase58()}`);
265
+ const nullifier0Account = await connection.getAccountInfo(nullifier0PDA);
266
+ if (nullifier0Account !== null) {
267
+ logger.debug(`UTXO is spent (nullifier0 account exists)`);
268
+ return true;
269
+ }
270
+
271
+
272
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync(
273
+ [Buffer.from("nullifier1"), Buffer.from(nullifierBytes)],
274
+ PROGRAM_ID
275
+ );
276
+
277
+ logger.debug(`Derived nullifier1 PDA: ${nullifier1PDA.toBase58()}`);
278
+ const nullifier1Account = await connection.getAccountInfo(nullifier1PDA);
279
+ if (nullifier1Account !== null) {
280
+ logger.debug(`UTXO is spent (nullifier1 account exists)`);
281
+ return true
282
+ }
283
+ return false;
284
+ } catch (error: any) {
285
+ console.error('Error checking if UTXO is spent:', error);
286
+ await new Promise(resolve => setTimeout(resolve, 3000));
287
+ return await isUtxoSpent(connection, utxo)
288
+ }
289
+ }
290
+
291
+ async function areUtxosSpent(
292
+ connection: Connection,
293
+ utxos: Utxo[]
294
+ ): Promise<boolean[]> {
295
+ try {
296
+ const allPDAs: { utxoIndex: number; pda: PublicKey }[] = [];
297
+
298
+ for (let i = 0; i < utxos.length; i++) {
299
+ const utxo = utxos[i];
300
+ const nullifier = await utxo.getNullifier();
301
+
302
+ const nullifierBytes = Array.from(
303
+ leInt2Buff(unstringifyBigInts(nullifier), 32)
304
+ ).reverse() as number[];
305
+
306
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync(
307
+ [Buffer.from("nullifier0"), Buffer.from(nullifierBytes)],
308
+ PROGRAM_ID
309
+ );
310
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync(
311
+ [Buffer.from("nullifier1"), Buffer.from(nullifierBytes)],
312
+ PROGRAM_ID
313
+ );
314
+
315
+ allPDAs.push({ utxoIndex: i, pda: nullifier0PDA });
316
+ allPDAs.push({ utxoIndex: i, pda: nullifier1PDA });
317
+ }
318
+
319
+ const results: any[] =
320
+ await connection.getMultipleAccountsInfo(allPDAs.map((x) => x.pda));
321
+
322
+ const spentFlags = new Array(utxos.length).fill(false);
323
+ for (let i = 0; i < allPDAs.length; i++) {
324
+ if (results[i] !== null) {
325
+ spentFlags[allPDAs[i].utxoIndex] = true;
326
+ }
327
+ }
328
+
329
+ return spentFlags;
330
+ } catch (error: any) {
331
+ console.error("Error checking if UTXOs are spent:", error);
332
+ await new Promise((resolve) => setTimeout(resolve, 3000));
333
+ return await areUtxosSpent(connection, utxos);
334
+ }
335
+ }
336
+
337
+ // Calculate total balance
338
+ export function getBalanceFromUtxosSPL(utxos: Utxo[]): {
339
+ base_units: number
340
+ /** @deprecated use base_units instead */
341
+ lamports: number
342
+ } {
343
+ const totalBalance = utxos.reduce((sum, utxo) => sum.add(utxo.amount), new BN(0));
344
+ return { base_units: totalBalance.toNumber(), lamports: totalBalance.toNumber() }
345
+ }
346
+
347
+ // Decrypt single output to Utxo
348
+ type DecryptRes = { status: 'decrypted' | 'skipped' | 'unDecrypted', utxo?: Utxo, encryptedOutput?: string }
349
+ async function decrypt_output(
350
+ encryptedOutput: string,
351
+ encryptionService: EncryptionService,
352
+ utxoKeypair: UtxoKeypair,
353
+ lightWasm: any,
354
+ connection: Connection
355
+ ): Promise<DecryptRes> {
356
+ let res: DecryptRes = { status: 'unDecrypted' }
357
+ try {
358
+ if (!encryptedOutput) {
359
+ return { status: 'skipped' }
360
+ }
361
+
362
+ // Try to decrypt the UTXO
363
+ res.utxo = await encryptionService.decryptUtxo(
364
+ encryptedOutput,
365
+ lightWasm
366
+ );
367
+
368
+ // If we got here, decryption succeeded, so this UTXO belongs to the user
369
+ res.status = 'decrypted'
370
+
371
+ // Get the real index from the on-chain commitment account
372
+ try {
373
+ if (!res.utxo) {
374
+ throw new Error('res.utxo undefined')
375
+ }
376
+ const commitment = await res.utxo.getCommitment();
377
+ // Convert decimal commitment string to byte array (same format as in proofs)
378
+ const commitmentBytes = Array.from(
379
+ leInt2Buff(unstringifyBigInts(commitment), 32)
380
+ ).reverse() as number[];
381
+
382
+ // Derive the commitment PDA (could be either commitment0 or commitment1)
383
+ // We'll try both seeds since we don't know which one it is
384
+ let commitmentAccount = null;
385
+ let realIndex = null;
386
+ // Try commitment0 seed
387
+ try {
388
+ const [commitment0PDA] = PublicKey.findProgramAddressSync(
389
+ [Buffer.from("commitment0"), Buffer.from(commitmentBytes)],
390
+ PROGRAM_ID
391
+ );
392
+
393
+ const account0Info = await connection.getAccountInfo(commitment0PDA);
394
+ if (account0Info) {
395
+ // Parse the index from the account data according to CommitmentAccount structure:
396
+ // 0-8: Anchor discriminator
397
+ // 8-40: commitment (32 bytes)
398
+ // 40-44: encrypted_output length (4 bytes)
399
+ // 44-44+len: encrypted_output data
400
+ // 44+len-52+len: index (8 bytes)
401
+ const encryptedOutputLength = account0Info.data.readUInt32LE(40);
402
+ const indexOffset = 44 + encryptedOutputLength;
403
+ const indexBytes = account0Info.data.slice(indexOffset, indexOffset + 8);
404
+ realIndex = new BN(indexBytes, 'le').toNumber();
405
+ }
406
+ } catch (e) {
407
+ // Try commitment1 seed if commitment0 fails
408
+ try {
409
+ const [commitment1PDA] = PublicKey.findProgramAddressSync(
410
+ [Buffer.from("commitment1"), Buffer.from(commitmentBytes)],
411
+ PROGRAM_ID
412
+ );
413
+
414
+ const account1Info = await connection.getAccountInfo(commitment1PDA);
415
+ if (account1Info) {
416
+ // Parse the index from the account data according to CommitmentAccount structure
417
+ const encryptedOutputLength = account1Info.data.readUInt32LE(40);
418
+ const indexOffset = 44 + encryptedOutputLength;
419
+ const indexBytes = account1Info.data.slice(indexOffset, indexOffset + 8);
420
+ realIndex = new BN(indexBytes, 'le').toNumber();
421
+ logger.debug(`Found commitment1 account with index: ${realIndex}`);
422
+ }
423
+ } catch (e2) {
424
+ logger.debug(`Could not find commitment account for ${commitment}, using encrypted index: ${res.utxo.index}`);
425
+ }
426
+ }
427
+
428
+ // Update the UTXO with the real index if we found it
429
+ if (realIndex !== null) {
430
+ const oldIndex = res.utxo.index;
431
+ res.utxo.index = realIndex;
432
+ }
433
+
434
+ } catch (error: any) {
435
+ logger.debug(`Failed to get real index for UTXO: ${error.message}`);
436
+ }
437
+ } catch (error: any) {
438
+ // this UTXO doesn't belong to the user
439
+ }
440
+ return res
441
+ }
442
+
443
+ async function decrypt_outputs(
444
+ encryptedOutputs: string[],
445
+ encryptionService: EncryptionService,
446
+ utxoKeypair: UtxoKeypair,
447
+ lightWasm: any,
448
+ ): Promise<DecryptRes[]> {
449
+ let results: DecryptRes[] = [];
450
+
451
+ // decript all UTXO
452
+ for (const encryptedOutput of encryptedOutputs) {
453
+ if (!encryptedOutput) {
454
+ results.push({ status: 'skipped' });
455
+ continue;
456
+ }
457
+ try {
458
+ const utxo = await encryptionService.decryptUtxo(
459
+ encryptedOutput,
460
+ lightWasm
461
+ );
462
+ results.push({ status: 'decrypted', utxo, encryptedOutput });
463
+ } catch {
464
+ results.push({ status: 'unDecrypted' });
465
+ }
466
+ }
467
+ results = results.filter(r => r.status == 'decrypted')
468
+ if (!results.length) {
469
+ return []
470
+ }
471
+
472
+ // update utxo index
473
+ if (results.length > 0) {
474
+ let encrypted_outputs = results.map(r => r.encryptedOutput)
475
+
476
+ let url = RELAYER_API_URL + `/utxos/indices`
477
+ let res = await fetch(url, {
478
+ method: 'POST', headers: { "Content-Type": "application/json" },
479
+ body: JSON.stringify({ encrypted_outputs, token: 'usdc' })
480
+ })
481
+ let j = await res.json()
482
+ if (!j.indices || !Array.isArray(j.indices) || j.indices.length != encrypted_outputs.length) {
483
+ throw new Error('failed fetching /utxos/indices')
484
+ }
485
+ for (let i = 0; i < results.length; i++) {
486
+ let utxo = results[i].utxo
487
+ if (utxo!.index !== j.indices[i] && typeof j.indices[i] == 'number') {
488
+ logger.debug(`Updated UTXO index from ${utxo!.index} to ${j.indices[i]}`);
489
+ utxo!.index = j.indices[i]
490
+ }
491
+ }
492
+ }
493
+
494
+ return results;
495
+ }