@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,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
|
+
};
|