@cfxdevkit/devnode 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1033 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ DevKitWithDevNode: () => DevKitWithDevNode,
34
+ ServerManager: () => ServerManager,
35
+ VERSION: () => VERSION,
36
+ devNodePlugin: () => devNodePlugin
37
+ });
38
+ module.exports = __toCommonJS(src_exports);
39
+
40
+ // src/server-manager.ts
41
+ var import_node_crypto = require("crypto");
42
+ var import_node_fs = require("fs");
43
+ var import_node_os = require("os");
44
+ var import_node_path = require("path");
45
+ var import_config = require("@cfxdevkit/core/config");
46
+ var import_wallet = require("@cfxdevkit/core/wallet");
47
+ var import_node = require("@xcfx/node");
48
+ var import_accounts = require("cive/accounts");
49
+ var import_accounts2 = require("viem/accounts");
50
+
51
+ // src/types.ts
52
+ var NodeError = class extends Error {
53
+ code;
54
+ chain;
55
+ context;
56
+ constructor(message, code, chain, context) {
57
+ super(message);
58
+ this.name = "NodeError";
59
+ this.code = code;
60
+ this.chain = chain;
61
+ this.context = context;
62
+ }
63
+ };
64
+
65
+ // src/server-manager.ts
66
+ var DEFAULT_CORE_RPC_PORT = 12537;
67
+ var DEFAULT_EVM_RPC_PORT = 8545;
68
+ var DEFAULT_WS_PORT = 12536;
69
+ var DEFAULT_EVM_WS_PORT = 8546;
70
+ var ServerManager = class {
71
+ nodeProcess = null;
72
+ server = null;
73
+ config;
74
+ status = "stopped";
75
+ accounts = [];
76
+ mnemonic = "";
77
+ miningAccount = null;
78
+ miningStatus;
79
+ miningTimer = null;
80
+ testClient = null;
81
+ /** True while packMine() is running — auto-miner skips its tick to avoid concurrent mining RPCs. */
82
+ _packMining = false;
83
+ constructor(config) {
84
+ this.config = {
85
+ ...config,
86
+ coreRpcPort: config.coreRpcPort || DEFAULT_CORE_RPC_PORT,
87
+ evmRpcPort: config.evmRpcPort || DEFAULT_EVM_RPC_PORT,
88
+ wsPort: config.wsPort || DEFAULT_WS_PORT,
89
+ evmWsPort: config.evmWsPort || DEFAULT_EVM_WS_PORT,
90
+ chainId: config.chainId || 2029,
91
+ // Local Core chain ID
92
+ evmChainId: config.evmChainId || 2030,
93
+ // Local eSpace chain ID
94
+ accounts: config.accounts || 10,
95
+ balance: config.balance || "1000000",
96
+ mnemonic: config.mnemonic,
97
+ // Following xcfx-node test pattern: devPackTxImmediately should be false
98
+ // All mining is managed via testClient.mine() calls
99
+ devPackTxImmediately: false
100
+ };
101
+ this.miningStatus = {
102
+ isRunning: false,
103
+ interval: 1e3,
104
+ blocksMined: 0,
105
+ startTime: void 0
106
+ };
107
+ this.mnemonic = this.config.mnemonic || (0, import_wallet.generateMnemonic)(128);
108
+ this.generateAccountsSync();
109
+ this.generateMiningAccountSync();
110
+ }
111
+ /**
112
+ * Return a sanitized copy of the server config with sensitive fields redacted.
113
+ */
114
+ redactConfig(config) {
115
+ const safe = { ...config };
116
+ if (safe.mnemonic) safe.mnemonic = "[REDACTED]";
117
+ if (safe.accounts) delete safe.accounts;
118
+ return safe;
119
+ }
120
+ /**
121
+ * Start the Conflux development node
122
+ */
123
+ async start() {
124
+ if (this.status === "running") {
125
+ throw new NodeError(
126
+ "Server is already running",
127
+ "SERVER_ALREADY_RUNNING"
128
+ );
129
+ }
130
+ try {
131
+ this.status = "starting";
132
+ const dataDir = this.config.dataDir || (0, import_node_path.join)((0, import_node_os.homedir)(), ".conflux-devkit", "data");
133
+ try {
134
+ await import_node_fs.promises.mkdir(dataDir, { recursive: true, mode: 493 });
135
+ } catch (error) {
136
+ console.warn("Failed to create data directory:", error);
137
+ }
138
+ const serverConfig = {
139
+ // Correct property names according to @xcfx/node API
140
+ jsonrpcHttpPort: this.config.coreRpcPort,
141
+ jsonrpcHttpEthPort: this.config.evmRpcPort,
142
+ jsonrpcWsPort: this.config.wsPort,
143
+ jsonrpcWsEthPort: this.config.evmWsPort,
144
+ chainId: this.config.chainId,
145
+ evmChainId: this.config.evmChainId,
146
+ // Specify data directory to avoid permission issues
147
+ confluxDataDir: dataDir,
148
+ // Genesis accounts configuration - include mining account for initial funding
149
+ genesisSecrets: [
150
+ ...this.accounts.map((acc) => acc.privateKey),
151
+ this.requireMiningAccount().privateKey
152
+ // Add mining account to get initial funds
153
+ ],
154
+ genesisEvmSecrets: [
155
+ ...this.accounts.map((acc) => acc.evmPrivateKey || acc.privateKey),
156
+ this.requireMiningAccount().evmPrivateKey || this.requireMiningAccount().privateKey
157
+ // Add mining account EVM key
158
+ ],
159
+ // Mining configuration - use config value or default to mining account address
160
+ // 'auto' is a sentinel value meaning "use the derived mining account address"
161
+ miningAuthor: this.config.miningAuthor && this.config.miningAuthor !== "auto" ? this.config.miningAuthor : this.miningAccount?.coreAddress,
162
+ // Following the reference test (xcfx-node/evmManualBlockGeneration.test.ts):
163
+ // devPackTxImmediately: false — eSpace txs are ONLY packed by mine({ numTxs }),
164
+ // never by mine({ blocks }). This flag only affects Core space.
165
+ devPackTxImmediately: false,
166
+ log: this.config.logging || false,
167
+ // Genesis block initialization can take time; pass a generous timeout so
168
+ // the native binary doesn't abort the startup handshake prematurely.
169
+ timeout: 6e4,
170
+ retryInterval: 300
171
+ };
172
+ this.server = await (0, import_node.createServer)(serverConfig);
173
+ await this.server.start();
174
+ this.status = "running";
175
+ import_config.defaultNetworkSelector.updateLocalChainUrls(
176
+ this.config.coreRpcPort || DEFAULT_CORE_RPC_PORT,
177
+ this.config.evmRpcPort || DEFAULT_EVM_RPC_PORT,
178
+ this.config.wsPort || DEFAULT_WS_PORT
179
+ );
180
+ import_config.defaultNetworkSelector.onNodeStart(2029, 2030);
181
+ this.setupCleanupHandlers();
182
+ await this.startMining(2e3);
183
+ } catch (error) {
184
+ this.status = "error";
185
+ throw new NodeError(
186
+ `Failed to start server: ${error instanceof Error ? error.message : String(error)}`,
187
+ "SERVER_START_ERROR",
188
+ void 0,
189
+ {
190
+ config: this.redactConfig(this.config),
191
+ originalError: error
192
+ }
193
+ );
194
+ }
195
+ }
196
+ /**
197
+ * Stop the Conflux development node
198
+ */
199
+ async stop() {
200
+ if (this.status === "stopped") {
201
+ return;
202
+ }
203
+ try {
204
+ this.status = "stopping";
205
+ if (this.miningStatus.isRunning) {
206
+ try {
207
+ await this.stopMining();
208
+ } catch (error) {
209
+ console.warn("Failed to stop mining during server shutdown:", error);
210
+ }
211
+ }
212
+ if (this.server) {
213
+ await this.server.stop();
214
+ this.server = null;
215
+ }
216
+ if (this.nodeProcess) {
217
+ this.nodeProcess.kill("SIGTERM");
218
+ this.nodeProcess = null;
219
+ }
220
+ this.testClient = null;
221
+ this.status = "stopped";
222
+ import_config.defaultNetworkSelector.onNodeStop();
223
+ } catch (error) {
224
+ this.status = "error";
225
+ throw new NodeError(
226
+ `Failed to stop server: ${error instanceof Error ? error.message : String(error)}`,
227
+ "SERVER_STOP_ERROR",
228
+ void 0,
229
+ { originalError: error }
230
+ );
231
+ }
232
+ }
233
+ /**
234
+ * Restart the Conflux development node
235
+ */
236
+ async restart() {
237
+ await this.stop();
238
+ await this.start();
239
+ }
240
+ /**
241
+ * Get current server status
242
+ */
243
+ getStatus() {
244
+ return this.status;
245
+ }
246
+ /**
247
+ * Get comprehensive node status including mining
248
+ */
249
+ getNodeStatus() {
250
+ return {
251
+ server: this.status,
252
+ mining: this.getMiningStatus(),
253
+ config: this.redactConfig(this.config),
254
+ accounts: this.accounts.length,
255
+ rpcUrls: this.getRpcUrls()
256
+ };
257
+ }
258
+ /**
259
+ * Check if server is running
260
+ */
261
+ isRunning() {
262
+ return this.status === "running";
263
+ }
264
+ /**
265
+ * Get server configuration
266
+ */
267
+ getConfig() {
268
+ return {
269
+ ...this.redactConfig(this.config)
270
+ };
271
+ }
272
+ /**
273
+ * Get generated accounts
274
+ */
275
+ getAccounts() {
276
+ return [...this.accounts];
277
+ }
278
+ /**
279
+ * Get the mnemonic phrase
280
+ */
281
+ getMnemonic() {
282
+ return this.mnemonic;
283
+ }
284
+ /**
285
+ * Get RPC URLs
286
+ */
287
+ getRpcUrls() {
288
+ const coreWs = `ws://localhost:${this.config.wsPort}`;
289
+ return {
290
+ core: `http://localhost:${this.config.coreRpcPort}`,
291
+ evm: `http://localhost:${this.config.evmRpcPort}`,
292
+ coreWs,
293
+ evmWs: `ws://localhost:${this.config.evmWsPort}`,
294
+ ws: coreWs
295
+ // backward-compat alias for Core WS
296
+ };
297
+ }
298
+ /**
299
+ * Add a new account to the server
300
+ */
301
+ async addAccount(privateKey) {
302
+ const accountPrivateKey = privateKey || `0x${(0, import_node_crypto.randomBytes)(32).toString("hex")}`;
303
+ const coreAccount = (0, import_accounts.privateKeyToAccount)(
304
+ accountPrivateKey,
305
+ {
306
+ networkId: this.config.chainId || 1
307
+ }
308
+ );
309
+ const evmAccount = (0, import_accounts2.privateKeyToAccount)(
310
+ accountPrivateKey
311
+ );
312
+ const accountInfo = {
313
+ index: this.accounts.length,
314
+ privateKey: accountPrivateKey,
315
+ coreAddress: coreAccount.address,
316
+ evmAddress: evmAccount.address,
317
+ mnemonic: this.mnemonic,
318
+ path: `m/44'/503'/0'/0/${this.accounts.length}`
319
+ };
320
+ this.accounts.push(accountInfo);
321
+ return accountInfo;
322
+ }
323
+ /**
324
+ * Fund an account with CFX
325
+ * Note: @xcfx/node doesn't provide direct funding methods.
326
+ * This would require using RPC calls to send transactions from funded genesis accounts.
327
+ */
328
+ async fundAccount(address, amount, chainType = "core") {
329
+ if (!this.isRunning() || !this.server) {
330
+ throw new NodeError("Server is not running", "SERVER_NOT_RUNNING");
331
+ }
332
+ throw new NodeError(
333
+ "Direct account funding not implemented. Genesis accounts are automatically funded by @xcfx/node.",
334
+ "NOT_IMPLEMENTED",
335
+ chainType,
336
+ { address, amount, chainType }
337
+ );
338
+ }
339
+ /**
340
+ * Set next block timestamp (for testing)
341
+ * Note: @xcfx/node doesn't provide direct timestamp control.
342
+ * Use createTestClient from 'cive' and connect to the running node's RPC.
343
+ */
344
+ async setNextBlockTimestamp(timestamp) {
345
+ if (!this.isRunning() || !this.server) {
346
+ throw new NodeError("Server is not running", "SERVER_NOT_RUNNING");
347
+ }
348
+ throw new NodeError(
349
+ "Direct timestamp control not implemented. Use createTestClient from cive to control block timestamps via RPC.",
350
+ "NOT_IMPLEMENTED",
351
+ "core",
352
+ { timestamp }
353
+ );
354
+ }
355
+ /**
356
+ * Get server logs
357
+ * Note: @xcfx/node doesn't provide direct log access.
358
+ * Logs would need to be captured during server startup or accessed via system logs.
359
+ */
360
+ async getLogs(lines = 50) {
361
+ if (!this.isRunning() || !this.server) {
362
+ throw new NodeError("Server is not running", "SERVER_NOT_RUNNING");
363
+ }
364
+ return [
365
+ "Log access not implemented for @xcfx/node.",
366
+ "Consider capturing server output during startup or checking system logs.",
367
+ `Requested ${lines} lines of logs.`
368
+ ];
369
+ }
370
+ /**
371
+ * Save server configuration to file
372
+ */
373
+ async saveConfig(filepath) {
374
+ try {
375
+ const configData = {
376
+ ...this.redactConfig(this.config),
377
+ mnemonic: "[REDACTED]",
378
+ accounts: this.accounts.map((a) => ({
379
+ index: a.index,
380
+ coreAddress: a.coreAddress,
381
+ evmAddress: a.evmAddress,
382
+ path: a.path
383
+ })),
384
+ rpcUrls: this.getRpcUrls()
385
+ };
386
+ await import_node_fs.promises.writeFile(filepath, JSON.stringify(configData, null, 2), "utf8");
387
+ } catch (error) {
388
+ throw new NodeError(
389
+ `Failed to save config: ${error instanceof Error ? error.message : String(error)}`,
390
+ "CONFIG_SAVE_ERROR",
391
+ "core",
392
+ { filepath, originalError: error }
393
+ );
394
+ }
395
+ }
396
+ /**
397
+ * Load server configuration from file
398
+ */
399
+ static async loadConfig(filepath) {
400
+ try {
401
+ const configData = await import_node_fs.promises.readFile(filepath, "utf8");
402
+ return JSON.parse(configData);
403
+ } catch (error) {
404
+ throw new NodeError(
405
+ `Failed to load config: ${error instanceof Error ? error.message : String(error)}`,
406
+ "CONFIG_LOAD_ERROR",
407
+ "core",
408
+ { filepath, originalError: error }
409
+ );
410
+ }
411
+ }
412
+ /**
413
+ * Generate accounts from mnemonic using core wallet module
414
+ * Called from constructor to ensure accounts are always available
415
+ */
416
+ generateAccountsSync() {
417
+ const derivedAccounts = (0, import_wallet.deriveAccounts)(this.mnemonic, {
418
+ count: this.config.accounts || 10,
419
+ coreNetworkId: this.config.chainId || 2029
420
+ });
421
+ this.accounts = derivedAccounts.map((account) => ({
422
+ index: account.index,
423
+ privateKey: account.corePrivateKey,
424
+ // Keep Conflux private key as primary
425
+ coreAddress: account.coreAddress,
426
+ evmAddress: account.evmAddress,
427
+ mnemonic: this.mnemonic,
428
+ path: account.paths.core,
429
+ // Store additional EVM-specific info
430
+ evmPrivateKey: account.evmPrivateKey,
431
+ evmPath: account.paths.evm
432
+ }));
433
+ }
434
+ /**
435
+ * Generate dedicated mining account (separate from genesis accounts)
436
+ * This account will receive mining rewards and serve as the faucet
437
+ * Called synchronously from constructor to ensure always available
438
+ */
439
+ generateMiningAccountSync() {
440
+ const faucetAccount = (0, import_wallet.deriveFaucetAccount)(
441
+ this.mnemonic,
442
+ this.config.chainId || 2029
443
+ );
444
+ this.miningAccount = {
445
+ index: -1,
446
+ // Special index for mining account
447
+ privateKey: faucetAccount.corePrivateKey,
448
+ // Core/Conflux private key
449
+ coreAddress: faucetAccount.coreAddress,
450
+ evmAddress: faucetAccount.evmAddress,
451
+ mnemonic: this.mnemonic,
452
+ path: faucetAccount.paths.core,
453
+ // Store additional EVM-specific info
454
+ evmPrivateKey: faucetAccount.evmPrivateKey,
455
+ evmPath: faucetAccount.paths.evm
456
+ };
457
+ console.log(
458
+ `Generated mining account: Core=${this.miningAccount.coreAddress}, eSpace=${this.miningAccount.evmAddress}`
459
+ );
460
+ }
461
+ /**
462
+ * Returns the mining account, throwing if it has not been initialized.
463
+ * In practice this is always set by generateMiningAccountSync() in the
464
+ * constructor, so the throw is a safety net for unexpected states.
465
+ */
466
+ requireMiningAccount() {
467
+ if (!this.miningAccount) {
468
+ throw new Error("Mining account has not been initialized.");
469
+ }
470
+ return this.miningAccount;
471
+ }
472
+ // ===== MINING METHODS =====
473
+ /**
474
+ * Start automatic block mining using testClient
475
+ * This creates an interval that mines blocks automatically
476
+ * @param interval Mining interval in milliseconds (default: 2000ms)
477
+ */
478
+ async startMining(interval) {
479
+ if (!this.isRunning()) {
480
+ throw new NodeError(
481
+ "Server must be running to start mining",
482
+ "SERVER_NOT_RUNNING"
483
+ );
484
+ }
485
+ if (this.miningStatus.isRunning) {
486
+ throw new NodeError(
487
+ "Mining is already running",
488
+ "MINING_ALREADY_RUNNING"
489
+ );
490
+ }
491
+ if (!this.testClient) {
492
+ const { createTestClient, http } = await import("cive");
493
+ this.testClient = createTestClient({
494
+ transport: http(`http://localhost:${this.config.coreRpcPort}`, {
495
+ timeout: 6e4
496
+ })
497
+ });
498
+ }
499
+ const miningInterval = interval || 2e3;
500
+ this.miningStatus = {
501
+ ...this.miningStatus,
502
+ isRunning: true,
503
+ interval: miningInterval,
504
+ startTime: /* @__PURE__ */ new Date()
505
+ };
506
+ this.miningTimer = setInterval(async () => {
507
+ try {
508
+ if (this.testClient && !this._packMining) {
509
+ await this.testClient.mine({ numTxs: 1 });
510
+ this.miningStatus = {
511
+ ...this.miningStatus,
512
+ blocksMined: (this.miningStatus.blocksMined || 0) + 5
513
+ };
514
+ }
515
+ } catch (error) {
516
+ console.error("Mining error:", error);
517
+ }
518
+ }, miningInterval);
519
+ console.log(`Mining started with ${miningInterval}ms interval`);
520
+ }
521
+ /**
522
+ * Stop automatic block mining
523
+ */
524
+ async stopMining() {
525
+ if (!this.miningStatus.isRunning) {
526
+ throw new NodeError("Mining is not running", "MINING_NOT_RUNNING");
527
+ }
528
+ if (this.miningTimer) {
529
+ clearInterval(this.miningTimer);
530
+ this.miningTimer = null;
531
+ }
532
+ this.miningStatus = {
533
+ ...this.miningStatus,
534
+ isRunning: false,
535
+ startTime: void 0
536
+ };
537
+ console.log("Mining stopped");
538
+ }
539
+ /**
540
+ * Change mining interval (stops and restarts mining with new interval)
541
+ */
542
+ async setMiningInterval(interval) {
543
+ if (interval < 100) {
544
+ throw new NodeError(
545
+ "Mining interval must be at least 100ms",
546
+ "INVALID_INTERVAL"
547
+ );
548
+ }
549
+ const wasRunning = this.miningStatus.isRunning;
550
+ if (wasRunning) {
551
+ await this.stopMining();
552
+ }
553
+ this.miningStatus = {
554
+ ...this.miningStatus,
555
+ interval
556
+ };
557
+ if (wasRunning) {
558
+ await this.startMining(interval);
559
+ }
560
+ console.log(`Mining interval set to ${interval}ms`);
561
+ }
562
+ /**
563
+ * Mine a specific number of blocks immediately
564
+ */
565
+ async mine(blocks = 1) {
566
+ if (!this.isRunning()) {
567
+ throw new NodeError(
568
+ "Server must be running to mine blocks",
569
+ "SERVER_NOT_RUNNING"
570
+ );
571
+ }
572
+ if (!this.testClient) {
573
+ const { createTestClient, http } = await import("cive");
574
+ this.testClient = createTestClient({
575
+ transport: http(`http://localhost:${this.config.coreRpcPort}`, {
576
+ timeout: 6e4
577
+ })
578
+ });
579
+ }
580
+ try {
581
+ await this.testClient.mine({ blocks });
582
+ this.miningStatus = {
583
+ ...this.miningStatus,
584
+ blocksMined: (this.miningStatus.blocksMined || 0) + blocks
585
+ };
586
+ console.log(`Mined ${blocks} empty block(s)`);
587
+ } catch (error) {
588
+ throw new NodeError(
589
+ `Failed to mine blocks: ${error instanceof Error ? error.message : String(error)}`,
590
+ "MINING_ERROR",
591
+ "core",
592
+ { blocks, originalError: error }
593
+ );
594
+ }
595
+ }
596
+ /**
597
+ * Pack and mine: calls test_generateOneBlock (mine({ numTxs:1 })) which
598
+ * forces pending eSpace/Core transactions into a block. Each call
599
+ * internally generates deferredStateEpochCount (default 5) blocks.
600
+ *
601
+ * This is the ONLY way to include eSpace (EVM) transactions — mine({ blocks })
602
+ * skips the txpool for eSpace. Uses a long timeout because
603
+ * test_generateOneBlock can take several seconds on slow machines.
604
+ */
605
+ async packMine() {
606
+ if (!this.isRunning()) {
607
+ throw new NodeError(
608
+ "Server must be running to mine blocks",
609
+ "SERVER_NOT_RUNNING"
610
+ );
611
+ }
612
+ const { createTestClient, http } = await import("cive");
613
+ const packClient = createTestClient({
614
+ transport: http(`http://localhost:${this.config.coreRpcPort}`, {
615
+ timeout: 12e4
616
+ })
617
+ });
618
+ this._packMining = true;
619
+ try {
620
+ await packClient.mine({ numTxs: 1 });
621
+ this.miningStatus = {
622
+ ...this.miningStatus,
623
+ blocksMined: (this.miningStatus.blocksMined || 0) + 5
624
+ // generates 5 blocks internally
625
+ };
626
+ console.log("Pack-mined: 5 blocks generated, pending txs included");
627
+ } catch (error) {
628
+ throw new NodeError(
629
+ `Failed to pack-mine: ${error instanceof Error ? error.message : String(error)}`,
630
+ "MINING_ERROR",
631
+ "core",
632
+ { originalError: error }
633
+ );
634
+ } finally {
635
+ this._packMining = false;
636
+ }
637
+ }
638
+ /**
639
+ * Get current mining status
640
+ */
641
+ getMiningStatus() {
642
+ return { ...this.miningStatus };
643
+ }
644
+ // ===== FAUCET METHODS =====
645
+ /**
646
+ * Get the faucet/mining account (dedicated mining account with separate derivation path)
647
+ * This account receives mining rewards and serves as the faucet
648
+ * Derivation paths: Core=m/44'/503'/1'/0/0, EVM=m/44'/60'/1'/0/0
649
+ */
650
+ getFaucetAccount() {
651
+ if (!this.miningAccount) {
652
+ throw new NodeError(
653
+ "Mining account not available. Server must be started first.",
654
+ "NO_MINING_ACCOUNT"
655
+ );
656
+ }
657
+ return this.miningAccount;
658
+ }
659
+ /**
660
+ * Fund a Core Space account using the faucet account
661
+ */
662
+ async fundCoreAccount(targetAddress, amount) {
663
+ if (!this.isRunning()) {
664
+ throw new NodeError(
665
+ "Server must be running to fund accounts",
666
+ "SERVER_NOT_RUNNING"
667
+ );
668
+ }
669
+ const faucetAccount = this.getFaucetAccount();
670
+ try {
671
+ const { createWalletClient, http } = await import("cive");
672
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("cive/accounts");
673
+ const account = privateKeyToAccount2(
674
+ faucetAccount.privateKey,
675
+ {
676
+ networkId: this.config.chainId || 1
677
+ }
678
+ );
679
+ const walletClient = createWalletClient({
680
+ account,
681
+ chain: (this.config.chainId || 1) === 1029 ? {
682
+ id: 1029,
683
+ name: "Conflux Core",
684
+ nativeCurrency: {
685
+ name: "Conflux",
686
+ symbol: "CFX",
687
+ decimals: 18
688
+ },
689
+ rpcUrls: {
690
+ default: {
691
+ http: [`http://localhost:${this.config.coreRpcPort}`]
692
+ }
693
+ }
694
+ } : {
695
+ id: this.config.chainId || 1,
696
+ name: "Conflux Core Testnet",
697
+ nativeCurrency: {
698
+ name: "Conflux",
699
+ symbol: "CFX",
700
+ decimals: 18
701
+ },
702
+ rpcUrls: {
703
+ default: {
704
+ http: [`http://localhost:${this.config.coreRpcPort}`]
705
+ }
706
+ }
707
+ },
708
+ transport: http(`http://localhost:${this.config.coreRpcPort}`)
709
+ });
710
+ const { parseCFX } = await import("cive");
711
+ const hash = await walletClient.sendTransaction({
712
+ account,
713
+ to: targetAddress,
714
+ value: parseCFX(amount)
715
+ });
716
+ console.log(
717
+ `Funded Core account ${targetAddress} with ${amount} CFX. TX: ${hash}`
718
+ );
719
+ return hash;
720
+ } catch (error) {
721
+ throw new NodeError(
722
+ `Failed to fund Core account: ${error instanceof Error ? error.message : String(error)}`,
723
+ "FAUCET_ERROR",
724
+ "core",
725
+ {
726
+ targetAddress,
727
+ amount,
728
+ faucetAccount: faucetAccount.coreAddress,
729
+ originalError: error
730
+ }
731
+ );
732
+ }
733
+ }
734
+ /**
735
+ * Fund an eSpace account from the Core-Space faucet/mining account.
736
+ *
737
+ * Funds ALWAYS originate from the Core-Space faucet wallet (which accumulates
738
+ * mining rewards). For eSpace (0x…) targets the transfer is routed through the
739
+ * Conflux internal cross-chain bridge contract (0x0888…0006 / transferEVM),
740
+ * which locks CFX on Core and mints it on eSpace — no separate eSpace balance is
741
+ * needed on the faucet account.
742
+ */
743
+ async fundEvmAccount(targetAddress, amount) {
744
+ if (!this.isRunning()) {
745
+ throw new NodeError(
746
+ "Server must be running to fund accounts",
747
+ "SERVER_NOT_RUNNING"
748
+ );
749
+ }
750
+ const faucetAccount = this.getFaucetAccount();
751
+ try {
752
+ const { createWalletClient, http, parseCFX } = await import("cive");
753
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("cive/accounts");
754
+ const { hexAddressToBase32, encodeFunctionData, defineChain } = await import("cive/utils");
755
+ const chainId = this.config.chainId || 2029;
756
+ const chain = defineChain({
757
+ id: chainId,
758
+ name: "Conflux Core Local",
759
+ nativeCurrency: { decimals: 18, name: "Conflux", symbol: "CFX" },
760
+ rpcUrls: {
761
+ default: { http: [`http://localhost:${this.config.coreRpcPort}`] }
762
+ }
763
+ });
764
+ const account = privateKeyToAccount2(
765
+ faucetAccount.privateKey,
766
+ { networkId: chainId }
767
+ );
768
+ const walletClient = createWalletClient({
769
+ account,
770
+ chain,
771
+ transport: http(`http://localhost:${this.config.coreRpcPort}`)
772
+ });
773
+ const bridgeAddress = hexAddressToBase32({
774
+ hexAddress: "0x0888000000000000000000000000000000000006",
775
+ networkId: chainId
776
+ });
777
+ const hash = await walletClient.sendTransaction({
778
+ account,
779
+ chain,
780
+ to: bridgeAddress,
781
+ value: parseCFX(amount),
782
+ data: encodeFunctionData({
783
+ abi: [
784
+ {
785
+ type: "function",
786
+ name: "transferEVM",
787
+ inputs: [{ name: "to", type: "bytes20" }],
788
+ outputs: [{ name: "output", type: "bytes" }],
789
+ stateMutability: "payable"
790
+ }
791
+ ],
792
+ functionName: "transferEVM",
793
+ args: [targetAddress]
794
+ })
795
+ });
796
+ console.log(
797
+ `Funded eSpace account ${targetAddress} with ${amount} CFX via Core\u2192eSpace bridge. TX: ${hash}`
798
+ );
799
+ return hash;
800
+ } catch (error) {
801
+ throw new NodeError(
802
+ `Failed to fund eSpace account: ${error instanceof Error ? error.message : String(error)}`,
803
+ "FAUCET_ERROR",
804
+ "evm",
805
+ {
806
+ targetAddress,
807
+ amount,
808
+ faucetCoreAddress: faucetAccount.coreAddress,
809
+ originalError: error
810
+ }
811
+ );
812
+ }
813
+ }
814
+ /**
815
+ * Fund both Core and eSpace accounts for the same private key
816
+ */
817
+ async fundDualChainAccount(privateKey, coreAmount, evmAmount) {
818
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("cive/accounts");
819
+ const { privateKeyToAccount: privateKeyToEvmAccount2 } = await import("viem/accounts");
820
+ const coreAccount = privateKeyToAccount2(privateKey, {
821
+ networkId: this.config.chainId || 1
822
+ });
823
+ const evmAccount = privateKeyToEvmAccount2(privateKey);
824
+ const [coreHash, evmHash] = await Promise.all([
825
+ this.fundCoreAccount(coreAccount.address, coreAmount),
826
+ this.fundEvmAccount(evmAccount.address, evmAmount)
827
+ ]);
828
+ return {
829
+ coreHash,
830
+ evmHash,
831
+ coreAddress: coreAccount.address,
832
+ evmAddress: evmAccount.address
833
+ };
834
+ }
835
+ /**
836
+ * Check faucet account balances on both chains
837
+ */
838
+ async getFaucetBalances() {
839
+ if (!this.isRunning()) {
840
+ throw new NodeError(
841
+ "Server must be running to check balances",
842
+ "SERVER_NOT_RUNNING"
843
+ );
844
+ }
845
+ const faucetAccount = this.getFaucetAccount();
846
+ try {
847
+ const [core, evm] = await Promise.all([
848
+ this.getCoreBalance(faucetAccount.coreAddress),
849
+ this.getEvmBalance(faucetAccount.evmAddress)
850
+ ]);
851
+ return { core, evm };
852
+ } catch (error) {
853
+ throw new NodeError(
854
+ `Failed to get faucet balances: ${error instanceof Error ? error.message : String(error)}`,
855
+ "BALANCE_CHECK_ERROR",
856
+ void 0,
857
+ { faucetAccount, originalError: error }
858
+ );
859
+ }
860
+ }
861
+ /**
862
+ * Check Core Space balance
863
+ */
864
+ async getCoreBalance(address) {
865
+ const { createPublicClient, http, formatCFX } = await import("cive");
866
+ const publicClient = createPublicClient({
867
+ chain: this.config.chainId === 1029 ? {
868
+ id: 1029,
869
+ name: "Conflux Core",
870
+ nativeCurrency: { name: "Conflux", symbol: "CFX", decimals: 18 },
871
+ rpcUrls: {
872
+ default: {
873
+ http: [`http://localhost:${this.config.coreRpcPort}`]
874
+ }
875
+ }
876
+ } : {
877
+ id: 1,
878
+ name: "Conflux Core Testnet",
879
+ nativeCurrency: { name: "Conflux", symbol: "CFX", decimals: 18 },
880
+ rpcUrls: {
881
+ default: {
882
+ http: [`http://localhost:${this.config.coreRpcPort}`]
883
+ }
884
+ }
885
+ },
886
+ transport: http(`http://localhost:${this.config.coreRpcPort}`)
887
+ });
888
+ const balance = await publicClient.getBalance({
889
+ address
890
+ });
891
+ return formatCFX(balance);
892
+ }
893
+ /**
894
+ * Check eSpace balance
895
+ */
896
+ async getEvmBalance(address) {
897
+ const { createPublicClient, http, formatEther } = await import("viem");
898
+ const publicClient = createPublicClient({
899
+ chain: {
900
+ id: this.config.evmChainId || 71,
901
+ name: "Conflux eSpace Local",
902
+ nativeCurrency: { name: "Conflux", symbol: "CFX", decimals: 18 },
903
+ rpcUrls: {
904
+ default: { http: [`http://localhost:${this.config.evmRpcPort}`] }
905
+ }
906
+ },
907
+ transport: http(`http://localhost:${this.config.evmRpcPort}`)
908
+ });
909
+ const balance = await publicClient.getBalance({
910
+ address
911
+ });
912
+ return formatEther(balance);
913
+ }
914
+ /**
915
+ * Get Ethereum-compatible admin address derived from mnemonic
916
+ * Uses the standard Ethereum derivation path: m/44'/60'/0'/0/0
917
+ * This address will match what MetaMask and other Ethereum wallets derive
918
+ */
919
+ getEthereumAdminAddress() {
920
+ const account = (0, import_wallet.deriveAccount)(
921
+ this.mnemonic,
922
+ 0,
923
+ this.config.chainId || 2029
924
+ );
925
+ return account.evmAddress.toLowerCase();
926
+ }
927
+ /**
928
+ * Set up cleanup handlers for graceful shutdown
929
+ */
930
+ setupCleanupHandlers() {
931
+ const cleanup = async () => {
932
+ if (this.isRunning()) {
933
+ console.log("Shutting down Conflux development node...");
934
+ await this.stop();
935
+ }
936
+ };
937
+ process.on("SIGINT", cleanup);
938
+ process.on("SIGTERM", cleanup);
939
+ process.on("exit", cleanup);
940
+ }
941
+ };
942
+
943
+ // src/plugin.ts
944
+ var DevKitWithDevNode = class {
945
+ server;
946
+ baseDevKit;
947
+ constructor(baseDevKit, config) {
948
+ this.baseDevKit = baseDevKit;
949
+ this.server = new ServerManager(config);
950
+ }
951
+ // Delegate to base DevKit
952
+ getConfig() {
953
+ return this.baseDevKit.getConfig();
954
+ }
955
+ getRpcUrls() {
956
+ return this.baseDevKit.getRpcUrls();
957
+ }
958
+ // Node lifecycle methods
959
+ async startNode(options = {}) {
960
+ await this.server.start();
961
+ if (options.mining !== false) {
962
+ await this.server.startMining();
963
+ }
964
+ }
965
+ async stopNode() {
966
+ await this.server.stop();
967
+ }
968
+ // Mining methods
969
+ async startMining() {
970
+ await this.server.startMining();
971
+ }
972
+ async stopMining() {
973
+ await this.server.stopMining();
974
+ }
975
+ async mine(blocks = 1) {
976
+ await this.server.mine(blocks);
977
+ }
978
+ getMiningStatus() {
979
+ return this.server.getMiningStatus();
980
+ }
981
+ async setMiningInterval(interval) {
982
+ await this.server.setMiningInterval(interval);
983
+ }
984
+ // Faucet methods
985
+ async getFaucetBalances() {
986
+ return await this.server.getFaucetBalances();
987
+ }
988
+ getFaucetAccount() {
989
+ return this.server.getFaucetAccount();
990
+ }
991
+ async fundAccount(address, amount, chain) {
992
+ if (chain === "core") {
993
+ return await this.server.fundCoreAccount(address, amount);
994
+ } else {
995
+ return await this.server.fundEvmAccount(address, amount);
996
+ }
997
+ }
998
+ // Account methods
999
+ async addAccount() {
1000
+ return await this.server.addAccount();
1001
+ }
1002
+ getAccounts() {
1003
+ return this.server.getAccounts();
1004
+ }
1005
+ // Utility methods
1006
+ getEthereumAdminAddress() {
1007
+ return this.server.getEthereumAdminAddress();
1008
+ }
1009
+ getServerStatus() {
1010
+ return this.server.getStatus();
1011
+ }
1012
+ isNodeRunning() {
1013
+ return this.server.isRunning();
1014
+ }
1015
+ };
1016
+ var devNodePlugin = {
1017
+ name: "devnode",
1018
+ version: "0.1.0",
1019
+ extendDevKit(devkit, config) {
1020
+ return new DevKitWithDevNode(devkit, config);
1021
+ }
1022
+ };
1023
+
1024
+ // src/index.ts
1025
+ var VERSION = "0.1.0";
1026
+ // Annotate the CommonJS export names for ESM import in node:
1027
+ 0 && (module.exports = {
1028
+ DevKitWithDevNode,
1029
+ ServerManager,
1030
+ VERSION,
1031
+ devNodePlugin
1032
+ });
1033
+ //# sourceMappingURL=index.cjs.map