Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <action> [item] [value] do a configuration action
help [command] display help for command
```
Expand Down Expand Up @@ -130,8 +131,13 @@ You can also start a proxy RPC server for public networks:
```sh
offckb node --network <testnet or mainnet>
```

Using a proxy RPC server for Testnet/Mainnet is especially helpful for debugging transactions, since failed transactions are dumped automatically.

**Watch Network With TUI**
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heading "Watch Network With TUI" should use consistent capitalization. Consider "Watch Network with TUI" (lowercase "with") to match standard title case conventions.

Suggested change
**Watch Network With TUI**
**Watch Network with TUI**

Copilot uses AI. Check for mistakes.

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:
Expand Down
8 changes: 8 additions & 0 deletions src/cfg/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@
transactionsPath: string;
};
tools: {
rootFolder: string;
ckbDebugger: {
minVersion: string;
};
ckbTui: {
version: string;
};
};
}

Expand Down Expand Up @@ -88,9 +92,13 @@
transactionsPath: path.resolve(dataPath, 'mainnet/transactions'),
},
tools: {
rootFolder: path.resolve(dataPath, 'tools'),
ckbDebugger: {
minVersion: '0.200.0',
},
ckbTui: {
version: 'v0.1.0',
},
},
};

Expand Down Expand Up @@ -127,7 +135,7 @@
return `${getCKBBinaryInstallPath(version)}/ckb`;
}

function deepMerge(target: any, source: any): any {

Check warning on line 138 in src/cfg/setting.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 138 in src/cfg/setting.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 138 in src/cfg/setting.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 138 in src/cfg/setting.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 138 in src/cfg/setting.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 138 in src/cfg/setting.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
for (const key in source) {
if (source[key] && typeof source[key] === 'object') {
if (!target[key]) {
Expand Down
9 changes: 9 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -154,6 +155,14 @@ program
return CKBDebugger.runWithArgs(process.argv.slice(2));
});

program
.command('status')
.description('Show ckb-tui status interface')
.option('--network <network>', 'Specify the network to deploy to', 'devnet')
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The network option accepts any string value without validation. If a user provides an invalid network name (e.g., "production" or "dev"), it would be passed through without error checking. Consider validating that the network value is one of the valid Network enum values ('devnet', 'testnet', 'mainnet') and providing a helpful error message if it's not.

Copilot uses AI. Check for mistakes.
.action(async (option) => {
status({ network: option.network });
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status function is called without await, but it's declared as async in the status.ts file. This means errors thrown in the function won't be caught by this action handler. Add await to properly handle the async operation.

Suggested change
status({ network: option.network });
return status({ network: option.network });

Copilot uses AI. Check for mistakes.
});

program
.command('config <action> [item] [value]')
.description('do a configuration action')
Expand Down
41 changes: 41 additions & 0 deletions src/cmd/status.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
const net = require('net');
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using require inline within a function is not consistent with the ES6 import style used elsewhere in the file. Consider importing the net module at the top of the file with import * as net from 'net'; for consistency.

Copilot uses AI. Check for mistakes.
const client = new net.Socket();
return new Promise<boolean>((resolve) => {
client.once('error', () => {
resolve(false);
});
client.connect(port, '127.0.0.1');
client.once('connect', () => {
client.end();
resolve(true);
});
Comment on lines +42 to +56
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The socket connection is never cleaned up if neither the 'error' nor 'connect' event fires in a timely manner. This can lead to resource leaks. Consider adding a timeout to ensure the socket is closed and the promise resolves even if the connection hangs indefinitely.

Suggested change
client.once('error', () => {
resolve(false);
});
client.connect(port, '127.0.0.1');
client.once('connect', () => {
client.end();
resolve(true);
});
let settled = false;
const TIMEOUT_MS = 5000;
const timeout = setTimeout(() => {
if (!settled) {
settled = true;
client.destroy();
resolve(false);
}
}, TIMEOUT_MS);
client.once('error', () => {
if (!settled) {
settled = true;
clearTimeout(timeout);
resolve(false);
}
});
client.once('connect', () => {
if (!settled) {
settled = true;
clearTimeout(timeout);
client.end();
resolve(true);
}
});
client.connect(port, '127.0.0.1');

Copilot uses AI. Check for mistakes.
});
}
120 changes: 120 additions & 0 deletions src/tools/ckb-tui.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
Comment on lines +34 to +41
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

macOS x64 architecture is not supported, but it's a valid and commonly used platform. Consider adding support for macOS x64 systems by including the appropriate asset name (similar to how Linux x64 is supported).

Copilot uses AI. Check for mistakes.
} 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}`;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download URL is constructed without validating the version string. If the version contains special characters or path traversal sequences (e.g., ../../malicious), it could potentially be used to download from unintended URLs. Consider validating that the version matches a expected pattern (e.g., v\\d+\\.\\d+\\.\\d+).

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO comments should be capitalized for consistency with common conventions.

Suggested change
// todo: fix the bin name
// TODO: fix the bin name

Copilot uses AI. Check for mistakes.
const extractedBinary = platform === 'win32' ? 'ckb-tui.exe' : 'ckb-tui-macos-amd64';
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extracted binary name ckb-tui-macos-amd64 doesn't match the asset name for macOS ARM64 (ckb-tui-with-node-macos-aarch64.tar.gz). When running on macOS ARM64, this will look for a file named ckb-tui-macos-amd64 which won't exist in the archive. The extracted binary name should match the architecture detected earlier (e.g., ckb-tui-macos-aarch64 for ARM64).

Suggested change
// 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 {
throw new Error(`Unsupported architecture for macOS: ${arch}`);
}
} 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}`);
}

Copilot uses AI. Check for mistakes.
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;
}
}
}
}

Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the binary is not found after extraction (neither in the bin directory nor in subdirectories), the code silently continues without throwing an error or logging a warning. This could lead to confusing behavior later when trying to execute the binary. Consider adding an error check after the loop at line 84 to verify that the binary was successfully extracted and moved.

Suggested change
// 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.');
}

Copilot uses AI. Check for mistakes.
// 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);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message logs (error as Error).message but doesn't provide context about what might have gone wrong. Consider including more specific guidance, such as checking network connectivity, verifying the version exists in the releases, or checking file system permissions.

Suggested change
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.'
);

Copilot uses AI. Check for mistakes.
throw error;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an error occurs during extraction or binary setup (lines 59-85), the downloaded archive file won't be cleaned up. Consider wrapping the cleanup in a finally block or moving it earlier to ensure cleanup happens even when errors occur.

Suggested change
// 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;
logger.info('ckb-tui installed successfully.');
} catch (error) {
logger.error('Failed to download/install ckb-tui:', (error as Error).message);
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);
}
}

Copilot uses AI. Check for mistakes.
}
}

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 });
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The run method constructs a shell command by joining args without sanitization, which could allow command injection if any argument contains shell metacharacters (e.g., ;, |, &, etc.). Consider using spawnSync with the arguments array directly instead of constructing a shell command string: spawnSync(binaryPath, args, { stdio: 'inherit' }).

Suggested change
const command = `"${binaryPath}" ${args.join(' ')}`;
return spawnSync(command, { stdio: 'inherit', shell: true });
return spawnSync(binaryPath, args, { stdio: 'inherit' });

Copilot uses AI. Check for mistakes.
}

static runWithArgs(args: string[]) {
this.run(args);
}
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runWithArgs method is redundant and simply calls the run method. This doesn't add any value and can be removed. All callers should directly use the run method instead.

Suggested change
static runWithArgs(args: string[]) {
this.run(args);
}

Copilot uses AI. Check for mistakes.
}
Loading