@velumdotcash/sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/__tests__/paylink.test.d.ts +9 -0
- package/dist/__tests__/paylink.test.js +254 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +12 -0
- package/dist/deposit.d.ts +22 -0
- package/dist/deposit.js +445 -0
- package/dist/depositSPL.d.ts +24 -0
- package/dist/depositSPL.js +499 -0
- package/dist/errors.d.ts +78 -0
- package/dist/errors.js +127 -0
- package/dist/exportUtils.d.ts +10 -0
- package/dist/exportUtils.js +10 -0
- package/dist/getUtxos.d.ts +30 -0
- package/dist/getUtxos.js +335 -0
- package/dist/getUtxosSPL.d.ts +34 -0
- package/dist/getUtxosSPL.js +442 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.js +436 -0
- package/dist/models/keypair.d.ts +26 -0
- package/dist/models/keypair.js +43 -0
- package/dist/models/utxo.d.ts +51 -0
- package/dist/models/utxo.js +99 -0
- package/dist/test_paylink_logic.test.d.ts +1 -0
- package/dist/test_paylink_logic.test.js +114 -0
- package/dist/utils/address_lookup_table.d.ts +9 -0
- package/dist/utils/address_lookup_table.js +45 -0
- package/dist/utils/constants.d.ts +27 -0
- package/dist/utils/constants.js +56 -0
- package/dist/utils/debug-logger.d.ts +250 -0
- package/dist/utils/debug-logger.js +688 -0
- package/dist/utils/encryption.d.ts +152 -0
- package/dist/utils/encryption.js +700 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/merkle_tree.d.ts +92 -0
- package/dist/utils/merkle_tree.js +186 -0
- package/dist/utils/node-shim.d.ts +14 -0
- package/dist/utils/node-shim.js +21 -0
- package/dist/utils/prover.d.ts +36 -0
- package/dist/utils/prover.js +169 -0
- package/dist/utils/utils.d.ts +64 -0
- package/dist/utils/utils.js +165 -0
- package/dist/withdraw.d.ts +22 -0
- package/dist/withdraw.js +290 -0
- package/dist/withdrawSPL.d.ts +24 -0
- package/dist/withdrawSPL.js +329 -0
- package/package.json +59 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import nacl from 'tweetnacl';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
4
|
+
import { gcm, ctr } from '@noble/ciphers/aes';
|
|
5
|
+
import { Utxo } from '../models/utxo.js';
|
|
6
|
+
import { WasmFactory } from '@lightprotocol/hasher.rs';
|
|
7
|
+
import { Keypair as UtxoKeypair } from '../models/keypair.js';
|
|
8
|
+
import { keccak256 } from '@ethersproject/keccak256';
|
|
9
|
+
import { TRANSACT_IX_DISCRIMINATOR, TRANSACT_SPL_IX_DISCRIMINATOR } from './constants.js';
|
|
10
|
+
import BN from 'bn.js';
|
|
11
|
+
import { debugLogger, hashForLog } from './debug-logger.js';
|
|
12
|
+
/**
|
|
13
|
+
* Service for handling encryption and decryption of UTXO data
|
|
14
|
+
*/
|
|
15
|
+
export class EncryptionService {
|
|
16
|
+
// Version identifier for encryption scheme (8-byte version)
|
|
17
|
+
static ENCRYPTION_VERSION_V2 = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]); // Version 2
|
|
18
|
+
static ENCRYPTION_VERSION_V3 = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03]); // Version 3 (Asymmetric)
|
|
19
|
+
// Signature schema version - identifies the signature message format and encryption format
|
|
20
|
+
// Version 0x01: Original format without recipient ID
|
|
21
|
+
// Version 0x02: New format with recipient ID hash for O(1) early termination
|
|
22
|
+
static SIGNATURE_SCHEMA_VERSION = Buffer.from([0x02]); // Schema version 2
|
|
23
|
+
// Length of recipient ID hash in bytes (first 8 bytes of SHA256(X25519 public key))
|
|
24
|
+
static RECIPIENT_ID_LENGTH = 8;
|
|
25
|
+
encryptionKeyV1 = null;
|
|
26
|
+
encryptionKeyV2 = null;
|
|
27
|
+
asymmetricSecretKey = null; // X25519 Secret Key
|
|
28
|
+
utxoPrivateKeyV1 = null;
|
|
29
|
+
utxoPrivateKeyV2 = null;
|
|
30
|
+
walletAddress = null; // Wallet address for key derivation logging
|
|
31
|
+
/**
|
|
32
|
+
* Generate an encryption key from a signature
|
|
33
|
+
* @param signature The user's signature
|
|
34
|
+
* @param walletAddress Optional wallet address for logging/verification
|
|
35
|
+
* @returns The generated encryption key
|
|
36
|
+
*/
|
|
37
|
+
deriveEncryptionKeyFromSignature(signature, walletAddress) {
|
|
38
|
+
// Store wallet address for logging
|
|
39
|
+
if (walletAddress) {
|
|
40
|
+
this.walletAddress = walletAddress;
|
|
41
|
+
debugLogger.walletKeyDerivation(walletAddress, 'derive');
|
|
42
|
+
}
|
|
43
|
+
debugLogger.keyDerivation('START', hashForLog(signature), 'wallet_signature');
|
|
44
|
+
// Extract the first 31 bytes of the signature to create a deterministic key (legacy method)
|
|
45
|
+
const encryptionKeyV1 = signature.slice(0, 31);
|
|
46
|
+
// Store the V1 key in the service
|
|
47
|
+
this.encryptionKeyV1 = encryptionKeyV1;
|
|
48
|
+
debugLogger.keyDerivation('V1_KEY_DERIVED', hashForLog(encryptionKeyV1), 'encryption_key_v1');
|
|
49
|
+
// Precompute and cache the UTXO private key
|
|
50
|
+
const hashedSeedV1 = sha256(encryptionKeyV1);
|
|
51
|
+
this.utxoPrivateKeyV1 = '0x' + Buffer.from(hashedSeedV1).toString('hex');
|
|
52
|
+
debugLogger.keyDerivation('V1_UTXO_PRIVATE_KEY', hashForLog(hashedSeedV1), 'utxo_private_key_v1');
|
|
53
|
+
// Use Keccak256 to derive a full 32-byte encryption key from the signature
|
|
54
|
+
const encryptionKeyV2 = Buffer.from(keccak256(signature).slice(2), 'hex');
|
|
55
|
+
// Store the V2 key in the service
|
|
56
|
+
this.encryptionKeyV2 = encryptionKeyV2;
|
|
57
|
+
debugLogger.keyDerivation('V2_KEY_DERIVED', hashForLog(encryptionKeyV2), 'encryption_key_v2');
|
|
58
|
+
// Derive asymmetric key from V2 key (deterministic)
|
|
59
|
+
// We use the first 32 bytes of a hash of the V2 key as the seed for the X25519 keypair
|
|
60
|
+
const asymmetricSeed = sha256(encryptionKeyV2);
|
|
61
|
+
debugLogger.keyDerivation('ASYMMETRIC_SEED', hashForLog(asymmetricSeed), 'x25519_seed');
|
|
62
|
+
const keypair = nacl.box.keyPair.fromSecretKey(asymmetricSeed);
|
|
63
|
+
this.asymmetricSecretKey = keypair.secretKey;
|
|
64
|
+
debugLogger.asymmetricKeyGenerated(hashForLog(keypair.publicKey), hashForLog(keypair.secretKey));
|
|
65
|
+
// Log X25519 public key derivation with wallet address for sender/recipient verification
|
|
66
|
+
const walletAddr = this.walletAddress || '<unknown>';
|
|
67
|
+
debugLogger.x25519KeyDerived(hashForLog(keypair.publicKey), walletAddr, 'recipient');
|
|
68
|
+
// Precompute and cache the UTXO private key
|
|
69
|
+
const hashedSeedV2 = Buffer.from(keccak256(encryptionKeyV2).slice(2), 'hex');
|
|
70
|
+
this.utxoPrivateKeyV2 = '0x' + hashedSeedV2.toString('hex');
|
|
71
|
+
debugLogger.keyDerivation('V2_UTXO_PRIVATE_KEY', hashForLog(hashedSeedV2), 'utxo_private_key_v2');
|
|
72
|
+
debugLogger.serviceInitialized(!!this.encryptionKeyV1, !!this.encryptionKeyV2, !!this.asymmetricSecretKey);
|
|
73
|
+
return {
|
|
74
|
+
v1: this.encryptionKeyV1,
|
|
75
|
+
v2: this.encryptionKeyV2
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Generate an encryption key from a wallet keypair (V2 format)
|
|
80
|
+
* @param keypair The Solana keypair to derive the encryption key from
|
|
81
|
+
* @returns The generated encryption key
|
|
82
|
+
*/
|
|
83
|
+
deriveEncryptionKeyFromWallet(keypair) {
|
|
84
|
+
// Sign a constant message with the keypair
|
|
85
|
+
const message = Buffer.from('Privacy Money account sign in');
|
|
86
|
+
const signature = nacl.sign.detached(message, keypair.secretKey);
|
|
87
|
+
// Pass wallet address for logging
|
|
88
|
+
return this.deriveEncryptionKeyFromSignature(signature, keypair.publicKey.toBase58());
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get the Asymmetric Public Key (X25519) derived from the encryption key
|
|
92
|
+
*/
|
|
93
|
+
getAsymmetricPublicKey() {
|
|
94
|
+
if (!this.asymmetricSecretKey) {
|
|
95
|
+
throw new Error('Asymmetric key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
96
|
+
}
|
|
97
|
+
return nacl.box.keyPair.fromSecretKey(this.asymmetricSecretKey).publicKey;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Derive recipient ID hash from X25519 public key
|
|
101
|
+
* Used for O(1) early termination during UTXO decryption
|
|
102
|
+
* @param publicKey Optional X25519 public key. If not provided, uses own public key.
|
|
103
|
+
* @returns First 8 bytes of SHA256(publicKey)
|
|
104
|
+
*/
|
|
105
|
+
deriveRecipientIdHash(publicKey) {
|
|
106
|
+
const key = publicKey || this.getAsymmetricPublicKey();
|
|
107
|
+
const hash = sha256(key);
|
|
108
|
+
return Buffer.from(hash.slice(0, EncryptionService.RECIPIENT_ID_LENGTH));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if encrypted data is for this wallet (O(1) operation)
|
|
112
|
+
* Enables early termination without attempting expensive crypto decryption
|
|
113
|
+
* @param encryptedBuffer The encrypted data buffer
|
|
114
|
+
* @returns true if we should attempt decryption, false to skip
|
|
115
|
+
*/
|
|
116
|
+
shouldAttemptDecryption(encryptedBuffer) {
|
|
117
|
+
// Minimum length for new format: version(8) + schema(1) + recipientId(8) = 17 bytes
|
|
118
|
+
if (encryptedBuffer.length < 17) {
|
|
119
|
+
// Too short for new format, let normal decryption handle it
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const schemaVersion = encryptedBuffer[8];
|
|
123
|
+
// Old format (schema version 0x01 or less) - attempt decryption for backward compat
|
|
124
|
+
if (schemaVersion < 0x02) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
// Unknown future schema version (> 0x02) - reject
|
|
128
|
+
if (schemaVersion > 0x02) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// Current format (schema version 0x02) - check recipient ID hash
|
|
132
|
+
const storedRecipientId = encryptedBuffer.slice(9, 9 + EncryptionService.RECIPIENT_ID_LENGTH);
|
|
133
|
+
const ourRecipientId = this.deriveRecipientIdHash();
|
|
134
|
+
return storedRecipientId.equals(ourRecipientId);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Encrypt data with the stored encryption key
|
|
138
|
+
* @param data The data to encrypt
|
|
139
|
+
* @returns The encrypted data as a Buffer
|
|
140
|
+
* @throws Error if the encryption key has not been generated
|
|
141
|
+
*/
|
|
142
|
+
encrypt(data) {
|
|
143
|
+
if (!this.encryptionKeyV2) {
|
|
144
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
145
|
+
}
|
|
146
|
+
// Convert string to Buffer if needed
|
|
147
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
148
|
+
// Generate a standard initialization vector (12 bytes for GCM)
|
|
149
|
+
const iv = nacl.randomBytes(12);
|
|
150
|
+
// Use the full 32-byte V2 encryption key for AES-256
|
|
151
|
+
const key = Buffer.from(this.encryptionKeyV2);
|
|
152
|
+
// Use AES-256-GCM for authenticated encryption
|
|
153
|
+
const stream = gcm(key, iv);
|
|
154
|
+
const encryptedWithTag = stream.encrypt(dataBuffer);
|
|
155
|
+
// Noble returns [ciphertext | authTag] (tag is last 16 bytes)
|
|
156
|
+
const authTag = Buffer.from(encryptedWithTag.slice(-16));
|
|
157
|
+
const encryptedData = Buffer.from(encryptedWithTag.slice(0, -16));
|
|
158
|
+
// Derive recipient ID hash for O(1) early termination during decryption
|
|
159
|
+
const recipientIdHash = this.deriveRecipientIdHash();
|
|
160
|
+
// Version 2 format (schema 0x02): [version(8)] + [schemaVersion(1)] + [recipientIdHash(8)] + [IV(12)] + [authTag(16)] + [encryptedData]
|
|
161
|
+
return Buffer.concat([
|
|
162
|
+
EncryptionService.ENCRYPTION_VERSION_V2,
|
|
163
|
+
EncryptionService.SIGNATURE_SCHEMA_VERSION,
|
|
164
|
+
recipientIdHash,
|
|
165
|
+
iv,
|
|
166
|
+
authTag,
|
|
167
|
+
encryptedData
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
// v1 encryption, only used for testing now
|
|
171
|
+
encryptDecryptedDoNotUse(data) {
|
|
172
|
+
if (!this.encryptionKeyV1) {
|
|
173
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
174
|
+
}
|
|
175
|
+
// Convert string to Buffer if needed
|
|
176
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
177
|
+
// Generate a standard initialization vector (16 bytes)
|
|
178
|
+
const iv = nacl.randomBytes(16);
|
|
179
|
+
// Create a key from our encryption key (using only first 16 bytes for AES-128)
|
|
180
|
+
const key = Buffer.from(this.encryptionKeyV1).slice(0, 16);
|
|
181
|
+
// Use a more compact encryption algorithm (aes-128-ctr)
|
|
182
|
+
const stream = ctr(key, iv);
|
|
183
|
+
const encryptedData = Buffer.from(stream.encrypt(dataBuffer));
|
|
184
|
+
// Create an authentication tag (HMAC) to verify decryption with correct key
|
|
185
|
+
const hmacKey = Buffer.from(this.encryptionKeyV1).slice(16, 31);
|
|
186
|
+
const authTag = Buffer.from(hmac(sha256, hmacKey, Buffer.concat([iv, encryptedData]))).slice(0, 16);
|
|
187
|
+
// Combine IV, auth tag and encrypted data
|
|
188
|
+
return Buffer.concat([iv, authTag, encryptedData]);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Decrypt data with the stored encryption key
|
|
192
|
+
* @param encryptedData The encrypted data to decrypt
|
|
193
|
+
* @returns The decrypted data as a Buffer, or null if legacy format (no schema version byte)
|
|
194
|
+
* @throws Error if the encryption key has not been generated or if the wrong key is used
|
|
195
|
+
*/
|
|
196
|
+
decrypt(encryptedData) {
|
|
197
|
+
// Check if this is the new version format (starts with 8-byte version identifier)
|
|
198
|
+
if (encryptedData.length >= 8 && encryptedData.subarray(0, 8).equals(EncryptionService.ENCRYPTION_VERSION_V2)) {
|
|
199
|
+
if (!this.encryptionKeyV2) {
|
|
200
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
201
|
+
}
|
|
202
|
+
return this.decryptV2(encryptedData);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// V1 format - need V1 key or keypair to derive it
|
|
206
|
+
if (!this.encryptionKeyV1) {
|
|
207
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
208
|
+
}
|
|
209
|
+
return this.decryptV1(encryptedData);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Decrypt data using the old V1 format (120-bit HMAC with SHA256)
|
|
214
|
+
* @param encryptedData The encrypted data to decrypt
|
|
215
|
+
* @param keypair Optional keypair to derive V1 key for backward compatibility
|
|
216
|
+
* @returns The decrypted data as a Buffer
|
|
217
|
+
*/
|
|
218
|
+
decryptV1(encryptedData) {
|
|
219
|
+
debugLogger.decryptionAttemptStart('V1', hashForLog(encryptedData), encryptedData.length);
|
|
220
|
+
if (!this.encryptionKeyV1) {
|
|
221
|
+
debugLogger.missingKey('encryptionKeyV1', 'V1 decryption');
|
|
222
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
223
|
+
}
|
|
224
|
+
// Extract the IV from the first 16 bytes
|
|
225
|
+
const iv = encryptedData.slice(0, 16);
|
|
226
|
+
// Extract the auth tag from the next 16 bytes
|
|
227
|
+
const authTag = encryptedData.slice(16, 32);
|
|
228
|
+
// The rest is the actual encrypted data
|
|
229
|
+
const data = encryptedData.slice(32);
|
|
230
|
+
// Verify the authentication tag
|
|
231
|
+
const hmacKey = Buffer.from(this.encryptionKeyV1).slice(16, 31);
|
|
232
|
+
const calculatedTag = Buffer.from(hmac(sha256, hmacKey, Buffer.concat([iv, data]))).slice(0, 16);
|
|
233
|
+
// Compare tags - if they don't match, the key is wrong
|
|
234
|
+
if (!this.timingSafeEqual(authTag, calculatedTag)) {
|
|
235
|
+
debugLogger.decryptionFailure('V1', 'HMAC_MISMATCH', 'Authentication tag verification failed', {
|
|
236
|
+
ivHash: hashForLog(iv),
|
|
237
|
+
authTagHash: hashForLog(authTag),
|
|
238
|
+
calculatedTagHash: hashForLog(calculatedTag)
|
|
239
|
+
});
|
|
240
|
+
throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
|
|
241
|
+
}
|
|
242
|
+
// Create a key from our encryption key (using only first 16 bytes for AES-128)
|
|
243
|
+
const key = Buffer.from(this.encryptionKeyV1).slice(0, 16);
|
|
244
|
+
// Use the same algorithm as in encrypt
|
|
245
|
+
try {
|
|
246
|
+
const stream = ctr(key, iv);
|
|
247
|
+
const decrypted = Buffer.from(stream.decrypt(data));
|
|
248
|
+
debugLogger.decryptionSuccess('V1', decrypted.length);
|
|
249
|
+
return decrypted;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
253
|
+
debugLogger.decryptionFailure('V1', 'AES_CTR_FAILED', errMsg);
|
|
254
|
+
throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Custom timingSafeEqual for browser compatibility
|
|
258
|
+
timingSafeEqual(a, b) {
|
|
259
|
+
if (a.length !== b.length) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
let diff = 0;
|
|
263
|
+
for (let i = 0; i < a.length; i++) {
|
|
264
|
+
diff |= a[i] ^ b[i];
|
|
265
|
+
}
|
|
266
|
+
return diff === 0;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Encrypt data using Asymmetric Encryption (X25519 + XSalsa20-Poly1305)
|
|
270
|
+
* @param data The data to encrypt
|
|
271
|
+
* @param recipientPublicKey The recipient's X25519 Public Key
|
|
272
|
+
*/
|
|
273
|
+
encryptAsymmetric(data, recipientPublicKey) {
|
|
274
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
275
|
+
// Log the recipient's X25519 public key being used for encryption (sender side)
|
|
276
|
+
debugLogger.x25519EncryptionKey(hashForLog(recipientPublicKey));
|
|
277
|
+
// Generate ephemeral keypair
|
|
278
|
+
const ephemeralKeypair = nacl.box.keyPair();
|
|
279
|
+
// Generate nonce
|
|
280
|
+
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
|
281
|
+
// Encrypt
|
|
282
|
+
const encrypted = nacl.box(dataBuffer, nonce, recipientPublicKey, ephemeralKeypair.secretKey);
|
|
283
|
+
// Derive recipient ID hash from recipient's public key for O(1) early termination
|
|
284
|
+
const recipientIdHash = this.deriveRecipientIdHash(recipientPublicKey);
|
|
285
|
+
// Format (schema 0x02): [version(8)] + [schemaVersion(1)] + [recipientIdHash(8)] + [ephemeralPublicKey(32)] + [nonce(24)] + [encryptedData]
|
|
286
|
+
return Buffer.concat([
|
|
287
|
+
EncryptionService.ENCRYPTION_VERSION_V3,
|
|
288
|
+
EncryptionService.SIGNATURE_SCHEMA_VERSION,
|
|
289
|
+
recipientIdHash,
|
|
290
|
+
ephemeralKeypair.publicKey,
|
|
291
|
+
nonce,
|
|
292
|
+
encrypted
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Decrypt data using Asymmetric Encryption
|
|
297
|
+
* @returns Buffer on success, null if legacy format (no schema version byte) - these are skipped
|
|
298
|
+
*/
|
|
299
|
+
decryptV3(encryptedData) {
|
|
300
|
+
debugLogger.decryptionAttemptStart('V3', hashForLog(encryptedData), encryptedData.length);
|
|
301
|
+
if (!this.asymmetricSecretKey) {
|
|
302
|
+
debugLogger.missingKey('asymmetricSecretKey', 'V3 decryption');
|
|
303
|
+
throw new Error('Asymmetric secret key not set.');
|
|
304
|
+
}
|
|
305
|
+
// Log the derived X25519 public key (recipient side) for verification
|
|
306
|
+
const derivedPublicKey = nacl.box.keyPair.fromSecretKey(this.asymmetricSecretKey).publicKey;
|
|
307
|
+
debugLogger.x25519DecryptionKey(hashForLog(derivedPublicKey));
|
|
308
|
+
// Check schema version byte at position 8
|
|
309
|
+
const schemaVersion = encryptedData.length >= 9 ? encryptedData[8] : 0;
|
|
310
|
+
// Skip very old format UTXOs (no schema version)
|
|
311
|
+
if (schemaVersion < 0x01) {
|
|
312
|
+
debugLogger.decryptionFailure('V3', 'LEGACY_FORMAT_SKIPPED', 'Very old format UTXO without schema version byte - skipping', {
|
|
313
|
+
dataLength: encryptedData.length,
|
|
314
|
+
byte8Value: schemaVersion
|
|
315
|
+
});
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
// Determine byte offsets based on schema version
|
|
319
|
+
// Schema 0x01: [version(8)] + [schema(1)] + [ephemeralPubKey(32)] + [nonce(24)] + [encrypted]
|
|
320
|
+
// Schema 0x02: [version(8)] + [schema(1)] + [recipientIdHash(8)] + [ephemeralPubKey(32)] + [nonce(24)] + [encrypted]
|
|
321
|
+
let ephemeralKeyStart, ephemeralKeyEnd, nonceStart, nonceEnd, boxStart;
|
|
322
|
+
if (schemaVersion >= 0x02) {
|
|
323
|
+
// New format with recipient ID hash (8 bytes)
|
|
324
|
+
ephemeralKeyStart = 9 + EncryptionService.RECIPIENT_ID_LENGTH; // 17
|
|
325
|
+
ephemeralKeyEnd = ephemeralKeyStart + 32; // 49
|
|
326
|
+
nonceStart = ephemeralKeyEnd; // 49
|
|
327
|
+
nonceEnd = nonceStart + 24; // 73
|
|
328
|
+
boxStart = nonceEnd; // 73
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Old format (schema 0x01) without recipient ID hash
|
|
332
|
+
ephemeralKeyStart = 9;
|
|
333
|
+
ephemeralKeyEnd = 41;
|
|
334
|
+
nonceStart = 41;
|
|
335
|
+
nonceEnd = 65;
|
|
336
|
+
boxStart = 65;
|
|
337
|
+
}
|
|
338
|
+
const ephemeralPublicKey = encryptedData.slice(ephemeralKeyStart, ephemeralKeyEnd);
|
|
339
|
+
const nonce = encryptedData.slice(nonceStart, nonceEnd);
|
|
340
|
+
const box = encryptedData.slice(boxStart);
|
|
341
|
+
debugLogger.v3DecryptionDetails(hashForLog(ephemeralPublicKey), hashForLog(nonce), box.length, hashForLog(this.asymmetricSecretKey));
|
|
342
|
+
const decrypted = nacl.box.open(box, nonce, ephemeralPublicKey, this.asymmetricSecretKey);
|
|
343
|
+
if (!decrypted) {
|
|
344
|
+
// Log key mismatch details for debugging
|
|
345
|
+
const derivedPubKeyHash = hashForLog(derivedPublicKey);
|
|
346
|
+
debugLogger.x25519KeyMismatch('<encrypted_for_different_key>', derivedPubKeyHash, this.walletAddress || undefined);
|
|
347
|
+
debugLogger.decryptionFailure('V3', 'NACL_BOX_OPEN_FAILED', 'nacl.box.open returned null - key mismatch or corrupted data', {
|
|
348
|
+
ephemeralPubKeyHash: hashForLog(ephemeralPublicKey),
|
|
349
|
+
nonceHash: hashForLog(nonce),
|
|
350
|
+
boxLength: box.length,
|
|
351
|
+
secretKeyHash: hashForLog(this.asymmetricSecretKey),
|
|
352
|
+
derivedPublicKeyHash: derivedPubKeyHash,
|
|
353
|
+
walletAddress: this.walletAddress || '<unknown>',
|
|
354
|
+
schemaVersion
|
|
355
|
+
});
|
|
356
|
+
throw new Error('Failed to decrypt asymmetric data');
|
|
357
|
+
}
|
|
358
|
+
debugLogger.decryptionSuccess('V3', decrypted.length);
|
|
359
|
+
return Buffer.from(decrypted);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Decrypt data using the new V2 format (256-bit Keccak HMAC)
|
|
363
|
+
* @param encryptedData The encrypted data to decrypt
|
|
364
|
+
* @returns The decrypted data as a Buffer, or null if legacy format (no schema version byte) - these are skipped
|
|
365
|
+
*/
|
|
366
|
+
decryptV2(encryptedData) {
|
|
367
|
+
debugLogger.decryptionAttemptStart('V2', hashForLog(encryptedData), encryptedData.length);
|
|
368
|
+
if (!this.encryptionKeyV2) {
|
|
369
|
+
debugLogger.missingKey('encryptionKeyV2', 'V2 decryption');
|
|
370
|
+
throw new Error('encryptionKeyV2 not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
371
|
+
}
|
|
372
|
+
// Check schema version byte at position 8
|
|
373
|
+
const schemaVersion = encryptedData.length >= 9 ? encryptedData[8] : 0;
|
|
374
|
+
// Skip very old format UTXOs (no schema version)
|
|
375
|
+
if (schemaVersion < 0x01) {
|
|
376
|
+
debugLogger.decryptionFailure('V2', 'LEGACY_FORMAT_SKIPPED', 'Very old format UTXO without schema version byte - skipping', {
|
|
377
|
+
dataLength: encryptedData.length,
|
|
378
|
+
byte8Value: schemaVersion
|
|
379
|
+
});
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
// Determine byte offsets based on schema version
|
|
383
|
+
// Schema 0x01: [version(8)] + [schema(1)] + [IV(12)] + [authTag(16)] + [encrypted]
|
|
384
|
+
// Schema 0x02: [version(8)] + [schema(1)] + [recipientIdHash(8)] + [IV(12)] + [authTag(16)] + [encrypted]
|
|
385
|
+
let ivStart, ivEnd, authTagStart, authTagEnd, dataStart;
|
|
386
|
+
if (schemaVersion >= 0x02) {
|
|
387
|
+
// New format with recipient ID hash (8 bytes)
|
|
388
|
+
ivStart = 9 + EncryptionService.RECIPIENT_ID_LENGTH; // 17
|
|
389
|
+
ivEnd = ivStart + 12; // 29
|
|
390
|
+
authTagStart = ivEnd; // 29
|
|
391
|
+
authTagEnd = authTagStart + 16; // 45
|
|
392
|
+
dataStart = authTagEnd; // 45
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Old format (schema 0x01) without recipient ID hash
|
|
396
|
+
ivStart = 9;
|
|
397
|
+
ivEnd = 21;
|
|
398
|
+
authTagStart = 21;
|
|
399
|
+
authTagEnd = 37;
|
|
400
|
+
dataStart = 37;
|
|
401
|
+
}
|
|
402
|
+
const iv = encryptedData.slice(ivStart, ivEnd);
|
|
403
|
+
const authTag = encryptedData.slice(authTagStart, authTagEnd);
|
|
404
|
+
const data = encryptedData.slice(dataStart);
|
|
405
|
+
// Use the full 32-byte V2 encryption key for AES-256
|
|
406
|
+
const key = Buffer.from(this.encryptionKeyV2);
|
|
407
|
+
// Use AES-256-GCM for authenticated decryption
|
|
408
|
+
// Noble ciphers expects ciphertext + authTag
|
|
409
|
+
const ciphertextWithTag = Buffer.concat([data, authTag]);
|
|
410
|
+
try {
|
|
411
|
+
const stream = gcm(key, iv);
|
|
412
|
+
const decrypted = Buffer.from(stream.decrypt(ciphertextWithTag));
|
|
413
|
+
debugLogger.decryptionSuccess('V2', decrypted.length);
|
|
414
|
+
return decrypted;
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
418
|
+
debugLogger.decryptionFailure('V2', 'AES_GCM_FAILED', errMsg, {
|
|
419
|
+
ivHash: hashForLog(iv),
|
|
420
|
+
authTagHash: hashForLog(authTag),
|
|
421
|
+
dataLength: data.length,
|
|
422
|
+
schemaVersion
|
|
423
|
+
});
|
|
424
|
+
throw new Error('Failed to decrypt data. Invalid encryption key or corrupted data.');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Reset the encryption keys (mainly for testing purposes)
|
|
429
|
+
*/
|
|
430
|
+
resetEncryptionKey() {
|
|
431
|
+
this.encryptionKeyV1 = null;
|
|
432
|
+
this.encryptionKeyV2 = null;
|
|
433
|
+
this.asymmetricSecretKey = null;
|
|
434
|
+
this.utxoPrivateKeyV1 = null;
|
|
435
|
+
this.utxoPrivateKeyV2 = null;
|
|
436
|
+
this.walletAddress = null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Encrypt a UTXO using a compact pipe-delimited format
|
|
440
|
+
* Always uses V2 encryption format. The UTXO's version property is used only for key derivation.
|
|
441
|
+
* @param utxo The UTXO to encrypt (includes version property)
|
|
442
|
+
* @param recipientEncryptionKey Optional recipient X25519 public key for asymmetric encryption
|
|
443
|
+
* @returns The encrypted UTXO data as a Buffer
|
|
444
|
+
* @throws Error if the V2 encryption key has not been set
|
|
445
|
+
*/
|
|
446
|
+
encryptUtxo(utxo, recipientEncryptionKey) {
|
|
447
|
+
if (!this.encryptionKeyV2) {
|
|
448
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
449
|
+
}
|
|
450
|
+
// Create a compact string representation using pipe delimiter
|
|
451
|
+
// Version is stored in the UTXO model, not in the encrypted content
|
|
452
|
+
const utxoString = `${utxo.amount.toString()}|${utxo.blinding.toString()}|${utxo.index}|${utxo.mintAddress}`;
|
|
453
|
+
if (recipientEncryptionKey) {
|
|
454
|
+
return this.encryptAsymmetric(utxoString, recipientEncryptionKey);
|
|
455
|
+
}
|
|
456
|
+
// Always use V2 encryption format (which adds version byte 0x02 at the beginning)
|
|
457
|
+
return this.encrypt(utxoString);
|
|
458
|
+
}
|
|
459
|
+
// Deprecated, only used for testing now
|
|
460
|
+
encryptUtxoDecryptedDoNotUse(utxo) {
|
|
461
|
+
if (!this.encryptionKeyV2) {
|
|
462
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
463
|
+
}
|
|
464
|
+
const utxoString = `${utxo.amount.toString()}|${utxo.blinding.toString()}|${utxo.index}|${utxo.mintAddress}`;
|
|
465
|
+
return this.encryptDecryptedDoNotUse(utxoString);
|
|
466
|
+
}
|
|
467
|
+
getEncryptionKeyVersion(encryptedData) {
|
|
468
|
+
const buffer = typeof encryptedData === 'string' ? Buffer.from(encryptedData, 'hex') : encryptedData;
|
|
469
|
+
const dataHash = hashForLog(buffer);
|
|
470
|
+
// Log the first 8 bytes (version prefix) for debugging
|
|
471
|
+
const prefixBytes = buffer.length >= 8 ? buffer.subarray(0, 8) : buffer;
|
|
472
|
+
const prefixHex = Buffer.from(prefixBytes).toString('hex');
|
|
473
|
+
debugLogger.versionPrefixBytes(prefixHex, buffer.length);
|
|
474
|
+
let version;
|
|
475
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(EncryptionService.ENCRYPTION_VERSION_V2)) {
|
|
476
|
+
// V2 encryption format → V2 UTXO
|
|
477
|
+
version = 'v2';
|
|
478
|
+
}
|
|
479
|
+
else if (buffer.length >= 8 && buffer.subarray(0, 8).equals(EncryptionService.ENCRYPTION_VERSION_V3)) {
|
|
480
|
+
version = 'v3';
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// V1 encryption format → UTXO (legacy mode fallback)
|
|
484
|
+
version = 'v1';
|
|
485
|
+
const reason = buffer.length < 8
|
|
486
|
+
? `Data too short (${buffer.length} bytes) to contain version prefix`
|
|
487
|
+
: `Unrecognized version prefix: ${prefixHex}`;
|
|
488
|
+
debugLogger.versionFallbackToLegacy(prefixHex, reason);
|
|
489
|
+
}
|
|
490
|
+
debugLogger.versionDetected(dataHash, version, buffer.length);
|
|
491
|
+
return version;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Check if the schema version at byte 9 matches the expected version.
|
|
495
|
+
* Returns null if schema version matches or data is legacy format (no schema version).
|
|
496
|
+
* Returns the found schema version if it doesn't match (for early termination).
|
|
497
|
+
* @param encryptedBuffer The encrypted data buffer
|
|
498
|
+
* @param encryptionVersion The encryption version (v1, v2, v3)
|
|
499
|
+
* @returns null if schema version matches or is legacy, otherwise the mismatched version byte
|
|
500
|
+
*/
|
|
501
|
+
checkSchemaVersionMismatch(encryptedBuffer, encryptionVersion) {
|
|
502
|
+
// V1 doesn't have schema version byte, skip check
|
|
503
|
+
if (encryptionVersion === 'v1') {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
// Check if we have enough bytes to contain schema version
|
|
507
|
+
if (encryptedBuffer.length < 9) {
|
|
508
|
+
return null; // Too short, let decryption handle the error
|
|
509
|
+
}
|
|
510
|
+
const schemaVersionByte = encryptedBuffer[8];
|
|
511
|
+
const expectedSchemaVersion = EncryptionService.SIGNATURE_SCHEMA_VERSION[0];
|
|
512
|
+
// If schema version matches, proceed with decryption
|
|
513
|
+
if (schemaVersionByte === expectedSchemaVersion) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
// For early termination, we only skip if the schema version byte is clearly
|
|
517
|
+
// a FUTURE schema version (greater than current). This maintains backward
|
|
518
|
+
// compatibility with legacy data that doesn't have a schema version byte.
|
|
519
|
+
//
|
|
520
|
+
// Legacy format detection:
|
|
521
|
+
// - Legacy V2/V3 data has IV or ephemeral pubkey bytes starting at position 8
|
|
522
|
+
// - These are random/crypto bytes that could have any value
|
|
523
|
+
// - We can't reliably distinguish legacy data from schema version bytes
|
|
524
|
+
//
|
|
525
|
+
// Conservative approach:
|
|
526
|
+
// - Only skip if byte 8 > current schema version (clearly a future version)
|
|
527
|
+
// - This handles the common case of different apps using different schema versions
|
|
528
|
+
// - Legacy data (where byte 8 could be anything) will attempt decryption and fail naturally
|
|
529
|
+
if (schemaVersionByte > expectedSchemaVersion) {
|
|
530
|
+
return schemaVersionByte;
|
|
531
|
+
}
|
|
532
|
+
return null; // Assume legacy format or compatible version, attempt decryption
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Decrypt an encrypted UTXO and parse it to a Utxo instance
|
|
536
|
+
* Automatically detects the UTXO version based on the encryption format
|
|
537
|
+
* @param encryptedData The encrypted UTXO data
|
|
538
|
+
* @param keypair The UTXO keypair to use for the decrypted UTXO
|
|
539
|
+
* @param lightWasm Optional LightWasm instance. If not provided, a new one will be created
|
|
540
|
+
* @param walletKeypair Optional wallet keypair for V1 backward compatibility
|
|
541
|
+
* @returns Promise resolving to the decrypted Utxo instance, or null if schema version mismatch
|
|
542
|
+
* @throws Error if the encryption key has not been set or if decryption fails
|
|
543
|
+
*/
|
|
544
|
+
async decryptUtxo(encryptedData, lightWasm) {
|
|
545
|
+
// Convert hex string to Buffer if needed
|
|
546
|
+
const encryptedBuffer = typeof encryptedData === 'string'
|
|
547
|
+
? Buffer.from(encryptedData, 'hex')
|
|
548
|
+
: encryptedData;
|
|
549
|
+
// O(1) Early termination: Check recipient ID hash BEFORE any expensive operations
|
|
550
|
+
// This is the fastest way to skip UTXOs that don't belong to this wallet
|
|
551
|
+
if (!this.shouldAttemptDecryption(encryptedBuffer)) {
|
|
552
|
+
debugLogger.recipientIdMismatch(hashForLog(encryptedBuffer));
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
// Detect UTXO version based on encryption format
|
|
556
|
+
let utxoVersion = this.getEncryptionKeyVersion(encryptedBuffer);
|
|
557
|
+
const originalVersion = utxoVersion;
|
|
558
|
+
// Skip V1 format entirely - V1 never had schema versions and is considered legacy
|
|
559
|
+
// No backward compatibility needed for V1 format UTXOs
|
|
560
|
+
if (utxoVersion === 'v1') {
|
|
561
|
+
debugLogger.decryptionFailure('V1', 'LEGACY_FORMAT_SKIPPED', 'V1 format UTXO - skipping (no backward compatibility)');
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
// Early termination: Check schema version at byte 9 BEFORE attempting decryption
|
|
565
|
+
// This provides O(1) skip decision for UTXOs with incompatible schema versions
|
|
566
|
+
const mismatchedSchemaVersion = this.checkSchemaVersionMismatch(encryptedBuffer, utxoVersion);
|
|
567
|
+
if (mismatchedSchemaVersion !== null) {
|
|
568
|
+
const expectedSchemaVersion = EncryptionService.SIGNATURE_SCHEMA_VERSION[0];
|
|
569
|
+
debugLogger.schemaVersionMismatch(mismatchedSchemaVersion, expectedSchemaVersion, hashForLog(encryptedBuffer));
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
let decrypted;
|
|
573
|
+
if (utxoVersion === 'v3') {
|
|
574
|
+
decrypted = this.decryptV3(encryptedBuffer);
|
|
575
|
+
// V3 also uses V2 private keys for the UTXO logic
|
|
576
|
+
utxoVersion = 'v2';
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// For V2 format, use decrypt() which calls decryptV2()
|
|
580
|
+
decrypted = this.decrypt(encryptedBuffer);
|
|
581
|
+
}
|
|
582
|
+
// Handle legacy format UTXOs (without schema version byte) - skip them
|
|
583
|
+
if (decrypted === null) {
|
|
584
|
+
debugLogger.decryptionFailure(originalVersion, 'LEGACY_FORMAT_SKIPPED', 'Old format UTXO without schema version byte - returning null');
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
// Parse the pipe-delimited format: amount|blinding|index|mintAddress
|
|
588
|
+
const decryptedStr = decrypted.toString();
|
|
589
|
+
const parts = decryptedStr.split('|');
|
|
590
|
+
if (parts.length !== 4) {
|
|
591
|
+
debugLogger.decryptionFailure(originalVersion, 'INVALID_UTXO_FORMAT', `Expected 4 pipe-delimited parts, got ${parts.length}`, {
|
|
592
|
+
partsCount: parts.length
|
|
593
|
+
});
|
|
594
|
+
throw new Error('Invalid UTXO format after decryption');
|
|
595
|
+
}
|
|
596
|
+
const [amount, blinding, index, mintAddress] = parts;
|
|
597
|
+
if (!amount || !blinding || index === undefined || mintAddress === undefined) {
|
|
598
|
+
debugLogger.decryptionFailure(originalVersion, 'MISSING_UTXO_FIELDS', 'One or more required UTXO fields are missing', {
|
|
599
|
+
hasAmount: !!amount,
|
|
600
|
+
hasBlinding: !!blinding,
|
|
601
|
+
hasIndex: index !== undefined,
|
|
602
|
+
hasMintAddress: mintAddress !== undefined
|
|
603
|
+
});
|
|
604
|
+
throw new Error('Invalid UTXO format after decryption');
|
|
605
|
+
}
|
|
606
|
+
// Get or create a LightWasm instance
|
|
607
|
+
const wasmInstance = lightWasm || await WasmFactory.getInstance();
|
|
608
|
+
const privateKey = this.getUtxoPrivateKeyWithVersion(utxoVersion);
|
|
609
|
+
// Create a Utxo instance with the detected version
|
|
610
|
+
const utxo = new Utxo({
|
|
611
|
+
lightWasm: wasmInstance,
|
|
612
|
+
amount: amount,
|
|
613
|
+
blinding: blinding,
|
|
614
|
+
keypair: new UtxoKeypair(privateKey, wasmInstance),
|
|
615
|
+
index: Number(index),
|
|
616
|
+
mintAddress: mintAddress,
|
|
617
|
+
version: utxoVersion
|
|
618
|
+
});
|
|
619
|
+
// Log UTXO metadata after successful decryption
|
|
620
|
+
const commitment = await utxo.getCommitment();
|
|
621
|
+
debugLogger.utxoDecrypted(hashForLog(Buffer.from(commitment)), mintAddress, encryptedBuffer.length, index, originalVersion);
|
|
622
|
+
return utxo;
|
|
623
|
+
}
|
|
624
|
+
getUtxoPrivateKeyWithVersion(version) {
|
|
625
|
+
if (version === 'v1') {
|
|
626
|
+
return this.getUtxoPrivateKeyV1();
|
|
627
|
+
}
|
|
628
|
+
return this.getUtxoPrivateKeyV2();
|
|
629
|
+
}
|
|
630
|
+
deriveUtxoPrivateKey(encryptedData) {
|
|
631
|
+
if (encryptedData && this.getEncryptionKeyVersion(encryptedData) === 'v2') {
|
|
632
|
+
return this.getUtxoPrivateKeyWithVersion('v2');
|
|
633
|
+
}
|
|
634
|
+
if (encryptedData && this.getEncryptionKeyVersion(encryptedData) === 'v3') {
|
|
635
|
+
return this.getUtxoPrivateKeyWithVersion('v2');
|
|
636
|
+
}
|
|
637
|
+
return this.getUtxoPrivateKeyWithVersion('v1');
|
|
638
|
+
}
|
|
639
|
+
hasUtxoPrivateKeyWithVersion(version) {
|
|
640
|
+
if (version === 'v1') {
|
|
641
|
+
return !!this.utxoPrivateKeyV1;
|
|
642
|
+
}
|
|
643
|
+
return !!this.utxoPrivateKeyV2;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Get the cached V1 UTXO private key
|
|
647
|
+
* @returns A private key in hex format that can be used to create a UTXO keypair
|
|
648
|
+
* @throws Error if V1 encryption key has not been set
|
|
649
|
+
*/
|
|
650
|
+
getUtxoPrivateKeyV1() {
|
|
651
|
+
if (!this.utxoPrivateKeyV1) {
|
|
652
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
653
|
+
}
|
|
654
|
+
return this.utxoPrivateKeyV1;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Get the cached V2 UTXO private key
|
|
658
|
+
* @returns A private key in hex format that can be used to create a UTXO keypair
|
|
659
|
+
* @throws Error if V2 encryption key has not been set
|
|
660
|
+
*/
|
|
661
|
+
getUtxoPrivateKeyV2() {
|
|
662
|
+
if (!this.utxoPrivateKeyV2) {
|
|
663
|
+
throw new Error('Encryption key not set. Call setEncryptionKey or deriveEncryptionKeyFromWallet first.');
|
|
664
|
+
}
|
|
665
|
+
return this.utxoPrivateKeyV2;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
export function serializeProofAndExtData(proof, extData, isSpl = false) {
|
|
669
|
+
// Create the ExtDataMinified object for the program call (only extAmount and fee)
|
|
670
|
+
const extDataMinified = {
|
|
671
|
+
extAmount: extData.extAmount,
|
|
672
|
+
fee: extData.fee
|
|
673
|
+
};
|
|
674
|
+
// Use the appropriate discriminator based on whether this is SPL or native SOL
|
|
675
|
+
const discriminator = isSpl ? TRANSACT_SPL_IX_DISCRIMINATOR : TRANSACT_IX_DISCRIMINATOR;
|
|
676
|
+
// Use the same serialization approach as deposit script
|
|
677
|
+
const instructionData = Buffer.concat([
|
|
678
|
+
discriminator,
|
|
679
|
+
// Serialize proof
|
|
680
|
+
Buffer.from(proof.proofA),
|
|
681
|
+
Buffer.from(proof.proofB),
|
|
682
|
+
Buffer.from(proof.proofC),
|
|
683
|
+
Buffer.from(proof.root),
|
|
684
|
+
Buffer.from(proof.publicAmount),
|
|
685
|
+
Buffer.from(proof.extDataHash),
|
|
686
|
+
Buffer.from(proof.inputNullifiers[0]),
|
|
687
|
+
Buffer.from(proof.inputNullifiers[1]),
|
|
688
|
+
Buffer.from(proof.outputCommitments[0]),
|
|
689
|
+
Buffer.from(proof.outputCommitments[1]),
|
|
690
|
+
// Serialize ExtDataMinified (only extAmount and fee)
|
|
691
|
+
Buffer.from(new BN(extDataMinified.extAmount).toTwos(64).toArray('le', 8)),
|
|
692
|
+
Buffer.from(new BN(extDataMinified.fee).toArray('le', 8)),
|
|
693
|
+
// Serialize encrypted outputs as separate parameters
|
|
694
|
+
Buffer.from(new BN(extData.encryptedOutput1.length).toArray('le', 4)),
|
|
695
|
+
extData.encryptedOutput1,
|
|
696
|
+
Buffer.from(new BN(extData.encryptedOutput2.length).toArray('le', 4)),
|
|
697
|
+
extData.encryptedOutput2,
|
|
698
|
+
]);
|
|
699
|
+
return instructionData;
|
|
700
|
+
}
|