From c6e7be1cdd6b35e2be20a38b696448df768a6b31 Mon Sep 17 00:00:00 2001 From: RetricSu Date: Fri, 5 Dec 2025 14:25:15 +0800 Subject: [PATCH 1/2] feat(cmd): add status command with ckb-tui --- README.md | 6 +++ src/cfg/setting.ts | 8 +++ src/cli.ts | 9 ++++ src/cmd/status.ts | 41 +++++++++++++++ src/tools/ckb-tui.ts | 120 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 src/cmd/status.ts create mode 100644 src/tools/ckb-tui.ts diff --git a/README.md b/README.md index 2574a67..3748e62 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Commands: transfer-all [options] [toAddress] Transfer All CKB tokens to address, only devnet and testnet balance [options] [toAddress] Check account balance, only devnet and testnet debugger Port of the raw CKB Standalone Debugger + status [options] Show ckb-tui status interface config [item] [value] do a configuration action help [command] display help for command ``` @@ -130,8 +131,13 @@ You can also start a proxy RPC server for public networks: ```sh offckb node --network ``` + Using a proxy RPC server for Testnet/Mainnet is especially helpful for debugging transactions, since failed transactions are dumped automatically. +**Watch Network With TUI** + +Once you start the CKB Node, you can use `offckb status --network devnet/testnet/mainnet` to start a CKB-TUI interface to monitor the CKB network from your node. + ### 2. Create a New Contract Project {#create-project} Generate a ready-to-use smart-contract project in JS/TS using templates: diff --git a/src/cfg/setting.ts b/src/cfg/setting.ts index 7d99e9d..556aaca 100644 --- a/src/cfg/setting.ts +++ b/src/cfg/setting.ts @@ -52,9 +52,13 @@ export interface Settings { transactionsPath: string; }; tools: { + rootFolder: string; ckbDebugger: { minVersion: string; }; + ckbTui: { + version: string; + }; }; } @@ -88,9 +92,13 @@ export const defaultSettings: Settings = { transactionsPath: path.resolve(dataPath, 'mainnet/transactions'), }, tools: { + rootFolder: path.resolve(dataPath, 'tools'), ckbDebugger: { minVersion: '0.200.0', }, + ckbTui: { + version: 'v0.1.0', + }, }, }; diff --git a/src/cli.ts b/src/cli.ts index 011e934..f3a9b46 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,7 @@ import { genSystemScriptsJsonFile } from './scripts/gen'; import { CKBDebugger } from './tools/ckb-debugger'; import { logger } from './util/logger'; import { Network } from './type/base'; +import { status } from './cmd/status'; const version = require('../package.json').version; const description = require('../package.json').description; @@ -154,6 +155,14 @@ program return CKBDebugger.runWithArgs(process.argv.slice(2)); }); +program + .command('status') + .description('Show ckb-tui status interface') + .option('--network ', 'Specify the network to deploy to', 'devnet') + .action(async (option) => { + status({ network: option.network }); + }); + program .command('config [item] [value]') .description('do a configuration action') diff --git a/src/cmd/status.ts b/src/cmd/status.ts new file mode 100644 index 0000000..de55454 --- /dev/null +++ b/src/cmd/status.ts @@ -0,0 +1,41 @@ +import { readSettings } from '../cfg/setting'; +import { CKBTui } from '../tools/ckb-tui'; +import { Network } from '../type/base'; +import { logger } from '../util/logger'; + +export interface StatusOptions { + network?: Network; +} + +export async function status({ network }: StatusOptions) { + const settings = readSettings(); + const port = + network === Network.devnet + ? settings.devnet.rpcProxyPort + : network === Network.testnet + ? settings.testnet.rpcProxyPort + : settings.mainnet.rpcProxyPort; + const url = `http://127.0.0.1:${port}`; + const isListening = await isRPCPortListening(port); + if (!isListening) { + return logger.error( + `RPC port ${port} is not listening. Please make sure the ${network} node is running and Proxy RPC is enabled.`, + ); + } + return CKBTui.runWithArgs(['-r', url]); +} + +async function isRPCPortListening(port: number): Promise { + const net = require('net'); + const client = new net.Socket(); + return new Promise((resolve) => { + client.once('error', () => { + resolve(false); + }); + client.connect(port, '127.0.0.1'); + client.once('connect', () => { + client.end(); + resolve(true); + }); + }); +} diff --git a/src/tools/ckb-tui.ts b/src/tools/ckb-tui.ts new file mode 100644 index 0000000..def8615 --- /dev/null +++ b/src/tools/ckb-tui.ts @@ -0,0 +1,120 @@ +import { spawnSync, execSync } from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; +import { readSettings } from '../cfg/setting'; +import { logger } from '../util/logger'; + +export class CKBTui { + private static binaryPath: string | null = null; + + private static getBinaryPath(): string { + if (!this.binaryPath) { + const settings = readSettings(); + const binDir = settings.tools.rootFolder; + const version = settings.tools.ckbTui.version; + this.binaryPath = path.join(binDir, 'ckb-tui'); + + if (!fs.existsSync(this.binaryPath)) { + this.downloadBinary(version); + } + } + return this.binaryPath; + } + + private static downloadBinary(version: string) { + const platform = process.platform; + const arch = process.arch; + let assetName: string; + + if (platform === 'darwin') { + if (arch === 'arm64') { + assetName = `ckb-tui-with-node-macos-aarch64.tar.gz`; + } else { + throw new Error(`Unsupported architecture for macOS: ${arch}`); + } + } else if (platform === 'linux') { + if (arch === 'x64') { + assetName = `ckb-tui-with-node-linux-amd64.tar.gz`; + } else { + throw new Error(`Unsupported architecture for Linux: ${arch}`); + } + } else if (platform === 'win32') { + if (arch === 'x64') { + assetName = `ckb-tui-with-node-windows-amd64.zip`; + } else { + throw new Error(`Unsupported architecture for Windows: ${arch}`); + } + } else { + throw new Error(`Unsupported platform: ${platform}`); + } + + const downloadUrl = `https://github.com/Officeyutong/ckb-tui/releases/download/${version}/${assetName}`; + const binDir = path.dirname(this.binaryPath!); + const archivePath = path.join(binDir, assetName); + + try { + logger.info(`Downloading ckb-tui from ${downloadUrl}...`); + execSync(`curl -L -o "${archivePath}" "${downloadUrl}"`, { stdio: 'inherit' }); + + logger.info('Extracting...'); + if (assetName.endsWith('.tar.gz')) { + execSync(`tar -xzf "${archivePath}" -C "${binDir}"`, { stdio: 'inherit' }); + } else if (assetName.endsWith('.zip')) { + execSync(`unzip "${archivePath}" -d "${binDir}"`, { stdio: 'inherit' }); + } + + // Assume the binary is extracted as 'ckb-tui' or 'ckb-tui.exe' + // todo: fix the bin name + const extractedBinary = platform === 'win32' ? 'ckb-tui.exe' : 'ckb-tui-macos-amd64'; + const extractedPath = path.join(binDir, extractedBinary); + if (fs.existsSync(extractedPath)) { + fs.renameSync(extractedPath, this.binaryPath!); + } else { + // If in a subfolder, find it + const files = fs.readdirSync(binDir); + for (const file of files) { + const filePath = path.join(binDir, file); + if (fs.statSync(filePath).isDirectory()) { + const candidate = path.join(filePath, extractedBinary); + if (fs.existsSync(candidate)) { + fs.renameSync(candidate, this.binaryPath!); + break; + } + } + } + } + + // Make executable on Unix + if (platform !== 'win32') { + execSync(`chmod +x "${this.binaryPath}"`); + } + + // Clean up archive + fs.unlinkSync(archivePath); + + logger.info('ckb-tui installed successfully.'); + } catch (error) { + logger.error('Failed to download/install ckb-tui:', (error as Error).message); + throw error; + } + } + + static isInstalled(): boolean { + try { + const path = this.getBinaryPath(); + return fs.existsSync(path); + } catch { + return false; + } + } + + static run(args: string[] = []) { + const binaryPath = this.getBinaryPath(); + const command = `"${binaryPath}" ${args.join(' ')}`; + return spawnSync(command, { stdio: 'inherit', shell: true }); + } + + static runWithArgs(args: string[]) { + this.run(args); + } +} From 801836e785f377c1aa1b1ad006692d6abebe61ac Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 06:17:24 +0800 Subject: [PATCH 2/2] Address PR review comments: fix security vulnerabilities and improve error handling (#342) * Initial plan * Address PR review comments - fix security, correctness, and style issues Co-authored-by: RetricSu <23436060+RetricSu@users.noreply.github.com> * Remove TODO comment and use Network enum for validation Co-authored-by: RetricSu <23436060+RetricSu@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RetricSu <23436060+RetricSu@users.noreply.github.com> --- .gitignore | 1 + README.md | 2 +- src/cli.ts | 7 +++++- src/cmd/status.ts | 32 +++++++++++++++++------ src/tools/ckb-tui.ts | 60 ++++++++++++++++++++++++++++++++++---------- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 1b993bd..d3e662f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ .vscode templates/temp-clone-folder build/ +package-lock.json diff --git a/README.md b/README.md index 3748e62..4fb9061 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ offckb node --network Using a proxy RPC server for Testnet/Mainnet is especially helpful for debugging transactions, since failed transactions are dumped automatically. -**Watch Network With TUI** +**Watch Network with TUI** Once you start the CKB Node, you can use `offckb status --network devnet/testnet/mainnet` to start a CKB-TUI interface to monitor the CKB network from your node. diff --git a/src/cli.ts b/src/cli.ts index f3a9b46..4e5c4a3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -160,7 +160,12 @@ program .description('Show ckb-tui status interface') .option('--network ', 'Specify the network to deploy to', 'devnet') .action(async (option) => { - status({ network: option.network }); + const validNetworks = Object.values(Network); + if (!validNetworks.includes(option.network)) { + logger.error(`Invalid network: ${option.network}. Must be one of: ${validNetworks.join(', ')}`); + process.exit(1); + } + return await status({ network: option.network }); }); program diff --git a/src/cmd/status.ts b/src/cmd/status.ts index de55454..502e89e 100644 --- a/src/cmd/status.ts +++ b/src/cmd/status.ts @@ -2,6 +2,7 @@ import { readSettings } from '../cfg/setting'; import { CKBTui } from '../tools/ckb-tui'; import { Network } from '../type/base'; import { logger } from '../util/logger'; +import * as net from 'net'; export interface StatusOptions { network?: Network; @@ -18,24 +19,41 @@ export async function status({ network }: StatusOptions) { const url = `http://127.0.0.1:${port}`; const isListening = await isRPCPortListening(port); if (!isListening) { - return logger.error( + logger.error( `RPC port ${port} is not listening. Please make sure the ${network} node is running and Proxy RPC is enabled.`, ); + return; } - return CKBTui.runWithArgs(['-r', url]); + CKBTui.run(['-r', url]); } async function isRPCPortListening(port: number): Promise { - const net = require('net'); const client = new net.Socket(); return new Promise((resolve) => { + let settled = false; + const TIMEOUT_MS = 5000; + const timeout = setTimeout(() => { + if (!settled) { + settled = true; + client.destroy(); + resolve(false); + } + }, TIMEOUT_MS); client.once('error', () => { - resolve(false); + if (!settled) { + settled = true; + clearTimeout(timeout); + resolve(false); + } }); - client.connect(port, '127.0.0.1'); client.once('connect', () => { - client.end(); - resolve(true); + if (!settled) { + settled = true; + clearTimeout(timeout); + client.end(); + resolve(true); + } }); + client.connect(port, '127.0.0.1'); }); } diff --git a/src/tools/ckb-tui.ts b/src/tools/ckb-tui.ts index def8615..2f8826a 100644 --- a/src/tools/ckb-tui.ts +++ b/src/tools/ckb-tui.ts @@ -22,6 +22,11 @@ export class CKBTui { } private static downloadBinary(version: string) { + // Validate version format to prevent URL manipulation + if (!/^v\d+\.\d+\.\d+$/.test(version)) { + throw new Error(`Invalid version format: ${version}. Expected format: vX.Y.Z`); + } + const platform = process.platform; const arch = process.arch; let assetName: string; @@ -29,6 +34,8 @@ export class CKBTui { if (platform === 'darwin') { if (arch === 'arm64') { assetName = `ckb-tui-with-node-macos-aarch64.tar.gz`; + } else if (arch === 'x64') { + assetName = `ckb-tui-with-node-macos-amd64.tar.gz`; } else { throw new Error(`Unsupported architecture for macOS: ${arch}`); } @@ -63,9 +70,25 @@ export class CKBTui { execSync(`unzip "${archivePath}" -d "${binDir}"`, { stdio: 'inherit' }); } - // Assume the binary is extracted as 'ckb-tui' or 'ckb-tui.exe' - // todo: fix the bin name - const extractedBinary = platform === 'win32' ? 'ckb-tui.exe' : 'ckb-tui-macos-amd64'; + // Set the correct binary name based on platform and architecture + let extractedBinary: string; + if (platform === 'win32') { + extractedBinary = 'ckb-tui.exe'; + } else if (platform === 'darwin') { + if (arch === 'arm64') { + extractedBinary = 'ckb-tui-macos-aarch64'; + } else { + extractedBinary = 'ckb-tui-macos-amd64'; + } + } else if (platform === 'linux') { + if (arch === 'x64') { + extractedBinary = 'ckb-tui-linux-amd64'; + } else { + throw new Error(`Unsupported architecture for Linux: ${arch}`); + } + } else { + throw new Error(`Unsupported platform: ${platform}`); + } const extractedPath = path.join(binDir, extractedBinary); if (fs.existsSync(extractedPath)) { fs.renameSync(extractedPath, this.binaryPath!); @@ -84,18 +107,34 @@ export class CKBTui { } } + // Check that the binary was successfully extracted and moved + if (!fs.existsSync(this.binaryPath!)) { + logger.error(`ckb-tui binary was not found after extraction. Expected at: ${this.binaryPath}`); + throw new Error('Failed to extract and locate ckb-tui binary.'); + } + // Make executable on Unix if (platform !== 'win32') { execSync(`chmod +x "${this.binaryPath}"`); } - // Clean up archive - fs.unlinkSync(archivePath); - logger.info('ckb-tui installed successfully.'); } catch (error) { - logger.error('Failed to download/install ckb-tui:', (error as Error).message); + logger.error( + 'Failed to download/install ckb-tui:', + (error as Error).message, + '\nPlease check your network connectivity, verify that the specified version exists in the releases, and ensure you have sufficient file system permissions.' + ); throw error; + } finally { + // Clean up archive even if error occurs + if (fs.existsSync(archivePath)) { + try { + fs.unlinkSync(archivePath); + } catch (cleanupError) { + logger.warn('Failed to clean up archive file:', (cleanupError as Error).message); + } + } } } @@ -110,11 +149,6 @@ export class CKBTui { static run(args: string[] = []) { const binaryPath = this.getBinaryPath(); - const command = `"${binaryPath}" ${args.join(' ')}`; - return spawnSync(command, { stdio: 'inherit', shell: true }); - } - - static runWithArgs(args: string[]) { - this.run(args); + return spawnSync(binaryPath, args, { stdio: 'inherit' }); } }