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