privacycash 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.github/workflows/npm-publish.yml +67 -0
  2. package/README.md +22 -0
  3. package/__tests__/e2e.test.ts +52 -0
  4. package/__tests__/encryption.test.ts +1635 -0
  5. package/circuit2/transaction2.wasm +0 -0
  6. package/circuit2/transaction2.zkey +0 -0
  7. package/dist/config.d.ts +7 -0
  8. package/dist/config.js +16 -0
  9. package/dist/deposit.d.ts +18 -0
  10. package/dist/deposit.js +402 -0
  11. package/dist/exportUtils.d.ts +6 -0
  12. package/dist/exportUtils.js +6 -0
  13. package/dist/getUtxos.d.ts +27 -0
  14. package/dist/getUtxos.js +352 -0
  15. package/dist/index.d.ts +61 -0
  16. package/dist/index.js +169 -0
  17. package/dist/models/keypair.d.ts +26 -0
  18. package/dist/models/keypair.js +43 -0
  19. package/dist/models/utxo.d.ts +49 -0
  20. package/dist/models/utxo.js +76 -0
  21. package/dist/utils/address_lookup_table.d.ts +8 -0
  22. package/dist/utils/address_lookup_table.js +21 -0
  23. package/dist/utils/constants.d.ts +14 -0
  24. package/dist/utils/constants.js +15 -0
  25. package/dist/utils/encryption.d.ts +107 -0
  26. package/dist/utils/encryption.js +374 -0
  27. package/dist/utils/logger.d.ts +9 -0
  28. package/dist/utils/logger.js +35 -0
  29. package/dist/utils/merkle_tree.d.ts +92 -0
  30. package/dist/utils/merkle_tree.js +186 -0
  31. package/dist/utils/node-shim.d.ts +5 -0
  32. package/dist/utils/node-shim.js +5 -0
  33. package/dist/utils/prover.d.ts +33 -0
  34. package/dist/utils/prover.js +123 -0
  35. package/dist/utils/utils.d.ts +67 -0
  36. package/dist/utils/utils.js +151 -0
  37. package/dist/withdraw.d.ts +21 -0
  38. package/dist/withdraw.js +270 -0
  39. package/package.json +48 -0
  40. package/src/config.ts +28 -0
  41. package/src/deposit.ts +496 -0
  42. package/src/exportUtils.ts +6 -0
  43. package/src/getUtxos.ts +466 -0
  44. package/src/index.ts +191 -0
  45. package/src/models/keypair.ts +52 -0
  46. package/src/models/utxo.ts +97 -0
  47. package/src/utils/address_lookup_table.ts +29 -0
  48. package/src/utils/constants.ts +26 -0
  49. package/src/utils/encryption.ts +461 -0
  50. package/src/utils/logger.ts +42 -0
  51. package/src/utils/merkle_tree.ts +207 -0
  52. package/src/utils/node-shim.ts +6 -0
  53. package/src/utils/prover.ts +189 -0
  54. package/src/utils/utils.ts +213 -0
  55. package/src/withdraw.ts +334 -0
  56. package/tsconfig.json +28 -0
@@ -0,0 +1,352 @@
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, INDEXER_API_URL, LSK_ENCRYPTED_OUTPUTS, LSK_FETCH_OFFSET, PROGRAM_ID } from './utils/constants.js';
8
+ import { logger } from './utils/logger.js';
9
+ // Use type assertion for the utility functions (same pattern as in get_verification_keys.ts)
10
+ const utils = ffjavascript.utils;
11
+ const { unstringifyBigInts, leInt2Buff } = utils;
12
+ function sleep(ms) {
13
+ return new Promise(resolve => setTimeout(() => {
14
+ resolve('ok');
15
+ }, ms));
16
+ }
17
+ export function localstorageKey(key) {
18
+ return PROGRAM_ID.toString().substring(0, 6) + key.toString();
19
+ }
20
+ let getMyUtxosPromise = null;
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 }) {
31
+ if (!getMyUtxosPromise) {
32
+ getMyUtxosPromise = (async () => {
33
+ let valid_utxos = [];
34
+ let valid_strings = [];
35
+ try {
36
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey));
37
+ if (offsetStr) {
38
+ roundStartIndex = Number(offsetStr);
39
+ }
40
+ else {
41
+ roundStartIndex = 0;
42
+ }
43
+ decryptionTaskFinished = 0;
44
+ while (true) {
45
+ let offsetStr = storage.getItem(LSK_FETCH_OFFSET + localstorageKey(publicKey));
46
+ let fetch_utxo_offset = offsetStr ? Number(offsetStr) : 0;
47
+ let fetch_utxo_end = fetch_utxo_offset + FETCH_UTXOS_GROUP_SIZE;
48
+ let fetch_utxo_url = `${INDEXER_API_URL}/utxos/range?start=${fetch_utxo_offset}&end=${fetch_utxo_end}`;
49
+ let fetched = await fetchUserUtxos({ publicKey, connection, url: fetch_utxo_url, encryptionService, storage });
50
+ let am = 0;
51
+ const nonZeroUtxos = [];
52
+ const nonZeroEncrypted = [];
53
+ for (let [k, utxo] of fetched.utxos.entries()) {
54
+ if (utxo.amount.toNumber() > 0) {
55
+ nonZeroUtxos.push(utxo);
56
+ nonZeroEncrypted.push(fetched.encryptedOutputs[k]);
57
+ }
58
+ }
59
+ if (nonZeroUtxos.length > 0) {
60
+ const spentFlags = await areUtxosSpent(connection, nonZeroUtxos);
61
+ for (let i = 0; i < nonZeroUtxos.length; i++) {
62
+ if (!spentFlags[i]) {
63
+ am += nonZeroUtxos[i].amount.toNumber();
64
+ valid_utxos.push(nonZeroUtxos[i]);
65
+ valid_strings.push(nonZeroEncrypted[i]);
66
+ }
67
+ }
68
+ }
69
+ storage.setItem(LSK_FETCH_OFFSET + localstorageKey(publicKey), (fetch_utxo_offset + fetched.len).toString());
70
+ if (!fetched.hasMore) {
71
+ break;
72
+ }
73
+ await sleep(100);
74
+ }
75
+ }
76
+ catch (e) {
77
+ throw e;
78
+ }
79
+ finally {
80
+ getMyUtxosPromise = null;
81
+ }
82
+ // store valid strings
83
+ valid_strings = [...new Set(valid_strings)];
84
+ storage.setItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey), JSON.stringify(valid_strings));
85
+ return valid_utxos;
86
+ })();
87
+ }
88
+ return getMyUtxosPromise;
89
+ }
90
+ async function fetchUserUtxos({ publicKey, connection, url, storage, encryptionService }) {
91
+ const lightWasm = await WasmFactory.getInstance();
92
+ // Derive the UTXO keypair from the wallet keypair
93
+ const utxoPrivateKey = encryptionService.deriveUtxoPrivateKey();
94
+ const utxoKeypair = new UtxoKeypair(utxoPrivateKey, lightWasm);
95
+ // Fetch all UTXOs from the API
96
+ let encryptedOutputs = [];
97
+ logger.debug('fetching utxo data', url);
98
+ let res = await fetch(url);
99
+ if (!res.ok)
100
+ throw new Error(`HTTP error! status: ${res.status}`);
101
+ const data = await res.json();
102
+ logger.debug('got utxo data');
103
+ if (!data) {
104
+ throw new Error('API returned empty data');
105
+ }
106
+ else if (Array.isArray(data)) {
107
+ // Handle the case where the API returns an array of UTXOs
108
+ const utxos = data;
109
+ // Extract encrypted outputs from the array of UTXOs
110
+ encryptedOutputs = utxos
111
+ .filter(utxo => utxo.encrypted_output)
112
+ .map(utxo => utxo.encrypted_output);
113
+ }
114
+ else if (typeof data === 'object' && data.encrypted_outputs) {
115
+ // Handle the case where the API returns an object with encrypted_outputs array
116
+ const apiResponse = data;
117
+ encryptedOutputs = apiResponse.encrypted_outputs;
118
+ }
119
+ else {
120
+ throw new Error(`API returned unexpected data format: ${JSON.stringify(data).substring(0, 100)}...`);
121
+ }
122
+ // Try to decrypt each encrypted output
123
+ const myUtxos = [];
124
+ const myEncryptedOutputs = [];
125
+ let decryptionAttempts = 0;
126
+ let successfulDecryptions = 0;
127
+ let cachedStringNum = 0;
128
+ let cachedString = storage.getItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(publicKey));
129
+ if (cachedString) {
130
+ cachedStringNum = JSON.parse(cachedString).length;
131
+ }
132
+ let decryptionTaskTotal = data.total + cachedStringNum - roundStartIndex;
133
+ let batchRes = await decrypt_outputs(encryptedOutputs, encryptionService, utxoKeypair, lightWasm);
134
+ decryptionTaskFinished += encryptedOutputs.length;
135
+ logger.debug('batchReslen', batchRes.length);
136
+ for (let i = 0; i < batchRes.length; i++) {
137
+ let dres = batchRes[i];
138
+ if (dres.status == 'decrypted' && dres.utxo) {
139
+ myUtxos.push(dres.utxo);
140
+ myEncryptedOutputs.push(dres.encryptedOutput);
141
+ }
142
+ }
143
+ logger.info(`(decrypting cached utxo: ${decryptionTaskFinished + 1}/${decryptionTaskTotal}...)`);
144
+ // check cached string when no more fetching tasks
145
+ if (!data.hasMore) {
146
+ if (cachedString) {
147
+ let cachedEncryptedOutputs = JSON.parse(cachedString);
148
+ for (let encryptedOutput of cachedEncryptedOutputs) {
149
+ if (decryptionTaskFinished % 100 == 0) {
150
+ logger.info(`(decrypting cached utxo: ${decryptionTaskFinished + 1}/${decryptionTaskTotal}...)`);
151
+ }
152
+ let batchRes = await decrypt_outputs(cachedEncryptedOutputs, encryptionService, utxoKeypair, lightWasm);
153
+ decryptionTaskFinished += cachedEncryptedOutputs.length;
154
+ logger.debug('cachedbatchReslen', batchRes.length, ' source', cachedEncryptedOutputs.length);
155
+ for (let i = 0; i < batchRes.length; i++) {
156
+ let dres = batchRes[i];
157
+ if (dres.status == 'decrypted' && dres.utxo) {
158
+ myUtxos.push(dres.utxo);
159
+ myEncryptedOutputs.push(dres.encryptedOutput);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return { encryptedOutputs: myEncryptedOutputs, utxos: myUtxos, hasMore: data.hasMore, len: encryptedOutputs.length };
166
+ }
167
+ /**
168
+ * Check if a UTXO has been spent
169
+ * @param connection Solana connection
170
+ * @param utxo The UTXO to check
171
+ * @returns Promise<boolean> true if spent, false if unspent
172
+ */
173
+ export async function isUtxoSpent(connection, utxo) {
174
+ try {
175
+ // Get the nullifier for this UTXO
176
+ const nullifier = await utxo.getNullifier();
177
+ logger.debug(`Checking if UTXO with nullifier ${nullifier} is spent`);
178
+ // Convert decimal nullifier string to byte array (same format as in proofs)
179
+ // This matches how commitments are handled and how the Rust code expects the seeds
180
+ const nullifierBytes = Array.from(leInt2Buff(unstringifyBigInts(nullifier), 32)).reverse();
181
+ // Try nullifier0 seed
182
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier0"), Buffer.from(nullifierBytes)], PROGRAM_ID);
183
+ logger.debug(`Derived nullifier0 PDA: ${nullifier0PDA.toBase58()}`);
184
+ const nullifier0Account = await connection.getAccountInfo(nullifier0PDA);
185
+ if (nullifier0Account !== null) {
186
+ logger.debug(`UTXO is spent (nullifier0 account exists)`);
187
+ return true;
188
+ }
189
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier1"), Buffer.from(nullifierBytes)], PROGRAM_ID);
190
+ logger.debug(`Derived nullifier1 PDA: ${nullifier1PDA.toBase58()}`);
191
+ const nullifier1Account = await connection.getAccountInfo(nullifier1PDA);
192
+ if (nullifier1Account !== null) {
193
+ logger.debug(`UTXO is spent (nullifier1 account exists)`);
194
+ return true;
195
+ }
196
+ return false;
197
+ }
198
+ catch (error) {
199
+ console.error('Error checking if UTXO is spent:', error);
200
+ await new Promise(resolve => setTimeout(resolve, 3000));
201
+ return await isUtxoSpent(connection, utxo);
202
+ }
203
+ }
204
+ async function areUtxosSpent(connection, utxos) {
205
+ try {
206
+ const allPDAs = [];
207
+ for (let i = 0; i < utxos.length; i++) {
208
+ const utxo = utxos[i];
209
+ const nullifier = await utxo.getNullifier();
210
+ const nullifierBytes = Array.from(leInt2Buff(unstringifyBigInts(nullifier), 32)).reverse();
211
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier0"), Buffer.from(nullifierBytes)], PROGRAM_ID);
212
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync([Buffer.from("nullifier1"), Buffer.from(nullifierBytes)], PROGRAM_ID);
213
+ allPDAs.push({ utxoIndex: i, pda: nullifier0PDA });
214
+ allPDAs.push({ utxoIndex: i, pda: nullifier1PDA });
215
+ }
216
+ const results = await connection.getMultipleAccountsInfo(allPDAs.map((x) => x.pda));
217
+ const spentFlags = new Array(utxos.length).fill(false);
218
+ for (let i = 0; i < allPDAs.length; i++) {
219
+ if (results[i] !== null) {
220
+ spentFlags[allPDAs[i].utxoIndex] = true;
221
+ }
222
+ }
223
+ return spentFlags;
224
+ }
225
+ catch (error) {
226
+ console.error("Error checking if UTXOs are spent:", error);
227
+ await new Promise((resolve) => setTimeout(resolve, 3000));
228
+ return await areUtxosSpent(connection, utxos);
229
+ }
230
+ }
231
+ // Calculate total balance
232
+ export function getBalanceFromUtxos(utxos) {
233
+ const totalBalance = utxos.reduce((sum, utxo) => sum.add(utxo.amount), new BN(0));
234
+ // const LAMPORTS_PER_SOL = new BN(1_000_000_000);
235
+ // const balanceInSol = totalBalance.div(LAMPORTS_PER_SOL);
236
+ // const remainderLamports = totalBalance.mod(LAMPORTS_PER_SOL);
237
+ return { lamports: totalBalance.toNumber() };
238
+ }
239
+ async function decrypt_output(encryptedOutput, encryptionService, utxoKeypair, lightWasm, connection) {
240
+ let res = { status: 'unDecrypted' };
241
+ try {
242
+ if (!encryptedOutput) {
243
+ return { status: 'skipped' };
244
+ }
245
+ // Try to decrypt the UTXO
246
+ res.utxo = await encryptionService.decryptUtxo(encryptedOutput, lightWasm);
247
+ // If we got here, decryption succeeded, so this UTXO belongs to the user
248
+ res.status = 'decrypted';
249
+ // Get the real index from the on-chain commitment account
250
+ try {
251
+ if (!res.utxo) {
252
+ throw new Error('res.utxo undefined');
253
+ }
254
+ const commitment = await res.utxo.getCommitment();
255
+ // Convert decimal commitment string to byte array (same format as in proofs)
256
+ const commitmentBytes = Array.from(leInt2Buff(unstringifyBigInts(commitment), 32)).reverse();
257
+ // Derive the commitment PDA (could be either commitment0 or commitment1)
258
+ // We'll try both seeds since we don't know which one it is
259
+ let commitmentAccount = null;
260
+ let realIndex = null;
261
+ // Try commitment0 seed
262
+ try {
263
+ const [commitment0PDA] = PublicKey.findProgramAddressSync([Buffer.from("commitment0"), Buffer.from(commitmentBytes)], PROGRAM_ID);
264
+ const account0Info = await connection.getAccountInfo(commitment0PDA);
265
+ if (account0Info) {
266
+ // Parse the index from the account data according to CommitmentAccount structure:
267
+ // 0-8: Anchor discriminator
268
+ // 8-40: commitment (32 bytes)
269
+ // 40-44: encrypted_output length (4 bytes)
270
+ // 44-44+len: encrypted_output data
271
+ // 44+len-52+len: index (8 bytes)
272
+ const encryptedOutputLength = account0Info.data.readUInt32LE(40);
273
+ const indexOffset = 44 + encryptedOutputLength;
274
+ const indexBytes = account0Info.data.slice(indexOffset, indexOffset + 8);
275
+ realIndex = new BN(indexBytes, 'le').toNumber();
276
+ }
277
+ }
278
+ catch (e) {
279
+ // Try commitment1 seed if commitment0 fails
280
+ try {
281
+ const [commitment1PDA] = PublicKey.findProgramAddressSync([Buffer.from("commitment1"), Buffer.from(commitmentBytes)], PROGRAM_ID);
282
+ const account1Info = await connection.getAccountInfo(commitment1PDA);
283
+ if (account1Info) {
284
+ // Parse the index from the account data according to CommitmentAccount structure
285
+ const encryptedOutputLength = account1Info.data.readUInt32LE(40);
286
+ const indexOffset = 44 + encryptedOutputLength;
287
+ const indexBytes = account1Info.data.slice(indexOffset, indexOffset + 8);
288
+ realIndex = new BN(indexBytes, 'le').toNumber();
289
+ logger.debug(`Found commitment1 account with index: ${realIndex}`);
290
+ }
291
+ }
292
+ catch (e2) {
293
+ logger.debug(`Could not find commitment account for ${commitment}, using encrypted index: ${res.utxo.index}`);
294
+ }
295
+ }
296
+ // Update the UTXO with the real index if we found it
297
+ if (realIndex !== null) {
298
+ const oldIndex = res.utxo.index;
299
+ res.utxo.index = realIndex;
300
+ }
301
+ }
302
+ catch (error) {
303
+ logger.debug(`Failed to get real index for UTXO: ${error.message}`);
304
+ }
305
+ }
306
+ catch (error) {
307
+ // this UTXO doesn't belong to the user
308
+ }
309
+ return res;
310
+ }
311
+ async function decrypt_outputs(encryptedOutputs, encryptionService, utxoKeypair, lightWasm) {
312
+ let results = [];
313
+ // decript all UTXO
314
+ for (const encryptedOutput of encryptedOutputs) {
315
+ if (!encryptedOutput) {
316
+ results.push({ status: 'skipped' });
317
+ continue;
318
+ }
319
+ try {
320
+ const utxo = await encryptionService.decryptUtxo(encryptedOutput, lightWasm);
321
+ results.push({ status: 'decrypted', utxo, encryptedOutput });
322
+ }
323
+ catch {
324
+ results.push({ status: 'unDecrypted' });
325
+ }
326
+ }
327
+ results = results.filter(r => r.status == 'decrypted');
328
+ if (!results.length) {
329
+ return [];
330
+ }
331
+ // update utxo index
332
+ if (results.length > 0) {
333
+ let encrypted_outputs = results.map(r => r.encryptedOutput);
334
+ let url = INDEXER_API_URL + `/utxos/indices`;
335
+ let res = await fetch(url, {
336
+ method: 'POST', headers: { "Content-Type": "application/json" },
337
+ body: JSON.stringify({ encrypted_outputs })
338
+ });
339
+ let j = await res.json();
340
+ if (!j.indices || !Array.isArray(j.indices) || j.indices.length != encrypted_outputs.length) {
341
+ throw new Error('failed fetching /utxos/indices');
342
+ }
343
+ for (let i = 0; i < results.length; i++) {
344
+ let utxo = results[i].utxo;
345
+ if (utxo.index !== j.indices[i] && typeof j.indices[i] == 'number') {
346
+ logger.debug(`Updated UTXO index from ${utxo.index} to ${j.indices[i]}`);
347
+ utxo.index = j.indices[i];
348
+ }
349
+ }
350
+ }
351
+ return results;
352
+ }
@@ -0,0 +1,61 @@
1
+ import { Keypair, PublicKey } from '@solana/web3.js';
2
+ import { type LoggerFn } from './utils/logger.js';
3
+ export declare class PrivacyCash {
4
+ private connection;
5
+ publicKey: PublicKey;
6
+ private encryptionService;
7
+ private keypair;
8
+ private isRuning?;
9
+ private status;
10
+ constructor({ RPC_url, owner, enableDebug }: {
11
+ RPC_url: string;
12
+ owner: string | number[] | Uint8Array | Keypair;
13
+ enableDebug?: boolean;
14
+ });
15
+ setLogger(loger: LoggerFn): this;
16
+ /**
17
+ * Clears the cache of utxos.
18
+ *
19
+ * By default, downloaded utxos will be cached in the local storage. Thus the next time when you makes another
20
+ * deposit or withdraw or getPrivateBalance, the SDK only fetches the utxos that are not in the cache.
21
+ *
22
+ * This method clears the cache of utxos.
23
+ */
24
+ clearCache(): Promise<this>;
25
+ /**
26
+ * Deposit SOL to the Privacy Cash.
27
+ *
28
+ * Lamports is the amount of SOL in lamports. e.g. if you want to deposit 0.01 SOL (10000000 lamports), call deposit({ lamports: 10000000 })
29
+ */
30
+ deposit({ lamports }: {
31
+ lamports: number;
32
+ }): Promise<{
33
+ tx: string;
34
+ }>;
35
+ /**
36
+ * Withdraw SOL from the Privacy Cash.
37
+ *
38
+ * Lamports is the amount of SOL in lamports. e.g. if you want to withdraw 0.01 SOL (10000000 lamports), call withdraw({ lamports: 10000000 })
39
+ */
40
+ withdraw({ lamports, recipientAddress }: {
41
+ lamports: number;
42
+ recipientAddress?: string;
43
+ }): Promise<{
44
+ isPartial: boolean;
45
+ tx: string;
46
+ recipient: string;
47
+ amount_in_lamports: number;
48
+ fee_in_lamports: number;
49
+ }>;
50
+ /**
51
+ * Returns the amount of lamports current wallet has in Privacy Cash.
52
+ */
53
+ getPrivateBalance(): Promise<{
54
+ lamports: number;
55
+ }>;
56
+ /**
57
+ * Returns true if the code is running in a browser.
58
+ */
59
+ isBrowser(): boolean;
60
+ startStatusRender(): Promise<void>;
61
+ }
package/dist/index.js ADDED
@@ -0,0 +1,169 @@
1
+ import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
2
+ import { deposit } from './deposit.js';
3
+ import { getBalanceFromUtxos, getUtxos, localstorageKey } from './getUtxos.js';
4
+ import { LSK_ENCRYPTED_OUTPUTS, LSK_FETCH_OFFSET } from './utils/constants.js';
5
+ import { logger, setLogger } from './utils/logger.js';
6
+ import { EncryptionService } from './utils/encryption.js';
7
+ import { WasmFactory } from '@lightprotocol/hasher.rs';
8
+ import bs58 from 'bs58';
9
+ import { withdraw } from './withdraw.js';
10
+ import { LocalStorage } from "node-localstorage";
11
+ import path from 'node:path';
12
+ let storage = new LocalStorage(path.join(process.cwd(), "cache"));
13
+ export class PrivacyCash {
14
+ connection;
15
+ publicKey;
16
+ encryptionService;
17
+ keypair;
18
+ isRuning = false;
19
+ status = '';
20
+ constructor({ RPC_url, owner, enableDebug }) {
21
+ let keypair = getSolanaKeypair(owner);
22
+ if (!keypair) {
23
+ throw new Error('param "owner" is not a valid Private Key or Keypair');
24
+ }
25
+ this.keypair = keypair;
26
+ this.connection = new Connection(RPC_url, 'confirmed');
27
+ this.publicKey = keypair.publicKey;
28
+ this.encryptionService = new EncryptionService();
29
+ this.encryptionService.deriveEncryptionKeyFromWallet(this.keypair);
30
+ if (!enableDebug) {
31
+ this.startStatusRender();
32
+ this.setLogger((level, message) => {
33
+ if (level == 'info') {
34
+ this.status = message;
35
+ }
36
+ else if (level == 'error') {
37
+ console.log('error message: ', message);
38
+ }
39
+ });
40
+ }
41
+ }
42
+ setLogger(loger) {
43
+ setLogger(loger);
44
+ return this;
45
+ }
46
+ /**
47
+ * Clears the cache of utxos.
48
+ *
49
+ * By default, downloaded utxos will be cached in the local storage. Thus the next time when you makes another
50
+ * deposit or withdraw or getPrivateBalance, the SDK only fetches the utxos that are not in the cache.
51
+ *
52
+ * This method clears the cache of utxos.
53
+ */
54
+ async clearCache() {
55
+ if (!this.publicKey) {
56
+ return this;
57
+ }
58
+ storage.removeItem(LSK_FETCH_OFFSET + localstorageKey(this.publicKey));
59
+ storage.removeItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(this.publicKey));
60
+ return this;
61
+ }
62
+ /**
63
+ * Deposit SOL to the Privacy Cash.
64
+ *
65
+ * Lamports is the amount of SOL in lamports. e.g. if you want to deposit 0.01 SOL (10000000 lamports), call deposit({ lamports: 10000000 })
66
+ */
67
+ async deposit({ lamports }) {
68
+ this.isRuning = true;
69
+ logger.info('start depositting');
70
+ let lightWasm = await WasmFactory.getInstance();
71
+ let res = await deposit({
72
+ lightWasm,
73
+ amount_in_lamports: lamports,
74
+ connection: this.connection,
75
+ encryptionService: this.encryptionService,
76
+ publicKey: this.publicKey,
77
+ transactionSigner: async (tx) => {
78
+ tx.sign([this.keypair]);
79
+ return tx;
80
+ },
81
+ keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
82
+ storage
83
+ });
84
+ this.isRuning = false;
85
+ return res;
86
+ }
87
+ /**
88
+ * Withdraw SOL from the Privacy Cash.
89
+ *
90
+ * Lamports is the amount of SOL in lamports. e.g. if you want to withdraw 0.01 SOL (10000000 lamports), call withdraw({ lamports: 10000000 })
91
+ */
92
+ async withdraw({ lamports, recipientAddress }) {
93
+ this.isRuning = true;
94
+ logger.info('start withdrawing');
95
+ let lightWasm = await WasmFactory.getInstance();
96
+ let recipient = recipientAddress ? new PublicKey(recipientAddress) : this.publicKey;
97
+ let res = await withdraw({
98
+ lightWasm,
99
+ amount_in_lamports: lamports,
100
+ connection: this.connection,
101
+ encryptionService: this.encryptionService,
102
+ publicKey: this.publicKey,
103
+ recipient,
104
+ keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
105
+ storage
106
+ });
107
+ console.log(`Withdraw successful. Recipient ${recipient} received ${res.amount_in_lamports / LAMPORTS_PER_SOL} SOL, with ${res.fee_in_lamports / LAMPORTS_PER_SOL} SOL relayers fees`);
108
+ this.isRuning = false;
109
+ return res;
110
+ }
111
+ /**
112
+ * Returns the amount of lamports current wallet has in Privacy Cash.
113
+ */
114
+ async getPrivateBalance() {
115
+ logger.info('getting private balance');
116
+ this.isRuning = true;
117
+ let utxos = await getUtxos({ publicKey: this.publicKey, connection: this.connection, encryptionService: this.encryptionService, storage });
118
+ this.isRuning = false;
119
+ return getBalanceFromUtxos(utxos);
120
+ }
121
+ /**
122
+ * Returns true if the code is running in a browser.
123
+ */
124
+ isBrowser() {
125
+ return typeof window !== "undefined";
126
+ }
127
+ async startStatusRender() {
128
+ let frames = ['-', '\\', '|', '/'];
129
+ let i = 0;
130
+ while (true) {
131
+ if (this.isRuning) {
132
+ let k = i % frames.length;
133
+ i++;
134
+ stdWrite(this.status, frames[k]);
135
+ }
136
+ await new Promise(r => setTimeout(r, 250));
137
+ }
138
+ }
139
+ }
140
+ function getSolanaKeypair(secret) {
141
+ try {
142
+ if (secret instanceof Keypair) {
143
+ return secret;
144
+ }
145
+ let keyArray;
146
+ if (typeof secret === "string") {
147
+ keyArray = bs58.decode(secret);
148
+ }
149
+ else if (secret instanceof Uint8Array) {
150
+ keyArray = secret;
151
+ }
152
+ else {
153
+ // number[]
154
+ keyArray = Uint8Array.from(secret);
155
+ }
156
+ if (keyArray.length !== 32 && keyArray.length !== 64) {
157
+ return null;
158
+ }
159
+ return Keypair.fromSecretKey(keyArray);
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function stdWrite(status, frame) {
166
+ let blue = "\x1b[34m";
167
+ let reset = "\x1b[0m";
168
+ process.stdout.write(`${frame}status: ${blue}${status}${reset}\r`);
169
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Keypair module for ZK Cash
3
+ *
4
+ * Provides cryptographic keypair functionality for the ZK Cash system
5
+ * Based on: https://github.com/tornadocash/tornado-nova
6
+ */
7
+ import BN from 'bn.js';
8
+ import * as hasher from '@lightprotocol/hasher.rs';
9
+ /**
10
+ * Simplified version of Keypair
11
+ */
12
+ export declare class Keypair {
13
+ privkey: BN;
14
+ pubkey: BN;
15
+ private lightWasm;
16
+ constructor(privkeyHex: string, lightWasm: hasher.LightWasm);
17
+ /**
18
+ * Sign a message using keypair private key
19
+ *
20
+ * @param {string|number|BigNumber} commitment a hex string with commitment
21
+ * @param {string|number|BigNumber} merklePath a hex string with merkle path
22
+ * @returns {BigNumber} a hex string with signature
23
+ */
24
+ sign(commitment: string, merklePath: string): string;
25
+ static generateNew(lightWasm: hasher.LightWasm): Promise<Keypair>;
26
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Keypair module for ZK Cash
3
+ *
4
+ * Provides cryptographic keypair functionality for the ZK Cash system
5
+ * Based on: https://github.com/tornadocash/tornado-nova
6
+ */
7
+ import BN from 'bn.js';
8
+ import { ethers } from 'ethers';
9
+ // Field size constant
10
+ const FIELD_SIZE = new BN('21888242871839275222246405745257275088548364400416034343698204186575808495617');
11
+ /**
12
+ * Simplified version of Keypair
13
+ */
14
+ export class Keypair {
15
+ privkey;
16
+ pubkey;
17
+ lightWasm;
18
+ constructor(privkeyHex, lightWasm) {
19
+ const rawDecimal = BigInt(privkeyHex);
20
+ this.privkey = new BN((rawDecimal % BigInt(FIELD_SIZE.toString())).toString());
21
+ this.lightWasm = lightWasm;
22
+ // TODO: lazily compute pubkey
23
+ this.pubkey = new BN(this.lightWasm.poseidonHashString([this.privkey.toString()]));
24
+ }
25
+ /**
26
+ * Sign a message using keypair private key
27
+ *
28
+ * @param {string|number|BigNumber} commitment a hex string with commitment
29
+ * @param {string|number|BigNumber} merklePath a hex string with merkle path
30
+ * @returns {BigNumber} a hex string with signature
31
+ */
32
+ sign(commitment, merklePath) {
33
+ return this.lightWasm.poseidonHashString([this.privkey.toString(), commitment, merklePath]);
34
+ }
35
+ static async generateNew(lightWasm) {
36
+ // Tornado Cash Nova uses ethers.js to generate a random private key
37
+ // We can't generate Solana keypairs because it won't fit in the field size
38
+ // It's OK to use ethereum secret keys, because the secret key is only used for the proof generation.
39
+ // Namely, it's used to guarantee the uniqueness of the nullifier.
40
+ const wallet = ethers.Wallet.createRandom();
41
+ return new Keypair(wallet.privateKey, lightWasm);
42
+ }
43
+ }