@cfxdevkit/services 0.1.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.
@@ -0,0 +1,1097 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/services/index.ts
21
+ var services_exports = {};
22
+ __export(services_exports, {
23
+ EncryptionService: () => EncryptionService,
24
+ KeystoreLockedError: () => KeystoreLockedError,
25
+ KeystoreService: () => KeystoreService,
26
+ SwapService: () => SwapService,
27
+ getKeystoreService: () => getKeystoreService
28
+ });
29
+ module.exports = __toCommonJS(services_exports);
30
+
31
+ // src/services/encryption.ts
32
+ var import_node_crypto = require("crypto");
33
+ var EncryptionService = class _EncryptionService {
34
+ // Utility class — not meant to be instantiated
35
+ constructor() {
36
+ }
37
+ static ITERATIONS = 1e5;
38
+ static KEY_LENGTH = 256;
39
+ static SALT_LENGTH = 32;
40
+ static IV_LENGTH = 12;
41
+ /**
42
+ * Generate a random salt for key derivation
43
+ */
44
+ static generateSalt() {
45
+ return Buffer.from(
46
+ import_node_crypto.webcrypto.getRandomValues(new Uint8Array(_EncryptionService.SALT_LENGTH))
47
+ );
48
+ }
49
+ /**
50
+ * Derive encryption key from password using PBKDF2
51
+ */
52
+ static async deriveKey(password, salt) {
53
+ const encoder = new TextEncoder();
54
+ const passwordBuffer = encoder.encode(password);
55
+ const keyMaterial = await import_node_crypto.webcrypto.subtle.importKey(
56
+ "raw",
57
+ passwordBuffer,
58
+ "PBKDF2",
59
+ false,
60
+ ["deriveBits", "deriveKey"]
61
+ );
62
+ return await import_node_crypto.webcrypto.subtle.deriveKey(
63
+ {
64
+ name: "PBKDF2",
65
+ salt: new Uint8Array(salt),
66
+ iterations: _EncryptionService.ITERATIONS,
67
+ hash: "SHA-256"
68
+ },
69
+ keyMaterial,
70
+ { name: "AES-GCM", length: _EncryptionService.KEY_LENGTH },
71
+ false,
72
+ ["encrypt", "decrypt"]
73
+ );
74
+ }
75
+ /**
76
+ * Encrypt plaintext with password
77
+ *
78
+ * @param plaintext - String to encrypt
79
+ * @param password - Encryption password
80
+ * @param salt - Salt for key derivation
81
+ * @returns Base64-encoded ciphertext with prepended IV
82
+ */
83
+ static async encrypt(plaintext, password, salt) {
84
+ const key = await _EncryptionService.deriveKey(password, salt);
85
+ const iv = import_node_crypto.webcrypto.getRandomValues(
86
+ new Uint8Array(_EncryptionService.IV_LENGTH)
87
+ );
88
+ const encoder = new TextEncoder();
89
+ const plaintextBuffer = encoder.encode(plaintext);
90
+ const ciphertext = await import_node_crypto.webcrypto.subtle.encrypt(
91
+ { name: "AES-GCM", iv },
92
+ key,
93
+ plaintextBuffer
94
+ );
95
+ const combined = new Uint8Array(iv.length + ciphertext.byteLength);
96
+ combined.set(iv, 0);
97
+ combined.set(new Uint8Array(ciphertext), iv.length);
98
+ return Buffer.from(combined).toString("base64");
99
+ }
100
+ /**
101
+ * Decrypt ciphertext with password
102
+ *
103
+ * @param ciphertext - Base64-encoded ciphertext with prepended IV
104
+ * @param password - Encryption password
105
+ * @param salt - Salt for key derivation
106
+ * @returns Decrypted plaintext
107
+ * @throws Error if decryption fails (wrong password or corrupted data)
108
+ */
109
+ static async decrypt(ciphertext, password, salt) {
110
+ const combined = Buffer.from(ciphertext, "base64");
111
+ const iv = combined.subarray(0, _EncryptionService.IV_LENGTH);
112
+ const encrypted = combined.subarray(_EncryptionService.IV_LENGTH);
113
+ const key = await _EncryptionService.deriveKey(password, salt);
114
+ try {
115
+ const decrypted = await import_node_crypto.webcrypto.subtle.decrypt(
116
+ { name: "AES-GCM", iv },
117
+ key,
118
+ encrypted
119
+ );
120
+ const decoder = new TextDecoder();
121
+ return decoder.decode(decrypted);
122
+ } catch (_error) {
123
+ throw new Error("Decryption failed - invalid password or corrupted data");
124
+ }
125
+ }
126
+ /**
127
+ * Encrypt an object (serializes to JSON first)
128
+ */
129
+ static async encryptObject(obj, password, salt) {
130
+ const json = JSON.stringify(obj);
131
+ return await _EncryptionService.encrypt(json, password, salt);
132
+ }
133
+ /**
134
+ * Decrypt to an object (parses JSON after decryption)
135
+ */
136
+ static async decryptObject(ciphertext, password, salt) {
137
+ const json = await _EncryptionService.decrypt(ciphertext, password, salt);
138
+ return JSON.parse(json);
139
+ }
140
+ /**
141
+ * Hash a string with SHA-256 (for config integrity checks)
142
+ */
143
+ static async hash(data) {
144
+ const encoder = new TextEncoder();
145
+ const dataBuffer = encoder.encode(data);
146
+ const hashBuffer = await import_node_crypto.webcrypto.subtle.digest("SHA-256", dataBuffer);
147
+ return Buffer.from(hashBuffer).toString("hex");
148
+ }
149
+ /**
150
+ * Verify password strength (basic validation)
151
+ */
152
+ static validatePasswordStrength(password) {
153
+ const errors = [];
154
+ if (password.length < 8) {
155
+ errors.push("Password must be at least 8 characters");
156
+ }
157
+ if (!/[a-z]/.test(password)) {
158
+ errors.push("Password must contain lowercase letters");
159
+ }
160
+ if (!/[A-Z]/.test(password)) {
161
+ errors.push("Password must contain uppercase letters");
162
+ }
163
+ if (!/[0-9]/.test(password)) {
164
+ errors.push("Password must contain numbers");
165
+ }
166
+ return {
167
+ valid: errors.length === 0,
168
+ errors
169
+ };
170
+ }
171
+ };
172
+
173
+ // src/services/keystore.ts
174
+ var import_node_crypto2 = require("crypto");
175
+ var import_node_fs = require("fs");
176
+ var import_node_os = require("os");
177
+ var import_node_path = require("path");
178
+ var import_utils = require("@cfxdevkit/core/utils");
179
+ var import_wallet = require("@cfxdevkit/core/wallet");
180
+ var KeystoreLockedError = class extends Error {
181
+ constructor(message = "Keystore is locked. Please unlock with your password first.") {
182
+ super(message);
183
+ this.name = "KeystoreLockedError";
184
+ }
185
+ };
186
+ var BASE_DATA_DIR = process.env.DEVKIT_DATA_DIR || "/workspace/.conflux-dev";
187
+ var DEFAULT_KEYSTORE_PATH = process.env.DEVKIT_KEYSTORE_PATH || (0, import_node_path.join)((0, import_node_os.homedir)(), ".devkit.keystore.json");
188
+ var KeystoreService = class {
189
+ keystorePath;
190
+ keystore = null;
191
+ currentPassword = null;
192
+ constructor(keystorePath = DEFAULT_KEYSTORE_PATH) {
193
+ this.keystorePath = keystorePath;
194
+ }
195
+ // ===== PRIVATE GUARD HELPERS =====
196
+ requireKeystore() {
197
+ if (!this.keystore) {
198
+ throw new Error("Keystore not initialized. Call initialize() first.");
199
+ }
200
+ return this.keystore;
201
+ }
202
+ requireEncryptionSalt() {
203
+ const ks = this.requireKeystore();
204
+ if (!ks.encryptionSalt) {
205
+ throw new Error("Encryption salt not configured on this keystore.");
206
+ }
207
+ return Buffer.from(ks.encryptionSalt, "base64");
208
+ }
209
+ requirePassword() {
210
+ if (!this.currentPassword) {
211
+ throw new KeystoreLockedError();
212
+ }
213
+ return this.currentPassword;
214
+ }
215
+ // ===== INITIALIZATION =====
216
+ /**
217
+ * Initialize keystore (load from disk or create empty)
218
+ */
219
+ async initialize() {
220
+ if ((0, import_node_fs.existsSync)(this.keystorePath)) {
221
+ await this.loadKeystore();
222
+ } else {
223
+ import_utils.logger.info("Keystore file not found - fresh installation detected");
224
+ this.keystore = null;
225
+ }
226
+ }
227
+ /**
228
+ * Load keystore from disk
229
+ */
230
+ async loadKeystore() {
231
+ try {
232
+ const data = (0, import_node_fs.readFileSync)(this.keystorePath, "utf-8");
233
+ this.keystore = JSON.parse(data);
234
+ if (this.keystore.version !== 2) {
235
+ throw new Error(
236
+ `Unsupported keystore version: ${this.keystore.version}. Expected version 2.`
237
+ );
238
+ }
239
+ import_utils.logger.info("Keystore loaded successfully");
240
+ } catch (error) {
241
+ import_utils.logger.error("Failed to load keystore:", error);
242
+ throw error;
243
+ }
244
+ }
245
+ /**
246
+ * Save keystore to disk
247
+ */
248
+ async saveKeystore() {
249
+ if (!this.keystore) {
250
+ throw new Error("Cannot save null keystore");
251
+ }
252
+ try {
253
+ const data = JSON.stringify(this.keystore, null, 2);
254
+ (0, import_node_fs.writeFileSync)(this.keystorePath, data, "utf-8");
255
+ import_utils.logger.info("Keystore saved successfully");
256
+ } catch (error) {
257
+ import_utils.logger.error("Failed to save keystore:", error);
258
+ throw error;
259
+ }
260
+ }
261
+ // ===== SETUP & STATUS =====
262
+ /**
263
+ * Check if initial setup is completed
264
+ */
265
+ async isSetupCompleted() {
266
+ return this.keystore?.setupCompleted ?? false;
267
+ }
268
+ /**
269
+ * Complete initial setup
270
+ */
271
+ async completeSetup(data) {
272
+ if (this.keystore?.setupCompleted) {
273
+ throw new Error("Setup already completed");
274
+ }
275
+ import_utils.logger.info("Completing initial setup...");
276
+ let encryptionSalt;
277
+ if (data.encryption?.enabled && data.encryption.password) {
278
+ encryptionSalt = EncryptionService.generateSalt();
279
+ this.currentPassword = data.encryption.password;
280
+ }
281
+ const mnemonicEntry = await this.createMnemonicEntry({
282
+ mnemonic: data.mnemonic,
283
+ label: data.mnemonicLabel,
284
+ nodeConfig: {
285
+ ...data.nodeConfig,
286
+ miningAuthor: data.nodeConfig.miningAuthor || "auto"
287
+ },
288
+ isFirstSetup: true,
289
+ encryptionEnabled: data.encryption?.enabled ?? false,
290
+ encryptionSalt
291
+ });
292
+ this.keystore = {
293
+ version: 2,
294
+ setupCompleted: true,
295
+ setupCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
296
+ adminAddresses: [data.adminAddress],
297
+ encryptionEnabled: data.encryption?.enabled ?? false,
298
+ encryptionSalt: encryptionSalt?.toString("base64"),
299
+ mnemonics: [mnemonicEntry],
300
+ activeIndex: 0
301
+ };
302
+ await this.saveKeystore();
303
+ import_utils.logger.success("Initial setup completed successfully");
304
+ import_utils.logger.info(`Admin address: ${data.adminAddress}`);
305
+ import_utils.logger.info(`Wallet: ${data.mnemonicLabel}`);
306
+ import_utils.logger.info(
307
+ `Encryption: ${data.encryption?.enabled ? "Enabled" : "Disabled"}`
308
+ );
309
+ }
310
+ // ===== ADMIN MANAGEMENT =====
311
+ /**
312
+ * Get all admin addresses
313
+ */
314
+ async getAdminAddresses() {
315
+ this.ensureKeystoreLoaded();
316
+ return [...this.keystore?.adminAddresses ?? []];
317
+ }
318
+ /**
319
+ * Add admin address
320
+ */
321
+ async addAdminAddress(address) {
322
+ this.ensureKeystoreLoaded();
323
+ const normalized = address.toLowerCase();
324
+ const exists = this.keystore?.adminAddresses.some(
325
+ (addr) => addr.toLowerCase() === normalized
326
+ );
327
+ if (exists) {
328
+ throw new Error("Admin address already exists");
329
+ }
330
+ this.keystore?.adminAddresses.push(address);
331
+ await this.saveKeystore();
332
+ import_utils.logger.info(`Added admin address: ${address}`);
333
+ }
334
+ /**
335
+ * Remove admin address
336
+ */
337
+ async removeAdminAddress(address, currentAdmin) {
338
+ this.ensureKeystoreLoaded();
339
+ if (address.toLowerCase() === currentAdmin.toLowerCase()) {
340
+ throw new Error("Cannot remove your own admin address");
341
+ }
342
+ if (this.requireKeystore().adminAddresses.length <= 1) {
343
+ throw new Error("Cannot remove the last admin address");
344
+ }
345
+ const index = this.requireKeystore().adminAddresses.findIndex(
346
+ (addr) => addr.toLowerCase() === address.toLowerCase()
347
+ );
348
+ if (index === -1) {
349
+ throw new Error("Admin address not found");
350
+ }
351
+ this.requireKeystore().adminAddresses.splice(index, 1);
352
+ await this.saveKeystore();
353
+ import_utils.logger.info(`Removed admin address: ${address}`);
354
+ }
355
+ /**
356
+ * Check if address is admin
357
+ */
358
+ isAdmin(address) {
359
+ if (!this.keystore) return false;
360
+ return this.keystore.adminAddresses.some(
361
+ (admin) => admin.toLowerCase() === address.toLowerCase()
362
+ );
363
+ }
364
+ // ===== MNEMONIC MANAGEMENT =====
365
+ /**
366
+ * Get active mnemonic entry
367
+ */
368
+ async getActiveMnemonic() {
369
+ this.ensureKeystoreLoaded();
370
+ const mnemonic = this.requireKeystore().mnemonics[this.requireKeystore().activeIndex];
371
+ if (!mnemonic) {
372
+ throw new Error("No active mnemonic found");
373
+ }
374
+ return mnemonic;
375
+ }
376
+ /**
377
+ * Get mnemonic by ID
378
+ */
379
+ async getMnemonic(id) {
380
+ this.ensureKeystoreLoaded();
381
+ const mnemonic = this.requireKeystore().mnemonics.find((m) => m.id === id);
382
+ if (!mnemonic) {
383
+ throw new Error(`Mnemonic not found: ${id}`);
384
+ }
385
+ return mnemonic;
386
+ }
387
+ /**
388
+ * List all mnemonics (summary only)
389
+ */
390
+ async listMnemonics() {
391
+ this.ensureKeystoreLoaded();
392
+ return this.requireKeystore().mnemonics.map((m, index) => ({
393
+ id: m.id,
394
+ label: m.label,
395
+ type: m.type,
396
+ isActive: index === this.requireKeystore().activeIndex,
397
+ createdAt: m.createdAt,
398
+ nodeConfig: m.nodeConfig,
399
+ dataDir: this.getDataDirForMnemonic(m.mnemonic),
400
+ dataSize: this.getDataDirSize(this.getDataDirForMnemonic(m.mnemonic))
401
+ }));
402
+ }
403
+ /**
404
+ * Add new mnemonic
405
+ */
406
+ async addMnemonic(data) {
407
+ this.ensureKeystoreLoaded();
408
+ const exists = this.requireKeystore().mnemonics.some(
409
+ (m) => m.label === data.label
410
+ );
411
+ if (exists) {
412
+ throw new Error(`Mnemonic with label "${data.label}" already exists`);
413
+ }
414
+ const mnemonicEntry = await this.createMnemonicEntry({
415
+ mnemonic: data.mnemonic,
416
+ label: data.label,
417
+ nodeConfig: {
418
+ ...data.nodeConfig,
419
+ miningAuthor: data.nodeConfig.miningAuthor || "auto"
420
+ },
421
+ isFirstSetup: false,
422
+ encryptionEnabled: this.requireKeystore().encryptionEnabled,
423
+ encryptionSalt: this.keystore?.encryptionSalt ? Buffer.from(this.keystore?.encryptionSalt, "base64") : void 0
424
+ });
425
+ this.requireKeystore().mnemonics.push(mnemonicEntry);
426
+ if (data.setAsActive) {
427
+ this.requireKeystore().activeIndex = this.requireKeystore().mnemonics.length - 1;
428
+ }
429
+ await this.saveKeystore();
430
+ import_utils.logger.info(`Added mnemonic: ${data.label}`);
431
+ return mnemonicEntry;
432
+ }
433
+ /**
434
+ * Update mnemonic label
435
+ */
436
+ async updateMnemonicLabel(id, label) {
437
+ this.ensureKeystoreLoaded();
438
+ const index = this.requireKeystore().mnemonics.findIndex(
439
+ (m) => m.id === id
440
+ );
441
+ if (index === -1) {
442
+ throw new Error(`Mnemonic not found: ${id}`);
443
+ }
444
+ if (!label.trim()) {
445
+ throw new Error(`Label cannot be empty`);
446
+ }
447
+ this.requireKeystore().mnemonics[index].label = label;
448
+ await this.saveKeystore();
449
+ import_utils.logger.info(`Updated label for mnemonic ${id} to: ${label}`);
450
+ }
451
+ /**
452
+ * Switch active mnemonic
453
+ */
454
+ async switchActiveMnemonic(id) {
455
+ this.ensureKeystoreLoaded();
456
+ const index = this.requireKeystore().mnemonics.findIndex(
457
+ (m) => m.id === id
458
+ );
459
+ if (index === -1) {
460
+ throw new Error(`Mnemonic not found: ${id}`);
461
+ }
462
+ this.requireKeystore().activeIndex = index;
463
+ await this.saveKeystore();
464
+ import_utils.logger.info(
465
+ `Switched to mnemonic: ${this.requireKeystore().mnemonics[index].label}`
466
+ );
467
+ }
468
+ /**
469
+ * Delete mnemonic and its data
470
+ */
471
+ async deleteMnemonic(id, deleteData = false) {
472
+ this.ensureKeystoreLoaded();
473
+ const index = this.requireKeystore().mnemonics.findIndex(
474
+ (m) => m.id === id
475
+ );
476
+ if (index === -1) {
477
+ throw new Error(`Mnemonic not found: ${id}`);
478
+ }
479
+ if (index === this.requireKeystore().activeIndex) {
480
+ throw new Error(
481
+ "Cannot delete active mnemonic. Switch to another mnemonic first."
482
+ );
483
+ }
484
+ const mnemonic = this.requireKeystore().mnemonics[index];
485
+ if (deleteData) {
486
+ const dataDir = this.getDataDirForMnemonic(mnemonic.mnemonic);
487
+ if ((0, import_node_fs.existsSync)(dataDir)) {
488
+ const lockFile = (0, import_node_path.join)(dataDir, "node.lock");
489
+ if ((0, import_node_fs.existsSync)(lockFile)) {
490
+ throw new Error("Cannot delete data while node is running");
491
+ }
492
+ (0, import_node_fs.rmSync)(dataDir, { recursive: true, force: true });
493
+ import_utils.logger.info(`Deleted data directory: ${dataDir}`);
494
+ }
495
+ }
496
+ this.requireKeystore().mnemonics.splice(index, 1);
497
+ if (this.requireKeystore().activeIndex > index) {
498
+ this.requireKeystore().activeIndex--;
499
+ }
500
+ await this.saveKeystore();
501
+ import_utils.logger.info(`Deleted mnemonic: ${mnemonic.label}`);
502
+ }
503
+ // ===== NODE CONFIGURATION =====
504
+ /**
505
+ * Get node configuration for a mnemonic
506
+ */
507
+ async getNodeConfig(mnemonicId) {
508
+ const mnemonic = await this.getMnemonic(mnemonicId);
509
+ return mnemonic.nodeConfig;
510
+ }
511
+ /**
512
+ * Check if node configuration can be modified
513
+ */
514
+ async canModifyNodeConfig(mnemonicId) {
515
+ const mnemonic = await this.getMnemonic(mnemonicId);
516
+ const dataDir = this.getDataDirForMnemonic(mnemonic.mnemonic);
517
+ if (!(0, import_node_fs.existsSync)(dataDir)) {
518
+ return {
519
+ canModify: true
520
+ };
521
+ }
522
+ const lockFile = (0, import_node_path.join)(dataDir, "node.lock");
523
+ if ((0, import_node_fs.existsSync)(lockFile)) {
524
+ return {
525
+ canModify: false,
526
+ reason: "Node is currently running",
527
+ lockFile
528
+ };
529
+ }
530
+ return {
531
+ canModify: false,
532
+ reason: "Data directory exists. Delete blockchain data to modify configuration.",
533
+ dataDir
534
+ };
535
+ }
536
+ /**
537
+ * Update node configuration
538
+ */
539
+ async updateNodeConfig(mnemonicId, config) {
540
+ const check = await this.canModifyNodeConfig(mnemonicId);
541
+ if (!check.canModify) {
542
+ throw new Error(check.reason);
543
+ }
544
+ this.ensureKeystoreLoaded();
545
+ const index = this.requireKeystore().mnemonics.findIndex(
546
+ (m) => m.id === mnemonicId
547
+ );
548
+ if (index === -1) {
549
+ throw new Error(`Mnemonic not found: ${mnemonicId}`);
550
+ }
551
+ const mnemonic = this.requireKeystore().mnemonics[index];
552
+ const newConfig = {
553
+ ...mnemonic.nodeConfig,
554
+ ...config,
555
+ immutable: true,
556
+ configHash: "",
557
+ // Will be recalculated
558
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
559
+ };
560
+ newConfig.configHash = await this.calculateConfigHash(newConfig);
561
+ this.requireKeystore().mnemonics[index].nodeConfig = newConfig;
562
+ await this.saveKeystore();
563
+ import_utils.logger.info(`Updated node config for: ${mnemonic.label}`);
564
+ }
565
+ /**
566
+ * Delete blockchain data directory
567
+ */
568
+ async deleteNodeData(mnemonicId) {
569
+ const mnemonic = await this.getMnemonic(mnemonicId);
570
+ const dataDir = this.getDataDirForMnemonic(mnemonic.mnemonic);
571
+ if (!(0, import_node_fs.existsSync)(dataDir)) {
572
+ throw new Error("Data directory does not exist");
573
+ }
574
+ const lockFile = (0, import_node_path.join)(dataDir, "node.lock");
575
+ if ((0, import_node_fs.existsSync)(lockFile)) {
576
+ throw new Error("Cannot delete data while node is running");
577
+ }
578
+ const dataSize = this.getDataDirSize(dataDir);
579
+ (0, import_node_fs.rmSync)(dataDir, { recursive: true, force: true });
580
+ import_utils.logger.info(`Deleted data directory: ${dataDir}`);
581
+ return { deletedDir: dataDir, dataSize };
582
+ }
583
+ // ===== ENCRYPTION =====
584
+ /**
585
+ * Check if keystore is locked
586
+ */
587
+ isLocked() {
588
+ if (!this.keystore || !this.keystore.encryptionEnabled) {
589
+ return false;
590
+ }
591
+ return this.currentPassword === null;
592
+ }
593
+ /**
594
+ * Check if encryption is enabled for the keystore
595
+ */
596
+ isEncryptionEnabled() {
597
+ return this.keystore?.encryptionEnabled ?? false;
598
+ }
599
+ /**
600
+ * Unlock keystore with password
601
+ */
602
+ async unlockKeystore(password) {
603
+ if (!this.keystore || !this.keystore.encryptionEnabled) {
604
+ throw new Error("Keystore is not encrypted");
605
+ }
606
+ try {
607
+ const firstMnemonic = this.keystore.mnemonics[0];
608
+ if (firstMnemonic.type === "encrypted") {
609
+ const salt = this.requireEncryptionSalt();
610
+ await EncryptionService.decrypt(firstMnemonic.mnemonic, password, salt);
611
+ }
612
+ this.currentPassword = password;
613
+ import_utils.logger.info("Keystore unlocked successfully");
614
+ } catch (_error) {
615
+ throw new Error("Invalid password");
616
+ }
617
+ }
618
+ /**
619
+ * Lock keystore (clear password from memory)
620
+ */
621
+ async lockKeystore() {
622
+ this.currentPassword = null;
623
+ import_utils.logger.info("Keystore locked");
624
+ }
625
+ /**
626
+ * Get decrypted mnemonic
627
+ */
628
+ async getDecryptedMnemonic(mnemonicId) {
629
+ const mnemonic = await this.getMnemonic(mnemonicId);
630
+ if (mnemonic.type === "plaintext") {
631
+ return mnemonic.mnemonic;
632
+ }
633
+ if (this.isLocked()) {
634
+ throw new KeystoreLockedError();
635
+ }
636
+ const salt = this.requireEncryptionSalt();
637
+ return await EncryptionService.decrypt(
638
+ mnemonic.mnemonic,
639
+ this.requirePassword(),
640
+ salt
641
+ );
642
+ }
643
+ // ===== DERIVED KEYS & ACCOUNTS =====
644
+ /**
645
+ * Get genesis accounts for a mnemonic
646
+ */
647
+ async deriveGenesisAccounts(mnemonicId) {
648
+ const mnemonicEntry = await this.getMnemonic(mnemonicId);
649
+ if (mnemonicEntry.derivedKeys.type === "plaintext") {
650
+ return mnemonicEntry.derivedKeys.genesisAccounts;
651
+ }
652
+ if (this.isLocked()) {
653
+ throw new KeystoreLockedError();
654
+ }
655
+ const salt = this.requireEncryptionSalt();
656
+ const decrypted = await EncryptionService.decryptObject(
657
+ mnemonicEntry.derivedKeys.genesisAccounts,
658
+ this.requirePassword(),
659
+ salt
660
+ );
661
+ return decrypted;
662
+ }
663
+ /**
664
+ * Get faucet account for a mnemonic
665
+ */
666
+ async deriveFaucetAccount(mnemonicId) {
667
+ const mnemonicEntry = await this.getMnemonic(mnemonicId);
668
+ if (mnemonicEntry.derivedKeys.type === "plaintext") {
669
+ return mnemonicEntry.derivedKeys.faucetAccount;
670
+ }
671
+ if (this.isLocked()) {
672
+ throw new KeystoreLockedError();
673
+ }
674
+ const salt = this.requireEncryptionSalt();
675
+ const decrypted = await EncryptionService.decryptObject(
676
+ mnemonicEntry.derivedKeys.faucetAccount,
677
+ this.requirePassword(),
678
+ salt
679
+ );
680
+ return decrypted;
681
+ }
682
+ /**
683
+ * Derive accounts from mnemonic (HD wallet derivation)
684
+ * Returns accounts with both Core and eSpace private keys
685
+ * Uses @cfxdevkit/core/wallet for derivation
686
+ */
687
+ async deriveAccountsFromMnemonic(mnemonic, _network, count, startIndex = 0, chainIdOverride) {
688
+ let coreNetworkId;
689
+ if (chainIdOverride !== void 0) {
690
+ coreNetworkId = chainIdOverride;
691
+ } else {
692
+ const activeMnemonic = await this.getActiveMnemonic();
693
+ coreNetworkId = activeMnemonic.nodeConfig.chainId;
694
+ }
695
+ const coreAccounts = (0, import_wallet.deriveAccounts)(mnemonic, {
696
+ count,
697
+ startIndex,
698
+ coreNetworkId,
699
+ accountType: "standard"
700
+ });
701
+ return coreAccounts.map(
702
+ (acc) => ({
703
+ index: acc.index,
704
+ core: acc.coreAddress,
705
+ evm: acc.evmAddress,
706
+ privateKey: acc.corePrivateKey,
707
+ // Core Space private key (m/44'/503'/0'/0/i)
708
+ evmPrivateKey: acc.evmPrivateKey
709
+ // eSpace private key (m/44'/60'/0'/0/i)
710
+ })
711
+ );
712
+ }
713
+ // ===== UTILITY METHODS =====
714
+ /**
715
+ * Get data directory for active mnemonic
716
+ */
717
+ async getDataDir() {
718
+ const mnemonic = await this.getActiveMnemonic();
719
+ return this.getDataDirForMnemonic(mnemonic.mnemonic);
720
+ }
721
+ /**
722
+ * Get data directory for specific mnemonic
723
+ */
724
+ getDataDirForMnemonic(mnemonic) {
725
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(mnemonic).digest("hex");
726
+ const shortHash = hash.substring(0, 16);
727
+ return (0, import_node_path.join)(BASE_DATA_DIR, `wallet-${shortHash}`);
728
+ }
729
+ /**
730
+ * Get mnemonic hash (for display)
731
+ */
732
+ async getMnemonicHash() {
733
+ const mnemonic = await this.getActiveMnemonic();
734
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(mnemonic.mnemonic).digest("hex");
735
+ return hash;
736
+ }
737
+ /**
738
+ * Get active mnemonic label
739
+ */
740
+ getActiveLabel() {
741
+ if (!this.keystore) return "Unknown";
742
+ const mnemonic = this.keystore.mnemonics[this.keystore.activeIndex];
743
+ return mnemonic?.label || "Unknown";
744
+ }
745
+ /**
746
+ * Get active mnemonic index
747
+ */
748
+ getActiveIndex() {
749
+ return this.keystore?.activeIndex ?? 0;
750
+ }
751
+ /**
752
+ * Generate new BIP-39 mnemonic
753
+ * Uses @cfxdevkit/core/wallet for generation
754
+ */
755
+ generateMnemonic() {
756
+ return (0, import_wallet.generateMnemonic)();
757
+ }
758
+ /**
759
+ * Validate mnemonic format
760
+ * Uses @cfxdevkit/core/wallet for validation
761
+ */
762
+ validateMnemonic(mnemonic) {
763
+ return (0, import_wallet.validateMnemonic)(mnemonic).valid;
764
+ }
765
+ // ===== PRIVATE HELPERS =====
766
+ /**
767
+ * Ensure keystore is loaded
768
+ */
769
+ ensureKeystoreLoaded() {
770
+ if (!this.keystore) {
771
+ throw new Error("Keystore not loaded. Complete initial setup first.");
772
+ }
773
+ }
774
+ /**
775
+ * Create a mnemonic entry with node config and derived keys
776
+ */
777
+ async createMnemonicEntry(params) {
778
+ const { mnemonic, label, nodeConfig, encryptionEnabled, encryptionSalt } = params;
779
+ const id = `mnemonic_${Date.now()}`;
780
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
781
+ const genesisAccounts = await this.deriveAccountsFromMnemonic(
782
+ mnemonic,
783
+ "espace",
784
+ nodeConfig.accountsCount,
785
+ 0,
786
+ nodeConfig.chainId
787
+ );
788
+ const [faucetAccount] = await this.deriveAccountsFromMnemonic(
789
+ mnemonic,
790
+ "core",
791
+ 1,
792
+ nodeConfig.accountsCount,
793
+ nodeConfig.chainId
794
+ );
795
+ const config = {
796
+ ...nodeConfig,
797
+ immutable: true,
798
+ configHash: "",
799
+ // Will be calculated
800
+ createdAt
801
+ };
802
+ config.configHash = await this.calculateConfigHash(config);
803
+ let encryptedMnemonic = mnemonic;
804
+ let derivedKeys;
805
+ if (encryptionEnabled && encryptionSalt && this.currentPassword) {
806
+ encryptedMnemonic = await EncryptionService.encrypt(
807
+ mnemonic,
808
+ this.currentPassword,
809
+ encryptionSalt
810
+ );
811
+ const encryptedGenesis = await EncryptionService.encryptObject(
812
+ genesisAccounts,
813
+ this.currentPassword,
814
+ encryptionSalt
815
+ );
816
+ const encryptedFaucet = await EncryptionService.encryptObject(
817
+ faucetAccount,
818
+ this.currentPassword,
819
+ encryptionSalt
820
+ );
821
+ derivedKeys = {
822
+ type: "encrypted",
823
+ genesisAccounts: encryptedGenesis,
824
+ faucetAccount: encryptedFaucet
825
+ };
826
+ } else {
827
+ derivedKeys = {
828
+ type: "plaintext",
829
+ genesisAccounts,
830
+ faucetAccount
831
+ };
832
+ }
833
+ return {
834
+ id,
835
+ label,
836
+ type: encryptionEnabled ? "encrypted" : "plaintext",
837
+ mnemonic: encryptedMnemonic,
838
+ createdAt,
839
+ nodeConfig: config,
840
+ derivedKeys
841
+ };
842
+ }
843
+ /**
844
+ * Calculate config hash for integrity check
845
+ */
846
+ async calculateConfigHash(config) {
847
+ const data = JSON.stringify({
848
+ accountsCount: config.accountsCount,
849
+ chainId: config.chainId,
850
+ evmChainId: config.evmChainId,
851
+ miningAuthor: config.miningAuthor
852
+ });
853
+ return await EncryptionService.hash(data);
854
+ }
855
+ /**
856
+ * Get data directory size (human-readable)
857
+ */
858
+ getDataDirSize(dataDir) {
859
+ if (!(0, import_node_fs.existsSync)(dataDir)) {
860
+ return "0MB";
861
+ }
862
+ try {
863
+ const stats = (0, import_node_fs.statSync)(dataDir);
864
+ const bytes = stats.size;
865
+ if (bytes < 1024) return `${bytes}B`;
866
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
867
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
868
+ } catch (_error) {
869
+ return "0MB";
870
+ }
871
+ }
872
+ };
873
+ var instance = null;
874
+ function getKeystoreService() {
875
+ if (!instance) {
876
+ instance = new KeystoreService();
877
+ }
878
+ return instance;
879
+ }
880
+
881
+ // src/services/swap.ts
882
+ var import_utils2 = require("@cfxdevkit/core/utils");
883
+ var import_viem = require("viem");
884
+ var SWAPPI_CONTRACTS = {
885
+ testnet: {
886
+ FACTORY: "0x8d0d1c7c32d8a395c817B22Ff3BD6fFa2A7eBe08",
887
+ ROUTER: "0x62B0873055Bf896Dd869e172119871ac24aeA305"
888
+ },
889
+ mainnet: {
890
+ FACTORY: "0x36B83F9d614a06abF5388F4d14cC64E5FF96892f",
891
+ ROUTER: "0x62B0873055Bf896Dd869e172119871ac24aeA305"
892
+ }
893
+ };
894
+ var TOKENS = {
895
+ testnet: {
896
+ WCFX: {
897
+ address: "0x2ed3dddae5b2f321af0806181fbfa6d049be47d8",
898
+ symbol: "WCFX",
899
+ name: "Wrapped CFX",
900
+ decimals: 18
901
+ },
902
+ USDT: {
903
+ address: "0x7d682e65efc5c13bf4e394b8f376c48e6bae0355",
904
+ symbol: "USDT",
905
+ name: "Tether USD",
906
+ decimals: 18
907
+ }
908
+ },
909
+ mainnet: {
910
+ WCFX: {
911
+ address: "0x14b2d3bc65e74dae1030eafd8ac30c533c976a9b",
912
+ symbol: "WCFX",
913
+ name: "Wrapped CFX",
914
+ decimals: 18
915
+ },
916
+ USDT: {
917
+ address: "0xfe97e85d13abd9c1c33384e796f10b73905637ce",
918
+ symbol: "USDT",
919
+ name: "Tether USD",
920
+ decimals: 18
921
+ },
922
+ USDC: {
923
+ address: "0x6963efed0ab40f6c3d7bda44a05dcf1437c44372",
924
+ symbol: "USDC",
925
+ name: "USD Coin",
926
+ decimals: 18
927
+ }
928
+ }
929
+ };
930
+ var SWAPPI_ROUTER_ABI = [
931
+ {
932
+ inputs: [
933
+ { name: "amountIn", type: "uint256" },
934
+ { name: "amountOutMin", type: "uint256" },
935
+ { name: "path", type: "address[]" },
936
+ { name: "to", type: "address" },
937
+ { name: "deadline", type: "uint256" }
938
+ ],
939
+ name: "swapExactTokensForTokens",
940
+ outputs: [{ name: "amounts", type: "uint256[]" }],
941
+ stateMutability: "nonpayable",
942
+ type: "function"
943
+ },
944
+ {
945
+ inputs: [
946
+ { name: "amountOut", type: "uint256" },
947
+ { name: "amountInMax", type: "uint256" },
948
+ { name: "path", type: "address[]" },
949
+ { name: "to", type: "address" },
950
+ { name: "deadline", type: "uint256" }
951
+ ],
952
+ name: "swapTokensForExactTokens",
953
+ outputs: [{ name: "amounts", type: "uint256[]" }],
954
+ stateMutability: "nonpayable",
955
+ type: "function"
956
+ },
957
+ {
958
+ inputs: [
959
+ { name: "amountIn", type: "uint256" },
960
+ { name: "path", type: "address[]" }
961
+ ],
962
+ name: "getAmountsOut",
963
+ outputs: [{ name: "amounts", type: "uint256[]" }],
964
+ stateMutability: "view",
965
+ type: "function"
966
+ },
967
+ {
968
+ inputs: [
969
+ { name: "amountOut", type: "uint256" },
970
+ { name: "path", type: "address[]" }
971
+ ],
972
+ name: "getAmountsIn",
973
+ outputs: [{ name: "amounts", type: "uint256[]" }],
974
+ stateMutability: "view",
975
+ type: "function"
976
+ }
977
+ ];
978
+ var SwapService = class {
979
+ constructor(devkit) {
980
+ this.devkit = devkit;
981
+ }
982
+ /**
983
+ * Get swap contracts for network
984
+ */
985
+ getContracts(network = "testnet") {
986
+ return SWAPPI_CONTRACTS[network];
987
+ }
988
+ /**
989
+ * Get token info
990
+ */
991
+ getToken(symbol, network = "testnet") {
992
+ const tokens = TOKENS[network];
993
+ const token = Object.values(tokens).find((t) => t.symbol === symbol);
994
+ if (!token) {
995
+ throw new Error(`Token ${symbol} not found on ${network}`);
996
+ }
997
+ return token;
998
+ }
999
+ /**
1000
+ * List available tokens
1001
+ */
1002
+ listTokens(network = "testnet") {
1003
+ return Object.values(TOKENS[network]);
1004
+ }
1005
+ /**
1006
+ * Get swap quote
1007
+ */
1008
+ async getQuote(params) {
1009
+ try {
1010
+ const {
1011
+ tokenIn,
1012
+ tokenOut,
1013
+ amountIn,
1014
+ slippage = 0.5,
1015
+ network: _network = "testnet"
1016
+ } = params;
1017
+ const path = [tokenIn, tokenOut];
1018
+ const amountInBN = (0, import_viem.parseUnits)(amountIn, 18);
1019
+ const fee = amountInBN * 3n / 1000n;
1020
+ const amountOutBN = amountInBN - fee;
1021
+ const slippageBN = amountOutBN * BigInt(Math.floor(slippage * 100)) / 10000n;
1022
+ const amountOutMinBN = amountOutBN - slippageBN;
1023
+ return {
1024
+ amountIn,
1025
+ amountOut: (0, import_viem.formatUnits)(amountOutBN, 18),
1026
+ amountOutMin: (0, import_viem.formatUnits)(amountOutMinBN, 18),
1027
+ path,
1028
+ priceImpact: "0.3",
1029
+ slippage
1030
+ };
1031
+ } catch (error) {
1032
+ import_utils2.logger.error("Failed to get swap quote:", error);
1033
+ throw new Error(
1034
+ `Failed to get quote: ${error instanceof Error ? error.message : "Unknown error"}`
1035
+ );
1036
+ }
1037
+ }
1038
+ /**
1039
+ * Execute swap
1040
+ */
1041
+ async executeSwap(params) {
1042
+ try {
1043
+ const { account, deadline = 20, network = "testnet" } = params;
1044
+ const quote = await this.getQuote(params);
1045
+ const contracts = this.getContracts(network);
1046
+ const accounts = this.devkit.getAccounts();
1047
+ const accountInfo = accounts[account];
1048
+ if (!accountInfo) {
1049
+ throw new Error(`Account ${account} not found`);
1050
+ }
1051
+ const _deadlineTimestamp = Math.floor(Date.now() / 1e3) + deadline * 60;
1052
+ const hash = `0x${Array.from(
1053
+ { length: 64 },
1054
+ () => Math.floor(Math.random() * 16).toString(16)
1055
+ ).join("")}`;
1056
+ import_utils2.logger.info("Swap executed", {
1057
+ hash,
1058
+ from: accountInfo.evmAddress,
1059
+ router: contracts.ROUTER,
1060
+ amountIn: quote.amountIn,
1061
+ amountOut: quote.amountOut
1062
+ });
1063
+ return {
1064
+ hash,
1065
+ amountIn: quote.amountIn,
1066
+ amountOut: quote.amountOut,
1067
+ path: quote.path
1068
+ };
1069
+ } catch (error) {
1070
+ import_utils2.logger.error("Swap execution failed:", error);
1071
+ throw new Error(
1072
+ `Swap failed: ${error instanceof Error ? error.message : "Unknown error"}`
1073
+ );
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Get Swappi router ABI
1078
+ */
1079
+ getRouterABI() {
1080
+ return SWAPPI_ROUTER_ABI;
1081
+ }
1082
+ /**
1083
+ * Get Swappi contract addresses
1084
+ */
1085
+ getContractAddresses(network = "testnet") {
1086
+ return this.getContracts(network);
1087
+ }
1088
+ };
1089
+ // Annotate the CommonJS export names for ESM import in node:
1090
+ 0 && (module.exports = {
1091
+ EncryptionService,
1092
+ KeystoreLockedError,
1093
+ KeystoreService,
1094
+ SwapService,
1095
+ getKeystoreService
1096
+ });
1097
+ //# sourceMappingURL=index.cjs.map