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,52 @@
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
+
8
+ import BN from 'bn.js';
9
+ import { ethers } from 'ethers';
10
+ import * as hasher from '@lightprotocol/hasher.rs';
11
+
12
+ // Field size constant
13
+ const FIELD_SIZE = new BN(
14
+ '21888242871839275222246405745257275088548364400416034343698204186575808495617'
15
+ );
16
+
17
+ /**
18
+ * Simplified version of Keypair
19
+ */
20
+ export class Keypair {
21
+ public privkey: BN;
22
+ public pubkey: BN;
23
+ private lightWasm: hasher.LightWasm;
24
+
25
+ constructor(privkeyHex: string, lightWasm: hasher.LightWasm) {
26
+ const rawDecimal = BigInt(privkeyHex);
27
+ this.privkey = new BN((rawDecimal % BigInt(FIELD_SIZE.toString())).toString());
28
+ this.lightWasm = lightWasm;
29
+ // TODO: lazily compute pubkey
30
+ this.pubkey = new BN(this.lightWasm.poseidonHashString([this.privkey.toString()]));
31
+ }
32
+
33
+ /**
34
+ * Sign a message using keypair private key
35
+ *
36
+ * @param {string|number|BigNumber} commitment a hex string with commitment
37
+ * @param {string|number|BigNumber} merklePath a hex string with merkle path
38
+ * @returns {BigNumber} a hex string with signature
39
+ */
40
+ sign(commitment: string, merklePath: string): string {
41
+ return this.lightWasm.poseidonHashString([this.privkey.toString(), commitment, merklePath]);
42
+ }
43
+
44
+ static async generateNew(lightWasm: hasher.LightWasm): Promise<Keypair> {
45
+ // Tornado Cash Nova uses ethers.js to generate a random private key
46
+ // We can't generate Solana keypairs because it won't fit in the field size
47
+ // It's OK to use ethereum secret keys, because the secret key is only used for the proof generation.
48
+ // Namely, it's used to guarantee the uniqueness of the nullifier.
49
+ const wallet = ethers.Wallet.createRandom();
50
+ return new Keypair(wallet.privateKey, lightWasm);
51
+ }
52
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * UTXO (Unspent Transaction Output) module for ZK Cash
3
+ *
4
+ * Provides UTXO functionality for the ZK Cash system
5
+ * Based on: https://github.com/tornadocash/tornado-nova
6
+ */
7
+
8
+ import BN from 'bn.js';
9
+ import { Keypair } from './keypair.js';
10
+ import * as hasher from '@lightprotocol/hasher.rs';
11
+ import { ethers } from 'ethers';
12
+ /**
13
+ * Simplified Utxo class inspired by Tornado Cash Nova
14
+ * Based on: https://github.com/tornadocash/tornado-nova/blob/f9264eeffe48bf5e04e19d8086ee6ec58cdf0d9e/src/utxo.js
15
+ */
16
+ export class Utxo {
17
+ amount: BN;
18
+ blinding: BN;
19
+ keypair: Keypair;
20
+ index: number;
21
+ mintAddress: string;
22
+ version: 'v1' | 'v2';
23
+ private lightWasm: hasher.LightWasm;
24
+
25
+ constructor({
26
+ lightWasm,
27
+ amount = new BN(0),
28
+ /**
29
+ * Tornado nova doesn't use solana eddsa with curve 25519 but their own "keypair"
30
+ * which is:
31
+ * - private key: random [31;u8]
32
+ * - public key: PoseidonHash(privateKey)
33
+ *
34
+ * Generate a new keypair for each UTXO
35
+ */
36
+ keypair,
37
+ blinding = new BN(Math.floor(Math.random() * 1000000000)), // Use fixed value for consistency instead of randomBN()
38
+ index = 0,
39
+ mintAddress = '11111111111111111111111111111112', // Default to Solana native SOL mint address,
40
+ version = 'v2'
41
+ }: {
42
+ lightWasm: hasher.LightWasm,
43
+ amount?: BN | number | string,
44
+ keypair?: Keypair,
45
+ blinding?: BN | number | string,
46
+ index?: number,
47
+ mintAddress?: string,
48
+ version?: 'v1' | 'v2'
49
+ }) {
50
+ this.amount = new BN(amount.toString());
51
+ this.blinding = new BN(blinding.toString());
52
+ this.lightWasm = lightWasm;
53
+ this.keypair = keypair || new Keypair(ethers.Wallet.createRandom().privateKey, lightWasm);
54
+ this.index = index;
55
+ this.mintAddress = mintAddress;
56
+ this.version = version;
57
+ }
58
+
59
+ async getCommitment(): Promise<string> {
60
+ return this.lightWasm.poseidonHashString([this.amount.toString(), this.keypair.pubkey.toString(), this.blinding.toString(), this.mintAddress]);
61
+ }
62
+
63
+ async getNullifier(): Promise<string> {
64
+ const commitmentValue = await this.getCommitment();
65
+ const signature = this.keypair.sign(commitmentValue, new BN(this.index).toString());
66
+
67
+ return this.lightWasm.poseidonHashString([commitmentValue, new BN(this.index).toString(), signature]);
68
+ }
69
+
70
+ /**
71
+ * Log all the UTXO's public properties and derived values in JSON format
72
+ * @returns Promise that resolves once all logging is complete
73
+ */
74
+ async log(): Promise<void> {
75
+ // Prepare the UTXO data object
76
+ const utxoData: any = {
77
+ amount: this.amount.toString(),
78
+ blinding: this.blinding.toString(),
79
+ index: this.index,
80
+ mintAddress: this.mintAddress,
81
+ keypair: {
82
+ pubkey: this.keypair.pubkey.toString()
83
+ }
84
+ };
85
+
86
+ // Add derived values
87
+ try {
88
+ utxoData.commitment = await this.getCommitment();
89
+ utxoData.nullifier = await this.getNullifier();
90
+ } catch (error: any) {
91
+ utxoData.error = error.message;
92
+ }
93
+
94
+ // Output as formatted JSON
95
+ console.log(JSON.stringify(utxoData, null, 2));
96
+ }
97
+ }
@@ -0,0 +1,29 @@
1
+ import {
2
+ Connection,
3
+ PublicKey
4
+ } from '@solana/web3.js';
5
+
6
+ /**
7
+ * Helper function to use an existing ALT (recommended for production)
8
+ * Use create_alt.ts to create the ALT once, then hardcode the address and use this function
9
+ */
10
+ export async function useExistingALT(
11
+ connection: Connection,
12
+ altAddress: PublicKey
13
+ ): Promise<{ value: any } | null> {
14
+ try {
15
+ console.log(`Using existing ALT: ${altAddress.toString()}`);
16
+ const altAccount = await connection.getAddressLookupTable(altAddress);
17
+
18
+ if (altAccount.value) {
19
+ console.log(`✅ ALT found with ${altAccount.value.state.addresses.length} addresses`);
20
+ } else {
21
+ console.log('❌ ALT not found');
22
+ }
23
+
24
+ return altAccount;
25
+ } catch (error) {
26
+ console.error('Error getting existing ALT:', error);
27
+ return null;
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ import BN from 'bn.js';
3
+
4
+ export const FIELD_SIZE = new BN('21888242871839275222246405745257275088548364400416034343698204186575808495617')
5
+
6
+ export const PROGRAM_ID = new PublicKey('9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD');
7
+
8
+ export const DEPLOYER_ID = new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM')
9
+
10
+ export const FEE_RECIPIENT = new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM')
11
+
12
+ export const FETCH_UTXOS_GROUP_SIZE = 2000
13
+
14
+ export const TRANSACT_IX_DISCRIMINATOR = Buffer.from([217, 149, 130, 143, 221, 52, 252, 119]);
15
+
16
+ export const MERKLE_TREE_DEPTH = 26;
17
+
18
+ export const ALT_ADDRESS = new PublicKey('72bpRay17JKp4k8H87p7ieU9C6aRDy5yCqwvtpTN2wuU');
19
+
20
+ export const INDEXER_API_URL = process.env.NEXT_PUBLIC_INDEXER_API_URL ?? 'https://api3.privacycash.org';
21
+
22
+ export const SIGN_MESSAGE = `Privacy Money account sign in`
23
+
24
+ // localStorage cache keys
25
+ export const LSK_FETCH_OFFSET = 'fetch_offset'
26
+ export const LSK_ENCRYPTED_OUTPUTS = 'encrypted_outputs'
@@ -0,0 +1,461 @@
1
+ import { Keypair, PublicKey } from '@solana/web3.js';
2
+ import nacl from 'tweetnacl';
3
+ import * as crypto from 'crypto';
4
+ import { Utxo } from '../models/utxo.js';
5
+ import { WasmFactory } from '@lightprotocol/hasher.rs';
6
+ import { Keypair as UtxoKeypair } from '../models/keypair.js';
7
+ import { keccak256 } from '@ethersproject/keccak256';
8
+ import { PROGRAM_ID, TRANSACT_IX_DISCRIMINATOR } from './constants.js';
9
+ import BN from 'bn.js';
10
+
11
+
12
+ /**
13
+ * Represents a UTXO with minimal required fields
14
+ */
15
+ export interface UtxoData {
16
+ amount: string;
17
+ blinding: string;
18
+ index: number | string;
19
+ // Optional additional fields
20
+ [key: string]: any;
21
+ }
22
+
23
+ export interface EncryptionKey {
24
+ v1: Uint8Array;
25
+ v2: Uint8Array;
26
+ }
27
+
28
+ /**
29
+ * Service for handling encryption and decryption of UTXO data
30
+ */
31
+ export class EncryptionService {// Version identifier for encryption scheme (8-byte version)
32
+ public static readonly ENCRYPTION_VERSION_V2 = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]); // Version 2
33
+
34
+ private encryptionKeyV1: Uint8Array | null = null;
35
+ private encryptionKeyV2: Uint8Array | null = null;
36
+ private utxoPrivateKeyV1: string | null = null;
37
+ private utxoPrivateKeyV2: string | null = null;
38
+
39
+ /**
40
+ * Generate an encryption key from a signature
41
+ * @param signature The user's signature
42
+ * @returns The generated encryption key
43
+ */
44
+ public deriveEncryptionKeyFromSignature(signature: Uint8Array): EncryptionKey {
45
+ // Extract the first 31 bytes of the signature to create a deterministic key (legacy method)
46
+ const encryptionKeyV1 = signature.slice(0, 31);
47
+
48
+ // Store the V1 key in the service
49
+ this.encryptionKeyV1 = encryptionKeyV1;
50
+
51
+ // Precompute and cache the UTXO private key
52
+ const hashedSeedV1 = crypto.createHash('sha256').update(encryptionKeyV1).digest();
53
+ this.utxoPrivateKeyV1 = '0x' + hashedSeedV1.toString('hex');
54
+
55
+ // Use Keccak256 to derive a full 32-byte encryption key from the signature
56
+ const encryptionKeyV2 = Buffer.from(keccak256(signature).slice(2), 'hex');
57
+
58
+ // Store the V2 key in the service
59
+ this.encryptionKeyV2 = encryptionKeyV2;
60
+
61
+ // Precompute and cache the UTXO private key
62
+ const hashedSeedV2 = Buffer.from(keccak256(encryptionKeyV2).slice(2), 'hex');
63
+ this.utxoPrivateKeyV2 = '0x' + hashedSeedV2.toString('hex');
64
+
65
+ return {
66
+ v1: this.encryptionKeyV1,
67
+ v2: this.encryptionKeyV2
68
+ };
69
+
70
+ }
71
+
72
+ /**
73
+ * Generate an encryption key from a wallet keypair (V2 format)
74
+ * @param keypair The Solana keypair to derive the encryption key from
75
+ * @returns The generated encryption key
76
+ */
77
+ public deriveEncryptionKeyFromWallet(keypair: Keypair): EncryptionKey {
78
+ // Sign a constant message with the keypair
79
+ const message = Buffer.from('Privacy Money account sign in');
80
+ const signature = nacl.sign.detached(message, keypair.secretKey);
81
+ return this.deriveEncryptionKeyFromSignature(signature)
82
+ }
83
+
84
+ /**
85
+ * Encrypt data with the stored encryption key
86
+ * @param data The data to encrypt
87
+ * @returns The encrypted data as a Buffer
88
+ * @throws Error if the encryption key has not been generated
89
+ */
90
+ public encrypt(data: Buffer | string): Buffer {
91
+ if (!this.encryptionKeyV2) {
92
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
93
+ }
94
+
95
+ // Convert string to Buffer if needed
96
+ const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
97
+
98
+ // Generate a standard initialization vector (12 bytes for GCM)
99
+ const iv = crypto.randomBytes(12);
100
+
101
+ // Use the full 32-byte V2 encryption key for AES-256
102
+ const key = Buffer.from(this.encryptionKeyV2);
103
+
104
+ // Use AES-256-GCM for authenticated encryption
105
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
106
+ const encryptedData = Buffer.concat([
107
+ cipher.update(dataBuffer),
108
+ cipher.final()
109
+ ]);
110
+
111
+ // Get the authentication tag from GCM (16 bytes)
112
+ const authTag = cipher.getAuthTag();
113
+
114
+ // Version 2 format: [version(8)] + [IV(12)] + [authTag(16)] + [encryptedData]
115
+ return Buffer.concat([
116
+ EncryptionService.ENCRYPTION_VERSION_V2,
117
+ iv,
118
+ authTag,
119
+ encryptedData
120
+ ]);
121
+ }
122
+
123
+ // v1 encryption, only used for testing now
124
+ public encryptDecryptedDoNotUse(data: Buffer | string): Buffer {
125
+ if (!this.encryptionKeyV1) {
126
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
127
+ }
128
+
129
+ // Convert string to Buffer if needed
130
+ const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
131
+
132
+ // Generate a standard initialization vector (16 bytes)
133
+ const iv = crypto.randomBytes(16);
134
+
135
+ // Create a key from our encryption key (using only first 16 bytes for AES-128)
136
+ const key = Buffer.from(this.encryptionKeyV1).slice(0, 16);
137
+
138
+ // Use a more compact encryption algorithm (aes-128-ctr)
139
+ const cipher = crypto.createCipheriv('aes-128-ctr', key, iv);
140
+ const encryptedData = Buffer.concat([
141
+ cipher.update(dataBuffer),
142
+ cipher.final()
143
+ ]);
144
+
145
+ // Create an authentication tag (HMAC) to verify decryption with correct key
146
+ const hmacKey = Buffer.from(this.encryptionKeyV1).slice(16, 31);
147
+ const hmac = crypto.createHmac('sha256', hmacKey);
148
+ hmac.update(iv);
149
+ hmac.update(encryptedData);
150
+ const authTag = hmac.digest().slice(0, 16); // Use first 16 bytes of HMAC as auth tag
151
+
152
+ // Combine IV, auth tag and encrypted data
153
+ return Buffer.concat([iv, authTag, encryptedData]);
154
+ }
155
+
156
+ /**
157
+ * Decrypt data with the stored encryption key
158
+ * @param encryptedData The encrypted data to decrypt
159
+ * @returns The decrypted data as a Buffer
160
+ * @throws Error if the encryption key has not been generated or if the wrong key is used
161
+ */
162
+ public decrypt(encryptedData: Buffer): Buffer {
163
+ // Check if this is the new version format (starts with 8-byte version identifier)
164
+ if (encryptedData.length >= 8 && encryptedData.subarray(0, 8).equals(EncryptionService.ENCRYPTION_VERSION_V2)) {
165
+ if (!this.encryptionKeyV2) {
166
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
167
+ }
168
+ return this.decryptV2(encryptedData);
169
+ } else {
170
+ // V1 format - need V1 key or keypair to derive it
171
+ if (!this.encryptionKeyV1) {
172
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
173
+ }
174
+ return this.decryptV1(encryptedData);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Decrypt data using the old V1 format (120-bit HMAC with SHA256)
180
+ * @param encryptedData The encrypted data to decrypt
181
+ * @param keypair Optional keypair to derive V1 key for backward compatibility
182
+ * @returns The decrypted data as a Buffer
183
+ */
184
+ private decryptV1(encryptedData: Buffer): Buffer {
185
+ if (!this.encryptionKeyV1) {
186
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
187
+ }
188
+
189
+ // Extract the IV from the first 16 bytes
190
+ const iv = encryptedData.slice(0, 16);
191
+ // Extract the auth tag from the next 16 bytes
192
+ const authTag = encryptedData.slice(16, 32);
193
+ // The rest is the actual encrypted data
194
+ const data = encryptedData.slice(32);
195
+
196
+ // Verify the authentication tag
197
+ const hmacKey = Buffer.from(this.encryptionKeyV1).slice(16, 31);
198
+ const hmac = crypto.createHmac('sha256', hmacKey);
199
+ hmac.update(iv);
200
+ hmac.update(data);
201
+ const calculatedTag = hmac.digest().slice(0, 16);
202
+
203
+ // Compare tags - if they don't match, the key is wrong
204
+ if (!this.timingSafeEqual(authTag, calculatedTag)) {
205
+ throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
206
+ }
207
+
208
+ // Create a key from our encryption key (using only first 16 bytes for AES-128)
209
+ const key = Buffer.from(this.encryptionKeyV1).slice(0, 16);
210
+
211
+ // Use the same algorithm as in encrypt
212
+ const decipher = crypto.createDecipheriv('aes-128-ctr', key, iv);
213
+
214
+ try {
215
+ return Buffer.concat([
216
+ decipher.update(data),
217
+ decipher.final()
218
+ ]);
219
+ } catch (error) {
220
+ throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
221
+ }
222
+ }
223
+
224
+ // Custom timingSafeEqual for browser compatibility
225
+ private timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
226
+ if (a.length !== b.length) {
227
+ return false;
228
+ }
229
+ let diff = 0;
230
+ for (let i = 0; i < a.length; i++) {
231
+ diff |= a[i] ^ b[i];
232
+ }
233
+ return diff === 0;
234
+ }
235
+
236
+ /**
237
+ * Decrypt data using the new V2 format (256-bit Keccak HMAC)
238
+ * @param encryptedData The encrypted data to decrypt
239
+ * @returns The decrypted data as a Buffer
240
+ */
241
+ private decryptV2(encryptedData: Buffer): Buffer {
242
+ if (!this.encryptionKeyV2) {
243
+ throw new Error('encryptionKeyV2 not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
244
+ }
245
+
246
+ // Skip 8-byte version identifier and extract components for GCM format
247
+ const iv = encryptedData.slice(8, 20); // bytes 8-19 (12 bytes for GCM)
248
+ const authTag = encryptedData.slice(20, 36); // bytes 20-35 (16 bytes for GCM)
249
+ const data = encryptedData.slice(36); // remaining bytes
250
+
251
+ // Use the full 32-byte V2 encryption key for AES-256
252
+ const key = Buffer.from(this.encryptionKeyV2!);
253
+
254
+ // Use AES-256-GCM for authenticated decryption
255
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
256
+ decipher.setAuthTag(authTag);
257
+
258
+ try {
259
+ return Buffer.concat([
260
+ decipher.update(data),
261
+ decipher.final()
262
+ ]);
263
+ } catch (error) {
264
+ throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Reset the encryption keys (mainly for testing purposes)
270
+ */
271
+ public resetEncryptionKey(): void {
272
+ this.encryptionKeyV1 = null;
273
+ this.encryptionKeyV2 = null;
274
+ this.utxoPrivateKeyV1 = null;
275
+ this.utxoPrivateKeyV2 = null;
276
+ }
277
+
278
+ /**
279
+ * Encrypt a UTXO using a compact pipe-delimited format
280
+ * Always uses V2 encryption format. The UTXO's version property is used only for key derivation.
281
+ * @param utxo The UTXO to encrypt (includes version property)
282
+ * @returns The encrypted UTXO data as a Buffer
283
+ * @throws Error if the V2 encryption key has not been set
284
+ */
285
+ public encryptUtxo(utxo: Utxo): Buffer {
286
+ if (!this.encryptionKeyV2) {
287
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
288
+ }
289
+
290
+ // Create a compact string representation using pipe delimiter
291
+ // Version is stored in the UTXO model, not in the encrypted content
292
+ const utxoString = `${utxo.amount.toString()}|${utxo.blinding.toString()}|${utxo.index}|${utxo.mintAddress}`;
293
+
294
+ // Always use V2 encryption format (which adds version byte 0x02 at the beginning)
295
+ return this.encrypt(utxoString);
296
+ }
297
+
298
+ // Deprecated, only used for testing now
299
+ public encryptUtxoDecryptedDoNotUse(utxo: Utxo): Buffer {
300
+ if (!this.encryptionKeyV2) {
301
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
302
+ }
303
+
304
+ const utxoString = `${utxo.amount.toString()}|${utxo.blinding.toString()}|${utxo.index}|${utxo.mintAddress}`;
305
+
306
+ return this.encryptDecryptedDoNotUse(utxoString);
307
+ }
308
+
309
+ public getEncryptionKeyVersion(encryptedData: Buffer | string): 'v1' | 'v2' {
310
+ const buffer = typeof encryptedData === 'string' ? Buffer.from(encryptedData, 'hex') : encryptedData;
311
+
312
+ if (buffer.length >= 8 && buffer.subarray(0, 8).equals(EncryptionService.ENCRYPTION_VERSION_V2)) {
313
+ // V2 encryption format → V2 UTXO
314
+ return 'v2';
315
+ } else {
316
+ // V1 encryption format → UTXO
317
+ return 'v1';
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Decrypt an encrypted UTXO and parse it to a Utxo instance
323
+ * Automatically detects the UTXO version based on the encryption format
324
+ * @param encryptedData The encrypted UTXO data
325
+ * @param keypair The UTXO keypair to use for the decrypted UTXO
326
+ * @param lightWasm Optional LightWasm instance. If not provided, a new one will be created
327
+ * @param walletKeypair Optional wallet keypair for V1 backward compatibility
328
+ * @returns Promise resolving to the decrypted Utxo instance
329
+ * @throws Error if the encryption key has not been set or if decryption fails
330
+ */
331
+ public async decryptUtxo(
332
+ encryptedData: Buffer | string,
333
+ lightWasm?: any
334
+ ): Promise<Utxo> {
335
+ // Convert hex string to Buffer if needed
336
+ const encryptedBuffer = typeof encryptedData === 'string'
337
+ ? Buffer.from(encryptedData, 'hex')
338
+ : encryptedData;
339
+
340
+ // Detect UTXO version based on encryption format
341
+ let utxoVersion = this.getEncryptionKeyVersion(encryptedBuffer)
342
+
343
+ // The decrypt() method already handles encryption format version detection (V1 vs V2)
344
+ // It checks the first byte to determine whether to use decryptV1() or decryptV2()
345
+ const decrypted = this.decrypt(encryptedBuffer);
346
+
347
+ // Parse the pipe-delimited format: amount|blinding|index|mintAddress
348
+ const decryptedStr = decrypted.toString();
349
+ const parts = decryptedStr.split('|');
350
+
351
+ if (parts.length !== 4) {
352
+ throw new Error('Invalid UTXO format after decryption');
353
+ }
354
+
355
+ const [amount, blinding, index, mintAddress] = parts;
356
+
357
+ if (!amount || !blinding || index === undefined || mintAddress === undefined) {
358
+ throw new Error('Invalid UTXO format after decryption');
359
+ }
360
+
361
+ // Get or create a LightWasm instance
362
+ const wasmInstance = lightWasm || await WasmFactory.getInstance();
363
+
364
+ const privateKey = this.getUtxoPrivateKeyWithVersion(utxoVersion);
365
+
366
+ // Create a Utxo instance with the detected version
367
+ const utxo = new Utxo({
368
+ lightWasm: wasmInstance,
369
+ amount: amount,
370
+ blinding: blinding,
371
+ keypair: new UtxoKeypair(privateKey, wasmInstance),
372
+ index: Number(index),
373
+ mintAddress: mintAddress,
374
+ version: utxoVersion
375
+ });
376
+
377
+ return utxo;
378
+ }
379
+
380
+ public getUtxoPrivateKeyWithVersion(version: 'v1' | 'v2'): string {
381
+ if (version === 'v1') {
382
+ return this.getUtxoPrivateKeyV1();
383
+ }
384
+
385
+ return this.getUtxoPrivateKeyV2();
386
+ }
387
+
388
+ public deriveUtxoPrivateKey(encryptedData?: Buffer | string): string {
389
+ if (encryptedData && this.getEncryptionKeyVersion(encryptedData) === 'v2') {
390
+ return this.getUtxoPrivateKeyWithVersion('v2');
391
+ }
392
+
393
+ return this.getUtxoPrivateKeyWithVersion('v1');
394
+ }
395
+
396
+ public hasUtxoPrivateKeyWithVersion(version: 'v1' | 'v2'): boolean {
397
+ if (version === 'v1') {
398
+ return !!this.utxoPrivateKeyV1;
399
+ }
400
+
401
+ return !!this.utxoPrivateKeyV2;
402
+ }
403
+
404
+ /**
405
+ * Get the cached V1 UTXO private key
406
+ * @returns A private key in hex format that can be used to create a UTXO keypair
407
+ * @throws Error if V1 encryption key has not been set
408
+ */
409
+ public getUtxoPrivateKeyV1(): string {
410
+ if (!this.utxoPrivateKeyV1) {
411
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
412
+ }
413
+ return this.utxoPrivateKeyV1;
414
+ }
415
+
416
+ /**
417
+ * Get the cached V2 UTXO private key
418
+ * @returns A private key in hex format that can be used to create a UTXO keypair
419
+ * @throws Error if V2 encryption key has not been set
420
+ */
421
+ public getUtxoPrivateKeyV2(): string {
422
+ if (!this.utxoPrivateKeyV2) {
423
+ throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
424
+ }
425
+ return this.utxoPrivateKeyV2;
426
+ }
427
+ }
428
+
429
+ export function serializeProofAndExtData(proof: any, extData: any) {
430
+ // Create the ExtDataMinified object for the program call (only extAmount and fee)
431
+ const extDataMinified = {
432
+ extAmount: extData.extAmount,
433
+ fee: extData.fee
434
+ };
435
+
436
+ // Use the same serialization approach as deposit script
437
+ const instructionData = Buffer.concat([
438
+ TRANSACT_IX_DISCRIMINATOR,
439
+ // Serialize proof
440
+ Buffer.from(proof.proofA),
441
+ Buffer.from(proof.proofB),
442
+ Buffer.from(proof.proofC),
443
+ Buffer.from(proof.root),
444
+ Buffer.from(proof.publicAmount),
445
+ Buffer.from(proof.extDataHash),
446
+ Buffer.from(proof.inputNullifiers[0]),
447
+ Buffer.from(proof.inputNullifiers[1]),
448
+ Buffer.from(proof.outputCommitments[0]),
449
+ Buffer.from(proof.outputCommitments[1]),
450
+ // Serialize ExtDataMinified (only extAmount and fee)
451
+ Buffer.from(new BN(extDataMinified.extAmount).toTwos(64).toArray('le', 8)),
452
+ Buffer.from(new BN(extDataMinified.fee).toArray('le', 8)),
453
+ // Serialize encrypted outputs as separate parameters
454
+ Buffer.from(new BN(extData.encryptedOutput1.length).toArray('le', 4)),
455
+ extData.encryptedOutput1,
456
+ Buffer.from(new BN(extData.encryptedOutput2.length).toArray('le', 4)),
457
+ extData.encryptedOutput2,
458
+ ]);
459
+
460
+ return instructionData;
461
+ }