@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/dist/__tests__/paylink.test.d.ts +9 -0
  4. package/dist/__tests__/paylink.test.js +254 -0
  5. package/dist/config.d.ts +9 -0
  6. package/dist/config.js +12 -0
  7. package/dist/deposit.d.ts +22 -0
  8. package/dist/deposit.js +445 -0
  9. package/dist/depositSPL.d.ts +24 -0
  10. package/dist/depositSPL.js +499 -0
  11. package/dist/errors.d.ts +78 -0
  12. package/dist/errors.js +127 -0
  13. package/dist/exportUtils.d.ts +10 -0
  14. package/dist/exportUtils.js +10 -0
  15. package/dist/getUtxos.d.ts +30 -0
  16. package/dist/getUtxos.js +335 -0
  17. package/dist/getUtxosSPL.d.ts +34 -0
  18. package/dist/getUtxosSPL.js +442 -0
  19. package/dist/index.d.ts +183 -0
  20. package/dist/index.js +436 -0
  21. package/dist/models/keypair.d.ts +26 -0
  22. package/dist/models/keypair.js +43 -0
  23. package/dist/models/utxo.d.ts +51 -0
  24. package/dist/models/utxo.js +99 -0
  25. package/dist/test_paylink_logic.test.d.ts +1 -0
  26. package/dist/test_paylink_logic.test.js +114 -0
  27. package/dist/utils/address_lookup_table.d.ts +9 -0
  28. package/dist/utils/address_lookup_table.js +45 -0
  29. package/dist/utils/constants.d.ts +27 -0
  30. package/dist/utils/constants.js +56 -0
  31. package/dist/utils/debug-logger.d.ts +250 -0
  32. package/dist/utils/debug-logger.js +688 -0
  33. package/dist/utils/encryption.d.ts +152 -0
  34. package/dist/utils/encryption.js +700 -0
  35. package/dist/utils/logger.d.ts +9 -0
  36. package/dist/utils/logger.js +35 -0
  37. package/dist/utils/merkle_tree.d.ts +92 -0
  38. package/dist/utils/merkle_tree.js +186 -0
  39. package/dist/utils/node-shim.d.ts +14 -0
  40. package/dist/utils/node-shim.js +21 -0
  41. package/dist/utils/prover.d.ts +36 -0
  42. package/dist/utils/prover.js +169 -0
  43. package/dist/utils/utils.d.ts +64 -0
  44. package/dist/utils/utils.js +165 -0
  45. package/dist/withdraw.d.ts +22 -0
  46. package/dist/withdraw.js +290 -0
  47. package/dist/withdrawSPL.d.ts +24 -0
  48. package/dist/withdrawSPL.js +329 -0
  49. package/package.json +59 -0
@@ -0,0 +1,688 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ // Global failure tracking for current balance fetch operation
3
+ let currentFailureSummary = null;
4
+ // Global debug state
5
+ let debugEnabled = false;
6
+ let verboseEnabled = false;
7
+ let debugLoggerFn = null;
8
+ let urlParamChecked = false;
9
+ /**
10
+ * Check if running in development mode
11
+ */
12
+ function isDevelopmentMode() {
13
+ // Node.js environment
14
+ if (typeof process !== 'undefined' && process.env) {
15
+ const nodeEnv = process.env.NODE_ENV;
16
+ if (nodeEnv === 'development') {
17
+ return true;
18
+ }
19
+ }
20
+ // Browser environment - check for common development indicators
21
+ if (typeof window !== 'undefined') {
22
+ // Check if NEXT_PUBLIC_NODE_ENV is set (Next.js)
23
+ const win = window;
24
+ // Check for localhost
25
+ if (typeof location !== 'undefined' &&
26
+ (location.hostname === 'localhost' || location.hostname === '127.0.0.1')) {
27
+ return true;
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * Check URL parameters for debug enablement (production feature)
34
+ * Enables via ?privacy_cash_debug=1 or ?privacy_cash_debug=true
35
+ */
36
+ function checkUrlParamDebugEnabled() {
37
+ if (typeof window === 'undefined' || typeof location === 'undefined') {
38
+ return false;
39
+ }
40
+ // Only check once to avoid repeated URL parsing
41
+ if (urlParamChecked) {
42
+ return false;
43
+ }
44
+ urlParamChecked = true;
45
+ try {
46
+ const params = new URLSearchParams(location.search);
47
+ const debugParam = params.get('privacy_cash_debug');
48
+ return debugParam === 'true' || debugParam === '1';
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /**
55
+ * Check if debug mode should be enabled based on environment variable
56
+ */
57
+ function checkEnvDebugEnabled() {
58
+ // Check for PRIVACY_CASH_DEBUG environment variable
59
+ if (typeof process !== 'undefined' && process.env) {
60
+ const envValue = process.env.PRIVACY_CASH_DEBUG;
61
+ return envValue === 'true' || envValue === '1';
62
+ }
63
+ // Browser environment - check window global
64
+ if (typeof window !== 'undefined') {
65
+ const win = window;
66
+ if (win.PRIVACY_CASH_DEBUG === true || win.PRIVACY_CASH_DEBUG === 'true' || win.PRIVACY_CASH_DEBUG === '1') {
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+ /**
73
+ * Check if verbose mode should be enabled based on environment variable
74
+ */
75
+ function checkEnvVerboseEnabled() {
76
+ // Check for PRIVACY_CASH_VERBOSE environment variable
77
+ if (typeof process !== 'undefined' && process.env) {
78
+ const envValue = process.env.PRIVACY_CASH_VERBOSE;
79
+ return envValue === 'true' || envValue === '1';
80
+ }
81
+ // Browser environment - check window global
82
+ if (typeof window !== 'undefined') {
83
+ const win = window;
84
+ if (win.PRIVACY_CASH_VERBOSE === true || win.PRIVACY_CASH_VERBOSE === 'true' || win.PRIVACY_CASH_VERBOSE === '1') {
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+ /**
91
+ * Enable debug logging programmatically
92
+ * @param customLogger Optional custom logger function
93
+ * @param verbose Enable verbose/trace mode for individual UTXO logs (default: false)
94
+ */
95
+ export function enableDebugLogging(customLogger, verbose) {
96
+ debugEnabled = true;
97
+ if (customLogger) {
98
+ debugLoggerFn = customLogger;
99
+ }
100
+ if (verbose !== undefined) {
101
+ verboseEnabled = verbose;
102
+ }
103
+ }
104
+ /**
105
+ * Enable verbose mode for individual UTXO logs
106
+ */
107
+ export function enableVerboseLogging() {
108
+ verboseEnabled = true;
109
+ }
110
+ /**
111
+ * Disable verbose mode
112
+ */
113
+ export function disableVerboseLogging() {
114
+ verboseEnabled = false;
115
+ }
116
+ /**
117
+ * Check if verbose logging is enabled
118
+ */
119
+ export function isVerboseEnabled() {
120
+ return verboseEnabled || checkEnvVerboseEnabled();
121
+ }
122
+ /**
123
+ * Disable debug logging
124
+ */
125
+ export function disableDebugLogging() {
126
+ debugEnabled = false;
127
+ verboseEnabled = false;
128
+ debugLoggerFn = null;
129
+ }
130
+ /**
131
+ * Install debug commands on the window object for production debugging
132
+ * Call this from browser console: window.privacyCashDebug.enable()
133
+ */
134
+ export function installDebugCommands() {
135
+ if (typeof window === 'undefined') {
136
+ return;
137
+ }
138
+ const win = window;
139
+ win.privacyCashDebug = {
140
+ enable: () => {
141
+ enableDebugLogging();
142
+ console.log('[PRIVACY-CASH-DEBUG] Debug logging enabled. Refresh or retry operations to see logs.');
143
+ },
144
+ disable: () => {
145
+ disableDebugLogging();
146
+ console.log('[PRIVACY-CASH-DEBUG] Debug logging disabled.');
147
+ },
148
+ verbose: (enable) => {
149
+ if (enable === false) {
150
+ disableVerboseLogging();
151
+ console.log('[PRIVACY-CASH-DEBUG] Verbose logging disabled. Individual UTXO logs suppressed.');
152
+ }
153
+ else {
154
+ enableVerboseLogging();
155
+ console.log('[PRIVACY-CASH-DEBUG] Verbose logging enabled. Individual UTXO logs will be shown.');
156
+ }
157
+ },
158
+ status: () => {
159
+ const enabled = isDebugEnabled();
160
+ const verbose = isVerboseEnabled();
161
+ const mode = isDevelopmentMode() ? 'development' : 'production';
162
+ console.log(`[PRIVACY-CASH-DEBUG] Status: ${enabled ? 'ENABLED' : 'DISABLED'}`);
163
+ console.log(`[PRIVACY-CASH-DEBUG] Verbose: ${verbose ? 'ENABLED' : 'DISABLED'}`);
164
+ console.log(`[PRIVACY-CASH-DEBUG] Mode: ${mode}`);
165
+ console.log('[PRIVACY-CASH-DEBUG] To enable: window.privacyCashDebug.enable() or add ?privacy_cash_debug=1 to URL');
166
+ console.log('[PRIVACY-CASH-DEBUG] For verbose: window.privacyCashDebug.verbose() or set PRIVACY_CASH_VERBOSE=1');
167
+ }
168
+ };
169
+ }
170
+ // Auto-install debug commands in browser environment
171
+ if (typeof window !== 'undefined') {
172
+ installDebugCommands();
173
+ }
174
+ /**
175
+ * Check if debug logging is enabled
176
+ *
177
+ * Debug logging is enabled if any of the following conditions are met:
178
+ * 1. Explicitly enabled via enableDebugLogging()
179
+ * 2. PRIVACY_CASH_DEBUG environment variable is set to 'true' or '1'
180
+ * 3. window.PRIVACY_CASH_DEBUG is set to true, 'true', or '1'
181
+ * 4. Running in development mode (NODE_ENV=development or localhost)
182
+ * 5. URL contains ?privacy_cash_debug=true or ?privacy_cash_debug=1 (production feature)
183
+ */
184
+ export function isDebugEnabled() {
185
+ return debugEnabled || checkEnvDebugEnabled() || isDevelopmentMode() || checkUrlParamDebugEnabled();
186
+ }
187
+ /**
188
+ * Set a custom debug logger function
189
+ */
190
+ export function setDebugLogger(fn) {
191
+ debugLoggerFn = fn;
192
+ }
193
+ /**
194
+ * Hash sensitive data for safe logging (first 8 chars of hex)
195
+ */
196
+ export function hashForLog(data) {
197
+ if (!data)
198
+ return '<null>';
199
+ const bytes = typeof data === 'string' ? Buffer.from(data, 'hex') : data;
200
+ if (bytes.length === 0)
201
+ return '<empty>';
202
+ const hash = sha256(bytes);
203
+ return Buffer.from(hash).toString('hex').substring(0, 16) + '...';
204
+ }
205
+ /**
206
+ * Format bytes length for logging
207
+ */
208
+ export function bytesInfo(data) {
209
+ if (!data)
210
+ return '<null>';
211
+ const len = typeof data === 'string' ? data.length / 2 : data.length;
212
+ return `${len} bytes`;
213
+ }
214
+ /**
215
+ * Default console logger implementation
216
+ */
217
+ const defaultDebugLogger = (entry) => {
218
+ const prefix = `[PRIVACY-CASH-DEBUG][${entry.level.toUpperCase()}][${entry.category}]`;
219
+ const message = `${prefix} ${entry.message}`;
220
+ if (entry.data) {
221
+ console.log(message, entry.data);
222
+ }
223
+ else {
224
+ console.log(message);
225
+ }
226
+ };
227
+ /**
228
+ * Core debug logging function
229
+ *
230
+ * Log levels:
231
+ * - trace: Only shown in verbose mode (individual UTXO logs)
232
+ * - debug: Detailed debugging info (shown when debug enabled)
233
+ * - info: General information (shown when debug enabled)
234
+ * - warn: Warnings (always shown when debug enabled)
235
+ * - error: Errors (always shown when debug enabled)
236
+ */
237
+ function logDebug(level, category, message, data) {
238
+ if (!isDebugEnabled())
239
+ return;
240
+ // Trace level requires verbose mode
241
+ if (level === 'trace' && !isVerboseEnabled())
242
+ return;
243
+ const entry = {
244
+ timestamp: new Date().toISOString(),
245
+ level,
246
+ category,
247
+ message,
248
+ data
249
+ };
250
+ const logger = debugLoggerFn || defaultDebugLogger;
251
+ logger(entry);
252
+ }
253
+ /**
254
+ * Debug logger for encryption-related operations
255
+ */
256
+ export const debugLogger = {
257
+ /**
258
+ * Log the first 8 bytes (version prefix) of encrypted data
259
+ */
260
+ versionPrefixBytes(prefixHex, dataLength) {
261
+ logDebug('debug', 'VERSION_PREFIX', `First 8 bytes of encrypted data: ${prefixHex}`, {
262
+ prefixHex,
263
+ dataLength
264
+ });
265
+ },
266
+ /**
267
+ * Log encryption version detection
268
+ */
269
+ versionDetected(encryptedDataHash, version, dataLength) {
270
+ logDebug('info', 'VERSION_DETECT', `Detected encryption version: ${version}`, {
271
+ encryptedDataHash,
272
+ version,
273
+ dataLength
274
+ });
275
+ },
276
+ /**
277
+ * Log when version detection falls back to legacy V1 mode
278
+ */
279
+ versionFallbackToLegacy(prefixHex, reason) {
280
+ logDebug('warn', 'VERSION_FALLBACK', `Falling back to legacy V1 mode: ${reason}`, {
281
+ prefixHex,
282
+ reason
283
+ });
284
+ },
285
+ /**
286
+ * Log key derivation steps (with hashed keys for privacy)
287
+ */
288
+ keyDerivation(step, keyHash, keyType) {
289
+ logDebug('debug', 'KEY_DERIVATION', `${step}: ${keyType}`, {
290
+ step,
291
+ keyHash,
292
+ keyType
293
+ });
294
+ },
295
+ /**
296
+ * Log asymmetric key pair generation
297
+ */
298
+ asymmetricKeyGenerated(publicKeyHash, secretKeyHash) {
299
+ logDebug('info', 'ASYMMETRIC_KEY', 'X25519 keypair derived from signature', {
300
+ publicKeyHash,
301
+ secretKeyHash: secretKeyHash.substring(0, 8) + '...'
302
+ });
303
+ },
304
+ /**
305
+ * Log decryption attempt start
306
+ */
307
+ decryptionAttemptStart(version, encryptedDataHash, dataLength) {
308
+ logDebug('info', 'DECRYPT_ATTEMPT', `Starting ${version} decryption`, {
309
+ version,
310
+ encryptedDataHash,
311
+ dataLength
312
+ });
313
+ },
314
+ /**
315
+ * Log schema version mismatch for early termination
316
+ * Individual UTXO logs are verbose-only; tracking is always updated
317
+ * @param foundVersion The schema version byte found in the encrypted data
318
+ * @param expectedVersion The expected schema version byte
319
+ * @param encryptedDataHash Hash of the encrypted data for identification
320
+ */
321
+ schemaVersionMismatch(foundVersion, expectedVersion, encryptedDataHash) {
322
+ // Always track the mismatch for summary
323
+ this.recordSchemaMismatch();
324
+ // Individual log is verbose-only (trace level) to avoid console spam
325
+ logDebug('trace', 'SCHEMA_VERSION_MISMATCH', 'Skipping UTXO due to incompatible schema version', {
326
+ foundVersion: `0x${foundVersion.toString(16).padStart(2, '0')}`,
327
+ expectedVersion: `0x${expectedVersion.toString(16).padStart(2, '0')}`,
328
+ encryptedDataHash,
329
+ action: 'early_termination'
330
+ });
331
+ },
332
+ /**
333
+ * Log recipient ID hash mismatch (O(1) early termination)
334
+ * This is the fastest way to skip UTXOs that don't belong to this wallet
335
+ */
336
+ recipientIdMismatch(encryptedDataHash) {
337
+ // Track as skipped (not failed, because this is expected behavior for other users' UTXOs)
338
+ this.recordDecryptionSkipped();
339
+ // Individual log is verbose-only (trace level) to avoid console spam
340
+ // In production, 140,000+ UTXOs will trigger this, so we only log at trace level
341
+ logDebug('trace', 'RECIPIENT_ID_MISMATCH', 'Skipping UTXO - recipient ID hash does not match this wallet', {
342
+ encryptedDataHash,
343
+ action: 'early_termination_o1'
344
+ });
345
+ },
346
+ /**
347
+ * Log decryption success
348
+ */
349
+ decryptionSuccess(version, decryptedLength) {
350
+ logDebug('info', 'DECRYPT_SUCCESS', `${version} decryption succeeded`, {
351
+ version,
352
+ decryptedLength
353
+ });
354
+ },
355
+ /**
356
+ * Log decryption failure with detailed error info
357
+ */
358
+ decryptionFailure(version, errorType, errorMessage, context) {
359
+ logDebug('error', 'DECRYPT_FAILURE', `${version} decryption failed: ${errorType}`, {
360
+ version,
361
+ errorType,
362
+ errorMessage,
363
+ ...context
364
+ });
365
+ },
366
+ /**
367
+ * Log V3 asymmetric decryption details
368
+ */
369
+ v3DecryptionDetails(ephemeralPubKeyHash, nonceHash, boxLength, recipientSecretKeyHash) {
370
+ logDebug('debug', 'V3_DECRYPT', 'V3 asymmetric decryption parameters', {
371
+ ephemeralPubKeyHash,
372
+ nonceHash,
373
+ boxLength,
374
+ recipientSecretKeyHash: recipientSecretKeyHash.substring(0, 8) + '...'
375
+ });
376
+ },
377
+ /**
378
+ * Log UTXO metadata after successful decryption
379
+ */
380
+ utxoDecrypted(commitmentHash, tokenMint, encryptedLength, utxoIndex, version) {
381
+ logDebug('info', 'UTXO_DECRYPTED', 'UTXO successfully decrypted', {
382
+ commitmentHash,
383
+ tokenMint,
384
+ encryptedLength,
385
+ utxoIndex,
386
+ version
387
+ });
388
+ },
389
+ /**
390
+ * Log UTXO decryption batch summary
391
+ */
392
+ utxoBatchSummary(total, decrypted, skipped, failed) {
393
+ logDebug('info', 'UTXO_BATCH', `Batch decryption complete: ${decrypted}/${total} successful`, {
394
+ total,
395
+ decrypted,
396
+ skipped,
397
+ failed
398
+ });
399
+ },
400
+ /**
401
+ * Log encryption service initialization
402
+ */
403
+ serviceInitialized(hasV1Key, hasV2Key, hasAsymmetricKey) {
404
+ logDebug('info', 'SERVICE_INIT', 'EncryptionService key state', {
405
+ hasV1Key,
406
+ hasV2Key,
407
+ hasAsymmetricKey
408
+ });
409
+ },
410
+ /**
411
+ * Log when attempting decryption without required key
412
+ */
413
+ missingKey(keyType, operation) {
414
+ logDebug('error', 'MISSING_KEY', `Missing ${keyType} for ${operation}`, {
415
+ keyType,
416
+ operation
417
+ });
418
+ },
419
+ /**
420
+ * Generic debug log
421
+ */
422
+ debug(category, message, data) {
423
+ logDebug('debug', category, message, data);
424
+ },
425
+ /**
426
+ * Generic info log
427
+ */
428
+ info(category, message, data) {
429
+ logDebug('info', category, message, data);
430
+ },
431
+ /**
432
+ * Generic warning log
433
+ */
434
+ warn(category, message, data) {
435
+ logDebug('warn', category, message, data);
436
+ },
437
+ /**
438
+ * Generic error log
439
+ */
440
+ error(category, message, data) {
441
+ logDebug('error', category, message, data);
442
+ },
443
+ /**
444
+ * Log X25519 public key derivation for sender-side verification
445
+ * @param publicKeyHash Hash of the derived X25519 public key
446
+ * @param walletAddress The wallet address used for key derivation
447
+ * @param context Whether this is sender or recipient side
448
+ */
449
+ x25519KeyDerived(publicKeyHash, walletAddress, context) {
450
+ logDebug('info', 'X25519_KEY_DERIVED', `X25519 public key derived (${context} side)`, {
451
+ publicKeyHash,
452
+ walletAddress,
453
+ context
454
+ });
455
+ },
456
+ /**
457
+ * Log X25519 public key used during encryption (sender side)
458
+ * @param recipientPublicKeyHash Hash of the recipient's X25519 public key being encrypted to
459
+ * @param walletAddress Sender's wallet address
460
+ */
461
+ x25519EncryptionKey(recipientPublicKeyHash, walletAddress) {
462
+ logDebug('info', 'X25519_ENCRYPT', 'Encrypting with recipient X25519 public key', {
463
+ recipientPublicKeyHash,
464
+ walletAddress: walletAddress || '<not provided>'
465
+ });
466
+ },
467
+ /**
468
+ * Log X25519 public key used during decryption (recipient side)
469
+ * @param derivedPublicKeyHash Hash of the recipient's derived X25519 public key
470
+ * @param walletAddress Recipient's wallet address
471
+ */
472
+ x25519DecryptionKey(derivedPublicKeyHash, walletAddress) {
473
+ logDebug('info', 'X25519_DECRYPT', 'Decrypting with derived X25519 public key', {
474
+ derivedPublicKeyHash,
475
+ walletAddress: walletAddress || '<not provided>'
476
+ });
477
+ },
478
+ /**
479
+ * Log key mismatch comparison when decryption fails
480
+ * @param expectedKeyHash Hash of the expected public key (from encrypted data)
481
+ * @param derivedKeyHash Hash of the derived public key (from wallet signature)
482
+ * @param walletAddress The wallet address used for derivation
483
+ */
484
+ x25519KeyMismatch(expectedKeyHash, derivedKeyHash, walletAddress) {
485
+ logDebug('warn', 'X25519_KEY_MISMATCH', 'X25519 public key mismatch detected - possible different wallet or signature', {
486
+ expectedKeyHash,
487
+ derivedKeyHash,
488
+ walletAddress: walletAddress || '<not provided>',
489
+ keysMatch: expectedKeyHash === derivedKeyHash
490
+ });
491
+ },
492
+ /**
493
+ * Log wallet address association with key derivation
494
+ * @param walletAddress The wallet address being used
495
+ * @param operation The operation being performed (encryption/decryption)
496
+ */
497
+ walletKeyDerivation(walletAddress, operation) {
498
+ logDebug('debug', 'WALLET_KEY_DERIVATION', `Wallet address associated with ${operation} operation`, {
499
+ walletAddress,
500
+ operation
501
+ });
502
+ },
503
+ // ======== Failure Tracking Methods ========
504
+ /**
505
+ * Initialize failure tracking for a new balance fetch operation
506
+ */
507
+ startFailureTracking() {
508
+ currentFailureSummary = {
509
+ totalAttempted: 0,
510
+ totalDecrypted: 0,
511
+ totalFailed: 0,
512
+ totalSkipped: 0,
513
+ totalSchemaMismatch: 0,
514
+ failuresByCategory: {
515
+ key_mismatch: 0,
516
+ malformed_data: 0,
517
+ version_error: 0,
518
+ missing_key: 0,
519
+ unknown: 0
520
+ },
521
+ failures: []
522
+ };
523
+ logDebug('trace', 'FAILURE_TRACKING', 'Started failure tracking for balance fetch');
524
+ },
525
+ /**
526
+ * Categorize an error based on its message and type
527
+ */
528
+ categorizeError(error) {
529
+ if (!error)
530
+ return 'unknown';
531
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
532
+ // Key mismatch patterns
533
+ if (message.includes('wrong tag') ||
534
+ message.includes('authentication failed') ||
535
+ message.includes('decrypt') && message.includes('fail') ||
536
+ message.includes('nacl.box.open') ||
537
+ message.includes('null') && message.includes('box')) {
538
+ return 'key_mismatch';
539
+ }
540
+ // Malformed data patterns
541
+ if (message.includes('malformed') ||
542
+ message.includes('invalid') && (message.includes('format') || message.includes('data')) ||
543
+ message.includes('unexpected') && message.includes('length') ||
544
+ message.includes('cannot read') ||
545
+ message.includes('parse') ||
546
+ message.includes('buffer') ||
547
+ message.includes('too short') ||
548
+ message.includes('truncated')) {
549
+ return 'malformed_data';
550
+ }
551
+ // Version error patterns
552
+ if (message.includes('version') ||
553
+ message.includes('unsupported') ||
554
+ message.includes('unknown encryption')) {
555
+ return 'version_error';
556
+ }
557
+ // Missing key patterns
558
+ if (message.includes('no encryption key') ||
559
+ message.includes('key not') ||
560
+ message.includes('missing key') ||
561
+ message.includes('derive')) {
562
+ return 'missing_key';
563
+ }
564
+ return 'unknown';
565
+ },
566
+ /**
567
+ * Record a decryption failure with full context
568
+ * Individual failure logs are only shown in verbose mode to avoid console spam
569
+ */
570
+ recordDecryptionFailure(error, encryptedDataHex, attemptedVersions) {
571
+ const category = this.categorizeError(error);
572
+ const errorMessage = error instanceof Error ? error.message : String(error);
573
+ const stackTrace = error instanceof Error ? error.stack : undefined;
574
+ const record = {
575
+ category,
576
+ errorMessage,
577
+ stackTrace,
578
+ encryptedDataHash: hashForLog(encryptedDataHex),
579
+ encryptedDataLength: encryptedDataHex ? encryptedDataHex.length / 2 : 0,
580
+ attemptedVersions,
581
+ timestamp: new Date().toISOString()
582
+ };
583
+ if (currentFailureSummary) {
584
+ currentFailureSummary.totalFailed++;
585
+ currentFailureSummary.failuresByCategory[category]++;
586
+ // Keep last 100 failure records to avoid memory issues
587
+ if (currentFailureSummary.failures.length < 100) {
588
+ currentFailureSummary.failures.push(record);
589
+ }
590
+ }
591
+ // Individual UTXO failure logs are verbose-only (trace level) to avoid console spam
592
+ logDebug('trace', 'DECRYPT_FAILURE_RECORDED', `Decryption failed: ${category}`, {
593
+ category,
594
+ errorMessage,
595
+ encryptedDataHash: record.encryptedDataHash,
596
+ encryptedDataLength: record.encryptedDataLength,
597
+ attemptedVersions,
598
+ hasStackTrace: !!stackTrace
599
+ });
600
+ },
601
+ /**
602
+ * Record a schema version mismatch (early termination)
603
+ * These are tracked separately since they're expected behavior for UTXOs not belonging to the user
604
+ */
605
+ recordSchemaMismatch() {
606
+ if (currentFailureSummary) {
607
+ currentFailureSummary.totalSchemaMismatch++;
608
+ }
609
+ },
610
+ /**
611
+ * Record a successful decryption
612
+ */
613
+ recordDecryptionSuccess() {
614
+ if (currentFailureSummary) {
615
+ currentFailureSummary.totalDecrypted++;
616
+ }
617
+ },
618
+ /**
619
+ * Record a skipped UTXO (empty/null encrypted data)
620
+ */
621
+ recordDecryptionSkipped() {
622
+ if (currentFailureSummary) {
623
+ currentFailureSummary.totalSkipped++;
624
+ }
625
+ },
626
+ /**
627
+ * Increment the total attempted counter
628
+ */
629
+ recordDecryptionAttempt() {
630
+ if (currentFailureSummary) {
631
+ currentFailureSummary.totalAttempted++;
632
+ }
633
+ },
634
+ /**
635
+ * Get the current failure summary and log it
636
+ * Logs a single summary line per balance fetch (not per-UTXO)
637
+ */
638
+ endFailureTracking() {
639
+ if (!currentFailureSummary) {
640
+ return null;
641
+ }
642
+ const summary = { ...currentFailureSummary };
643
+ // Only log summary if there were any UTXOs processed
644
+ if (summary.totalAttempted === 0 && summary.totalSchemaMismatch === 0 && summary.totalSkipped === 0) {
645
+ currentFailureSummary = null;
646
+ return summary;
647
+ }
648
+ // Build summary message - single line for quick understanding
649
+ const parts = [];
650
+ parts.push(`processed: ${summary.totalAttempted}`);
651
+ if (summary.totalSchemaMismatch > 0) {
652
+ parts.push(`schema_skipped: ${summary.totalSchemaMismatch}`);
653
+ }
654
+ if (summary.totalFailed > 0) {
655
+ parts.push(`failed: ${summary.totalFailed}`);
656
+ }
657
+ parts.push(`decrypted: ${summary.totalDecrypted}`);
658
+ const hasIssues = summary.totalFailed > 0;
659
+ const level = hasIssues ? 'warn' : 'info';
660
+ // Log single summary line
661
+ logDebug(level, 'BALANCE_FETCH_SUMMARY', `Balance fetch complete - ${parts.join(', ')}`, {
662
+ totalAttempted: summary.totalAttempted,
663
+ totalDecrypted: summary.totalDecrypted,
664
+ totalFailed: summary.totalFailed,
665
+ totalSkipped: summary.totalSkipped,
666
+ totalSchemaMismatch: summary.totalSchemaMismatch,
667
+ failuresByCategory: summary.failuresByCategory
668
+ });
669
+ // Log category breakdown only if there are actual failures (not schema mismatches)
670
+ if (hasIssues) {
671
+ const categoryBreakdown = Object.entries(summary.failuresByCategory)
672
+ .filter(([_, count]) => count > 0)
673
+ .map(([cat, count]) => `${cat}: ${count}`)
674
+ .join(', ');
675
+ logDebug('warn', 'FAILURE_BREAKDOWN', `Failure categories: ${categoryBreakdown}`, {
676
+ breakdown: summary.failuresByCategory
677
+ });
678
+ }
679
+ currentFailureSummary = null;
680
+ return summary;
681
+ },
682
+ /**
683
+ * Get the current failure summary without ending tracking
684
+ */
685
+ getCurrentFailureSummary() {
686
+ return currentFailureSummary ? { ...currentFailureSummary } : null;
687
+ }
688
+ };