@offckb/cli 0.4.9-canary-807f2ef.0 → 0.4.9

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.
Files changed (3) hide show
  1. package/README.md +42 -11
  2. package/build/index.js +1234 -455
  3. package/package.json +1 -1
package/build/index.js CHANGED
@@ -353,6 +353,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
353
353
  });
354
354
  };
355
355
  Object.defineProperty(exports, "__esModule", ({ value: true }));
356
+ exports.runCli = runCli;
356
357
  const commander_1 = __nccwpck_require__(53497);
357
358
  const node_1 = __nccwpck_require__(998);
358
359
  const accounts_1 = __nccwpck_require__(87398);
@@ -367,6 +368,7 @@ const create_1 = __nccwpck_require__(51534);
367
368
  const config_1 = __nccwpck_require__(93004);
368
369
  const devnet_config_1 = __nccwpck_require__(10015);
369
370
  const devnet_fork_1 = __nccwpck_require__(36709);
371
+ const devnet_info_1 = __nccwpck_require__(60905);
370
372
  const debug_1 = __nccwpck_require__(39591);
371
373
  const system_scripts_1 = __nccwpck_require__(86198);
372
374
  const transfer_all_1 = __nccwpck_require__(92039);
@@ -374,16 +376,26 @@ const gen_1 = __nccwpck_require__(750);
374
376
  const ckb_debugger_1 = __nccwpck_require__(14815);
375
377
  const logger_1 = __nccwpck_require__(65370);
376
378
  const status_1 = __nccwpck_require__(71420);
377
- const validator_1 = __nccwpck_require__(39534);
378
379
  const version = (__nccwpck_require__(8330)/* .version */ .rE);
379
380
  const description = (__nccwpck_require__(8330)/* .description */ .h_);
380
381
  // fix windows terminal encoding of simplified chinese text
381
382
  (0, encoding_1.setUTF8EncodingForWindows)();
382
383
  const program = new commander_1.Command();
383
384
  program.name('offckb').description(description).version(version).enablePositionalOptions();
385
+ let activeCommand = 'offckb';
386
+ function commandPath(command) {
387
+ const names = [];
388
+ let current = command;
389
+ while (current === null || current === void 0 ? void 0 : current.parent) {
390
+ names.unshift(current.name());
391
+ current = current.parent;
392
+ }
393
+ return names.join('.') || 'offckb';
394
+ }
384
395
  program.option('--json', 'Output logs in JSON format for agent/programmatic consumption');
385
- program.hook('preAction', (thisCommand) => {
386
- const opts = thisCommand.opts();
396
+ program.hook('preAction', (_thisCommand, actionCommand) => {
397
+ activeCommand = commandPath(actionCommand);
398
+ const opts = actionCommand.optsWithGlobals();
387
399
  if (opts.json) {
388
400
  logger_1.logger.setJsonMode(true);
389
401
  }
@@ -420,7 +432,8 @@ program
420
432
  .option('--target <target>', 'Specify the script binaries file/folder path to deploy', './')
421
433
  .option('-o, --output <output>', 'Specify the output folder path for the deployment record files', './deployment')
422
434
  .option('-t, --type-id', 'Specify if use upgradable type id to deploy the script')
423
- .option('--privkey <privkey>', 'Specify the private key to deploy scripts')
435
+ .option('--privkey <privkey>', 'Specify the private key to deploy scripts (visible in shell history)')
436
+ .option('--privkey-file <path>', 'Read the private key from a local file')
424
437
  .option('-y, --yes', 'Skip confirmation prompt and deploy immediately')
425
438
  .action((options) => (0, deploy_1.deploy)(options));
426
439
  program
@@ -433,8 +446,7 @@ program
433
446
  .action((option) => __awaiter(void 0, void 0, void 0, function* () {
434
447
  // For debugging, tx-hash is required
435
448
  if (!option.txHash) {
436
- logger_1.logger.error('Error: --tx-hash is required for debugging operations');
437
- process.exit(1);
449
+ throw new Error('--tx-hash is required for debugging operations');
438
450
  }
439
451
  const txHash = option.txHash;
440
452
  if (option.singleScript) {
@@ -464,34 +476,43 @@ program
464
476
  .description('Clean the devnet data, need to stop running the chain first')
465
477
  .option('-d, --data', 'Only remove chain data, keep devnet config files')
466
478
  .action((options) => (0, clean_1.clean)(options));
467
- program.command('accounts').description('Print account list info').action(accounts_1.accounts);
479
+ program
480
+ .command('accounts')
481
+ .description('Print account list info')
482
+ .option('--show-private-keys', 'Include built-in dev private keys (hidden by default)')
483
+ .action((options) => __awaiter(void 0, void 0, void 0, function* () {
484
+ yield (0, accounts_1.accounts)(options);
485
+ }));
468
486
  program
469
487
  .command('deposit [toAddress] [amountInCKB]')
470
488
  .description('Deposit CKB tokens to address, only devnet and testnet')
471
489
  .option('--network <network>', 'Specify the network to deposit to', 'devnet')
472
490
  .option('-r, --proxy-rpc', 'Use Proxy RPC to connect to blockchain')
473
491
  .action((toAddress, amountInCKB, options) => __awaiter(void 0, void 0, void 0, function* () {
474
- return (0, deposit_1.deposit)(toAddress, amountInCKB, options);
492
+ yield (0, deposit_1.deposit)(toAddress, amountInCKB, options);
475
493
  }));
476
494
  program
477
495
  .command('transfer [toAddress] [amount]')
478
496
  .description('Transfer CKB or UDT tokens to address, only devnet and testnet')
479
497
  .option('--network <network>', 'Specify the network to transfer to', 'devnet')
480
- .option('--privkey <privkey>', 'Specify the private key to transfer')
481
- .addOption(new commander_1.Option('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
498
+ .option('--privkey <privkey>', 'Specify the private key to transfer (visible in shell history)')
499
+ .option('--privkey-file <path>', 'Read the private key from a local file')
500
+ .addOption(new commander_1.Option('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']))
482
501
  .option('--udt-type-args <typeArgs>', 'Specify the UDT type script args to transfer UDT')
502
+ .option('--allow-mainnet-replay-risk', 'Allow a non-built-in key on a Mainnet fork (copied inputs remain blocked)')
483
503
  .option('-r, --proxy-rpc', 'Use Proxy RPC to connect to blockchain')
484
504
  .action((toAddress, amount, options) => __awaiter(void 0, void 0, void 0, function* () {
485
- return (0, transfer_1.transfer)(toAddress, amount, options);
505
+ yield (0, transfer_1.transfer)(toAddress, amount, options);
486
506
  }));
487
507
  program
488
508
  .command('transfer-all [toAddress]')
489
509
  .description('Transfer All CKB tokens to address, only devnet and testnet')
490
510
  .option('--network <network>', 'Specify the network to transfer to', 'devnet')
491
- .option('--privkey <privkey>', 'Specify the private key to deploy scripts')
511
+ .option('--privkey <privkey>', 'Specify the private key (visible in shell history)')
512
+ .option('--privkey-file <path>', 'Read the private key from a local file')
492
513
  .option('-r, --proxy-rpc', 'Use Proxy RPC to connect to blockchain')
493
514
  .action((toAddress, options) => __awaiter(void 0, void 0, void 0, function* () {
494
- return (0, transfer_all_1.transferAll)(toAddress, options);
515
+ yield (0, transfer_all_1.transferAll)(toAddress, options);
495
516
  }));
496
517
  program
497
518
  .command('balance [toAddress]')
@@ -501,7 +522,7 @@ program
501
522
  .option('--udt-type-args <typeArgs>', 'Filter by UDT type script args')
502
523
  .option('--no-udt', 'Skip UDT balance scan')
503
524
  .action((toAddress, options) => __awaiter(void 0, void 0, void 0, function* () {
504
- return (0, balance_1.balanceOf)(toAddress, options);
525
+ yield (0, balance_1.balanceOf)(toAddress, options);
505
526
  }));
506
527
  const udtCommand = program.command('udt').description('UDT token commands');
507
528
  udtCommand
@@ -511,9 +532,10 @@ udtCommand
511
532
  .addOption(new commander_1.Option('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
512
533
  .option('--type-args <typeArgs>', 'Specify the UDT type script args (xudt only; defaults to signer lock hash)')
513
534
  .option('--to <toAddress>', 'Specify the receiver address (defaults to signer)')
514
- .option('--privkey <privkey>', 'Specify the private key to issue UDT')
535
+ .option('--privkey <privkey>', 'Specify the private key to issue UDT (visible in shell history)')
536
+ .option('--privkey-file <path>', 'Read the private key from a local file')
515
537
  .action((amount, options) => __awaiter(void 0, void 0, void 0, function* () {
516
- return (0, udt_1.udtIssue)(amount, options);
538
+ yield (0, udt_1.udtIssue)(amount, options);
517
539
  }));
518
540
  udtCommand
519
541
  .command('destroy <amount>')
@@ -521,9 +543,10 @@ udtCommand
521
543
  .option('--network <network>', 'Specify the network', 'devnet')
522
544
  .addOption(new commander_1.Option('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
523
545
  .requiredOption('--type-args <typeArgs>', 'Specify the UDT type script args')
524
- .option('--privkey <privkey>', 'Specify the private key to destroy UDT')
546
+ .option('--privkey <privkey>', 'Specify the private key to destroy UDT (visible in shell history)')
547
+ .option('--privkey-file <path>', 'Read the private key from a local file')
525
548
  .action((amount, options) => __awaiter(void 0, void 0, void 0, function* () {
526
- return (0, udt_1.udtDestroy)(amount, options);
549
+ yield (0, udt_1.udtDestroy)(amount, options);
527
550
  }));
528
551
  program
529
552
  .command('debugger')
@@ -537,10 +560,11 @@ program
537
560
  program
538
561
  .command('status')
539
562
  .description('Show ckb-tui status interface')
540
- .option('--network <network>', 'Specify the network whose node status to monitor', 'devnet')
563
+ .addOption(new commander_1.Option('--network <network>', 'Specify the network whose node status to monitor')
564
+ .choices(['devnet', 'testnet', 'mainnet'])
565
+ .default('devnet'))
541
566
  .action((option) => __awaiter(void 0, void 0, void 0, function* () {
542
- (0, validator_1.validateNetworkOpt)(option.network);
543
- return yield (0, status_1.status)({ network: option.network });
567
+ yield (0, status_1.status)({ network: option.network });
544
568
  }));
545
569
  program
546
570
  .command('config <action> [item] [value]')
@@ -552,18 +576,77 @@ devnetCommand
552
576
  .description('Open a full-screen editor to tweak devnet config files')
553
577
  .option('-s, --set <key=value>', 'Set a devnet config field non-interactively (repeatable), e.g. --set ckb.logger.filter=info', (value, previous = []) => [...previous, value], [])
554
578
  .action(devnet_config_1.devnetConfig);
579
+ devnetCommand
580
+ .command('info')
581
+ .description('Show fork metadata and node/indexer readiness')
582
+ .action(() => __awaiter(void 0, void 0, void 0, function* () {
583
+ yield (0, devnet_info_1.devnetInfo)();
584
+ }));
555
585
  devnetCommand
556
586
  .command('fork')
557
587
  .description('Fork an existing mainnet/testnet chain data directory into the local devnet')
558
- .requiredOption('--from <dir>', 'Path to the source CKB node directory (the one passed to ckb -C)')
588
+ .option('--from <dir>', 'Path to the source CKB node directory used with `ckb -C`')
559
589
  .option('--source <source>', 'Source chain: mainnet or testnet (auto-detected from the source ckb.toml when omitted)')
560
590
  .option('--spec-file <path>', 'Use a local chain spec file instead of downloading it')
561
591
  .option('--force', 'Replace the existing devnet (or a previous fork)')
592
+ .option('--migrate', 'Migrate only the copied database when the selected CKB version requires it')
593
+ .option('--dry-run', 'Run source/spec/database preflight without replacing the current devnet')
562
594
  .action(devnet_fork_1.devnetFork);
563
- program.parse(process.argv);
564
- // If no command is specified, display help
565
- if (!process.argv.slice(2).length) {
566
- program.outputHelp();
595
+ function normalizeGlobalJsonFlag(argv) {
596
+ const jsonRequested = argv.slice(2).includes('--json');
597
+ if (!jsonRequested)
598
+ return argv;
599
+ return [argv[0], argv[1], '--json', ...argv.slice(2).filter((arg) => arg !== '--json')];
600
+ }
601
+ function installBrokenPipeHandlers() {
602
+ for (const stream of [process.stdout, process.stderr]) {
603
+ stream.on('error', (error) => {
604
+ if (error.code === 'EPIPE') {
605
+ process.exit(0);
606
+ }
607
+ throw error;
608
+ });
609
+ }
610
+ }
611
+ function configureCommanderErrors(command) {
612
+ command.exitOverride();
613
+ command.configureOutput({
614
+ writeErr: (text) => {
615
+ if (!logger_1.logger.isJsonMode())
616
+ process.stderr.write(text);
617
+ },
618
+ });
619
+ command.commands.forEach(configureCommanderErrors);
620
+ }
621
+ function runCli() {
622
+ return __awaiter(this, arguments, void 0, function* (argv = process.argv) {
623
+ installBrokenPipeHandlers();
624
+ const normalizedArgv = normalizeGlobalJsonFlag(argv);
625
+ if (normalizedArgv.includes('--json'))
626
+ logger_1.logger.setJsonMode(true);
627
+ if (!normalizedArgv.slice(2).length) {
628
+ program.outputHelp();
629
+ return;
630
+ }
631
+ configureCommanderErrors(program);
632
+ try {
633
+ yield program.parseAsync(normalizedArgv);
634
+ if (logger_1.logger.isJsonMode() && !logger_1.logger.hasResult() && (process.exitCode == null || process.exitCode === 0)) {
635
+ logger_1.logger.result({ command: activeCommand, completed: true });
636
+ }
637
+ }
638
+ catch (error) {
639
+ if (error instanceof commander_1.CommanderError && error.exitCode === 0)
640
+ return;
641
+ const message = error instanceof Error ? error.message : String(error);
642
+ const code = error instanceof commander_1.CommanderError ? error.code : 'COMMAND_FAILED';
643
+ logger_1.logger.failure(code, message);
644
+ process.exitCode = error instanceof commander_1.CommanderError ? error.exitCode : 1;
645
+ }
646
+ });
647
+ }
648
+ if (require.main === require.cache[eval('__filename')]) {
649
+ void runCli();
567
650
  }
568
651
 
569
652
 
@@ -574,40 +657,90 @@ if (!process.argv.slice(2).length) {
574
657
 
575
658
  "use strict";
576
659
 
660
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
661
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
662
+ return new (P || (P = Promise))(function (resolve, reject) {
663
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
664
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
665
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
666
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
667
+ });
668
+ };
577
669
  var __importDefault = (this && this.__importDefault) || function (mod) {
578
670
  return (mod && mod.__esModule) ? mod : { "default": mod };
579
671
  };
580
672
  Object.defineProperty(exports, "__esModule", ({ value: true }));
581
673
  exports.accounts = accounts;
582
674
  const account_json_1 = __importDefault(__nccwpck_require__(13849));
675
+ const core_1 = __nccwpck_require__(39842);
676
+ const setting_1 = __nccwpck_require__(25546);
677
+ const fork_1 = __nccwpck_require__(77912);
678
+ const readiness_1 = __nccwpck_require__(19326);
679
+ const base_1 = __nccwpck_require__(69951);
583
680
  const logger_1 = __nccwpck_require__(65370);
584
681
  function accounts() {
585
- logger_1.logger.warn([
586
- '#### All Accounts are for test and develop only ####'.toUpperCase(),
587
- "#### DON'T use these accounts on Mainnet ####".toUpperCase(),
588
- '#### Otherwise You will loose your money ####'.toUpperCase(),
589
- '',
590
- ]);
591
- logger_1.logger.info([
592
- 'Print account list, each account is funded with 42_000_000_00000000 capacity in the devnet genesis block.',
593
- '',
594
- ]);
595
- const accountDetails = account_json_1.default.map((account, index) => {
596
- return [
597
- `- "#": ${index}`,
598
- `address: ${account.address}`,
599
- `privkey: ${account.privkey}`,
600
- `pubkey: ${account.pubkey}`,
601
- `lock_arg: ${account.lockScript.args}`,
602
- 'lockScript:',
603
- ` codeHash: ${account.lockScript.codeHash}`,
604
- ` hashType: ${account.lockScript.hashType}`,
605
- ` args: ${account.lockScript.args}`,
682
+ return __awaiter(this, arguments, void 0, function* (options = {}) {
683
+ const settings = (0, setting_1.readSettings)();
684
+ const fork = (0, fork_1.readForkState)(settings.devnet.configPath);
685
+ const isMainnetFork = (fork === null || fork === void 0 ? void 0 : fork.source) === 'mainnet';
686
+ const client = isMainnetFork
687
+ ? new core_1.ccc.ClientPublicMainnet({ url: settings.devnet.rpcUrl, fallbacks: [] })
688
+ : new core_1.ccc.ClientPublicTestnet({ url: settings.devnet.rpcUrl, fallbacks: [] });
689
+ const context = fork ? `DEVNET (fork of ${fork.source.toUpperCase()})` : 'DEVNET';
690
+ const readiness = fork ? yield (0, readiness_1.warnIfForkIndexerIsBehind)(base_1.Network.devnet) : undefined;
691
+ const canReadSpendableBalance = (readiness === null || readiness === void 0 ? void 0 : readiness.ready) === true && readiness.indexerTip != null && readiness.indexerLag === BigInt(0);
692
+ logger_1.logger.warn([
693
+ '#### All Accounts are for test and develop only ####'.toUpperCase(),
694
+ "#### DON'T use these accounts on Mainnet ####".toUpperCase(),
695
+ '#### Otherwise You will lose your money ####'.toUpperCase(),
606
696
  '',
607
- ];
608
- });
609
- accountDetails.forEach((details, _index) => {
610
- logger_1.logger.info(details);
697
+ ]);
698
+ if (fork) {
699
+ logger_1.logger.info([
700
+ `Print account list for ${context}. Addresses use the source-chain prefix.`,
701
+ 'Forked devnets do not include the standard offckb genesis allocation; funds come from fork-mined cellbase cells.',
702
+ 'Run `offckb devnet info` before trusting balances while the indexer catches up.',
703
+ '',
704
+ ]);
705
+ }
706
+ else {
707
+ logger_1.logger.info([
708
+ 'Print account list, each account is funded with 42_000_000_00000000 capacity in the devnet genesis block.',
709
+ '',
710
+ ]);
711
+ }
712
+ const resolvedAccounts = yield Promise.all(account_json_1.default.map((account, index) => __awaiter(this, void 0, void 0, function* () {
713
+ const script = core_1.ccc.Script.from(account.lockScript);
714
+ const address = core_1.ccc.Address.fromScript(script, client).toString();
715
+ const spendableCkb = canReadSpendableBalance
716
+ ? core_1.ccc.fixedPointToString(yield client.getBalanceSingle(script))
717
+ : undefined;
718
+ return Object.assign(Object.assign(Object.assign({ index,
719
+ address }, (spendableCkb == null ? {} : { spendableCkb })), (options.showPrivateKeys ? { privkey: account.privkey } : {})), { pubkey: account.pubkey, lockArg: account.lockScript.args, lockScript: account.lockScript });
720
+ })));
721
+ const accountDetails = resolvedAccounts.map((account) => {
722
+ return [
723
+ `- "#": ${account.index}`,
724
+ `address: ${account.address}`,
725
+ ...('spendableCkb' in account ? [`spendable_ckb: ${account.spendableCkb}`] : []),
726
+ ...(options.showPrivateKeys ? [`privkey: ${account.privkey}`] : []),
727
+ `pubkey: ${account.pubkey}`,
728
+ `lock_arg: ${account.lockArg}`,
729
+ 'lockScript:',
730
+ ` codeHash: ${account.lockScript.codeHash}`,
731
+ ` hashType: ${account.lockScript.hashType}`,
732
+ ` args: ${account.lockScript.args}`,
733
+ '',
734
+ ];
735
+ });
736
+ accountDetails.forEach((details, _index) => {
737
+ logger_1.logger.info(details);
738
+ });
739
+ if (!options.showPrivateKeys) {
740
+ logger_1.logger.info('Private keys are hidden by default. Use --show-private-keys only in a trusted local terminal.');
741
+ }
742
+ logger_1.logger.result({ command: 'accounts', context, forked: Boolean(fork), accounts: resolvedAccounts });
743
+ return resolvedAccounts;
611
744
  });
612
745
  }
613
746
 
@@ -634,10 +767,12 @@ const ckb_1 = __nccwpck_require__(83748);
634
767
  const validator_1 = __nccwpck_require__(39534);
635
768
  const base_1 = __nccwpck_require__(69951);
636
769
  const logger_1 = __nccwpck_require__(65370);
770
+ const readiness_1 = __nccwpck_require__(19326);
637
771
  function balanceOf(address_1) {
638
772
  return __awaiter(this, arguments, void 0, function* (address, opt = { network: base_1.Network.devnet }) {
639
773
  const network = opt.network;
640
774
  (0, validator_1.validateNetworkOpt)(network);
775
+ yield (0, readiness_1.warnIfForkIndexerIsBehind)(network);
641
776
  const ckb = new ckb_1.CKB({ network });
642
777
  const [balanceInCKB, udtBalances] = yield Promise.all([
643
778
  ckb.balance(address),
@@ -651,7 +786,9 @@ function balanceOf(address_1) {
651
786
  logger_1.logger.info(` ${udt.kind} (args=${udt.args}): ${udt.balance}`);
652
787
  }
653
788
  }
654
- process.exit(0);
789
+ const result = { command: 'balance', network, address, ckb: balanceInCKB, udt: filtered };
790
+ logger_1.logger.result(result);
791
+ return result;
655
792
  });
656
793
  }
657
794
  function filterUdtBalances(balances, opt) {
@@ -695,8 +832,7 @@ function clean(options) {
695
832
  logger_1.logger.info(`Chain data cleaned. Devnet config files preserved.`);
696
833
  }
697
834
  catch (error) {
698
- logger_1.logger.info(`Did you stop running the chain first?`);
699
- logger_1.logger.error(error.message);
835
+ throw new Error(`Failed to clean chain data. Did you stop the chain first? ${error.message}`);
700
836
  }
701
837
  }
702
838
  else {
@@ -712,8 +848,7 @@ function clean(options) {
712
848
  logger_1.logger.info(`Chain data cleaned.`);
713
849
  }
714
850
  catch (error) {
715
- logger_1.logger.info(`Did you stop running the chain first?`);
716
- logger_1.logger.error(error.message);
851
+ throw new Error(`Failed to clean devnet data. Did you stop the chain first? ${error.message}`);
717
852
  }
718
853
  }
719
854
  else {
@@ -770,8 +905,7 @@ function Config(action, item, value) {
770
905
  const settings = (0, setting_1.readSettings)();
771
906
  const proxy = settings.proxy;
772
907
  if (proxy == null) {
773
- logger_1.logger.info(`No Proxy.`);
774
- process.exit(0);
908
+ return logger_1.logger.info(`No Proxy.`);
775
909
  }
776
910
  return logger_1.logger.info(`${request_1.Request.proxyConfigToUrl(proxy)}`);
777
911
  }
@@ -796,7 +930,7 @@ function Config(action, item, value) {
796
930
  return (0, setting_1.writeSettings)(settings);
797
931
  }
798
932
  catch (error) {
799
- return logger_1.logger.error(`invalid proxyURL, `, error.message);
933
+ throw new Error(`invalid proxyURL: ${error.message}`);
800
934
  }
801
935
  }
802
936
  case ConfigItem.ckbVersion: {
@@ -808,11 +942,11 @@ function Config(action, item, value) {
808
942
  return (0, setting_1.writeSettings)(settings);
809
943
  }
810
944
  else {
811
- return logger_1.logger.error(`invalid version value, ${value}. Check available versions on https://github.com/nervosnetwork/ckb/tags`);
945
+ throw new Error(`invalid version value, ${value}. Check available versions on https://github.com/nervosnetwork/ckb/tags`);
812
946
  }
813
947
  }
814
948
  catch (error) {
815
- return logger_1.logger.error(`invalid version value, `, error.message);
949
+ throw new Error(`invalid version value: ${error.message}`);
816
950
  }
817
951
  }
818
952
  default:
@@ -932,8 +1066,7 @@ function createScriptProject(name_1) {
932
1066
  const fullProjectPath = path.resolve(projectPath);
933
1067
  // Check if directory already exists
934
1068
  if (fs.existsSync(fullProjectPath)) {
935
- logger_1.logger.error(`❌ Directory '${projectPath}' already exists!`);
936
- process.exit(1);
1069
+ throw new Error(`Directory '${projectPath}' already exists!`);
937
1070
  }
938
1071
  logger_1.logger.info([
939
1072
  '📝 Project details:',
@@ -954,8 +1087,7 @@ function createScriptProject(name_1) {
954
1087
  // Get template directory (adjust path based on whether we're running from source or built)
955
1088
  const templateDir = possiblePaths.find((p) => fs.existsSync(p)) || possiblePaths[0];
956
1089
  if (!fs.existsSync(templateDir)) {
957
- logger_1.logger.error(`❌ Template directory not found: ${templateDir}`);
958
- process.exit(1);
1090
+ throw new Error(`Template directory not found: ${templateDir}`);
959
1091
  }
960
1092
  // Initialize template processor
961
1093
  const processor = new processor_1.TemplateProcessor(templateDir);
@@ -1031,8 +1163,7 @@ function createScriptProject(name_1) {
1031
1163
  }
1032
1164
  }
1033
1165
  catch (error) {
1034
- logger_1.logger.error(`\n❌ Failed to create project: ${error.message}`);
1035
- process.exit(1);
1166
+ throw new Error(`Failed to create project: ${error.message}`);
1036
1167
  }
1037
1168
  });
1038
1169
  }
@@ -1257,8 +1388,7 @@ function buildContract(jsFile, outputFile, jsVmPath) {
1257
1388
  logger_1.logger.success(`✅ Contract built successfully: ${outputFile}`);
1258
1389
  }
1259
1390
  catch (error) {
1260
- logger_1.logger.error(`❌ Build failed: ${error}`);
1261
- process.exit(1);
1391
+ throw new Error(`Build failed: ${error instanceof Error ? error.message : String(error)}`);
1262
1392
  }
1263
1393
  });
1264
1394
  }
@@ -1294,14 +1424,19 @@ const deploy_1 = __nccwpck_require__(45615);
1294
1424
  const ckb_1 = __nccwpck_require__(83748);
1295
1425
  const prompts_1 = __nccwpck_require__(18167);
1296
1426
  const logger_1 = __nccwpck_require__(65370);
1427
+ const private_key_1 = __nccwpck_require__(26241);
1428
+ const readiness_1 = __nccwpck_require__(19326);
1429
+ const fork_safety_1 = __nccwpck_require__(47625);
1297
1430
  function deploy() {
1298
1431
  return __awaiter(this, arguments, void 0, function* (opt = { network: base_1.Network.devnet, typeId: false, target: './', output: './deployment' }) {
1299
1432
  var _a;
1300
1433
  const network = opt.network;
1301
1434
  (0, validator_1.validateNetworkOpt)(network);
1302
- const ckb = new ckb_1.CKB({ network });
1303
1435
  // we use deployerAccount to deploy contract by default
1304
- const privateKey = opt.privkey || account_1.deployerAccount.privkey;
1436
+ const privateKey = (0, private_key_1.resolvePrivateKey)(opt, account_1.deployerAccount.privkey);
1437
+ (0, fork_safety_1.warnIfMainnetForkSigning)(network, privateKey);
1438
+ yield (0, readiness_1.warnIfForkIndexerIsBehind)(network);
1439
+ const ckb = new ckb_1.CKB({ network });
1305
1440
  const enableTypeId = (_a = opt.typeId) !== null && _a !== void 0 ? _a : false;
1306
1441
  const targetFolder = opt.target;
1307
1442
  const output = opt.output;
@@ -1325,7 +1460,7 @@ function deploy() {
1325
1460
  '',
1326
1461
  ` 📁 Deployment artifacts will be saved to: ${outputFolder}`,
1327
1462
  ` 🌐 Network: ${network}`,
1328
- ` 🔑 Using ${opt.privkey ? 'custom' : 'default'} private key`,
1463
+ ` 🔑 Using ${opt.privkey || opt.privkeyFile || process.env.OFFCKB_PRIVATE_KEY ? 'custom' : 'default'} private key`,
1329
1464
  ` 🔄 Type ID: ${enableTypeId ? 'enabled (upgradable)' : 'disabled (immutable)'}`,
1330
1465
  ]);
1331
1466
  // Skip confirmation if -y flag is provided
@@ -1371,22 +1506,40 @@ const link_1 = __nccwpck_require__(27814);
1371
1506
  const validator_1 = __nccwpck_require__(39534);
1372
1507
  const request_1 = __nccwpck_require__(65897);
1373
1508
  const logger_1 = __nccwpck_require__(65370);
1509
+ const readiness_1 = __nccwpck_require__(19326);
1510
+ const fork_safety_1 = __nccwpck_require__(47625);
1511
+ const TESTNET_FAUCET_CLAIM_AMOUNT = '10000';
1374
1512
  function deposit(toAddress_1, amountInCKB_1) {
1375
1513
  return __awaiter(this, arguments, void 0, function* (toAddress, amountInCKB, opt = { network: base_1.Network.devnet }) {
1376
1514
  const network = opt.network;
1377
1515
  (0, validator_1.validateNetworkOpt)(network);
1378
1516
  const ckb = new ckb_1.CKB({ network });
1379
1517
  if (network === 'testnet') {
1380
- return yield depositFromTestnetFaucet(toAddress, ckb);
1518
+ const txHash = yield depositFromTestnetFaucet(toAddress, ckb);
1519
+ logger_1.logger.result({
1520
+ command: 'deposit',
1521
+ network,
1522
+ source: 'fixed-testnet-faucet-claim',
1523
+ requestedAmount: amountInCKB,
1524
+ faucetClaimAmount: TESTNET_FAUCET_CLAIM_AMOUNT,
1525
+ toAddress,
1526
+ txHash,
1527
+ });
1528
+ return txHash;
1381
1529
  }
1382
1530
  // deposit from devnet miner
1383
1531
  const privateKey = account_1.ckbDevnetMinerAccount.privkey;
1532
+ const rejectInputsAtOrBeforeBlock = (0, fork_safety_1.validateMainnetForkSigning)(network, privateKey);
1533
+ yield (0, readiness_1.warnIfForkIndexerIsBehind)(network);
1384
1534
  const txHash = yield ckb.transfer({
1385
1535
  toAddress,
1386
1536
  privateKey,
1387
1537
  amountInCKB,
1538
+ rejectInputsAtOrBeforeBlock,
1388
1539
  });
1389
1540
  logger_1.logger.info('tx hash: ', txHash);
1541
+ logger_1.logger.result({ command: 'deposit', network, amount: amountInCKB, toAddress, txHash });
1542
+ return txHash;
1390
1543
  });
1391
1544
  }
1392
1545
  function depositFromTestnetFaucet(ckbAddress, ckb) {
@@ -1412,6 +1565,7 @@ function depositFromTestnetFaucet(ckbAddress, ckb) {
1412
1565
  }
1413
1566
  const txHash = yield ckb.transferAll({ privateKey: randomAccountPrivateKey, toAddress: ckbAddress });
1414
1567
  logger_1.logger.info(`Done, check ${(0, link_1.buildTestnetTxLink)(txHash)} for details.`);
1568
+ return txHash;
1415
1569
  });
1416
1570
  }
1417
1571
  function sendClaimRequest(toAddress) {
@@ -1427,7 +1581,7 @@ function sendClaimRequest(toAddress) {
1427
1581
  const body = JSON.stringify({
1428
1582
  claim_event: {
1429
1583
  address_hash: toAddress,
1430
- amount: '10000', // unit: CKB
1584
+ amount: TESTNET_FAUCET_CLAIM_AMOUNT, // unit: CKB
1431
1585
  },
1432
1586
  });
1433
1587
  const config = {
@@ -1509,12 +1663,8 @@ function devnetConfig() {
1509
1663
  return;
1510
1664
  }
1511
1665
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
1512
- logger_1.logger.error('Interactive devnet config editor requires a TTY terminal.');
1513
- logger_1.logger.info('Use non-interactive mode instead, e.g.:');
1514
- logger_1.logger.info(' offckb devnet config --set ckb.logger.filter=info');
1515
- logger_1.logger.info(' offckb devnet config --set miner.client.poll_interval=1500');
1516
- process.exitCode = 1;
1517
- return;
1666
+ throw new Error('Interactive devnet config editor requires a TTY terminal. Use non-interactive mode, e.g. ' +
1667
+ '`offckb devnet config --set ckb.logger.filter=info`.');
1518
1668
  }
1519
1669
  const isSaved = yield (0, devnet_config_tui_1.runDevnetConfigTui)(editor, configPath);
1520
1670
  if (isSaved) {
@@ -1525,12 +1675,11 @@ function devnetConfig() {
1525
1675
  logger_1.logger.info('No changes saved.');
1526
1676
  }
1527
1677
  catch (error) {
1528
- const message = error instanceof Error ? error.message : String(error);
1529
- logger_1.logger.error(message);
1678
+ let message = error instanceof Error ? error.message : String(error);
1530
1679
  if (error instanceof config_editor_1.InitializationError) {
1531
- logger_1.logger.info('Tip: run `offckb node` once to initialize devnet config files first.');
1680
+ message += ' Tip: run `offckb node` once to initialize devnet config files first.';
1532
1681
  }
1533
- process.exitCode = 1;
1682
+ throw new Error(message);
1534
1683
  }
1535
1684
  });
1536
1685
  }
@@ -1555,20 +1704,92 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
1555
1704
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1556
1705
  exports.devnetFork = devnetFork;
1557
1706
  const fork_1 = __nccwpck_require__(77912);
1558
- const logger_1 = __nccwpck_require__(65370);
1559
1707
  function devnetFork(options) {
1560
1708
  return __awaiter(this, void 0, void 0, function* () {
1561
1709
  if (options.source && options.source !== 'mainnet' && options.source !== 'testnet') {
1562
- logger_1.logger.error(`Invalid --source value: ${options.source}. Expected mainnet or testnet.`);
1563
- process.exit(1);
1564
- }
1565
- try {
1566
- yield (0, fork_1.forkDevnet)(options);
1567
- }
1568
- catch (error) {
1569
- logger_1.logger.error(error.message);
1570
- process.exit(1);
1710
+ throw new Error(`Invalid --source value: ${options.source}. Expected mainnet or testnet.`);
1571
1711
  }
1712
+ yield (0, fork_1.forkDevnet)(options);
1713
+ });
1714
+ }
1715
+
1716
+
1717
+ /***/ }),
1718
+
1719
+ /***/ 60905:
1720
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
1721
+
1722
+ "use strict";
1723
+
1724
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
1725
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
1726
+ return new (P || (P = Promise))(function (resolve, reject) {
1727
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
1728
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
1729
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
1730
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
1731
+ });
1732
+ };
1733
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1734
+ exports.devnetInfo = devnetInfo;
1735
+ const setting_1 = __nccwpck_require__(25546);
1736
+ const fork_1 = __nccwpck_require__(77912);
1737
+ const readiness_1 = __nccwpck_require__(19326);
1738
+ const logger_1 = __nccwpck_require__(65370);
1739
+ function devnetInfo() {
1740
+ return __awaiter(this, void 0, void 0, function* () {
1741
+ var _a, _b, _c;
1742
+ const settings = (0, setting_1.readSettings)();
1743
+ const fork = (0, fork_1.readForkState)(settings.devnet.configPath);
1744
+ const readiness = yield (0, readiness_1.checkNodeReadiness)(settings.devnet.rpcUrl);
1745
+ const indexerReady = readiness.indexerTip != null && readiness.indexerLag === BigInt(0);
1746
+ const networkIsolated = fork && readiness.peers != null ? readiness.peers === 0 : undefined;
1747
+ const result = {
1748
+ command: 'devnet.info',
1749
+ kind: fork ? `fork-of-${fork.source}` : 'pure-devnet',
1750
+ configPath: settings.devnet.configPath,
1751
+ rpcUrl: settings.devnet.rpcUrl,
1752
+ proxyUrl: `http://127.0.0.1:${settings.devnet.rpcProxyPort}`,
1753
+ ready: readiness.ready,
1754
+ nodeTip: (_a = readiness.nodeTip) === null || _a === void 0 ? void 0 : _a.toString(),
1755
+ indexerTip: (_b = readiness.indexerTip) === null || _b === void 0 ? void 0 : _b.toString(),
1756
+ indexerLag: (_c = readiness.indexerLag) === null || _c === void 0 ? void 0 : _c.toString(),
1757
+ indexerReady,
1758
+ peers: readiness.peers,
1759
+ networkIsolated,
1760
+ error: readiness.error,
1761
+ fork,
1762
+ };
1763
+ logger_1.logger.info(`Devnet: ${result.kind}`);
1764
+ logger_1.logger.info(`RPC: ${result.rpcUrl}`);
1765
+ logger_1.logger.info(`Proxy RPC: ${result.proxyUrl}`);
1766
+ logger_1.logger.info(`Node ready: ${result.ready ? 'yes' : 'no'}`);
1767
+ if (readiness.nodeTip != null)
1768
+ logger_1.logger.info(`Node tip: ${readiness.nodeTip}`);
1769
+ if (readiness.indexerTip != null)
1770
+ logger_1.logger.info(`Indexer tip: ${readiness.indexerTip}`);
1771
+ if (readiness.indexerLag != null) {
1772
+ const message = `Indexer lag: ${readiness.indexerLag}`;
1773
+ if (readiness.indexerLag > BigInt(0))
1774
+ logger_1.logger.warn(`${message}; indexed queries may be stale.`);
1775
+ else
1776
+ logger_1.logger.info(message);
1777
+ }
1778
+ logger_1.logger.info(`Indexer ready: ${indexerReady ? 'yes' : 'no'}`);
1779
+ if (readiness.peers != null)
1780
+ logger_1.logger.info(`Peers: ${readiness.peers}`);
1781
+ if (fork)
1782
+ logger_1.logger.info(`Public network isolated: ${networkIsolated == null ? 'unknown' : networkIsolated ? 'yes' : 'NO'}`);
1783
+ if (fork && readiness.peers != null && readiness.peers > 0) {
1784
+ logger_1.logger.warn('A forked devnet has connected peers. Stop it and inspect ckb.toml before signing or mining.');
1785
+ }
1786
+ if ((fork === null || fork === void 0 ? void 0 : fork.source) === 'mainnet') {
1787
+ logger_1.logger.warn('MAINNET FORK REPLAY RISK: only sign with built-in dev keys and fork-mined cells.');
1788
+ }
1789
+ if (readiness.error)
1790
+ logger_1.logger.warn(readiness.error);
1791
+ logger_1.logger.result(result);
1792
+ return result;
1572
1793
  });
1573
1794
  }
1574
1795
 
@@ -1634,16 +1855,23 @@ const path = __importStar(__nccwpck_require__(16928));
1634
1855
  const init_chain_1 = __nccwpck_require__(1360);
1635
1856
  const install_1 = __nccwpck_require__(65611);
1636
1857
  const setting_1 = __nccwpck_require__(25546);
1637
- const encoding_1 = __nccwpck_require__(6851);
1638
1858
  const rpc_proxy_1 = __nccwpck_require__(46589);
1639
1859
  const fork_1 = __nccwpck_require__(77912);
1640
1860
  const json_rpc_1 = __nccwpck_require__(51726);
1641
1861
  const base_1 = __nccwpck_require__(69951);
1642
1862
  const logger_1 = __nccwpck_require__(65370);
1863
+ const readiness_1 = __nccwpck_require__(19326);
1643
1864
  const DAEMON_LOG_DIR = 'logs';
1644
1865
  const DAEMON_LOG_FILE = 'daemon.log';
1645
1866
  const DAEMON_PID_FILE = 'daemon.pid';
1646
1867
  const DAEMON_CHILD_ENV = 'OFFCKB_DAEMON_CHILD';
1868
+ const NODE_READY_TIMEOUT_MS = 90000;
1869
+ const FORK_NODE_READY_TIMEOUT_MS = 10 * 60000;
1870
+ function cleanChildOutput(data) {
1871
+ // CKB colors its output even when it is redirected. Strip ANSI control
1872
+ // sequences so JSON logs stay machine-readable.
1873
+ return String(data).replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
1874
+ }
1647
1875
  function startNode({ version, network = base_1.Network.devnet, binaryPath, daemon }) {
1648
1876
  if (binaryPath && network !== base_1.Network.devnet) {
1649
1877
  logger_1.logger.warn('Custom binaryPath is only supported for devnet. The provided binaryPath will be ignored.');
@@ -1664,7 +1892,7 @@ function startNode({ version, network = base_1.Network.devnet, binaryPath, daemo
1664
1892
  }
1665
1893
  function nodeDevnet(_a) {
1666
1894
  return __awaiter(this, arguments, void 0, function* ({ version, binaryPath, daemon }) {
1667
- var _b, _c;
1895
+ var _b, _c, _d, _e, _f;
1668
1896
  if (daemon) {
1669
1897
  return startDaemon();
1670
1898
  }
@@ -1672,15 +1900,15 @@ function nodeDevnet(_a) {
1672
1900
  const ckbVersion = version || settings.bins.defaultCKBVersion;
1673
1901
  let ckbBinPath = '';
1674
1902
  if (binaryPath) {
1675
- ckbBinPath = (0, encoding_1.encodeBinPathForTerminal)(binaryPath);
1903
+ ckbBinPath = binaryPath;
1676
1904
  logger_1.logger.info(`Using custom CKB binary path: ${ckbBinPath}`);
1677
1905
  }
1678
1906
  else {
1679
1907
  yield (0, install_1.installCKBBinary)(ckbVersion);
1680
- ckbBinPath = (0, encoding_1.encodeBinPathForTerminal)((0, setting_1.getCKBBinaryPath)(ckbVersion));
1908
+ ckbBinPath = (0, setting_1.getCKBBinaryPath)(ckbVersion);
1681
1909
  }
1682
1910
  yield (0, init_chain_1.initChainIfNeeded)();
1683
- const devnetConfigPath = (0, encoding_1.encodeBinPathForTerminal)(settings.devnet.configPath);
1911
+ const devnetConfigPath = settings.devnet.configPath;
1684
1912
  // A forked devnet must boot once with --skip-spec-check --overwrite-spec so
1685
1913
  // the imported (and patched) spec replaces the source chain's stored spec.
1686
1914
  const forkState = (0, fork_1.readForkState)(settings.devnet.configPath);
@@ -1688,50 +1916,98 @@ function nodeDevnet(_a) {
1688
1916
  if (forkState === null || forkState === void 0 ? void 0 : forkState.firstRunPending) {
1689
1917
  logger_1.logger.info(`Forked devnet (${forkState.source}) detected, first run uses --skip-spec-check --overwrite-spec.`);
1690
1918
  }
1691
- const ckbCmd = `${ckbBinPath} run -C ${devnetConfigPath}${firstRunFlags}`;
1692
- const minerCmd = `${ckbBinPath} miner -C ${devnetConfigPath}`;
1693
1919
  logger_1.logger.info(`Launching CKB devnet Node...`);
1920
+ const runArgs = ['run', '-C', devnetConfigPath];
1921
+ if (firstRunFlags)
1922
+ runArgs.push('--skip-spec-check', '--overwrite-spec');
1923
+ const ckbProcess = (0, child_process_1.spawn)(ckbBinPath, runArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
1924
+ (_b = ckbProcess.stdout) === null || _b === void 0 ? void 0 : _b.on('data', (data) => logger_1.logger.info(['CKB:', cleanChildOutput(data)]));
1925
+ (_c = ckbProcess.stderr) === null || _c === void 0 ? void 0 : _c.on('data', (data) => logger_1.logger.error(['CKB error:', cleanChildOutput(data)]));
1926
+ let ckbExited = false;
1927
+ ckbProcess.once('exit', () => {
1928
+ ckbExited = true;
1929
+ });
1930
+ ckbProcess.once('error', () => {
1931
+ ckbExited = true;
1932
+ });
1933
+ const timeoutMs = forkState ? FORK_NODE_READY_TIMEOUT_MS : NODE_READY_TIMEOUT_MS;
1934
+ const readiness = yield (0, readiness_1.waitForNodeReady)(settings.devnet.rpcUrl, timeoutMs, () => !ckbExited);
1935
+ if (!readiness.ready) {
1936
+ if (!ckbExited)
1937
+ ckbProcess.kill('SIGTERM');
1938
+ throw new Error(`CKB devnet failed to become ready: ${(_d = readiness.error) !== null && _d !== void 0 ? _d : 'CKB process exited'}`);
1939
+ }
1940
+ if (ckbExited) {
1941
+ throw new Error('CKB devnet exited immediately after its readiness check.');
1942
+ }
1943
+ if (forkState === null || forkState === void 0 ? void 0 : forkState.firstRunPending) {
1944
+ yield clearForkFirstRunWhenNodeUp(ckbProcess, settings.devnet.rpcUrl, settings.devnet.configPath, forkState.genesisHash);
1945
+ }
1946
+ let minerProcess;
1694
1947
  try {
1695
- // Run first command
1696
- const ckbProcess = (0, child_process_1.exec)(ckbCmd);
1697
- // Log first command's output
1698
- (_b = ckbProcess.stdout) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
1699
- logger_1.logger.info(['CKB:', data.toString()]);
1700
- });
1701
- (_c = ckbProcess.stderr) === null || _c === void 0 ? void 0 : _c.on('data', (data) => {
1702
- logger_1.logger.error(['CKB error:', data.toString()]);
1703
- });
1704
- if (forkState === null || forkState === void 0 ? void 0 : forkState.firstRunPending) {
1705
- // Only clear the flag once the spawned node is actually up and reports
1706
- // the fork's genesis; if startup fails, the next run retries the flags.
1707
- void clearForkFirstRunWhenNodeUp(ckbProcess, settings.devnet.rpcUrl, settings.devnet.configPath, forkState.genesisHash);
1708
- }
1709
- // Start the second command after 3 seconds
1710
- setTimeout(() => __awaiter(this, void 0, void 0, function* () {
1711
- var _a, _b;
1712
- try {
1713
- // Run second command
1714
- const minerProcess = (0, child_process_1.exec)(minerCmd);
1715
- (_a = minerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
1716
- logger_1.logger.info(['CKB-Miner:', data.toString()]);
1717
- });
1718
- (_b = minerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
1719
- logger_1.logger.error(['CKB-Miner error:', data.toString()]);
1720
- });
1721
- // by default we start the proxy server
1722
- const ckbRpc = settings.devnet.rpcUrl;
1723
- const port = settings.devnet.rpcProxyPort;
1724
- const proxy = (0, rpc_proxy_1.createRPCProxy)(base_1.Network.devnet, ckbRpc, port);
1725
- proxy.start();
1726
- }
1727
- catch (error) {
1728
- logger_1.logger.error('Error running CKB-Miner:', error);
1729
- }
1730
- }), 3000);
1948
+ minerProcess = (0, child_process_1.spawn)(ckbBinPath, ['miner', '-C', devnetConfigPath], { stdio: ['ignore', 'pipe', 'pipe'] });
1731
1949
  }
1732
1950
  catch (error) {
1733
- logger_1.logger.error('Error:', error);
1951
+ ckbProcess.kill('SIGTERM');
1952
+ throw new Error(`CKB miner failed to start: ${error.message}`);
1734
1953
  }
1954
+ (_e = minerProcess.stdout) === null || _e === void 0 ? void 0 : _e.on('data', (data) => logger_1.logger.info(['CKB-Miner:', cleanChildOutput(data)]));
1955
+ (_f = minerProcess.stderr) === null || _f === void 0 ? void 0 : _f.on('data', (data) => logger_1.logger.error(['CKB-Miner error:', cleanChildOutput(data)]));
1956
+ try {
1957
+ yield waitForChildSpawn(minerProcess, 'CKB miner');
1958
+ }
1959
+ catch (error) {
1960
+ ckbProcess.kill('SIGTERM');
1961
+ throw error;
1962
+ }
1963
+ if (ckbExited) {
1964
+ if (!minerProcess.killed)
1965
+ minerProcess.kill('SIGTERM');
1966
+ throw new Error('CKB devnet exited while the miner was starting.');
1967
+ }
1968
+ const proxy = (0, rpc_proxy_1.createRPCProxy)(base_1.Network.devnet, settings.devnet.rpcUrl, settings.devnet.rpcProxyPort);
1969
+ proxy.start();
1970
+ logger_1.logger.success(`CKB devnet is ready at ${settings.devnet.rpcUrl}.`);
1971
+ logger_1.logger.result({
1972
+ command: 'node',
1973
+ network: base_1.Network.devnet,
1974
+ daemon: false,
1975
+ rpcUrl: settings.devnet.rpcUrl,
1976
+ proxyUrl: `http://127.0.0.1:${settings.devnet.rpcProxyPort}`,
1977
+ });
1978
+ // Treat CKB, miner and proxy as one service. A dead CKB must not leave a
1979
+ // healthy-looking proxy and a miner that retries forever.
1980
+ let serviceStopping = false;
1981
+ const stopService = (component, code, signal) => {
1982
+ if (serviceStopping)
1983
+ return;
1984
+ serviceStopping = true;
1985
+ if (component !== 'CKB node' && !ckbProcess.killed)
1986
+ ckbProcess.kill('SIGTERM');
1987
+ if (component !== 'CKB miner' && !minerProcess.killed)
1988
+ minerProcess.kill('SIGTERM');
1989
+ proxy.stop();
1990
+ if (process.env[DAEMON_CHILD_ENV] === '1')
1991
+ cleanupPidFile(resolveDaemonPaths().pidFile);
1992
+ logger_1.logger.error(`${component} exited unexpectedly (code=${code !== null && code !== void 0 ? code : 'null'}, signal=${signal !== null && signal !== void 0 ? signal : 'none'}).`);
1993
+ process.exitCode = typeof code === 'number' && code > 0 ? code : 1;
1994
+ };
1995
+ ckbProcess.once('exit', (code, signal) => stopService('CKB node', code, signal));
1996
+ minerProcess.once('exit', (code, signal) => stopService('CKB miner', code, signal));
1997
+ });
1998
+ }
1999
+ function waitForChildSpawn(child, label) {
2000
+ return new Promise((resolve, reject) => {
2001
+ const onSpawn = () => {
2002
+ child.removeListener('error', onError);
2003
+ resolve();
2004
+ };
2005
+ const onError = (error) => {
2006
+ child.removeListener('spawn', onSpawn);
2007
+ reject(new Error(`${label} failed to start: ${error.message}`));
2008
+ };
2009
+ child.once('spawn', onSpawn);
2010
+ child.once('error', onError);
1735
2011
  });
1736
2012
  }
1737
2013
  function resolveDaemonPaths() {
@@ -1765,7 +2041,10 @@ function clearForkFirstRunWhenNodeUp(ckbProcess, rpcUrl, configPath, expectedGen
1765
2041
  'leaving the first-run flags in place.');
1766
2042
  return;
1767
2043
  }
1768
- (0, fork_1.markForkFirstRunComplete)(configPath);
2044
+ // The miner has not started yet, so this tip is the exact boundary
2045
+ // between copied public-chain state and cells mined on the local fork.
2046
+ const forkBlockNumber = BigInt(String(yield (0, json_rpc_1.callJsonRpc)(rpcUrl, 'get_tip_block_number', [], 5000))).toString();
2047
+ (0, fork_1.markForkFirstRunComplete)(configPath, forkBlockNumber);
1769
2048
  logger_1.logger.success('Forked devnet is up; first-run spec flags cleared.');
1770
2049
  return;
1771
2050
  }
@@ -1807,6 +2086,7 @@ function readPidFile(pidFile) {
1807
2086
  pid,
1808
2087
  scriptPath: parsed.scriptPath,
1809
2088
  startedAt: (_b = parsed.startedAt) !== null && _b !== void 0 ? _b : new Date(0).toISOString(),
2089
+ status: parsed.status,
1810
2090
  };
1811
2091
  }
1812
2092
  }
@@ -1820,6 +2100,39 @@ function readPidFile(pidFile) {
1820
2100
  function writePidFile(pidFile, metadata) {
1821
2101
  fs.writeFileSync(pidFile, JSON.stringify(metadata, null, 2));
1822
2102
  }
2103
+ function reservePidFile(pidFile, scriptPath) {
2104
+ let fd;
2105
+ try {
2106
+ fd = fs.openSync(pidFile, 'wx');
2107
+ }
2108
+ catch (error) {
2109
+ const err = error;
2110
+ if (err.code === 'EEXIST') {
2111
+ throw new Error('A CKB devnet daemon startup is already in progress. Try again after it completes.');
2112
+ }
2113
+ throw new Error(`Failed to reserve daemon PID file ${pidFile}: ${err.message}`);
2114
+ }
2115
+ let writeError;
2116
+ try {
2117
+ const reservation = {
2118
+ pid: process.pid,
2119
+ scriptPath,
2120
+ startedAt: new Date().toISOString(),
2121
+ status: 'starting',
2122
+ };
2123
+ fs.writeFileSync(fd, JSON.stringify(reservation, null, 2));
2124
+ }
2125
+ catch (error) {
2126
+ writeError = error;
2127
+ }
2128
+ finally {
2129
+ fs.closeSync(fd);
2130
+ }
2131
+ if (writeError) {
2132
+ cleanupPidFile(pidFile);
2133
+ throw new Error(`Failed to initialize daemon PID reservation ${pidFile}: ${writeError.message}`);
2134
+ }
2135
+ }
1823
2136
  function resolveCliEntry() {
1824
2137
  var _a;
1825
2138
  // In priority order. process.argv[1] is the most reliable for a Node CLI.
@@ -1841,12 +2154,19 @@ function resolveCliEntry() {
1841
2154
  return null;
1842
2155
  }
1843
2156
  function isProcessAlive(pid) {
2157
+ if (!Number.isInteger(pid) || pid <= 0)
2158
+ return false;
1844
2159
  try {
1845
2160
  process.kill(pid, 0);
1846
2161
  return true;
1847
2162
  }
1848
- catch (_a) {
1849
- return false;
2163
+ catch (error) {
2164
+ const err = error;
2165
+ if (err.code === 'ESRCH')
2166
+ return false;
2167
+ if (err.code === 'EPERM')
2168
+ throw new Error(`Permission denied when checking daemon process ${pid}.`);
2169
+ throw error;
1850
2170
  }
1851
2171
  }
1852
2172
  function cleanupPidFile(pidFile) {
@@ -1859,10 +2179,16 @@ function cleanupPidFile(pidFile) {
1859
2179
  }
1860
2180
  function waitForProcessExit(pid, timeoutMs) {
1861
2181
  const start = Date.now();
1862
- return new Promise((resolve) => {
2182
+ return new Promise((resolve, reject) => {
1863
2183
  const check = () => {
1864
- if (!isProcessAlive(pid)) {
1865
- resolve(true);
2184
+ try {
2185
+ if (!isProcessAlive(pid)) {
2186
+ resolve(true);
2187
+ return;
2188
+ }
2189
+ }
2190
+ catch (error) {
2191
+ reject(error);
1866
2192
  return;
1867
2193
  }
1868
2194
  if (Date.now() - start >= timeoutMs) {
@@ -1942,73 +2268,159 @@ function terminateProcess(pid, signal) {
1942
2268
  }
1943
2269
  });
1944
2270
  }
2271
+ function failDaemonStartup(error, pid, pidFile) {
2272
+ return __awaiter(this, void 0, void 0, function* () {
2273
+ let exited = false;
2274
+ try {
2275
+ exited = !isProcessAlive(pid);
2276
+ if (!exited) {
2277
+ yield terminateProcess(pid, 'SIGTERM');
2278
+ exited = yield waitForProcessExit(pid, 5000);
2279
+ if (!exited) {
2280
+ yield terminateProcess(pid, 'SIGKILL');
2281
+ exited = yield waitForProcessExit(pid, 5000);
2282
+ }
2283
+ }
2284
+ }
2285
+ catch (_a) {
2286
+ // The child may have exited while cleanup signals were sent. If liveness
2287
+ // cannot be checked, preserve the PID file for a later explicit stop.
2288
+ try {
2289
+ exited = !isProcessAlive(pid);
2290
+ }
2291
+ catch (_b) {
2292
+ exited = false;
2293
+ }
2294
+ }
2295
+ if (exited) {
2296
+ cleanupPidFile(pidFile);
2297
+ }
2298
+ else {
2299
+ error.message += ` Process ${pid} is still running; PID file was preserved.`;
2300
+ }
2301
+ throw error;
2302
+ });
2303
+ }
1945
2304
  function startDaemon() {
1946
- const { logDir, logFile, pidFile } = resolveDaemonPaths();
1947
- // Prevent duplicate daemon starts. If a daemon is already running, refuse
1948
- // to overwrite its PID file.
1949
- const existing = readPidFile(pidFile);
1950
- if (existing && isProcessAlive(existing.pid)) {
1951
- logger_1.logger.error(`A CKB devnet daemon is already running (PID ${existing.pid}). Stop it first with: offckb node stop`);
1952
- return;
1953
- }
1954
- if (existing && !isProcessAlive(existing.pid)) {
1955
- // Stale PID file from a crashed daemon; clean it up before starting anew.
1956
- cleanupPidFile(pidFile);
1957
- }
1958
- let out;
1959
- let err;
1960
- try {
1961
- fs.mkdirSync(logDir, { recursive: true });
1962
- out = fs.openSync(logFile, 'a');
1963
- err = fs.openSync(logFile, 'a');
1964
- }
1965
- catch (error) {
1966
- logger_1.logger.error(`Failed to prepare daemon log directory or log file at ${logFile}:`, error);
1967
- return;
1968
- }
1969
- const scriptPath = resolveCliEntry();
1970
- if (!scriptPath) {
1971
- logger_1.logger.error('Unable to determine the CLI entry point for daemon mode. Set OFFCKB_CLI_PATH to the offckb script.');
1972
- closeFileDescriptors(out, err);
1973
- return;
1974
- }
1975
- const childArgs = process.argv.slice(2).filter((arg) => arg !== '--daemon');
1976
- const childEnv = Object.assign(Object.assign({}, process.env), { [DAEMON_CHILD_ENV]: '1' });
1977
- let child;
1978
- try {
1979
- child = (0, child_process_1.spawn)(process.execPath, [scriptPath, ...childArgs], {
1980
- detached: true,
1981
- stdio: ['ignore', out, err],
1982
- env: childEnv,
2305
+ return __awaiter(this, void 0, void 0, function* () {
2306
+ var _a;
2307
+ const { logDir, logFile, pidFile } = resolveDaemonPaths();
2308
+ try {
2309
+ fs.mkdirSync(logDir, { recursive: true });
2310
+ }
2311
+ catch (error) {
2312
+ throw new Error(`Failed to prepare daemon log directory at ${logDir}: ${error.message}`);
2313
+ }
2314
+ const settings = (0, setting_1.readSettings)();
2315
+ const activeNode = yield (0, readiness_1.checkNodeReadiness)(settings.devnet.rpcUrl, 1000);
2316
+ if (activeNode.ready) {
2317
+ throw new Error(`A CKB node is already answering at ${settings.devnet.rpcUrl}. Stop it before starting daemon mode.`);
2318
+ }
2319
+ // Prevent duplicate daemon starts. If a daemon is already running, refuse
2320
+ // to overwrite its PID file.
2321
+ const existing = readPidFile(pidFile);
2322
+ if (existing) {
2323
+ if (isProcessAlive(existing.pid)) {
2324
+ const identityOk = yield verifyDaemonIdentity(existing.pid, existing);
2325
+ if (identityOk) {
2326
+ if (existing.status === 'starting') {
2327
+ throw new Error(`Another CKB devnet daemon startup is already in progress (PID ${existing.pid}).`);
2328
+ }
2329
+ throw new Error(`A CKB devnet daemon is already running (PID ${existing.pid}). Stop it first with: offckb node stop`);
2330
+ }
2331
+ logger_1.logger.warn(`PID ${existing.pid} from ${pidFile} belongs to another process; removing stale daemon metadata without signaling it.`);
2332
+ }
2333
+ // Stale PID file from a crashed daemon; clean it up before atomically
2334
+ // reserving the same control file for this startup attempt.
2335
+ cleanupPidFile(pidFile);
2336
+ }
2337
+ const scriptPath = resolveCliEntry();
2338
+ if (!scriptPath) {
2339
+ throw new Error('Unable to determine the CLI entry point for daemon mode. Set OFFCKB_CLI_PATH to the offckb script.');
2340
+ }
2341
+ reservePidFile(pidFile, scriptPath);
2342
+ let out;
2343
+ let err;
2344
+ try {
2345
+ out = fs.openSync(logFile, 'a');
2346
+ err = fs.openSync(logFile, 'a');
2347
+ }
2348
+ catch (error) {
2349
+ closeFileDescriptors(out, err);
2350
+ cleanupPidFile(pidFile);
2351
+ throw new Error(`Failed to prepare daemon log directory or log file at ${logFile}: ${error.message}`);
2352
+ }
2353
+ const childArgs = process.argv.slice(2).filter((arg) => arg !== '--daemon');
2354
+ const childEnv = Object.assign(Object.assign({}, process.env), { [DAEMON_CHILD_ENV]: '1' });
2355
+ let child;
2356
+ try {
2357
+ child = (0, child_process_1.spawn)(process.execPath, [scriptPath, ...childArgs], {
2358
+ detached: true,
2359
+ stdio: ['ignore', out, err],
2360
+ env: childEnv,
2361
+ });
2362
+ }
2363
+ catch (error) {
2364
+ closeFileDescriptors(out, err);
2365
+ cleanupPidFile(pidFile);
2366
+ throw new Error(`Failed to spawn daemon process: ${error.message}`);
2367
+ }
2368
+ if (!child.pid) {
2369
+ closeFileDescriptors(out, err);
2370
+ cleanupPidFile(pidFile);
2371
+ throw new Error('Failed to spawn daemon process: no PID returned.');
2372
+ }
2373
+ child.unref();
2374
+ child.on('error', (error) => {
2375
+ logger_1.logger.error('Daemon child process failed to start:', error);
2376
+ cleanupPidFile(pidFile);
1983
2377
  });
1984
- }
1985
- catch (error) {
1986
- logger_1.logger.error('Failed to spawn daemon process:', error);
1987
- closeFileDescriptors(out, err);
1988
- return;
1989
- }
1990
- if (!child.pid) {
1991
- logger_1.logger.error('Failed to spawn daemon process: no PID returned.');
2378
+ const metadata = {
2379
+ pid: child.pid,
2380
+ scriptPath,
2381
+ startedAt: new Date().toISOString(),
2382
+ status: 'starting',
2383
+ };
2384
+ try {
2385
+ writePidFile(pidFile, metadata);
2386
+ }
2387
+ catch (error) {
2388
+ closeFileDescriptors(out, err);
2389
+ return failDaemonStartup(error, child.pid, pidFile);
2390
+ }
2391
+ // File descriptors are now owned by the spawned child; close our copies.
1992
2392
  closeFileDescriptors(out, err);
1993
- return;
1994
- }
1995
- child.unref();
1996
- child.on('error', (error) => {
1997
- logger_1.logger.error('Daemon child process failed to start:', error);
1998
- cleanupPidFile(pidFile);
2393
+ const proxyUrl = `http://127.0.0.1:${settings.devnet.rpcProxyPort}`;
2394
+ try {
2395
+ const forkState = (0, fork_1.readForkState)(settings.devnet.configPath);
2396
+ const timeoutMs = forkState ? FORK_NODE_READY_TIMEOUT_MS : NODE_READY_TIMEOUT_MS;
2397
+ // The proxy only starts after the child has a healthy CKB RPC and has
2398
+ // successfully spawned the miner, so this is the daemon's service-level
2399
+ // readiness check rather than a port/process check.
2400
+ const readiness = yield (0, readiness_1.waitForNodeReady)(proxyUrl, timeoutMs, () => isProcessAlive(child.pid));
2401
+ if (!readiness.ready) {
2402
+ throw new Error(`CKB devnet daemon failed to become ready. See ${logFile}. ${(_a = readiness.error) !== null && _a !== void 0 ? _a : 'Daemon process exited.'}`);
2403
+ }
2404
+ writePidFile(pidFile, Object.assign(Object.assign({}, metadata), { status: 'running' }));
2405
+ }
2406
+ catch (error) {
2407
+ return failDaemonStartup(error, child.pid, pidFile);
2408
+ }
2409
+ logger_1.logger.success(`CKB devnet daemon started with PID ${child.pid} and passed its RPC/proxy health check.`);
2410
+ logger_1.logger.info(`Logs: ${logFile}`);
2411
+ logger_1.logger.info(`PID file: ${pidFile}`);
2412
+ logger_1.logger.info('Stop the daemon with: offckb node stop');
2413
+ logger_1.logger.result({
2414
+ command: 'node',
2415
+ network: base_1.Network.devnet,
2416
+ daemon: true,
2417
+ pid: child.pid,
2418
+ rpcUrl: settings.devnet.rpcUrl,
2419
+ proxyUrl,
2420
+ logFile,
2421
+ pidFile,
2422
+ });
1999
2423
  });
2000
- const metadata = {
2001
- pid: child.pid,
2002
- scriptPath,
2003
- startedAt: new Date().toISOString(),
2004
- };
2005
- writePidFile(pidFile, metadata);
2006
- // File descriptors are now owned by the spawned child; close our copies.
2007
- closeFileDescriptors(out, err);
2008
- logger_1.logger.success(`CKB devnet daemon started with PID ${child.pid}.`);
2009
- logger_1.logger.info(`Logs: ${logFile}`);
2010
- logger_1.logger.info(`PID file: ${pidFile}`);
2011
- logger_1.logger.info('Stop the daemon with: offckb node stop');
2012
2424
  }
2013
2425
  function closeFileDescriptors(...fds) {
2014
2426
  for (const fd of fds) {
@@ -2028,24 +2440,28 @@ function stopNode() {
2028
2440
  const metadata = readPidFile(pidFile);
2029
2441
  if (!metadata) {
2030
2442
  logger_1.logger.warn(`No daemon PID file found at ${pidFile}. Is the devnet daemon running?`);
2443
+ logger_1.logger.result({ command: 'node.stop', stopped: false, reason: 'not-running' });
2031
2444
  return;
2032
2445
  }
2033
2446
  const pid = metadata.pid;
2034
2447
  if (!Number.isInteger(pid) || pid <= 0) {
2035
- logger_1.logger.error(`Invalid PID in ${pidFile}: ${pid}`);
2036
2448
  cleanupPidFile(pidFile);
2037
- return;
2449
+ throw new Error(`Invalid PID in ${pidFile}: ${pid}`);
2038
2450
  }
2039
- if (!isProcessAlive(pid)) {
2451
+ const processAlive = isProcessAlive(pid);
2452
+ if (!processAlive) {
2040
2453
  logger_1.logger.warn(`Daemon process ${pid} is not running.`);
2041
2454
  cleanupPidFile(pidFile);
2455
+ logger_1.logger.result({ command: 'node.stop', stopped: false, reason: 'stale-pid', pid });
2042
2456
  return;
2043
2457
  }
2458
+ if (metadata.status === 'starting') {
2459
+ throw new Error(`CKB devnet daemon startup is still in progress (PID ${pid}). Try stopping it again shortly.`);
2460
+ }
2044
2461
  const identityOk = yield verifyDaemonIdentity(pid, metadata);
2045
2462
  if (!identityOk) {
2046
- logger_1.logger.error(`Process ${pid} does not appear to be the offckb daemon. Refusing to send signals to avoid killing an unrelated process. ` +
2463
+ throw new Error(`Process ${pid} does not appear to be the offckb daemon. Refusing to send signals to avoid killing an unrelated process. ` +
2047
2464
  `If you are sure this is the daemon, stop it manually and remove ${pidFile}.`);
2048
- return;
2049
2465
  }
2050
2466
  logger_1.logger.info(`Stopping CKB devnet daemon (PID ${pid})...`);
2051
2467
  try {
@@ -2056,17 +2472,13 @@ function stopNode() {
2056
2472
  if (err.code === 'ESRCH') {
2057
2473
  logger_1.logger.warn(`Daemon process ${pid} is not running.`);
2058
2474
  cleanupPidFile(pidFile);
2475
+ logger_1.logger.result({ command: 'node.stop', stopped: false, reason: 'already-exited', pid });
2059
2476
  return;
2060
2477
  }
2061
2478
  if (err.code === 'EPERM') {
2062
- logger_1.logger.error(`Permission denied when sending SIGTERM to daemon process ${pid}.`);
2479
+ throw new Error(`Permission denied when sending SIGTERM to daemon process ${pid}.`);
2063
2480
  }
2064
- else {
2065
- logger_1.logger.error(`Failed to send SIGTERM to daemon process ${pid}:`, error);
2066
- }
2067
- // Still try to clean up the PID file so the user can recover.
2068
- cleanupPidFile(pidFile);
2069
- return;
2481
+ throw new Error(`Failed to send SIGTERM to daemon process ${pid}: ${err.message}`);
2070
2482
  }
2071
2483
  const exited = yield waitForProcessExit(pid, 5000);
2072
2484
  if (!exited) {
@@ -2075,11 +2487,12 @@ function stopNode() {
2075
2487
  yield terminateProcess(pid, 'SIGKILL');
2076
2488
  }
2077
2489
  catch (error) {
2078
- logger_1.logger.error(`Failed to send SIGKILL to daemon process ${pid}:`, error);
2490
+ throw new Error(`Failed to send SIGKILL to daemon process ${pid}: ${error.message}`);
2079
2491
  }
2080
2492
  }
2081
2493
  cleanupPidFile(pidFile);
2082
2494
  logger_1.logger.success('CKB devnet daemon stopped.');
2495
+ logger_1.logger.result({ command: 'node.stop', stopped: true, pid });
2083
2496
  });
2084
2497
  }
2085
2498
  function nodeTestnet() {
@@ -2113,39 +2526,6 @@ function nodeMainnet() {
2113
2526
 
2114
2527
  "use strict";
2115
2528
 
2116
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
2117
- if (k2 === undefined) k2 = k;
2118
- var desc = Object.getOwnPropertyDescriptor(m, k);
2119
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
2120
- desc = { enumerable: true, get: function() { return m[k]; } };
2121
- }
2122
- Object.defineProperty(o, k2, desc);
2123
- }) : (function(o, m, k, k2) {
2124
- if (k2 === undefined) k2 = k;
2125
- o[k2] = m[k];
2126
- }));
2127
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
2128
- Object.defineProperty(o, "default", { enumerable: true, value: v });
2129
- }) : function(o, v) {
2130
- o["default"] = v;
2131
- });
2132
- var __importStar = (this && this.__importStar) || (function () {
2133
- var ownKeys = function(o) {
2134
- ownKeys = Object.getOwnPropertyNames || function (o) {
2135
- var ar = [];
2136
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
2137
- return ar;
2138
- };
2139
- return ownKeys(o);
2140
- };
2141
- return function (mod) {
2142
- if (mod && mod.__esModule) return mod;
2143
- var result = {};
2144
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
2145
- __setModuleDefault(result, mod);
2146
- return result;
2147
- };
2148
- })();
2149
2529
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2150
2530
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
2151
2531
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -2160,8 +2540,7 @@ exports.status = status;
2160
2540
  const setting_1 = __nccwpck_require__(25546);
2161
2541
  const ckb_tui_1 = __nccwpck_require__(7020);
2162
2542
  const base_1 = __nccwpck_require__(69951);
2163
- const logger_1 = __nccwpck_require__(65370);
2164
- const net = __importStar(__nccwpck_require__(69278));
2543
+ const readiness_1 = __nccwpck_require__(19326);
2165
2544
  const NETWORK_SETTINGS_KEY = {
2166
2545
  [base_1.Network.devnet]: 'devnet',
2167
2546
  [base_1.Network.testnet]: 'testnet',
@@ -2169,65 +2548,27 @@ const NETWORK_SETTINGS_KEY = {
2169
2548
  };
2170
2549
  function status(_a) {
2171
2550
  return __awaiter(this, arguments, void 0, function* ({ network }) {
2172
- var _b;
2551
+ var _b, _c;
2173
2552
  // ckb-tui is an interactive terminal UI. Running it without a TTY
2174
2553
  // (pipe, redirect, CI) would hang or produce garbage output.
2175
2554
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
2176
- logger_1.logger.error('The status command requires an interactive terminal (TTY). ' +
2177
- 'It cannot be used in pipes, redirects, or non-interactive environments like CI.');
2178
- process.exit(1);
2555
+ throw new Error('The status command requires an interactive terminal (TTY). It cannot be used in pipes, redirects, or CI.');
2179
2556
  }
2180
2557
  const settings = (0, setting_1.readSettings)();
2181
2558
  const networkKey = NETWORK_SETTINGS_KEY[network];
2182
2559
  const port = settings[networkKey].rpcProxyPort;
2183
2560
  const url = `http://127.0.0.1:${port}`;
2184
- const isListening = yield isRPCPortListening(port);
2185
- if (!isListening) {
2186
- logger_1.logger.error(`RPC port ${port} is not listening. Please make sure the ${network} node is running and Proxy RPC is enabled.`);
2187
- return;
2561
+ const readiness = yield (0, readiness_1.checkNodeReadiness)(url);
2562
+ if (!readiness.ready) {
2563
+ throw new Error(`RPC proxy ${url} is not connected to a healthy ${network} node: ${(_b = readiness.error) !== null && _b !== void 0 ? _b : 'health check failed'}`);
2188
2564
  }
2189
2565
  const result = ckb_tui_1.CKBTui.run(['-r', url]);
2190
2566
  // Propagate ckb-tui exit code so scripts can detect TUI failure
2191
2567
  if (result.status !== 0) {
2192
- process.exitCode = (_b = result.status) !== null && _b !== void 0 ? _b : 1;
2568
+ throw new Error(`ckb-tui exited with code ${(_c = result.status) !== null && _c !== void 0 ? _c : 'unknown'}`);
2193
2569
  }
2194
2570
  });
2195
2571
  }
2196
- function isRPCPortListening(port) {
2197
- return __awaiter(this, void 0, void 0, function* () {
2198
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
2199
- return false;
2200
- }
2201
- const client = new net.Socket();
2202
- return new Promise((resolve) => {
2203
- let settled = false;
2204
- const TIMEOUT_MS = 2000;
2205
- const timeout = setTimeout(() => {
2206
- if (!settled) {
2207
- settled = true;
2208
- client.destroy();
2209
- resolve(false);
2210
- }
2211
- }, TIMEOUT_MS);
2212
- client.once('error', () => {
2213
- if (!settled) {
2214
- settled = true;
2215
- clearTimeout(timeout);
2216
- resolve(false);
2217
- }
2218
- });
2219
- client.once('connect', () => {
2220
- if (!settled) {
2221
- settled = true;
2222
- clearTimeout(timeout);
2223
- client.end();
2224
- resolve(true);
2225
- }
2226
- });
2227
- client.connect(port, '127.0.0.1');
2228
- });
2229
- });
2230
- }
2231
2572
 
2232
2573
 
2233
2574
  /***/ }),
@@ -2460,14 +2801,16 @@ const base_1 = __nccwpck_require__(69951);
2460
2801
  const link_1 = __nccwpck_require__(27814);
2461
2802
  const validator_1 = __nccwpck_require__(39534);
2462
2803
  const logger_1 = __nccwpck_require__(65370);
2804
+ const private_key_1 = __nccwpck_require__(26241);
2805
+ const readiness_1 = __nccwpck_require__(19326);
2806
+ const fork_safety_1 = __nccwpck_require__(47625);
2463
2807
  function transferAll(toAddress_1) {
2464
2808
  return __awaiter(this, arguments, void 0, function* (toAddress, opt = { network: base_1.Network.devnet }) {
2465
2809
  const network = opt.network;
2466
2810
  (0, validator_1.validateNetworkOpt)(network);
2467
- if (opt.privkey == null) {
2468
- throw new Error('--privkey is required!');
2469
- }
2470
- const privateKey = opt.privkey;
2811
+ const privateKey = (0, private_key_1.resolvePrivateKey)(opt);
2812
+ (0, fork_safety_1.warnIfMainnetForkSigning)(network, privateKey);
2813
+ yield (0, readiness_1.warnIfForkIndexerIsBehind)(network);
2471
2814
  const ckb = new ckb_1.CKB({ network });
2472
2815
  const txHash = yield ckb.transferAll({
2473
2816
  toAddress,
@@ -2475,9 +2818,12 @@ function transferAll(toAddress_1) {
2475
2818
  });
2476
2819
  if (network === 'testnet') {
2477
2820
  logger_1.logger.info(`Successfully transfer, check ${(0, link_1.buildTestnetTxLink)(txHash)} for details.`);
2478
- return;
2821
+ logger_1.logger.result({ command: 'transfer-all', network, toAddress, txHash });
2822
+ return txHash;
2479
2823
  }
2480
2824
  logger_1.logger.info('Successfully transfer, txHash:', txHash);
2825
+ logger_1.logger.result({ command: 'transfer-all', network, toAddress, txHash });
2826
+ return txHash;
2481
2827
  });
2482
2828
  }
2483
2829
 
@@ -2504,37 +2850,61 @@ const ckb_1 = __nccwpck_require__(83748);
2504
2850
  const base_1 = __nccwpck_require__(69951);
2505
2851
  const link_1 = __nccwpck_require__(27814);
2506
2852
  const validator_1 = __nccwpck_require__(39534);
2853
+ const private_key_1 = __nccwpck_require__(26241);
2854
+ const logger_1 = __nccwpck_require__(65370);
2855
+ const readiness_1 = __nccwpck_require__(19326);
2856
+ const fork_safety_1 = __nccwpck_require__(47625);
2507
2857
  function transfer(toAddress_1, amount_1) {
2508
2858
  return __awaiter(this, arguments, void 0, function* (toAddress, amount, opt = { network: base_1.Network.devnet }) {
2509
2859
  var _a;
2510
2860
  const network = opt.network;
2511
2861
  (0, validator_1.validateNetworkOpt)(network);
2512
- if (opt.privkey == null) {
2513
- throw new Error('--privkey is required!');
2514
- }
2515
- const privateKey = opt.privkey;
2862
+ let udtKind;
2863
+ let udtTypeArgs;
2864
+ if (opt.udtKind != null || opt.udtTypeArgs != null) {
2865
+ if (!opt.udtTypeArgs) {
2866
+ throw new Error('UDT type args are required for a UDT transfer');
2867
+ }
2868
+ (0, validator_1.validateUdtAmount)(amount);
2869
+ udtKind = (_a = opt.udtKind) !== null && _a !== void 0 ? _a : 'sudt';
2870
+ (0, validator_1.validateUdtKind)(udtKind);
2871
+ udtTypeArgs = (0, validator_1.validateUdtTypeArgs)(udtKind, opt.udtTypeArgs);
2872
+ }
2873
+ const privateKey = (0, private_key_1.resolvePrivateKey)(opt);
2874
+ const rejectInputsAtOrBeforeBlock = (0, fork_safety_1.validateMainnetForkSigning)(network, privateKey, opt.allowMainnetReplayRisk);
2875
+ yield (0, readiness_1.warnIfForkIndexerIsBehind)(network);
2516
2876
  const ckb = new ckb_1.CKB({ network });
2517
- if (opt.udtTypeArgs) {
2518
- const kind = (_a = opt.udtKind) !== null && _a !== void 0 ? _a : 'sudt';
2519
- (0, validator_1.validateUdtKind)(kind);
2520
- const udtTypeArgs = (0, validator_1.validateUdtTypeArgs)(kind, opt.udtTypeArgs);
2521
- const udtType = yield ckb.buildUdtTypeScript(kind, udtTypeArgs);
2877
+ if (udtKind && udtTypeArgs) {
2878
+ const udtType = yield ckb.buildUdtTypeScript(udtKind, udtTypeArgs);
2522
2879
  const txHash = yield ckb.udtTransfer({
2523
2880
  toAddress,
2524
2881
  amount,
2525
2882
  privateKey,
2526
2883
  udtType,
2527
- kind,
2884
+ kind: udtKind,
2885
+ rejectInputsAtOrBeforeBlock,
2528
2886
  });
2529
2887
  (0, link_1.logTxSuccess)(network, txHash, 'transfer UDT');
2530
- return;
2888
+ logger_1.logger.result({
2889
+ command: 'udt.transfer',
2890
+ network,
2891
+ kind: udtKind,
2892
+ amount,
2893
+ typeArgs: udtTypeArgs,
2894
+ toAddress,
2895
+ txHash,
2896
+ });
2897
+ return txHash;
2531
2898
  }
2532
2899
  const txHash = yield ckb.transfer({
2533
2900
  toAddress,
2534
2901
  amountInCKB: amount,
2535
2902
  privateKey,
2903
+ rejectInputsAtOrBeforeBlock,
2536
2904
  });
2537
2905
  (0, link_1.logTxSuccess)(network, txHash, 'transfer');
2906
+ logger_1.logger.result({ command: 'transfer', network, amount, toAddress, txHash });
2907
+ return txHash;
2538
2908
  });
2539
2909
  }
2540
2910
 
@@ -2562,41 +2932,65 @@ const ckb_1 = __nccwpck_require__(83748);
2562
2932
  const base_1 = __nccwpck_require__(69951);
2563
2933
  const link_1 = __nccwpck_require__(27814);
2564
2934
  const validator_1 = __nccwpck_require__(39534);
2935
+ const private_key_1 = __nccwpck_require__(26241);
2936
+ const logger_1 = __nccwpck_require__(65370);
2937
+ const readiness_1 = __nccwpck_require__(19326);
2938
+ const fork_safety_1 = __nccwpck_require__(47625);
2565
2939
  function udtIssue(amount_1) {
2566
- return __awaiter(this, arguments, void 0, function* (amount, opt = { network: base_1.Network.devnet, udtKind: 'sudt', privkey: '' }) {
2940
+ return __awaiter(this, arguments, void 0, function* (amount, opt = { network: base_1.Network.devnet, udtKind: 'sudt' }) {
2567
2941
  const network = opt.network;
2568
2942
  (0, validator_1.validateNetworkOpt)(network);
2569
2943
  (0, validator_1.validateUdtKind)(opt.udtKind);
2570
- if (!opt.privkey) {
2571
- throw new Error('--privkey is required!');
2572
- }
2944
+ (0, validator_1.validateUdtAmount)(amount);
2945
+ const typeArgs = opt.typeArgs ? (0, validator_1.validateUdtTypeArgs)(opt.udtKind, opt.typeArgs) : undefined;
2946
+ const privateKey = (0, private_key_1.resolvePrivateKey)(opt);
2947
+ (0, fork_safety_1.warnIfMainnetForkSigning)(network, privateKey);
2948
+ yield (0, readiness_1.warnIfForkIndexerIsBehind)(network);
2573
2949
  const ckb = new ckb_1.CKB({ network });
2574
- const txHash = yield ckb.udtIssue({
2575
- privateKey: opt.privkey,
2950
+ const result = yield ckb.udtIssue({
2951
+ privateKey,
2576
2952
  kind: opt.udtKind,
2577
2953
  amount,
2578
- typeArgs: opt.typeArgs ? (0, validator_1.validateUdtTypeArgs)(opt.udtKind, opt.typeArgs) : undefined,
2954
+ typeArgs,
2579
2955
  toAddress: opt.to,
2580
2956
  });
2581
- (0, link_1.logTxSuccess)(network, txHash, 'issued UDT');
2957
+ (0, link_1.logTxSuccess)(network, result.txHash, 'issued UDT');
2958
+ logger_1.logger.info(`UDT kind: ${opt.udtKind}`);
2959
+ logger_1.logger.info(`UDT type args: ${result.typeArgs}`);
2960
+ logger_1.logger.info(`Receiver: ${result.receiver}`);
2961
+ logger_1.logger.info(`Next: offckb balance ${result.receiver} --udt-kind ${opt.udtKind} --udt-type-args ${result.typeArgs}`);
2962
+ logger_1.logger.result({
2963
+ command: 'udt.issue',
2964
+ network,
2965
+ kind: opt.udtKind,
2966
+ amount,
2967
+ receiver: result.receiver,
2968
+ typeArgs: result.typeArgs,
2969
+ txHash: result.txHash,
2970
+ });
2971
+ return result;
2582
2972
  });
2583
2973
  }
2584
2974
  function udtDestroy(amount_1) {
2585
- return __awaiter(this, arguments, void 0, function* (amount, opt = { network: base_1.Network.devnet, udtKind: 'sudt', typeArgs: '', privkey: '' }) {
2975
+ return __awaiter(this, arguments, void 0, function* (amount, opt = { network: base_1.Network.devnet, udtKind: 'sudt', typeArgs: '' }) {
2586
2976
  const network = opt.network;
2587
2977
  (0, validator_1.validateNetworkOpt)(network);
2588
2978
  (0, validator_1.validateUdtKind)(opt.udtKind);
2589
- if (!opt.privkey) {
2590
- throw new Error('--privkey is required!');
2591
- }
2979
+ (0, validator_1.validateUdtAmount)(amount);
2980
+ const typeArgs = (0, validator_1.validateUdtTypeArgs)(opt.udtKind, opt.typeArgs);
2981
+ const privateKey = (0, private_key_1.resolvePrivateKey)(opt);
2982
+ (0, fork_safety_1.warnIfMainnetForkSigning)(network, privateKey);
2983
+ yield (0, readiness_1.warnIfForkIndexerIsBehind)(network);
2592
2984
  const ckb = new ckb_1.CKB({ network });
2593
2985
  const txHash = yield ckb.udtDestroy({
2594
- privateKey: opt.privkey,
2986
+ privateKey,
2595
2987
  kind: opt.udtKind,
2596
2988
  amount,
2597
- typeArgs: (0, validator_1.validateUdtTypeArgs)(opt.udtKind, opt.typeArgs),
2989
+ typeArgs,
2598
2990
  });
2599
2991
  (0, link_1.logTxSuccess)(network, txHash, 'destroyed UDT');
2992
+ logger_1.logger.result({ command: 'udt.destroy', network, kind: opt.udtKind, amount, typeArgs, txHash });
2993
+ return txHash;
2600
2994
  });
2601
2995
  }
2602
2996
 
@@ -3598,9 +3992,13 @@ exports.parseGenesisHashFromInitOutput = parseGenesisHashFromInitOutput;
3598
3992
  exports.parseGenesisHashFromListHashes = parseGenesisHashFromListHashes;
3599
3993
  exports.expectedGenesisHash = expectedGenesisHash;
3600
3994
  exports.patchDevSpecForFork = patchDevSpecForFork;
3995
+ exports.copySourceData = copySourceData;
3996
+ exports.isolateForkCkbConfig = isolateForkCkbConfig;
3997
+ exports.migrationNeededFromExitCode = migrationNeededFromExitCode;
3601
3998
  exports.forkDevnet = forkDevnet;
3602
3999
  const child_process_1 = __nccwpck_require__(35317);
3603
4000
  const fs_1 = __importDefault(__nccwpck_require__(79896));
4001
+ const os_1 = __importDefault(__nccwpck_require__(70857));
3604
4002
  const path_1 = __importDefault(__nccwpck_require__(16928));
3605
4003
  const toml_1 = __importDefault(__nccwpck_require__(2811));
3606
4004
  const setting_1 = __nccwpck_require__(25546);
@@ -3644,11 +4042,11 @@ function writeForkState(configPath, state) {
3644
4042
  fs_1.default.writeFileSync(tempPath, JSON.stringify(state, null, 2));
3645
4043
  fs_1.default.renameSync(tempPath, statePath);
3646
4044
  }
3647
- function markForkFirstRunComplete(configPath) {
4045
+ function markForkFirstRunComplete(configPath, forkBlockNumber) {
3648
4046
  const state = readForkState(configPath);
3649
4047
  if (!state || !state.firstRunPending)
3650
4048
  return;
3651
- writeForkState(configPath, Object.assign(Object.assign({}, state), { firstRunPending: false }));
4049
+ writeForkState(configPath, Object.assign(Object.assign(Object.assign({}, state), { firstRunPending: false }), (forkBlockNumber == null ? {} : { forkBlockNumber })));
3652
4050
  }
3653
4051
  function detectSourceFromCkbToml(ckbTomlContent) {
3654
4052
  let parsed;
@@ -3823,7 +4221,28 @@ function copySourceData(sourceDir, configPath) {
3823
4221
  fs_1.default.mkdirSync(configPath, { recursive: true });
3824
4222
  // Full copy on purpose: never hardlink — RocksDB appends to WAL/MANIFEST in
3825
4223
  // place, and linked files would corrupt the source chain.
3826
- fs_1.default.cpSync(sourceData, targetData, { recursive: true });
4224
+ const excludedTopLevelEntries = new Set(['network', 'logs', 'tmp']);
4225
+ fs_1.default.mkdirSync(targetData, { recursive: true });
4226
+ // Enumerate top-level entries instead of relying on fs.cp's filter paths,
4227
+ // which may use Windows extended-length prefixes and bypass relative-path
4228
+ // comparisons.
4229
+ for (const entry of fs_1.default.readdirSync(sourceData)) {
4230
+ if (excludedTopLevelEntries.has(entry))
4231
+ continue;
4232
+ fs_1.default.cpSync(path_1.default.join(sourceData, entry), path_1.default.join(targetData, entry), { recursive: true });
4233
+ }
4234
+ logger_1.logger.info('Excluded source network peers and transient logs/tmp data from the fork.');
4235
+ }
4236
+ function isolateForkCkbConfig(config) {
4237
+ var _a, _b;
4238
+ const network = Object.assign({}, ((_a = config.network) !== null && _a !== void 0 ? _a : {}));
4239
+ network.bootnodes = [];
4240
+ network.max_outbound_peers = 0;
4241
+ network.whitelist_only = true;
4242
+ network.discovery_local_address = false;
4243
+ const loggerConfig = Object.assign({}, ((_b = config.logger) !== null && _b !== void 0 ? _b : {}));
4244
+ loggerConfig.filter = 'warn';
4245
+ return Object.assign(Object.assign({}, config), { network, logger: loggerConfig });
3827
4246
  }
3828
4247
  function runCkbInit(ckbBinPath, configPath, specFile) {
3829
4248
  // argv form, never a shell: --spec-file is user input and quote-wrapping
@@ -3831,7 +4250,48 @@ function runCkbInit(ckbBinPath, configPath, specFile) {
3831
4250
  // inside double quotes).
3832
4251
  const args = ['init', '-C', configPath, '--chain', 'dev', '--import-spec', specFile, '--force'];
3833
4252
  logger_1.logger.debug(`Running: ${ckbBinPath} ${args.join(' ')}`);
3834
- return (0, child_process_1.execFileSync)(ckbBinPath, args, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
4253
+ return (0, child_process_1.execFileSync)(ckbBinPath, args, {
4254
+ encoding: 'utf8',
4255
+ maxBuffer: 16 * 1024 * 1024,
4256
+ stdio: ['ignore', 'pipe', 'pipe'],
4257
+ });
4258
+ }
4259
+ function migrationNeededFromExitCode(status) {
4260
+ if (status === 0)
4261
+ return true;
4262
+ if (status === 64)
4263
+ return false;
4264
+ throw new Error(`ckb migrate --check failed with exit code ${status !== null && status !== void 0 ? status : 'unknown'}`);
4265
+ }
4266
+ function isDatabaseMigrationNeeded(ckbBinPath, configPath) {
4267
+ const result = (0, child_process_1.spawnSync)(ckbBinPath, ['migrate', '--check', '-C', configPath], {
4268
+ encoding: 'utf8',
4269
+ stdio: ['ignore', 'pipe', 'pipe'],
4270
+ });
4271
+ if (result.error) {
4272
+ throw new Error(`Could not check the CKB database version: ${result.error.message}`);
4273
+ }
4274
+ try {
4275
+ return migrationNeededFromExitCode(result.status);
4276
+ }
4277
+ catch (error) {
4278
+ const details = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
4279
+ throw new Error(`${error.message}${details ? `: ${details}` : ''}`);
4280
+ }
4281
+ }
4282
+ function migrateDatabaseCopy(ckbBinPath, configPath) {
4283
+ var _a, _b;
4284
+ logger_1.logger.info('Migrating the copied database (the source directory remains untouched) ..');
4285
+ const result = (0, child_process_1.spawnSync)(ckbBinPath, ['migrate', '--force', '-C', configPath], {
4286
+ encoding: 'utf8',
4287
+ stdio: ['ignore', 'pipe', 'pipe'],
4288
+ });
4289
+ if (result.error || result.status !== 0) {
4290
+ const details = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
4291
+ throw new Error(`Failed to migrate the copied database: ${(_b = (_a = result.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : `exit code ${result.status}`}` +
4292
+ `${details ? `\n${details}` : ''}`);
4293
+ }
4294
+ logger_1.logger.success('Copied database migration completed.');
3835
4295
  }
3836
4296
  // Best-effort read of the source directory's own chain identity. `ckb
3837
4297
  // list-hashes` resolves the chain spec of the given config dir (no database
@@ -3850,6 +4310,34 @@ function readSourceGenesisHash(ckbBinPath, sourceDir) {
3850
4310
  return null;
3851
4311
  }
3852
4312
  }
4313
+ function validateForkSpec(ckbBinPath, sourceDir, specFile, declaredSource) {
4314
+ var _a;
4315
+ const tempConfigPath = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'offckb-fork-preflight-'));
4316
+ try {
4317
+ const initOutput = runCkbInit(ckbBinPath, tempConfigPath, specFile);
4318
+ const genesisHash = parseGenesisHashFromInitOutput(initOutput);
4319
+ if (!genesisHash) {
4320
+ throw new Error(`Could not parse the genesis hash from ckb init output:\n${initOutput}`);
4321
+ }
4322
+ const source = (_a = declaredSource !== null && declaredSource !== void 0 ? declaredSource : (0, const_1.identifyPublicChainByGenesisHash)(genesisHash)) !== null && _a !== void 0 ? _a : 'custom';
4323
+ if (source !== 'custom') {
4324
+ const expected = expectedGenesisHash(source);
4325
+ if (genesisHash !== expected) {
4326
+ throw new Error(`Genesis hash mismatch: expected ${expected} for ${source}, got ${genesisHash}. ` +
4327
+ 'This usually means the chain spec does not match the source data.');
4328
+ }
4329
+ }
4330
+ const sourceGenesisHash = readSourceGenesisHash(ckbBinPath, sourceDir);
4331
+ if (sourceGenesisHash && sourceGenesisHash !== genesisHash) {
4332
+ throw new Error(`The source directory is configured for a different chain (genesis ${sourceGenesisHash}) ` +
4333
+ `than the imported spec (genesis ${genesisHash}). Pass a matching --source or --spec-file.`);
4334
+ }
4335
+ return { source, genesisHash };
4336
+ }
4337
+ finally {
4338
+ fs_1.default.rmSync(tempConfigPath, { recursive: true, force: true });
4339
+ }
4340
+ }
3853
4341
  // Overwrite the ckb-init-generated configs with offckb's own devnet configs so
3854
4342
  // the fork behaves like a normal offckb devnet (miner account as block
3855
4343
  // assembler, RPC on 8114 with the Indexer module, proxy on 28114). The
@@ -3857,29 +4345,26 @@ function readSourceGenesisHash(ckbBinPath, sourceDir) {
3857
4345
  function alignConfigsWithOffckb(configPath) {
3858
4346
  const settings = (0, setting_1.readSettings)();
3859
4347
  const devnetSourcePath = path_1.default.resolve(setting_1.packageRootPath, './ckb/devnet');
3860
- fs_1.default.copyFileSync(path_1.default.join(devnetSourcePath, 'ckb.toml'), path_1.default.join(configPath, 'ckb.toml'));
4348
+ const ckbConfigPath = path_1.default.join(configPath, 'ckb.toml');
4349
+ const parsedCkbConfig = toml_1.default.parse(fs_1.default.readFileSync(path_1.default.join(devnetSourcePath, 'ckb.toml'), 'utf8'));
4350
+ const ckbConfig = isolateForkCkbConfig(parsedCkbConfig);
4351
+ fs_1.default.writeFileSync(ckbConfigPath, toml_1.default.stringify(ckbConfig));
3861
4352
  fs_1.default.copyFileSync(path_1.default.join(devnetSourcePath, 'default.db-options'), path_1.default.join(configPath, 'default.db-options'));
3862
4353
  const minerToml = fs_1.default.readFileSync(path_1.default.join(devnetSourcePath, 'ckb-miner.toml'), 'utf8');
3863
4354
  fs_1.default.writeFileSync(path_1.default.join(configPath, 'ckb-miner.toml'), minerToml.replace('http://ckb:8114/', settings.devnet.rpcUrl));
3864
4355
  }
3865
4356
  function forkDevnet(options) {
3866
4357
  return __awaiter(this, void 0, void 0, function* () {
3867
- var _a, _b;
4358
+ var _a;
4359
+ if (!options.from) {
4360
+ throw new Error('Database fork requires a source CKB directory: --from <dir>.');
4361
+ }
3868
4362
  const settings = (0, setting_1.readSettings)();
3869
4363
  const configPath = settings.devnet.configPath;
3870
4364
  const ckbVersion = settings.bins.defaultCKBVersion;
3871
4365
  const sourceDir = path_1.default.resolve(options.from);
3872
4366
  validateSourceDir(sourceDir);
3873
4367
  assertSourceNodeStopped(sourceDir);
3874
- assertOffckbDevnetStopped();
3875
- if ((0, fs_2.isFolderExists)(configPath)) {
3876
- if (!options.force) {
3877
- throw new Error(`A devnet already exists at ${configPath}. Re-run with --force to replace it, ` +
3878
- `or reset it first with: offckb clean`);
3879
- }
3880
- logger_1.logger.info(`Removing existing devnet at ${configPath} ..`);
3881
- fs_1.default.rmSync(configPath, { recursive: true, force: true });
3882
- }
3883
4368
  // Identify the source chain: explicit flag > ckb.toml bundled spec.
3884
4369
  let source = (_a = options.source) !== null && _a !== void 0 ? _a : null;
3885
4370
  if (source == null) {
@@ -3894,41 +4379,55 @@ function forkDevnet(options) {
3894
4379
  }
3895
4380
  yield (0, install_1.installCKBBinary)(ckbVersion);
3896
4381
  const ckbBinPath = (0, setting_1.getCKBBinaryPath)(ckbVersion);
4382
+ const databaseMigrationNeeded = isDatabaseMigrationNeeded(ckbBinPath, sourceDir);
4383
+ const sourcePeerStoreDetected = fs_1.default.existsSync(path_1.default.join(sourceDir, 'data', 'network', 'peer_store'));
4384
+ const specFile = yield resolveSpecFile(options, source !== null && source !== void 0 ? source : 'mainnet', ckbVersion);
4385
+ const specPreflight = validateForkSpec(ckbBinPath, sourceDir, specFile, source);
4386
+ source = specPreflight.source;
4387
+ logger_1.logger.info(`Fork preflight: source=${source}, genesis=${specPreflight.genesisHash}, CKB=${ckbVersion}, migration=${databaseMigrationNeeded ? 'required' : 'not required'}.`);
4388
+ if (sourcePeerStoreDetected) {
4389
+ logger_1.logger.info('A persisted source peer store was detected and will be excluded from the fork.');
4390
+ }
4391
+ if (options.dryRun) {
4392
+ logger_1.logger.success('Fork preflight passed. No devnet files were changed.');
4393
+ logger_1.logger.result({
4394
+ command: 'devnet.fork.preflight',
4395
+ sourceDir,
4396
+ source,
4397
+ genesisHash: specPreflight.genesisHash,
4398
+ ckbVersion,
4399
+ databaseMigrationNeeded,
4400
+ sourcePeerStoreDetected,
4401
+ sourcePeerStoreWillBeExcluded: true,
4402
+ targetDir: configPath,
4403
+ });
4404
+ return;
4405
+ }
4406
+ assertOffckbDevnetStopped();
4407
+ if (databaseMigrationNeeded && !options.migrate) {
4408
+ throw new Error(`The source database requires migration for CKB v${ckbVersion}. ` +
4409
+ 'Re-run with --migrate to migrate only the copied devnet, leaving the source untouched.');
4410
+ }
4411
+ if ((0, fs_2.isFolderExists)(configPath)) {
4412
+ if (!options.force) {
4413
+ throw new Error(`A devnet already exists at ${configPath}. Re-run with --force to replace it, ` +
4414
+ `or reset it first with: offckb clean`);
4415
+ }
4416
+ logger_1.logger.info(`Removing existing devnet at ${configPath} ..`);
4417
+ fs_1.default.rmSync(configPath, { recursive: true, force: true });
4418
+ }
3897
4419
  try {
3898
4420
  // Inside the try so a failed copy (disk full, permissions, I/O) gets the
3899
4421
  // same rollback: a partial configPath would otherwise block the next
3900
4422
  // attempt as an "existing devnet".
3901
4423
  copySourceData(sourceDir, configPath);
3902
- const specFile = yield resolveSpecFile(options, source !== null && source !== void 0 ? source : 'mainnet', ckbVersion);
3903
4424
  const initOutput = runCkbInit(ckbBinPath, configPath, specFile);
3904
4425
  const genesisHash = parseGenesisHashFromInitOutput(initOutput);
3905
4426
  if (!genesisHash) {
3906
4427
  throw new Error(`Could not parse the genesis hash from ckb init output:\n${initOutput}`);
3907
4428
  }
3908
- // A custom spec may still be a well-known chain; let the chain data
3909
- // self-identify via its genesis hash.
3910
- if (source == null) {
3911
- source = (_b = (0, const_1.identifyPublicChainByGenesisHash)(genesisHash)) !== null && _b !== void 0 ? _b : 'custom';
3912
- }
3913
- if (source !== 'custom') {
3914
- const expected = expectedGenesisHash(source);
3915
- if (genesisHash !== expected) {
3916
- throw new Error(`Genesis hash mismatch: expected ${expected} for ${source}, got ${genesisHash}. ` +
3917
- `This usually means the chain spec does not match the source data. ` +
3918
- `(Importing a testnet spec with a CKB older than v0.207.0 sets a wrong genesis_epoch_length, ` +
3919
- `see nervosnetwork/ckb#5205.)`);
3920
- }
3921
- }
3922
- // The genesis above comes from the imported spec alone — it cannot see
3923
- // that --source/--spec-file contradicts the copied data (e.g. a mainnet
3924
- // spec over testnet data would pass and only fail when the node boots).
3925
- // Cross-check the source directory's own configured genesis and reject
3926
- // mismatches now. Skipped (null) when the source is not a standard
3927
- // config dir; the node's boot-time genesis check remains the backstop.
3928
- const sourceGenesisHash = readSourceGenesisHash(ckbBinPath, sourceDir);
3929
- if (sourceGenesisHash && sourceGenesisHash !== genesisHash) {
3930
- throw new Error(`The source directory is configured for a different chain (genesis ${sourceGenesisHash}) ` +
3931
- `than the imported spec (genesis ${genesisHash}). Pass a matching --source or --spec-file.`);
4429
+ if (genesisHash !== specPreflight.genesisHash) {
4430
+ throw new Error(`Chain spec changed after preflight: expected genesis ${specPreflight.genesisHash}, got ${genesisHash}.`);
3932
4431
  }
3933
4432
  const devSpecPath = path_1.default.join(configPath, 'specs', 'dev.toml');
3934
4433
  const spec = toml_1.default.parse(fs_1.default.readFileSync(devSpecPath, 'utf8'));
@@ -3937,6 +4436,9 @@ function forkDevnet(options) {
3937
4436
  fs_1.default.writeFileSync(tempSpecPath, toml_1.default.stringify(patched));
3938
4437
  fs_1.default.renameSync(tempSpecPath, devSpecPath);
3939
4438
  alignConfigsWithOffckb(configPath);
4439
+ if (databaseMigrationNeeded) {
4440
+ migrateDatabaseCopy(ckbBinPath, configPath);
4441
+ }
3940
4442
  const state = {
3941
4443
  source,
3942
4444
  sourceDir,
@@ -3944,11 +4446,28 @@ function forkDevnet(options) {
3944
4446
  genesisHash,
3945
4447
  forkedAt: new Date().toISOString(),
3946
4448
  firstRunPending: true,
4449
+ databaseMigrated: databaseMigrationNeeded,
4450
+ networkIsolated: true,
3947
4451
  };
3948
4452
  writeForkState(configPath, state);
3949
4453
  logger_1.logger.success(`Devnet forked from ${sourceDir} (${source}, genesis ${genesisHash}).`);
3950
- logger_1.logger.info('Start it with: offckb node');
4454
+ logger_1.logger.info('Start it with: offckb node --daemon');
3951
4455
  logger_1.logger.info('The first run applies --skip-spec-check --overwrite-spec automatically.');
4456
+ logger_1.logger.info('Then inspect node, Indexer, and network isolation with: offckb devnet info');
4457
+ logger_1.logger.info('List source-prefix dev addresses with: offckb accounts');
4458
+ if (source === 'mainnet') {
4459
+ logger_1.logger.warn('MAINNET FORK REPLAY RISK: only sign with built-in dev keys and fork-mined cells.');
4460
+ }
4461
+ logger_1.logger.result({
4462
+ command: 'devnet.fork',
4463
+ sourceDir,
4464
+ source,
4465
+ genesisHash,
4466
+ ckbVersion,
4467
+ databaseMigrated: databaseMigrationNeeded,
4468
+ networkIsolated: true,
4469
+ next: ['offckb node --daemon', 'offckb devnet info', 'offckb accounts'],
4470
+ });
3952
4471
  }
3953
4472
  catch (error) {
3954
4473
  // Leave no half-forked devnet behind.
@@ -3959,6 +4478,124 @@ function forkDevnet(options) {
3959
4478
  }
3960
4479
 
3961
4480
 
4481
+ /***/ }),
4482
+
4483
+ /***/ 19326:
4484
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
4485
+
4486
+ "use strict";
4487
+
4488
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4489
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4490
+ return new (P || (P = Promise))(function (resolve, reject) {
4491
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
4492
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
4493
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
4494
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
4495
+ });
4496
+ };
4497
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
4498
+ exports.checkNodeReadiness = checkNodeReadiness;
4499
+ exports.waitForNodeReady = waitForNodeReady;
4500
+ exports.checkConfiguredDevnetReadiness = checkConfiguredDevnetReadiness;
4501
+ exports.warnIfForkIndexerIsBehind = warnIfForkIndexerIsBehind;
4502
+ const setting_1 = __nccwpck_require__(25546);
4503
+ const base_1 = __nccwpck_require__(69951);
4504
+ const logger_1 = __nccwpck_require__(65370);
4505
+ const json_rpc_1 = __nccwpck_require__(51726);
4506
+ const fork_1 = __nccwpck_require__(77912);
4507
+ function parseHexNumber(value) {
4508
+ if (typeof value !== 'string' || !/^0x[0-9a-f]+$/i.test(value))
4509
+ return undefined;
4510
+ return BigInt(value);
4511
+ }
4512
+ function checkNodeReadiness(rpcUrl_1) {
4513
+ return __awaiter(this, arguments, void 0, function* (rpcUrl, timeoutMs = 3000) {
4514
+ try {
4515
+ const [nodeInfo, tipValue] = yield Promise.all([
4516
+ (0, json_rpc_1.callJsonRpc)(rpcUrl, 'local_node_info', [], timeoutMs),
4517
+ (0, json_rpc_1.callJsonRpc)(rpcUrl, 'get_tip_block_number', [], timeoutMs),
4518
+ ]);
4519
+ const nodeTip = parseHexNumber(tipValue);
4520
+ if (nodeTip == null) {
4521
+ throw new Error(`Invalid node tip returned by ${rpcUrl}`);
4522
+ }
4523
+ let indexerTip;
4524
+ try {
4525
+ const value = yield (0, json_rpc_1.callJsonRpc)(rpcUrl, 'get_indexer_tip', [], timeoutMs);
4526
+ indexerTip = parseHexNumber(value === null || value === void 0 ? void 0 : value.block_number);
4527
+ }
4528
+ catch (_a) {
4529
+ // Indexer readiness is reported separately and does not make the node RPC unhealthy.
4530
+ }
4531
+ let peers;
4532
+ try {
4533
+ const value = yield (0, json_rpc_1.callJsonRpc)(rpcUrl, 'get_peers', [], timeoutMs);
4534
+ peers = Array.isArray(value) ? value.length : undefined;
4535
+ }
4536
+ catch (_b) {
4537
+ // The Net RPC module can be disabled on custom nodes.
4538
+ }
4539
+ const indexerLag = indexerTip == null ? undefined : indexerTip >= nodeTip ? BigInt(0) : nodeTip - indexerTip;
4540
+ return {
4541
+ ready: true,
4542
+ rpcUrl,
4543
+ version: typeof (nodeInfo === null || nodeInfo === void 0 ? void 0 : nodeInfo.version) === 'string' ? nodeInfo.version : undefined,
4544
+ nodeTip,
4545
+ indexerTip,
4546
+ indexerLag,
4547
+ peers,
4548
+ };
4549
+ }
4550
+ catch (error) {
4551
+ return {
4552
+ ready: false,
4553
+ rpcUrl,
4554
+ error: error instanceof Error ? error.message : String(error),
4555
+ };
4556
+ }
4557
+ });
4558
+ }
4559
+ function waitForNodeReady(rpcUrl_1, timeoutMs_1) {
4560
+ return __awaiter(this, arguments, void 0, function* (rpcUrl, timeoutMs, isProcessAlive = () => true) {
4561
+ const start = Date.now();
4562
+ let last = yield checkNodeReadiness(rpcUrl);
4563
+ while (!last.ready && isProcessAlive() && Date.now() - start < timeoutMs) {
4564
+ yield new Promise((resolve) => setTimeout(resolve, 500));
4565
+ last = yield checkNodeReadiness(rpcUrl);
4566
+ }
4567
+ return last;
4568
+ });
4569
+ }
4570
+ function checkConfiguredDevnetReadiness() {
4571
+ return __awaiter(this, void 0, void 0, function* () {
4572
+ return checkNodeReadiness((0, setting_1.readSettings)().devnet.rpcUrl);
4573
+ });
4574
+ }
4575
+ function warnIfForkIndexerIsBehind(network) {
4576
+ return __awaiter(this, void 0, void 0, function* () {
4577
+ var _a;
4578
+ if (network !== base_1.Network.devnet)
4579
+ return undefined;
4580
+ const settings = (0, setting_1.readSettings)();
4581
+ if (!(0, fork_1.readForkState)(settings.devnet.configPath))
4582
+ return undefined;
4583
+ const readiness = yield checkNodeReadiness(settings.devnet.rpcUrl);
4584
+ if (!readiness.ready) {
4585
+ logger_1.logger.warn(`The forked devnet is not RPC-ready: ${(_a = readiness.error) !== null && _a !== void 0 ? _a : 'health check failed'}`);
4586
+ }
4587
+ else if (readiness.indexerTip == null) {
4588
+ logger_1.logger.warn('The CKB indexer is not ready yet; cell and balance lookups may be incomplete. Check `offckb devnet info`.');
4589
+ }
4590
+ else if (readiness.indexerLag && readiness.indexerLag > BigInt(0)) {
4591
+ logger_1.logger.warn(`The CKB indexer is ${readiness.indexerLag} blocks behind the node; cell and balance lookups may be incomplete. ` +
4592
+ 'Check `offckb devnet info`.');
4593
+ }
4594
+ return readiness;
4595
+ });
4596
+ }
4597
+
4598
+
3962
4599
  /***/ }),
3963
4600
 
3964
4601
  /***/ 1360:
@@ -3990,19 +4627,27 @@ function initChainIfNeeded() {
3990
4627
  const settings = (0, setting_1.readSettings)();
3991
4628
  const devnetSourcePath = path_1.default.resolve(setting_1.packageRootPath, './ckb/devnet');
3992
4629
  const devnetConfigPath = settings.devnet.configPath;
3993
- if (!(0, fs_2.isFolderExists)(devnetConfigPath)) {
3994
- const devnetConfigPath = settings.devnet.configPath;
3995
- yield (0, fs_2.copyFilesWithExclusion)(devnetSourcePath, devnetConfigPath, ['data']);
4630
+ const requiredConfigFiles = ['ckb.toml', 'ckb-miner.toml', path_1.default.join('specs', 'dev.toml')];
4631
+ const isInitialized = (0, fs_2.isFolderExists)(devnetConfigPath) &&
4632
+ requiredConfigFiles.every((relativePath) => fs_1.default.existsSync(path_1.default.join(devnetConfigPath, relativePath)));
4633
+ const minerConfigPath = path_1.default.join(devnetConfigPath, 'ckb-miner.toml');
4634
+ const minerConfigWasMissing = !fs_1.default.existsSync(minerConfigPath);
4635
+ // Daemon mode creates data/logs before the child starts. A directory-only
4636
+ // check therefore mistakes a fresh install for an initialized chain. Check
4637
+ // the files CKB actually needs instead, and repair an incomplete directory.
4638
+ if (!isInitialized) {
4639
+ yield (0, fs_2.copyFilesWithExclusion)(devnetSourcePath, devnetConfigPath, ['data'], false);
3996
4640
  logger_1.logger.debug(`init devnet config folder: ${devnetConfigPath}`);
3997
4641
  // copy and edit ckb-miner.toml
3998
4642
  const minerToml = path_1.default.join(devnetSourcePath, 'ckb-miner.toml');
3999
- const newMinerToml = path_1.default.join(devnetConfigPath, 'ckb-miner.toml');
4000
- // Read the content of the ckb-miner.toml file
4001
- const data = fs_1.default.readFileSync(minerToml, 'utf8');
4002
- // Replace the URL
4003
- const modifiedData = data.replace('http://ckb:8114/', settings.devnet.rpcUrl);
4004
- // Write the modified content back to the file
4005
- fs_1.default.writeFileSync(newMinerToml, modifiedData, 'utf8');
4643
+ if (minerConfigWasMissing) {
4644
+ // Read the content of the ckb-miner.toml file
4645
+ const data = fs_1.default.readFileSync(minerToml, 'utf8');
4646
+ // Replace the URL
4647
+ const modifiedData = data.replace('http://ckb:8114/', settings.devnet.rpcUrl);
4648
+ // Write the modified content back to the file
4649
+ fs_1.default.writeFileSync(minerConfigPath, modifiedData, 'utf8');
4650
+ }
4006
4651
  }
4007
4652
  });
4008
4653
  }
@@ -4132,6 +4777,7 @@ function downloadCKBBinaryAndUnzip(version) {
4132
4777
  }
4133
4778
  catch (error) {
4134
4779
  logger_1.logger.error('Error installing dependency binary:', error.message);
4780
+ throw error;
4135
4781
  }
4136
4782
  });
4137
4783
  }
@@ -5193,7 +5839,7 @@ class CKB {
5193
5839
  });
5194
5840
  }
5195
5841
  transfer(_a) {
5196
- return __awaiter(this, arguments, void 0, function* ({ privateKey, toAddress, amountInCKB }) {
5842
+ return __awaiter(this, arguments, void 0, function* ({ privateKey, toAddress, amountInCKB, rejectInputsAtOrBeforeBlock, }) {
5197
5843
  const signer = this.buildSigner(privateKey);
5198
5844
  const to = yield core_1.ccc.Address.fromString(toAddress, this.client);
5199
5845
  const tx = core_1.ccc.Transaction.from({
@@ -5206,6 +5852,7 @@ class CKB {
5206
5852
  });
5207
5853
  yield tx.completeInputsByCapacity(signer);
5208
5854
  yield tx.completeFeeBy(signer, this.feeRate);
5855
+ yield this.assertInputsCreatedAfter(tx, rejectInputsAtOrBeforeBlock);
5209
5856
  const txHash = yield signer.sendTransaction(tx);
5210
5857
  return txHash;
5211
5858
  });
@@ -5375,7 +6022,7 @@ class CKB {
5375
6022
  });
5376
6023
  }
5377
6024
  udtTransfer(_a) {
5378
- return __awaiter(this, arguments, void 0, function* ({ privateKey, toAddress, amount, udtType, kind }) {
6025
+ return __awaiter(this, arguments, void 0, function* ({ privateKey, toAddress, amount, udtType, kind, rejectInputsAtOrBeforeBlock, }) {
5379
6026
  const signer = this.buildSigner(privateKey);
5380
6027
  const to = yield core_1.ccc.Address.fromString(toAddress, this.client);
5381
6028
  const amountBigInt = (0, validator_1.validateUdtAmount)(amount);
@@ -5406,10 +6053,28 @@ class CKB {
5406
6053
  tx.outputsData.push(core_1.ccc.hexFrom(core_1.ccc.numToBytes(changeAmount, 16)));
5407
6054
  }
5408
6055
  yield tx.completeFeeBy(signer, this.feeRate);
6056
+ yield this.assertInputsCreatedAfter(tx, rejectInputsAtOrBeforeBlock);
5409
6057
  const txHash = yield signer.sendTransaction(tx);
5410
6058
  return txHash;
5411
6059
  });
5412
6060
  }
6061
+ assertInputsCreatedAfter(tx, blockNumber) {
6062
+ return __awaiter(this, void 0, void 0, function* () {
6063
+ if (blockNumber == null)
6064
+ return;
6065
+ for (const input of tx.inputs) {
6066
+ const outPoint = input.previousOutput;
6067
+ const origin = yield this.client.getTransactionNoCache(outPoint.txHash);
6068
+ if ((origin === null || origin === void 0 ? void 0 : origin.blockNumber) == null) {
6069
+ throw new Error(`Refusing to sign: could not verify the origin block of input ${outPoint.txHash}:${outPoint.index}.`);
6070
+ }
6071
+ if (origin.blockNumber <= blockNumber) {
6072
+ throw new Error(`Refusing to sign: input ${outPoint.txHash}:${outPoint.index} was created at block ${origin.blockNumber}, ` +
6073
+ `at or before the Mainnet fork boundary ${blockNumber}.`);
6074
+ }
6075
+ }
6076
+ });
6077
+ }
5413
6078
  udtIssue(_a) {
5414
6079
  return __awaiter(this, arguments, void 0, function* ({ privateKey, kind, amount, typeArgs, toAddress }) {
5415
6080
  const signer = this.buildSigner(privateKey);
@@ -5422,7 +6087,7 @@ class CKB {
5422
6087
  logger_1.logger.warn('SUDT type args are derived from the issuer lock hash; --type-args is ignored');
5423
6088
  }
5424
6089
  const issuerLockHash = signerAddress.script.hash();
5425
- resolvedTypeArgs = ('0x' + issuerLockHash.slice(2, 42));
6090
+ resolvedTypeArgs = issuerLockHash;
5426
6091
  }
5427
6092
  else {
5428
6093
  if (typeArgs) {
@@ -5450,7 +6115,7 @@ class CKB {
5450
6115
  yield tx.completeInputsByCapacity(signer);
5451
6116
  yield tx.completeFeeBy(signer, this.feeRate);
5452
6117
  const txHash = yield signer.sendTransaction(tx);
5453
- return txHash;
6118
+ return { txHash, typeArgs: resolvedTypeArgs, receiver: to.toString() };
5454
6119
  });
5455
6120
  }
5456
6121
  udtDestroy(_a) {
@@ -6917,6 +7582,16 @@ const DOWNLOAD_TIMEOUT_MS = 120000;
6917
7582
  const EXTRACT_TIMEOUT_MS = 60000;
6918
7583
  // Strict semver regex: v<major>.<minor>.<patch> (no leading zeros on digits)
6919
7584
  const STRICT_VERSION_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
7585
+ // Independently pinned digests for the default release. Keeping these in
7586
+ // offckb makes the default installation verifiable even though ckb-tui v0.1.3
7587
+ // did not upload a checksums-sha256.txt asset.
7588
+ const KNOWN_SHA256 = {
7589
+ 'v0.1.3': {
7590
+ 'ckb-tui-with-node-linux-amd64.tar.gz': '33455cefe2c016149fa8fa3abde7960b348d4606afef9279d787ac8a8b59956f',
7591
+ 'ckb-tui-with-node-macos-aarch64.tar.gz': 'de18107ec179ced03608da956013e38ae82e6c1fae588f12c17d138ee6ee072c',
7592
+ 'ckb-tui-with-node-windows-amd64.zip': '749d8e09fd5d23fc8af12892b7d197add5aae004f7438678023e4a973f3fd58b',
7593
+ },
7594
+ };
6920
7595
  class CKBTui {
6921
7596
  /**
6922
7597
  * Pure lookup — returns the expected binary path without triggering any
@@ -7033,7 +7708,7 @@ class CKBTui {
7033
7708
  if (curlResult.status !== 0) {
7034
7709
  throw new Error(`curl exited with code ${curlResult.status}`);
7035
7710
  }
7036
- // 2. Verify checksum (best-effort: warns if checksum file is unavailable)
7711
+ // 2. Verify checksum. Installation fails closed if no trusted digest exists.
7037
7712
  this.verifyChecksum(version, assetName, archivePath);
7038
7713
  // 3. Extract to temp directory
7039
7714
  logger_1.logger.info('Extracting...');
@@ -7071,64 +7746,24 @@ class CKBTui {
7071
7746
  }
7072
7747
  }
7073
7748
  }
7074
- /**
7075
- * Best-effort SHA-256 checksum verification.
7076
- * Downloads the checksum file published alongside the release asset and
7077
- * verifies the downloaded archive. Logs a warning (but does not fail) if
7078
- * the checksum file is unavailable — this maintains compatibility while
7079
- * the upstream project adopts checksum publishing.
7080
- */
7749
+ /** Verify against an independently pinned digest. */
7081
7750
  static verifyChecksum(version, assetName, archivePath) {
7082
- const checksumUrl = `https://github.com/Officeyutong/ckb-tui/releases/download/${version}/checksums-sha256.txt`;
7083
- const checksumPath = path.join(path.dirname(archivePath), 'checksums-sha256.txt');
7084
- const fetchResult = (0, child_process_1.spawnSync)('curl', ['-fsSL', '--max-time', '30', '-o', checksumPath, checksumUrl], {
7085
- stdio: 'pipe',
7086
- timeout: 30000,
7087
- });
7088
- if (fetchResult.status !== 0) {
7089
- logger_1.logger.warn(`SHA-256 checksum file not available for version ${version}. ` +
7090
- 'Skipping integrity verification. Consider asking the upstream maintainer to publish checksum files.');
7091
- return;
7092
- }
7093
- try {
7094
- const checksumContent = fs.readFileSync(checksumPath, 'utf8');
7095
- const expectedHash = this.parseChecksumFile(checksumContent, assetName);
7096
- if (!expectedHash) {
7097
- logger_1.logger.warn(`Checksum entry for "${assetName}" not found in checksums file. Skipping verification.`);
7098
- return;
7099
- }
7100
- const actualHash = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
7101
- if (actualHash !== expectedHash) {
7102
- throw new Error(`SHA-256 checksum mismatch for ${assetName}.\n` +
7103
- `Expected: ${expectedHash}\nActual: ${actualHash}\n` +
7104
- 'The downloaded file may be corrupted or tampered with.');
7105
- }
7106
- logger_1.logger.info('SHA-256 checksum verified successfully.');
7107
- }
7108
- finally {
7109
- try {
7110
- fs.unlinkSync(checksumPath);
7111
- }
7112
- catch (_a) {
7113
- // Best effort
7114
- }
7751
+ var _a;
7752
+ const pinnedHash = (_a = KNOWN_SHA256[version]) === null || _a === void 0 ? void 0 : _a[assetName];
7753
+ if (!pinnedHash) {
7754
+ throw new Error(`No trusted SHA-256 checksum is pinned for ckb-tui ${version} (${assetName}). ` +
7755
+ 'Refusing to install an unverified binary.');
7115
7756
  }
7757
+ this.assertChecksum(archivePath, assetName, pinnedHash);
7116
7758
  }
7117
- /**
7118
- * Parse a standard SHA-256 checksum file (format: "<hash> <filename>" per line)
7119
- * and return the hex hash for the given asset name, or null if not found.
7120
- */
7121
- static parseChecksumFile(content, assetName) {
7122
- for (const line of content.split('\n')) {
7123
- const trimmed = line.trim();
7124
- if (!trimmed || trimmed.startsWith('#'))
7125
- continue;
7126
- const match = trimmed.match(/^([0-9a-fA-F]{64})\s+[*]?(.+)$/);
7127
- if (match && match[2] === assetName) {
7128
- return match[1].toLowerCase();
7129
- }
7759
+ static assertChecksum(archivePath, assetName, expectedHash) {
7760
+ const actualHash = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
7761
+ if (actualHash !== expectedHash) {
7762
+ throw new Error(`SHA-256 checksum mismatch for ${assetName}.\n` +
7763
+ `Expected: ${expectedHash}\nActual: ${actualHash}\n` +
7764
+ 'The downloaded file may be corrupted or tampered with.');
7130
7765
  }
7131
- return null;
7766
+ logger_1.logger.info('SHA-256 checksum verified successfully.');
7132
7767
  }
7133
7768
  /**
7134
7769
  * Extract a downloaded archive to the given directory.
@@ -9380,6 +10015,77 @@ function encodeBinPathForTerminal(path) {
9380
10015
  }
9381
10016
 
9382
10017
 
10018
+ /***/ }),
10019
+
10020
+ /***/ 47625:
10021
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
10022
+
10023
+ "use strict";
10024
+
10025
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10026
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10027
+ };
10028
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10029
+ exports.warnIfMainnetForkSigning = warnIfMainnetForkSigning;
10030
+ exports.validateMainnetForkSigning = validateMainnetForkSigning;
10031
+ const account_json_1 = __importDefault(__nccwpck_require__(13849));
10032
+ const account_1 = __nccwpck_require__(59489);
10033
+ const setting_1 = __nccwpck_require__(25546);
10034
+ const fork_1 = __nccwpck_require__(77912);
10035
+ const base_1 = __nccwpck_require__(69951);
10036
+ const logger_1 = __nccwpck_require__(65370);
10037
+ const BUILT_IN_DEV_KEYS = new Set([...account_json_1.default.map((account) => account.privkey), account_1.ckbDevnetMinerAccount.privkey].map((key) => key.toLowerCase()));
10038
+ function warnIfMainnetForkSigning(network, privateKey) {
10039
+ if (!readMainnetForkState(network))
10040
+ return;
10041
+ logMainnetForkSigningWarning(privateKey);
10042
+ }
10043
+ /**
10044
+ * Fail closed before a Mainnet-fork transfer is constructed or signed.
10045
+ * Returns the copied-chain tip so the transaction layer can reject inputs
10046
+ * created at or before the fork boundary after input selection.
10047
+ */
10048
+ function validateMainnetForkSigning(network, privateKey, allowMainnetReplayRisk = false) {
10049
+ const fork = readMainnetForkState(network);
10050
+ if (!fork)
10051
+ return undefined;
10052
+ logMainnetForkSigningWarning(privateKey);
10053
+ if (!BUILT_IN_DEV_KEYS.has(privateKey.trim().toLowerCase()) && !allowMainnetReplayRisk) {
10054
+ throw new Error('Refusing to sign with a non-built-in private key on a Mainnet fork. ' +
10055
+ 'Use --allow-mainnet-replay-risk only after verifying that no copied Mainnet input will be selected.');
10056
+ }
10057
+ if (fork.forkBlockNumber == null) {
10058
+ throw new Error('Mainnet fork boundary metadata is missing. Restart or recreate the fork before signing so input origins can be verified.');
10059
+ }
10060
+ try {
10061
+ const blockNumber = BigInt(fork.forkBlockNumber);
10062
+ if (blockNumber < BigInt(0))
10063
+ throw new Error('negative block number');
10064
+ return blockNumber;
10065
+ }
10066
+ catch (_a) {
10067
+ throw new Error(`Invalid Mainnet fork boundary metadata: ${fork.forkBlockNumber}`);
10068
+ }
10069
+ }
10070
+ function readMainnetForkState(network) {
10071
+ if (network !== base_1.Network.devnet)
10072
+ return null;
10073
+ const settings = (0, setting_1.readSettings)();
10074
+ const fork = (0, fork_1.readForkState)(settings.devnet.configPath);
10075
+ return (fork === null || fork === void 0 ? void 0 : fork.source) === 'mainnet' ? fork : null;
10076
+ }
10077
+ function logMainnetForkSigningWarning(privateKey) {
10078
+ logger_1.logger.warn([
10079
+ 'MAINNET FORK REPLAY RISK: CKB transactions have no chain id.',
10080
+ 'A transaction spending cells copied from Mainnet can also be valid on Mainnet.',
10081
+ 'Use only built-in dev keys and fork-mined cells unless you explicitly accept that risk.',
10082
+ ]);
10083
+ if (!BUILT_IN_DEV_KEYS.has(privateKey.trim().toLowerCase())) {
10084
+ logger_1.logger.warn('A non-built-in private key is being used on a Mainnet fork. Verify every input before signing.');
10085
+ }
10086
+ }
10087
+
10088
+
9383
10089
  /***/ }),
9384
10090
 
9385
10091
  /***/ 37293:
@@ -9485,22 +10191,23 @@ function copyFileSync(source, target) {
9485
10191
  }
9486
10192
  fs.writeFileSync(targetFile, fs.readFileSync(source));
9487
10193
  }
9488
- function copyFilesWithExclusion(sourceDir, destinationDir, excludedFolders) {
9489
- return __awaiter(this, void 0, void 0, function* () {
10194
+ function copyFilesWithExclusion(sourceDir_1, destinationDir_1, excludedFolders_1) {
10195
+ return __awaiter(this, arguments, void 0, function* (sourceDir, destinationDir, excludedFolders, overwrite = true) {
9490
10196
  try {
9491
10197
  // Ensure the destination directory exists
9492
10198
  yield fs.promises.mkdir(destinationDir, { recursive: true });
9493
10199
  // Start copying recursively from the source directory
9494
- yield copyRecursive(sourceDir, destinationDir, excludedFolders);
10200
+ yield copyRecursive(sourceDir, destinationDir, excludedFolders, overwrite);
9495
10201
  }
9496
10202
  catch (error) {
9497
10203
  logger_1.logger.error('An error occurred during copying files:', error);
10204
+ throw error;
9498
10205
  }
9499
10206
  });
9500
10207
  }
9501
10208
  // Function to recursively copy files and directories
9502
- function copyRecursive(source, destination, excludedFolders) {
9503
- return __awaiter(this, void 0, void 0, function* () {
10209
+ function copyRecursive(source_1, destination_1, excludedFolders_1) {
10210
+ return __awaiter(this, arguments, void 0, function* (source, destination, excludedFolders, overwrite = true) {
9504
10211
  // Get a list of all files and directories in the source directory
9505
10212
  const files = yield fs.promises.readdir(source);
9506
10213
  // Iterate through each file or directory
@@ -9517,12 +10224,14 @@ function copyRecursive(source, destination, excludedFolders) {
9517
10224
  else {
9518
10225
  // Ensure destination directory exists before copying
9519
10226
  yield fs.promises.mkdir(destPath, { recursive: true });
9520
- yield copyRecursive(sourcePath, destPath, excludedFolders);
10227
+ yield copyRecursive(sourcePath, destPath, excludedFolders, overwrite);
9521
10228
  }
9522
10229
  }
9523
10230
  else {
9524
10231
  // Otherwise, copy the file
9525
- yield fs.promises.copyFile(sourcePath, destPath);
10232
+ if (overwrite || !fs.existsSync(destPath)) {
10233
+ yield fs.promises.copyFile(sourcePath, destPath);
10234
+ }
9526
10235
  }
9527
10236
  }
9528
10237
  });
@@ -9767,6 +10476,7 @@ const levelColors = {
9767
10476
  };
9768
10477
  class UnifiedLogger {
9769
10478
  constructor(options = {}) {
10479
+ this.resultEmitted = false;
9770
10480
  this.enableColors = options.enableColors !== false;
9771
10481
  this.showLevel = options.showLevel !== false;
9772
10482
  this.jsonMode = options.jsonMode === true;
@@ -9776,8 +10486,8 @@ class UnifiedLogger {
9776
10486
  levels: {
9777
10487
  error: 0,
9778
10488
  warn: 1,
9779
- info: 2,
9780
- success: 3,
10489
+ success: 2,
10490
+ info: 3,
9781
10491
  debug: 4,
9782
10492
  },
9783
10493
  format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.errors({ stack: true }), winston_1.default.format.printf(({ level, message, timestamp }) => {
@@ -9789,6 +10499,7 @@ class UnifiedLogger {
9789
10499
  }),
9790
10500
  ],
9791
10501
  });
10502
+ this.setJsonMode(this.jsonMode);
9792
10503
  }
9793
10504
  /**
9794
10505
  * Toggle JSON output mode. When enabled, every log line is emitted as a
@@ -9796,6 +10507,36 @@ class UnifiedLogger {
9796
10507
  */
9797
10508
  setJsonMode(enabled) {
9798
10509
  this.jsonMode = enabled;
10510
+ // In machine mode stdout is reserved for command result records. Progress
10511
+ // and diagnostics remain NDJSON, but go to stderr so callers can parse one
10512
+ // stable stdout object without filtering human-oriented logs.
10513
+ for (const transport of this.logger.transports) {
10514
+ if (transport instanceof winston_1.default.transports.Console) {
10515
+ const consoleTransport = transport;
10516
+ consoleTransport.stderrLevels = Object.fromEntries((enabled ? Object.keys(levelColors) : ['error', 'warn']).map((level) => [level, true]));
10517
+ }
10518
+ }
10519
+ }
10520
+ isJsonMode() {
10521
+ return this.jsonMode;
10522
+ }
10523
+ /** Emit one stable, command-level result record for programmatic callers. */
10524
+ result(result) {
10525
+ if (this.jsonMode && !this.resultEmitted) {
10526
+ this.resultEmitted = true;
10527
+ node_process_1.default.stdout.write(`${JSON.stringify(Object.assign({ ok: true }, result))}\n`);
10528
+ }
10529
+ }
10530
+ hasResult() {
10531
+ return this.resultEmitted;
10532
+ }
10533
+ /** Emit a stable error record without exposing internal stack traces. */
10534
+ failure(code, message, details) {
10535
+ if (this.jsonMode) {
10536
+ node_process_1.default.stderr.write(`${JSON.stringify(Object.assign({ ok: false, code, message }, (details == null ? {} : { details })))}\n`);
10537
+ return;
10538
+ }
10539
+ this.error(message);
9799
10540
  }
9800
10541
  /**
9801
10542
  * Format the message with appropriate colors and structure
@@ -9978,6 +10719,43 @@ const logger = UnifiedLogger.create({ showLevel: false });
9978
10719
  exports.logger = logger;
9979
10720
 
9980
10721
 
10722
+ /***/ }),
10723
+
10724
+ /***/ 26241:
10725
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
10726
+
10727
+ "use strict";
10728
+
10729
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10730
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10731
+ };
10732
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10733
+ exports.resolvePrivateKey = resolvePrivateKey;
10734
+ const fs_1 = __importDefault(__nccwpck_require__(79896));
10735
+ const path_1 = __importDefault(__nccwpck_require__(16928));
10736
+ function resolvePrivateKey(input, defaultKey) {
10737
+ if (input.privkey && input.privkeyFile) {
10738
+ throw new Error('Use only one of --privkey or --privkey-file.');
10739
+ }
10740
+ if (input.privkey)
10741
+ return input.privkey;
10742
+ if (input.privkeyFile) {
10743
+ const filePath = path_1.default.resolve(input.privkeyFile);
10744
+ try {
10745
+ return fs_1.default.readFileSync(filePath, 'utf8').trim();
10746
+ }
10747
+ catch (error) {
10748
+ throw new Error(`Could not read private key file ${filePath}: ${error.message}`);
10749
+ }
10750
+ }
10751
+ if (process.env.OFFCKB_PRIVATE_KEY)
10752
+ return process.env.OFFCKB_PRIVATE_KEY;
10753
+ if (defaultKey)
10754
+ return defaultKey;
10755
+ throw new Error('--privkey, --privkey-file, or OFFCKB_PRIVATE_KEY is required!');
10756
+ }
10757
+
10758
+
9981
10759
  /***/ }),
9982
10760
 
9983
10761
  /***/ 65897:
@@ -10072,7 +10850,6 @@ exports.validateUdtTypeArgs = validateUdtTypeArgs;
10072
10850
  const path_1 = __importDefault(__nccwpck_require__(16928));
10073
10851
  const fs_1 = __importDefault(__nccwpck_require__(79896));
10074
10852
  const base_1 = __nccwpck_require__(69951);
10075
- const logger_1 = __nccwpck_require__(65370);
10076
10853
  function validateTypescriptWorkspace() {
10077
10854
  const cwd = process.cwd();
10078
10855
  // Check if package.json exists
@@ -10114,8 +10891,7 @@ function validateNetworkOpt(network) {
10114
10891
  throw new Error('invalid network option, ' + network);
10115
10892
  }
10116
10893
  if (network === base_1.Network.mainnet) {
10117
- logger_1.logger.info('Mainnet not support yet. Please use CKB-CLI to operate on mainnet for better security. Check https://github.com/nervosnetwork/ckb-cli');
10118
- process.exit(1);
10894
+ throw new Error('Mainnet not support yet. Please use CKB-CLI to operate on mainnet for better security. Check https://github.com/nervosnetwork/ckb-cli');
10119
10895
  }
10120
10896
  }
10121
10897
  function isValidVersion(version) {
@@ -10171,9 +10947,12 @@ function validateUdtKind(kind) {
10171
10947
  const U128_MAX = (BigInt(1) << BigInt(128)) - BigInt(1);
10172
10948
  function validateUdtAmount(amount) {
10173
10949
  if (!/^\d+$/.test(amount)) {
10174
- throw new Error(`invalid UDT amount "${amount}", must be a non-negative decimal integer`);
10950
+ throw new Error(`invalid UDT amount "${amount}", must be a positive decimal integer`);
10175
10951
  }
10176
10952
  const value = BigInt(amount);
10953
+ if (value === BigInt(0)) {
10954
+ throw new Error('invalid UDT amount "0", must be greater than zero');
10955
+ }
10177
10956
  if (value > U128_MAX) {
10178
10957
  throw new Error(`UDT amount exceeds 128-bit max: ${amount}`);
10179
10958
  }
@@ -10189,8 +10968,8 @@ function validateHexString(value, name) {
10189
10968
  function validateUdtTypeArgs(kind, typeArgs) {
10190
10969
  const hex = validateHexString(typeArgs, 'type args');
10191
10970
  const byteLength = (hex.length - 2) / 2;
10192
- if (kind === 'sudt' && byteLength !== 20) {
10193
- throw new Error(`invalid SUDT type args length: expected 20 bytes, got ${byteLength}`);
10971
+ if (kind === 'sudt' && byteLength !== 32) {
10972
+ throw new Error(`invalid SUDT type args length: expected 32 bytes, got ${byteLength}`);
10194
10973
  }
10195
10974
  if (kind === 'xudt' && byteLength !== 32) {
10196
10975
  throw new Error(`invalid xUDT type args length: expected 32 bytes, got ${byteLength}`);
@@ -130322,7 +131101,7 @@ module.exports = {"version":"3.17.0"};
130322
131101
  /***/ ((module) => {
130323
131102
 
130324
131103
  "use strict";
130325
- module.exports = /*#__PURE__*/JSON.parse('{"rE":"0.4.9-canary-807f2ef.0","h_":"ckb development network for your first try"}');
131104
+ module.exports = /*#__PURE__*/JSON.parse('{"rE":"0.4.9","h_":"ckb development network for your first try"}');
130326
131105
 
130327
131106
  /***/ })
130328
131107