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