Skip to content

Commit 463b2ff

Browse files
feat: add UDT balance and transfer CLI commands (#447)
* feat: add UDT balance and transfer CLI commands Add offckb udt-balance and udt-transfer commands leveraging the CCC SDK. Supports both SUDT and xUDT via --kind. - udt-balance: query UDT balance for an address - udt-transfer: transfer UDT amount to an address with change output Closes #445 Co-Authored-By: Claude <noreply@anthropic.com> * refactor: reuse balance/transfer for UDT and add issue/destroy commands - Reuse existing balance/transfer commands via --udt-type-args/--udt-kind flags - Make balance default show CKB plus detected SUDT/xUDT balances - Replace udt-balance/udt-transfer with udt issue/destroy subcommands - Add detectUdtBalances, udtIssue, and udtDestroy to CKB SDK - Update tests for the revised CLI Co-Authored-By: Claude <noreply@anthropic.com> * chore: add changeset and fix formatting for UDT CLI refactor - Add minor changeset for the new UDT issue/destroy commands and balance/transfer reuse. - Apply prettier formatting to src/cmd/transfer.ts. Co-Authored-By: Claude <noreply@anthropic.com> * fix(udt): address PR review comments - Use minimal cell capacity (capacity: 0) instead of hardcoded 61 CKB for UDT outputs - Add missing cell deps in udtTransfer - Reject full UDT destroy to avoid potential script rejection - Add max input cell limit for udtDestroy - Skip corrupted UDT cells in balance detection instead of failing - Remove hard 500-cell cap on UDT balance scan (now 1000 with warning) - Add input validation: amount, type args length, UDT kind enum - Warn when --type-args is provided for SUDT issue - Remove process.exit(0) from balance command - Support filtering balance by --udt-kind alone - Add CLI enum choices for --udt-kind and --kind - Add SDK-level UDT tests and expand validator tests Co-Authored-By: Claude <noreply@anthropic.com> * fix(udt): address code review comments from review squad - Extract getUdtScriptInfo helper to unify SUDT/xUDT script/cellDeps handling - Filter detectUdtBalances by UDT type script with prefix search instead of scanning all cells - Add --no-udt flag and parallel CKB/UDT queries in balance command - Restore process.exit(0) in balanceOf to prevent CLI hang - Unify UDT kind CLI flag to --udt-kind across balance/transfer/udt commands - Move UdtKind type to src/type/base.ts and reuse in validator - Add u128 upper bound check to validateUdtAmount - Add logTxSuccess helper to remove duplicated testnet/devnet success logging - Fix ckb.udt.test.ts test title/content mismatch and cover SUDT type-args warning - Update tests for renamed udtKind option and process.exit mocking Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6f54296 commit 463b2ff

12 files changed

Lines changed: 1039 additions & 33 deletions

File tree

.changeset/tasty-walls-appear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@offckb/cli": minor
3+
---
4+
5+
Refactor UDT CLI support: reuse `balance` and `transfer` commands for CKB and UDT queries, and add `offckb udt issue` / `offckb udt destroy` subcommands.

src/cli.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env node
2-
import { Command } from 'commander';
2+
import { Command, Option } from 'commander';
33
import { startNode, stopNode } from './cmd/node';
44
import { accounts } from './cmd/accounts';
55
import { clean } from './cmd/clean';
@@ -8,6 +8,7 @@ import { DepositOptions, deposit } from './cmd/deposit';
88
import { DeployOptions, deploy } from './cmd/deploy';
99
import { TransferOptions, transfer } from './cmd/transfer';
1010
import { BalanceOption, balanceOf } from './cmd/balance';
11+
import { udtIssue, udtDestroy, UdtIssueOption, UdtDestroyOption } from './cmd/udt';
1112
import { createScriptProject, CreateScriptProjectOptions } from './cmd/create';
1213
import { Config, ConfigItem } from './cmd/config';
1314
import { devnetConfig } from './cmd/devnet-config';
@@ -137,13 +138,15 @@ program
137138
});
138139

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

149152
program
@@ -158,12 +161,40 @@ program
158161

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

173+
const udtCommand = program.command('udt').description('UDT token commands');
174+
175+
udtCommand
176+
.command('issue <amount>')
177+
.description('Issue new UDT tokens, only devnet and testnet')
178+
.option('--network <network>', 'Specify the network', 'devnet')
179+
.addOption(new Option('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
180+
.option('--type-args <typeArgs>', 'Specify the UDT type script args (xudt only; defaults to signer lock hash)')
181+
.option('--to <toAddress>', 'Specify the receiver address (defaults to signer)')
182+
.option('--privkey <privkey>', 'Specify the private key to issue UDT')
183+
.action(async (amount: string, options: UdtIssueOption) => {
184+
return udtIssue(amount, options);
185+
});
186+
187+
udtCommand
188+
.command('destroy <amount>')
189+
.description('Destroy UDT tokens, only devnet and testnet')
190+
.option('--network <network>', 'Specify the network', 'devnet')
191+
.addOption(new Option('--udt-kind <kind>', 'Specify the UDT kind').choices(['sudt', 'xudt']).default('sudt'))
192+
.requiredOption('--type-args <typeArgs>', 'Specify the UDT type script args')
193+
.option('--privkey <privkey>', 'Specify the private key to destroy UDT')
194+
.action(async (amount: string, options: UdtDestroyOption) => {
195+
return udtDestroy(amount, options);
196+
});
197+
167198
program
168199
.command('debugger')
169200
.description('Port of the raw CKB Standalone Debugger')

src/cmd/balance.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
1-
import { CKB } from '../sdk/ckb';
1+
import { CKB, UdtBalanceInfo } from '../sdk/ckb';
22
import { validateNetworkOpt } from '../util/validator';
3-
import { NetworkOption, Network } from '../type/base';
3+
import { NetworkOption, Network, UdtKind } from '../type/base';
44
import { logger } from '../util/logger';
55

6-
export interface BalanceOption extends NetworkOption {}
6+
export interface BalanceOption extends NetworkOption {
7+
udtKind?: UdtKind;
8+
udtTypeArgs?: string;
9+
udt?: boolean;
10+
}
711

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

1216
const ckb = new CKB({ network });
1317

14-
const balanceInCKB = await ckb.balance(address);
15-
logger.info(`Balance: ${balanceInCKB} CKB`);
18+
const [balanceInCKB, udtBalances] = await Promise.all([
19+
ckb.balance(address),
20+
opt.udt !== false ? ckb.detectUdtBalances(address) : Promise.resolve([]),
21+
]);
22+
logger.info(`CKB: ${balanceInCKB}`);
23+
24+
const filtered = filterUdtBalances(udtBalances, opt);
25+
26+
if (filtered.length > 0) {
27+
logger.info('UDT:');
28+
for (const udt of filtered) {
29+
logger.info(` ${udt.kind} (args=${udt.args}): ${udt.balance}`);
30+
}
31+
}
32+
1633
process.exit(0);
1734
}
35+
36+
function filterUdtBalances(balances: UdtBalanceInfo[], opt: BalanceOption): UdtBalanceInfo[] {
37+
if (!opt.udtKind && !opt.udtTypeArgs) {
38+
return balances;
39+
}
40+
41+
return balances.filter((udt) => {
42+
const kindMatch = opt.udtKind ? udt.kind === opt.udtKind : true;
43+
const argsMatch = opt.udtTypeArgs ? udt.args === opt.udtTypeArgs : true;
44+
return kindMatch && argsMatch;
45+
});
46+
}

src/cmd/transfer.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import { CKB } from '../sdk/ckb';
2-
import { NetworkOption, Network } from '../type/base';
3-
import { buildTestnetTxLink } from '../util/link';
4-
import { validateNetworkOpt } from '../util/validator';
5-
import { logger } from '../util/logger';
2+
import { NetworkOption, Network, UdtKind } from '../type/base';
3+
import { logTxSuccess } from '../util/link';
4+
import { validateNetworkOpt, validateUdtKind, validateUdtTypeArgs } from '../util/validator';
65

76
export interface TransferOptions extends NetworkOption {
87
privkey?: string | null;
8+
udtKind?: UdtKind;
9+
udtTypeArgs?: string;
910
}
1011

11-
export async function transfer(
12-
toAddress: string,
13-
amountInCKB: string,
14-
opt: TransferOptions = { network: Network.devnet },
15-
) {
12+
export async function transfer(toAddress: string, amount: string, opt: TransferOptions = { network: Network.devnet }) {
1613
const network = opt.network;
1714
validateNetworkOpt(network);
1815

@@ -23,15 +20,27 @@ export async function transfer(
2320
const privateKey = opt.privkey;
2421
const ckb = new CKB({ network });
2522

23+
if (opt.udtTypeArgs) {
24+
const kind = opt.udtKind ?? 'sudt';
25+
validateUdtKind(kind);
26+
const udtTypeArgs = validateUdtTypeArgs(kind, opt.udtTypeArgs);
27+
const udtType = await ckb.buildUdtTypeScript(kind, udtTypeArgs);
28+
const txHash = await ckb.udtTransfer({
29+
toAddress,
30+
amount,
31+
privateKey,
32+
udtType,
33+
kind,
34+
});
35+
36+
logTxSuccess(network, txHash, 'transfer UDT');
37+
return;
38+
}
39+
2640
const txHash = await ckb.transfer({
2741
toAddress,
28-
amountInCKB,
42+
amountInCKB: amount,
2943
privateKey,
3044
});
31-
if (network === 'testnet') {
32-
logger.info(`Successfully transfer, check ${buildTestnetTxLink(txHash)} for details.`);
33-
return;
34-
}
35-
36-
logger.info('Successfully transfer, txHash:', txHash);
45+
logTxSuccess(network, txHash, 'transfer');
3746
}

src/cmd/udt.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { CKB } from '../sdk/ckb';
2+
import { NetworkOption, Network, UdtKind } from '../type/base';
3+
import { logTxSuccess } from '../util/link';
4+
import { validateNetworkOpt, validateUdtKind, validateUdtTypeArgs } from '../util/validator';
5+
6+
export interface UdtIssueOption extends NetworkOption {
7+
udtKind: UdtKind;
8+
typeArgs?: string;
9+
to?: string;
10+
privkey: string;
11+
}
12+
13+
export interface UdtDestroyOption extends NetworkOption {
14+
udtKind: UdtKind;
15+
typeArgs: string;
16+
privkey: string;
17+
}
18+
19+
export async function udtIssue(
20+
amount: string,
21+
opt: UdtIssueOption = { network: Network.devnet, udtKind: 'sudt', privkey: '' },
22+
) {
23+
const network = opt.network;
24+
validateNetworkOpt(network);
25+
validateUdtKind(opt.udtKind);
26+
27+
if (!opt.privkey) {
28+
throw new Error('--privkey is required!');
29+
}
30+
31+
const ckb = new CKB({ network });
32+
const txHash = await ckb.udtIssue({
33+
privateKey: opt.privkey,
34+
kind: opt.udtKind,
35+
amount,
36+
typeArgs: opt.typeArgs ? validateUdtTypeArgs(opt.udtKind, opt.typeArgs) : undefined,
37+
toAddress: opt.to,
38+
});
39+
40+
logTxSuccess(network, txHash, 'issued UDT');
41+
}
42+
43+
export async function udtDestroy(
44+
amount: string,
45+
opt: UdtDestroyOption = { network: Network.devnet, udtKind: 'sudt', typeArgs: '', privkey: '' },
46+
) {
47+
const network = opt.network;
48+
validateNetworkOpt(network);
49+
validateUdtKind(opt.udtKind);
50+
51+
if (!opt.privkey) {
52+
throw new Error('--privkey is required!');
53+
}
54+
55+
const ckb = new CKB({ network });
56+
const txHash = await ckb.udtDestroy({
57+
privateKey: opt.privkey,
58+
kind: opt.udtKind,
59+
amount,
60+
typeArgs: validateUdtTypeArgs(opt.udtKind, opt.typeArgs),
61+
});
62+
63+
logTxSuccess(network, txHash, 'destroyed UDT');
64+
}

0 commit comments

Comments
 (0)