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