Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
45 changes: 38 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,40 @@ 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')
.option('--no-udt', 'Skip UDT balance scan')
.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('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
.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
Contributor 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('--udt-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
39 changes: 34 additions & 5 deletions src/cmd/balance.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
import { CKB } from '../sdk/ckb';
import { CKB, UdtBalanceInfo } from '../sdk/ckb';
import { validateNetworkOpt } from '../util/validator';
import { NetworkOption, Network } from '../type/base';
import { NetworkOption, Network, UdtKind } from '../type/base';
import { logger } from '../util/logger';

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

export async function balanceOf(address: string, opt: BalanceOption = { network: Network.devnet }) {
const network = opt.network;
validateNetworkOpt(network);

const ckb = new CKB({ network });

const balanceInCKB = await ckb.balance(address);
logger.info(`Balance: ${balanceInCKB} CKB`);
const [balanceInCKB, udtBalances] = await Promise.all([
ckb.balance(address),
opt.udt !== false ? ckb.detectUdtBalances(address) : Promise.resolve([]),
]);
logger.info(`CKB: ${balanceInCKB}`);

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

process.exit(0);
}

Copy link
Copy Markdown
Contributor 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;
});
}
41 changes: 25 additions & 16 deletions src/cmd/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { CKB } from '../sdk/ckb';
import { NetworkOption, Network } from '../type/base';
import { buildTestnetTxLink } from '../util/link';
import { validateNetworkOpt } from '../util/validator';
import { logger } from '../util/logger';
import { NetworkOption, Network, UdtKind } from '../type/base';
import { logTxSuccess } from '../util/link';
import { validateNetworkOpt, validateUdtKind, validateUdtTypeArgs } from '../util/validator';

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,15 +20,27 @@ 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,
kind,
});

logTxSuccess(network, txHash, 'transfer UDT');
return;
}

const txHash = await ckb.transfer({
toAddress,
amountInCKB,
amountInCKB: amount,
privateKey,
});
if (network === 'testnet') {
logger.info(`Successfully transfer, check ${buildTestnetTxLink(txHash)} for details.`);
return;
}

logger.info('Successfully transfer, txHash:', txHash);
logTxSuccess(network, txHash, 'transfer');
}
64 changes: 64 additions & 0 deletions src/cmd/udt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { CKB } from '../sdk/ckb';
import { NetworkOption, Network, UdtKind } from '../type/base';
import { logTxSuccess } from '../util/link';
import { validateNetworkOpt, validateUdtKind, validateUdtTypeArgs } from '../util/validator';

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

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

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

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

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

logTxSuccess(network, txHash, 'issued UDT');
}

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

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

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

logTxSuccess(network, txHash, 'destroyed UDT');
}
Loading
Loading