Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
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/tasty-walls-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@offckb/cli": minor
---

Refactor UDT CLI support: reuse `balance` and `transfer` commands for CKB and UDT queries, and add `offckb udt issue` / `offckb udt destroy` subcommands.
44 changes: 37 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { Command, Option } from 'commander';
import { startNode, stopNode } from './cmd/node';
import { accounts } from './cmd/accounts';
import { clean } from './cmd/clean';
Expand All @@ -8,6 +8,7 @@ import { DepositOptions, deposit } from './cmd/deposit';
import { DeployOptions, deploy } from './cmd/deploy';
import { TransferOptions, transfer } from './cmd/transfer';
import { BalanceOption, balanceOf } from './cmd/balance';
import { udtIssue, udtDestroy, UdtIssueOption, UdtDestroyOption } from './cmd/udt';
import { createScriptProject, CreateScriptProjectOptions } from './cmd/create';
import { Config, ConfigItem } from './cmd/config';
import { devnetConfig } from './cmd/devnet-config';
Expand Down Expand Up @@ -137,13 +138,15 @@ program
});

program
.command('transfer [toAddress] [amountInCKB]')
.description('Transfer CKB tokens to address, only devnet and testnet')
.command('transfer [toAddress] [amount]')
.description('Transfer CKB or UDT tokens to address, only devnet and testnet')
.option('--network <network>', 'Specify the network to transfer to', 'devnet')
.option('--privkey <privkey>', 'Specify the private key to transfer CKB')
.option('--privkey <privkey>', 'Specify the private key to transfer')
.addOption(new Option('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
.option('--udt-type-args <typeArgs>', 'Specify the UDT type script args to transfer UDT')
.option('-r, --proxy-rpc', 'Use Proxy RPC to connect to blockchain')
.action(async (toAddress: string, amountInCKB: string, options: TransferOptions) => {
return transfer(toAddress, amountInCKB, options);
.action(async (toAddress: string, amount: string, options: TransferOptions) => {
return transfer(toAddress, amount, options);
});

program
Expand All @@ -158,12 +161,39 @@ program

program
.command('balance [toAddress]')
.description('Check account balance, only devnet and testnet')
.description('Check account balance (CKB + detected SUDT/xUDT), only devnet and testnet')
.option('--network <network>', 'Specify the network to check', 'devnet')
.addOption(new Option('--udt-kind <kind>', 'Filter by UDT kind').choices(['sudt', 'xudt']))
.option('--udt-type-args <typeArgs>', 'Filter by UDT type script args')
.action(async (toAddress: string, options: BalanceOption) => {
return balanceOf(toAddress, options);
});

const udtCommand = program.command('udt').description('UDT token commands');

udtCommand
.command('issue <amount>')
.description('Issue new UDT tokens, only devnet and testnet')
.option('--network <network>', 'Specify the network', 'devnet')
.addOption(new Option('--kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ [WARNING / Maintainability] UDT kind is specified with --kind here, but transfer/balance use --udt-kind. Unifying the flag name would make the CLI more predictable for users.

.option('--type-args <typeArgs>', 'Specify the UDT type script args (xudt only; defaults to signer lock hash)')
.option('--to <toAddress>', 'Specify the receiver address (defaults to signer)')
.option('--privkey <privkey>', 'Specify the private key to issue UDT')

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🛑 [CRITICAL / Security] Accepting the private key via --privkey exposes it to any process on the same host (ps, /proc/<pid>/cmdline). The same risk exists at line 192 for udt destroy. Please read the key from an environment variable (OFFCKB_PRIVATE_KEY) or a file (--privkey-file) instead.

.action(async (amount: string, options: UdtIssueOption) => {
return udtIssue(amount, options);
});

udtCommand
.command('destroy <amount>')
.description('Destroy UDT tokens, only devnet and testnet')
.option('--network <network>', 'Specify the network', 'devnet')
.addOption(new Option('--kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
.requiredOption('--type-args <typeArgs>', 'Specify the UDT type script args')
.option('--privkey <privkey>', 'Specify the private key to destroy UDT')
.action(async (amount: string, options: UdtDestroyOption) => {
return udtDestroy(amount, options);
});

program
.command('debugger')
.description('Port of the raw CKB Standalone Debugger')
Expand Down
32 changes: 28 additions & 4 deletions src/cmd/balance.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { CKB } from '../sdk/ckb';
import { CKB, UdtBalanceInfo, UdtKind } from '../sdk/ckb';
import { validateNetworkOpt } from '../util/validator';
import { NetworkOption, Network } from '../type/base';
import { logger } from '../util/logger';

export interface BalanceOption extends NetworkOption {}
export interface BalanceOption extends NetworkOption {
udtKind?: UdtKind;
udtTypeArgs?: string;
}

export async function balanceOf(address: string, opt: BalanceOption = { network: Network.devnet }) {
const network = opt.network;
Expand All @@ -12,6 +15,27 @@ export async function balanceOf(address: string, opt: BalanceOption = { network:
const ckb = new CKB({ network });

const balanceInCKB = await ckb.balance(address);
logger.info(`Balance: ${balanceInCKB} CKB`);
process.exit(0);
logger.info(`CKB: ${balanceInCKB}`);

const udtBalances = await ckb.detectUdtBalances(address);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ [WARNING / Performance] balanceOf always triggers a full UDT scan (detectUdtBalances), even when the user only wants the CKB balance. Consider skipping the scan unless requested, or at least running the two queries in parallel.

const filtered = filterUdtBalances(udtBalances, opt);

if (filtered.length > 0) {
logger.info('UDT:');
for (const udt of filtered) {
logger.info(` ${udt.kind} (args=${udt.args}): ${udt.balance}`);
}
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ [WARNING / Correctness] The original balanceOf ended with process.exit(0); removing it may leave the CKB client’s HTTP/fetch keep-alive handles open, causing the CLI process to hang. Consider restoring process.exit(0) or explicitly closing the client.


function filterUdtBalances(balances: UdtBalanceInfo[], opt: BalanceOption): UdtBalanceInfo[] {
if (!opt.udtKind && !opt.udtTypeArgs) {
return balances;
}

return balances.filter((udt) => {
const kindMatch = opt.udtKind ? udt.kind === opt.udtKind : true;
const argsMatch = opt.udtTypeArgs ? udt.args === opt.udtTypeArgs : true;
return kindMatch && argsMatch;
});
}
34 changes: 26 additions & 8 deletions src/cmd/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { CKB } from '../sdk/ckb';
import { CKB, UdtKind } from '../sdk/ckb';
import { NetworkOption, Network } from '../type/base';
import { buildTestnetTxLink } from '../util/link';
import { validateNetworkOpt } from '../util/validator';
import { validateNetworkOpt, validateUdtKind, validateUdtTypeArgs } from '../util/validator';
import { logger } from '../util/logger';

export interface TransferOptions extends NetworkOption {
privkey?: string | null;
udtKind?: UdtKind;
udtTypeArgs?: string;
}

export async function transfer(
toAddress: string,
amountInCKB: string,
opt: TransferOptions = { network: Network.devnet },
) {
export async function transfer(toAddress: string, amount: string, opt: TransferOptions = { network: Network.devnet }) {
const network = opt.network;
validateNetworkOpt(network);

Expand All @@ -23,9 +21,29 @@ export async function transfer(
const privateKey = opt.privkey;
const ckb = new CKB({ network });

if (opt.udtTypeArgs) {
const kind = opt.udtKind ?? 'sudt';
validateUdtKind(kind);
const udtTypeArgs = validateUdtTypeArgs(kind, opt.udtTypeArgs);
const udtType = await ckb.buildUdtTypeScript(kind, udtTypeArgs);
const txHash = await ckb.udtTransfer({
toAddress,
amount,
privateKey,
udtType,
});

if (network === 'testnet') {
logger.info(`Successfully transfer UDT, check ${buildTestnetTxLink(txHash)} for details.`);
return;
}
logger.info('Successfully transfer UDT, txHash:', txHash);
return;
}

const txHash = await ckb.transfer({
toAddress,
amountInCKB,
amountInCKB: amount,
privateKey,
});
if (network === 'testnet') {
Expand Down
73 changes: 73 additions & 0 deletions src/cmd/udt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { CKB, UdtKind } from '../sdk/ckb';
import { NetworkOption, Network } from '../type/base';
import { buildTestnetTxLink } from '../util/link';
import { validateNetworkOpt, validateUdtKind, validateUdtTypeArgs } from '../util/validator';
import { logger } from '../util/logger';

export interface UdtIssueOption extends NetworkOption {
kind: UdtKind;
typeArgs?: string;
to?: string;
privkey: string;
}

export interface UdtDestroyOption extends NetworkOption {
kind: UdtKind;
typeArgs: string;
privkey: string;
}

export async function udtIssue(
amount: string,
opt: UdtIssueOption = { network: Network.devnet, kind: 'sudt', privkey: '' },
) {
const network = opt.network;
validateNetworkOpt(network);
validateUdtKind(opt.kind);

if (!opt.privkey) {
throw new Error('--privkey is required!');
}

const ckb = new CKB({ network });
const txHash = await ckb.udtIssue({
privateKey: opt.privkey,
kind: opt.kind,
amount,
typeArgs: opt.typeArgs ? validateUdtTypeArgs(opt.kind, opt.typeArgs) : undefined,
toAddress: opt.to,
});

if (network === 'testnet') {
logger.info(`Successfully issued UDT, check ${buildTestnetTxLink(txHash)} for details.`);
return;
}
logger.info('Successfully issued UDT, txHash:', txHash);
}

export async function udtDestroy(
amount: string,
opt: UdtDestroyOption = { network: Network.devnet, kind: 'sudt', typeArgs: '', privkey: '' },
) {
const network = opt.network;
validateNetworkOpt(network);
validateUdtKind(opt.kind);

if (!opt.privkey) {
throw new Error('--privkey is required!');
}

const ckb = new CKB({ network });
const txHash = await ckb.udtDestroy({
privateKey: opt.privkey,
kind: opt.kind,
amount,
typeArgs: validateUdtTypeArgs(opt.kind, opt.typeArgs),
});

if (network === 'testnet') {
logger.info(`Successfully destroyed UDT, check ${buildTestnetTxLink(txHash)} for details.`);
return;
}
logger.info('Successfully destroyed UDT, txHash:', txHash);
}
Loading
Loading