Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/integrate-ckb-tui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@offckb/cli": minor
---

Add `status` command to launch ckb-tui for monitoring CKB network from your node
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,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 @@ -169,6 +170,10 @@ 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**

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.3',
},
},
};

Expand Down Expand Up @@ -129,7 +137,7 @@
return path.join(getCKBBinaryInstallPath(version), binaryName);
}

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

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 140 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
14 changes: 14 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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 @@ -205,6 +206,19 @@ 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')
.action(async (option) => {
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
.command('config <action> [item] [value]')
.description('do a configuration action')
Expand Down
59 changes: 59 additions & 0 deletions src/cmd/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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;
}

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) {
logger.error(
`RPC port ${port} is not listening. Please make sure the ${network} node is running and Proxy RPC is enabled.`,
);
return;
}
CKBTui.run(['-r', url]);
}

async function isRPCPortListening(port: number): Promise<boolean> {
const client = new net.Socket();
return new Promise<boolean>((resolve) => {
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');
});
}
137 changes: 137 additions & 0 deletions src/tools/ckb-tui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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) {
// 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;

if (platform === 'darwin') {
if (arch !== 'arm64') {
throw new Error(`Unsupported architecture for macOS: ${arch}. Only Apple Silicon (arm64) is supported.`);
}
assetName = `ckb-tui-with-node-macos-aarch64.tar.gz`;
} else if (platform === 'linux') {
if (arch !== 'x64') {
throw new Error(`Unsupported architecture for Linux: ${arch}`);
}
assetName = `ckb-tui-with-node-linux-amd64.tar.gz`;
} else if (platform === 'win32') {
if (arch !== 'x64') {
throw new Error(`Unsupported architecture for Windows: ${arch}`);
}
assetName = `ckb-tui-with-node-windows-amd64.zip`;
} 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);
const binaryName = platform === 'win32' ? 'ckb-tui.exe' : 'ckb-tui';

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' });
}

const extractedBinary = this.findBinary(binDir, binaryName);
if (!extractedBinary) {
logger.error(`ckb-tui binary was not found after extraction. Expected: ${binaryName}`);
throw new Error('Failed to extract and locate ckb-tui binary.');
}

fs.renameSync(extractedBinary, this.binaryPath!);

// Make executable on Unix
if (platform !== 'win32') {
execSync(`chmod +x "${this.binaryPath}"`);
}

logger.info('ckb-tui installed successfully.');
} catch (error) {
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);
}
}
}
}

private static findBinary(dir: string, binaryName: string): string | null {
// Check direct path first
const directPath = path.join(dir, binaryName);
if (fs.existsSync(directPath)) {
return directPath;
}

// Search in subdirectories
const entries = fs.readdirSync(dir);
for (const entry of entries) {
const entryPath = path.join(dir, entry);
if (fs.statSync(entryPath).isDirectory()) {
const candidate = path.join(entryPath, binaryName);
if (fs.existsSync(candidate)) {
return candidate;
}
}
}

return null;
}

static isInstalled(): boolean {
try {
const binPath = this.getBinaryPath();
return fs.existsSync(binPath);
} catch {
return false;
}
}

static run(args: string[] = []) {
const binaryPath = this.getBinaryPath();
return spawnSync(binaryPath, args, { stdio: 'inherit' });
}
}
Loading