From 242c5f4261bf5007b5f06d9e2c37e2bed4a8f1ea Mon Sep 17 00:00:00 2001 From: RetricSu Date: Mon, 30 Mar 2026 13:27:47 +0800 Subject: [PATCH 1/5] feat: replace ckb-transaction-dumper with ccc-based implementation - Rewrite src/tools/ckb-tx-dumper.ts to use ccc Client and molecule codecs - Implement dep_group unpacking using ccc.mol - Remove ckb-transaction-dumper from dependencies - Eliminates external npm dependency for transaction dumping --- .sisyphus/boulder.json | 9 ++ .sisyphus/plans/ckb-tx-dumper.md | 175 ++++++++++++++++++++++++ package.json | 1 - pnpm-lock.yaml | 17 ++- src/tools/ckb-tx-dumper.ts | 228 +++++++++++++++++++++++++++++-- 5 files changed, 412 insertions(+), 18 deletions(-) create mode 100644 .sisyphus/boulder.json create mode 100644 .sisyphus/plans/ckb-tx-dumper.md diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json new file mode 100644 index 0000000..b874504 --- /dev/null +++ b/.sisyphus/boulder.json @@ -0,0 +1,9 @@ +{ + "active_plan": "/Users/retric/Desktop/offckb/.sisyphus/plans/ckb-tx-dumper.md", + "started_at": "2026-03-30T05:21:40.692Z", + "session_ids": [ + "ses_2c33429e4ffezuzNe4AF50Ln2T" + ], + "plan_name": "ckb-tx-dumper", + "agent": "atlas" +} \ No newline at end of file diff --git a/.sisyphus/plans/ckb-tx-dumper.md b/.sisyphus/plans/ckb-tx-dumper.md new file mode 100644 index 0000000..ec6ac60 --- /dev/null +++ b/.sisyphus/plans/ckb-tx-dumper.md @@ -0,0 +1,175 @@ +# Replace ckb-transaction-dumper with ccc-based implementation + +## TL;DR + +> **Quick Summary**: Replace `ckb-transaction-dumper` npm package with a pure ccc-based implementation. +> +> **Deliverables**: +> +> - New `src/tools/ckb-tx-dumper.ts` (replaces old implementation) +> - Removed `ckb-transaction-dumper` from package.json +> +> **Estimated Effort**: Medium (2-3 hours) +> **Parallel Execution**: NO - sequential + +--- + +## Context + +### Request + +Replace `ckb-transaction-dumper` with ccc-based implementation (no external dependencies, use ccc throughout). + +### Current Implementation + +- `src/tools/ckb-tx-dumper.ts` spawns `ckb-transaction-dumper` binary +- Depends on npm package `ckb-transaction-dumper@0.4.2` + +### What TransactionDumper Does + +1. Load transaction (from file or fetch by hash) +2. Resolve cell deps (handle dep_group type) +3. Resolve inputs +4. Output mock transaction JSON for ckb-debugger + +### ccc Molecule Support + +ccc provides full molecule codec: + +- `ccc.molecule.struct()` - for OutPoint { tx_hash, index } +- `ccc.molecule.vector()` - for OutPointVec +- `ccc.Byte32`, `ccc.Uint32LE` - predefined codecs + +No manual bytes parsing needed! + +--- + +## Work Objectives + +### Core Objective + +Replace `ckb-transaction-dumper` with pure ccc implementation. + +### Must Have + +- Keep `DumpOption` interface +- Keep `dumpTransaction()` signature +- Same JSON output format + +### Must NOT Have + +- Breaking API changes +- New dependencies + +--- + +## TODOs + +- [ ] 1. Implement ccc-based transaction dumper + + **What to do**: + + - Rewrite `src/tools/ckb-tx-dumper.ts` + - Use ccc Client for RPC calls + - Use ccc molecule codecs for dep_group unpacking + + **Key implementation**: + + ```typescript + import { ccc } from '@ckb-ccc/core'; + + // OutPoint codec for dep_group unpacking + const OutPointCodec = ccc.molecule.struct({ + txHash: ccc.Byte32, + index: ccc.Uint32LE, + }); + + const OutPointVecCodec = ccc.molecule.vector(OutPointCodec); + + // Unpack dep_group data + function unpackDepGroup(data: string): ccc.OutPoint[] { + return OutPointVecCodec.decode(data).map((o) => + ccc.OutPoint.from({ txHash: o.txHash, index: '0x' + o.index.toString(16) }), + ); + } + ``` + + **Acceptance Criteria**: + + - [ ] Uses ccc Client for RPC + - [ ] Uses ccc molecule for dep_group + - [ ] Same output format + + **QA Scenarios**: + + ``` + Scenario: Compiles successfully + Tool: Bash + Steps: npm run typecheck + Expected: No errors + ``` + + **Commit**: `feat: implement transaction dumper with ccc` + +--- + +- [ ] 2. Remove ckb-transaction-dumper dependency + + **What to do**: + + - Remove from `package.json` + - Run `pnpm install` + + **Commit**: `chore: remove ckb-transaction-dumper` + +--- + +## Verification + +```bash +npm run typecheck +npm run lint +grep -c "ckb-transaction-dumper" package.json || echo "Clean" +``` + +## Key Implementation Notes + +### Dep Group Unpacking with ccc + +```typescript +const OutPointCodec = ccc.molecule.struct({ + txHash: ccc.Byte32, + index: ccc.Uint32LE, +}); +const OutPointVecCodec = ccc.molecule.vector(OutPointCodec); + +// Usage +const outpoints = OutPointVecCodec.decode(cellData); +``` + +### Mock Transaction Structure + +```typescript +interface MockTransaction { + mock_info: { + inputs: MockInput[]; + cell_deps: MockCellDep[]; + header_deps: any[]; + }; + tx: Transaction; +} +``` + +### Algorithm + +1. Load tx from file +2. For each cell_dep: + - Fetch cell + - If dep_type === 'dep_group': + - Decode cell.data as OutPointVec + - Fetch each referenced cell + - Add to mock_info.cell_deps +3. For each input: + - Fetch referenced cell + - Add to mock_info.inputs +4. Write JSON output diff --git a/package.json b/package.json index f072412..f437fe5 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "blessed": "0.1.81", "chalk": "4.1.2", "child_process": "^1.0.2", - "ckb-transaction-dumper": "^0.4.2", "commander": "^12.0.0", "http-proxy": "^1.18.1", "https-proxy-agent": "^7.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e710702..24f72dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,9 +32,6 @@ importers: child_process: specifier: ^1.0.2 version: 1.0.2 - ckb-transaction-dumper: - specifier: ^0.4.2 - version: 0.4.2 commander: specifier: ^12.0.0 version: 12.1.0 @@ -953,41 +950,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1344,10 +1349,6 @@ packages: cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - ckb-transaction-dumper@0.4.2: - resolution: {integrity: sha512-0frB1FYY3dlKLlef6ps8dfuwTXitdm4myYTc/+hAP3RCo/OrC1/MYXEedUCLXgqcXBnlOQ8VbK5PseNZovWPHQ==} - hasBin: true - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -4753,8 +4754,6 @@ snapshots: cjs-module-lexer@2.2.0: {} - ckb-transaction-dumper@0.4.2: {} - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 diff --git a/src/tools/ckb-tx-dumper.ts b/src/tools/ckb-tx-dumper.ts index 212aa33..4f12c64 100644 --- a/src/tools/ckb-tx-dumper.ts +++ b/src/tools/ckb-tx-dumper.ts @@ -1,6 +1,5 @@ -import path from 'path'; -import { execSync } from 'child_process'; -import { packageRootPath } from '../cfg/setting'; +import fs from 'fs'; +import { ccc } from '@ckb-ccc/core'; import { logger } from '../util/logger'; export interface DumpOption { @@ -9,15 +8,228 @@ export interface DumpOption { outputFilePath: string; } -export function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: DumpOption) { - const ckbTransactionDumperPath = path.resolve(packageRootPath, 'node_modules/.bin/ckb-transaction-dumper'); +const OutPointCodec = ccc.mol.struct({ + txHash: ccc.mol.Byte32, + index: ccc.mol.Uint32LE, +}); - const command = `${ckbTransactionDumperPath} --rpc ${rpc} --tx "${txJsonFilePath}" --output "${outputFilePath}"`; +const OutPointVecCodec = ccc.mol.vector(OutPointCodec); +interface MockCellDep { + cell_dep: { + out_point: { + tx_hash: string; + index: string; + }; + dep_type: string; + }; + output: MockOutput; + data: string; +} + +interface MockInput { + input: { + previous_output: { + tx_hash: string; + index: string; + }; + since: string; + }; + output: MockOutput; + data: string; +} + +interface MockOutput { + capacity: string; + lock: MockScript; + type?: MockScript | null; +} + +interface MockScript { + code_hash: string; + hash_type: string; + args: string; +} + +interface MockTransaction { + mock_info: { + inputs: MockInput[]; + cell_deps: MockCellDep[]; + header_deps: string[]; + }; + tx: { + version: string; + cell_deps: { + out_point: { + tx_hash: string; + index: string; + }; + dep_type: string; + }[]; + header_deps: string[]; + inputs: { + previous_output: { + tx_hash: string; + index: string; + }; + since: string; + }[]; + outputs: MockOutput[]; + outputs_data: string[]; + witnesses: string[]; + }; +} + +function toMockScript(script: ccc.Script | undefined): MockScript | undefined { + if (!script) return undefined; + return { + code_hash: script.codeHash, + hash_type: script.hashType, + args: script.args, + }; +} + +async function resolveCellDeps(client: ccc.Client, cellDeps: ccc.CellDep[]): Promise { + const resolved: MockCellDep[] = []; + + for (const cellDep of cellDeps) { + const cell = await client.getCell(cellDep.outPoint); + if (!cell) { + throw new Error(`Cell not found: ${JSON.stringify(cellDep.outPoint)}`); + } + + if (cellDep.depType === 'depGroup') { + const data = cell.outputData; + if (data && data !== '0x') { + const outpoints = OutPointVecCodec.decode(data); + for (const op of outpoints) { + const outPoint = ccc.OutPoint.from({ + txHash: op.txHash, + index: '0x' + op.index.toString(16), + }); + const refCell = await client.getCell(outPoint); + if (refCell) { + resolved.push({ + cell_dep: { + out_point: { + tx_hash: outPoint.txHash, + index: outPoint.index.toString(), + }, + dep_type: 'code', + }, + output: { + capacity: refCell.cellOutput.capacity.toString(), + lock: toMockScript(refCell.cellOutput.lock)!, + type: toMockScript(refCell.cellOutput.type), + }, + data: refCell.outputData, + }); + } + } + } + } else { + resolved.push({ + cell_dep: { + out_point: { + tx_hash: cellDep.outPoint.txHash, + index: cellDep.outPoint.index.toString(), + }, + dep_type: cellDep.depType, + }, + output: { + capacity: cell.cellOutput.capacity.toString(), + lock: toMockScript(cell.cellOutput.lock)!, + type: toMockScript(cell.cellOutput.type), + }, + data: cell.outputData, + }); + } + } + + return resolved; +} + +async function resolveInputs(client: ccc.Client, inputs: ccc.CellInput[]): Promise { + const resolved: MockInput[] = []; + + for (const input of inputs) { + const cell = await client.getCell(input.previousOutput); + if (!cell) { + throw new Error(`Input cell not found: ${JSON.stringify(input.previousOutput)}`); + } + + resolved.push({ + input: { + previous_output: { + tx_hash: input.previousOutput.txHash, + index: input.previousOutput.index.toString(), + }, + since: input.since.toString(), + }, + output: { + capacity: cell.cellOutput.capacity.toString(), + lock: toMockScript(cell.cellOutput.lock)!, + type: toMockScript(cell.cellOutput.type), + }, + data: cell.outputData, + }); + } + + return resolved; +} + +export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: DumpOption) { try { - execSync(command, { stdio: 'inherit' }); + const client = new ccc.ClientPublicTestnet({ + url: rpc, + fallbacks: [], + }); + + const txJson = JSON.parse(fs.readFileSync(txJsonFilePath, 'utf-8')); + const tx = ccc.Transaction.from(txJson); + + const [cell_deps, inputs] = await Promise.all([ + resolveCellDeps(client, tx.cellDeps), + resolveInputs(client, tx.inputs), + ]); + + const mockTx: MockTransaction = { + mock_info: { + inputs, + cell_deps, + header_deps: tx.headerDeps.map((h) => h.toString()), + }, + tx: { + version: tx.version.toString(), + cell_deps: tx.cellDeps.map((dep) => ({ + out_point: { + tx_hash: dep.outPoint.txHash, + index: dep.outPoint.index.toString(), + }, + dep_type: dep.depType, + })), + header_deps: tx.headerDeps.map((h) => h.toString()), + inputs: tx.inputs.map((input) => ({ + previous_output: { + tx_hash: input.previousOutput.txHash, + index: input.previousOutput.index.toString(), + }, + since: input.since.toString(), + })), + outputs: tx.outputs.map((output) => ({ + capacity: output.capacity.toString(), + lock: toMockScript(output.lock)!, + type: toMockScript(output.type), + })), + outputs_data: tx.outputsData, + witnesses: tx.witnesses.map((w) => w.toString()), + }, + }; + + fs.writeFileSync(outputFilePath, JSON.stringify(mockTx, null, 2)); logger.debug('Dump transaction successfully'); } catch (error: unknown) { - logger.error('Command failed:', (error as Error).message); + logger.error('Failed to dump transaction:', (error as Error).message); + throw error; } } From c300786ec4844f67c8577fffb43e95a0333920b8 Mon Sep 17 00:00:00 2001 From: RetricSu Date: Mon, 30 Mar 2026 13:34:44 +0800 Subject: [PATCH 2/5] chore: add changeset for ckb-transaction-dumper replacement --- .changeset/wise-candies-stop.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/wise-candies-stop.md diff --git a/.changeset/wise-candies-stop.md b/.changeset/wise-candies-stop.md new file mode 100644 index 0000000..9fec1cb --- /dev/null +++ b/.changeset/wise-candies-stop.md @@ -0,0 +1,9 @@ +--- +'@offckb/cli': minor +--- + +Replace ckb-transaction-dumper with ccc-based implementation + +- Rewrite transaction dumper to use ccc Client and molecule codecs +- Implement dep_group unpacking using ccc.mol +- Remove ckb-transaction-dumper npm dependency From 7a4ac6d18fff49ed17203fef7395a127030c7e6b Mon Sep 17 00:00:00 2001 From: RetricSu Date: Mon, 30 Mar 2026 14:20:23 +0800 Subject: [PATCH 3/5] fix: address PR review comments for ckb-tx-dumper - Remove .sisyphus/boulder.json and add to .gitignore - Fix type field serialization: emit null instead of undefined - Fix dep_type format: convert camelCase to snake_case (depGroup -> dep_group) - Fix depGroup error handling: throw error when refCell not found - Fix async race condition: make buildTxFileOptionBy and callers async --- .gitignore | 3 +++ .sisyphus/boulder.json | 9 ------- src/cli.ts | 4 +-- src/cmd/debug.ts | 12 ++++----- src/tools/ckb-tx-dumper.ts | 52 +++++++++++++++++++++++--------------- 5 files changed, 42 insertions(+), 38 deletions(-) delete mode 100644 .sisyphus/boulder.json diff --git a/.gitignore b/.gitignore index 10e5f20..4a2881d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ package-lock.json # Test coverage coverage/ *.lcov + +# Sisyphus plans and sessions +.sisyphus/ diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json deleted file mode 100644 index b874504..0000000 --- a/.sisyphus/boulder.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "active_plan": "/Users/retric/Desktop/offckb/.sisyphus/plans/ckb-tx-dumper.md", - "started_at": "2026-03-30T05:21:40.692Z", - "session_ids": [ - "ses_2c33429e4ffezuzNe4AF50Ln2T" - ], - "plan_name": "ckb-tx-dumper", - "agent": "atlas" -} \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index dc82791..c1ffa21 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -81,9 +81,9 @@ program const txHash = option.txHash; if (option.singleScript) { const { cellType, cellIndex, scriptType } = parseSingleScriptOption(option.singleScript); - return debugSingleScript(txHash, cellIndex, cellType, scriptType, option.network, option.bin); + return await debugSingleScript(txHash, cellIndex, cellType, scriptType, option.network, option.bin); } - return debugTransaction(txHash, option.network); + return await debugTransaction(txHash, option.network); }); program diff --git a/src/cmd/debug.ts b/src/cmd/debug.ts index 5acbcdc..5564f6c 100644 --- a/src/cmd/debug.ts +++ b/src/cmd/debug.ts @@ -8,8 +8,8 @@ import { Network } from '../type/base'; import { encodeBinPathForTerminal } from '../util/encoding'; import { logger } from '../util/logger'; -export function debugTransaction(txHash: string, network: Network) { - const txFile = buildTxFileOptionBy(txHash, network); +export async function debugTransaction(txHash: string, network: Network) { + const txFile = await buildTxFileOptionBy(txHash, network); const opts = buildTransactionDebugOptions(txHash, network); for (const opt of opts) { logger.section(opt.name, [], 'info'); @@ -48,7 +48,7 @@ export function buildTransactionDebugOptions(txHash: string, network: Network) { return result; } -export function debugSingleScript( +export async function debugSingleScript( txHash: string, cellIndex: number, cellType: 'input' | 'output', @@ -56,7 +56,7 @@ export function debugSingleScript( network: Network, bin?: string, ) { - const txFile = buildTxFileOptionBy(txHash, network); + const txFile = await buildTxFileOptionBy(txHash, network); let opt = `--cell-index ${cellIndex} --cell-type ${cellType} --script-group-type ${scriptType}`; if (bin) { opt = opt + ` --bin ${bin}`; @@ -81,7 +81,7 @@ export function parseSingleScriptOption(value: string) { }; } -export function buildTxFileOptionBy(txHash: string, network: Network) { +export async function buildTxFileOptionBy(txHash: string, network: Network) { const settings = readSettings(); const outputFilePath = buildDebugFullTransactionFilePath(network, txHash); if (!fs.existsSync(outputFilePath)) { @@ -90,7 +90,7 @@ export function buildTxFileOptionBy(txHash: string, network: Network) { if (!fs.existsSync(outputFilePath)) { fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); } - dumpTransaction({ rpc, txJsonFilePath, outputFilePath }); + await dumpTransaction({ rpc, txJsonFilePath, outputFilePath }); } const opt = `--tx-file ${encodeBinPathForTerminal(outputFilePath)}`; return opt; diff --git a/src/tools/ckb-tx-dumper.ts b/src/tools/ckb-tx-dumper.ts index 4f12c64..7af6bcf 100644 --- a/src/tools/ckb-tx-dumper.ts +++ b/src/tools/ckb-tx-dumper.ts @@ -42,7 +42,7 @@ interface MockInput { interface MockOutput { capacity: string; lock: MockScript; - type?: MockScript | null; + type: MockScript | null; } interface MockScript { @@ -80,8 +80,8 @@ interface MockTransaction { }; } -function toMockScript(script: ccc.Script | undefined): MockScript | undefined { - if (!script) return undefined; +function toMockScript(script: ccc.Script | undefined): MockScript | null { + if (!script) return null; return { code_hash: script.codeHash, hash_type: script.hashType, @@ -89,6 +89,12 @@ function toMockScript(script: ccc.Script | undefined): MockScript | undefined { }; } +function toDepType(depType: string): string { + // Convert camelCase to snake_case for CKB JSON format + if (depType === 'depGroup') return 'dep_group'; + return depType; +} + async function resolveCellDeps(client: ccc.Client, cellDeps: ccc.CellDep[]): Promise { const resolved: MockCellDep[] = []; @@ -108,23 +114,27 @@ async function resolveCellDeps(client: ccc.Client, cellDeps: ccc.CellDep[]): Pro index: '0x' + op.index.toString(16), }); const refCell = await client.getCell(outPoint); - if (refCell) { - resolved.push({ - cell_dep: { - out_point: { - tx_hash: outPoint.txHash, - index: outPoint.index.toString(), - }, - dep_type: 'code', - }, - output: { - capacity: refCell.cellOutput.capacity.toString(), - lock: toMockScript(refCell.cellOutput.lock)!, - type: toMockScript(refCell.cellOutput.type), - }, - data: refCell.outputData, - }); + if (!refCell) { + logger.error( + `Failed to resolve cell for depGroup out_point: tx_hash=${outPoint.txHash}, index=${outPoint.index.toString()}`, + ); + throw new Error('Failed to resolve all cells referenced by depGroup.'); } + resolved.push({ + cell_dep: { + out_point: { + tx_hash: outPoint.txHash, + index: outPoint.index.toString(), + }, + dep_type: 'code', + }, + output: { + capacity: refCell.cellOutput.capacity.toString(), + lock: toMockScript(refCell.cellOutput.lock)!, + type: toMockScript(refCell.cellOutput.type), + }, + data: refCell.outputData, + }); } } } else { @@ -134,7 +144,7 @@ async function resolveCellDeps(client: ccc.Client, cellDeps: ccc.CellDep[]): Pro tx_hash: cellDep.outPoint.txHash, index: cellDep.outPoint.index.toString(), }, - dep_type: cellDep.depType, + dep_type: toDepType(cellDep.depType), }, output: { capacity: cell.cellOutput.capacity.toString(), @@ -206,7 +216,7 @@ export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: D tx_hash: dep.outPoint.txHash, index: dep.outPoint.index.toString(), }, - dep_type: dep.depType, + dep_type: toDepType(dep.depType), })), header_deps: tx.headerDeps.map((h) => h.toString()), inputs: tx.inputs.map((input) => ({ From 1ebee290dcc611867e50ac22e96b6f85d30a9efb Mon Sep 17 00:00:00 2001 From: RetricSu Date: Mon, 30 Mar 2026 16:13:23 +0800 Subject: [PATCH 4/5] fix: fix CCC-based transaction dumper for debug command - Use cccA.JsonRpcTransformers.transactionTo() to handle snake_case input format - Convert all numeric values to hex format (since, index, version, capacity) - Add missing dep_group cell to mock_info.cell_deps before expansion - Add fs.mkdirSync to ensure output directory exists Fixes issues where debug command failed with 'undefined is not iterable', 'unprovided cell dep', or 'since is not a legal u64' errors. --- src/tools/ckb-tx-dumper.ts | 44 +++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/tools/ckb-tx-dumper.ts b/src/tools/ckb-tx-dumper.ts index 7af6bcf..16b96de 100644 --- a/src/tools/ckb-tx-dumper.ts +++ b/src/tools/ckb-tx-dumper.ts @@ -1,5 +1,7 @@ import fs from 'fs'; +import path from 'path'; import { ccc } from '@ckb-ccc/core'; +import { cccA } from '@ckb-ccc/core/advanced'; import { logger } from '../util/logger'; export interface DumpOption { @@ -105,6 +107,21 @@ async function resolveCellDeps(client: ccc.Client, cellDeps: ccc.CellDep[]): Pro } if (cellDep.depType === 'depGroup') { + resolved.push({ + cell_dep: { + out_point: { + tx_hash: cellDep.outPoint.txHash, + index: '0x' + cellDep.outPoint.index.toString(16), + }, + dep_type: toDepType(cellDep.depType), + }, + output: { + capacity: '0x' + cell.cellOutput.capacity.toString(16), + lock: toMockScript(cell.cellOutput.lock)!, + type: toMockScript(cell.cellOutput.type), + }, + data: cell.outputData, + }); const data = cell.outputData; if (data && data !== '0x') { const outpoints = OutPointVecCodec.decode(data); @@ -124,12 +141,12 @@ async function resolveCellDeps(client: ccc.Client, cellDeps: ccc.CellDep[]): Pro cell_dep: { out_point: { tx_hash: outPoint.txHash, - index: outPoint.index.toString(), + index: '0x' + outPoint.index.toString(16), }, dep_type: 'code', }, output: { - capacity: refCell.cellOutput.capacity.toString(), + capacity: '0x' + refCell.cellOutput.capacity.toString(16), lock: toMockScript(refCell.cellOutput.lock)!, type: toMockScript(refCell.cellOutput.type), }, @@ -142,12 +159,12 @@ async function resolveCellDeps(client: ccc.Client, cellDeps: ccc.CellDep[]): Pro cell_dep: { out_point: { tx_hash: cellDep.outPoint.txHash, - index: cellDep.outPoint.index.toString(), + index: '0x' + cellDep.outPoint.index.toString(16), }, dep_type: toDepType(cellDep.depType), }, output: { - capacity: cell.cellOutput.capacity.toString(), + capacity: '0x' + cell.cellOutput.capacity.toString(16), lock: toMockScript(cell.cellOutput.lock)!, type: toMockScript(cell.cellOutput.type), }, @@ -172,12 +189,12 @@ async function resolveInputs(client: ccc.Client, inputs: ccc.CellInput[]): Promi input: { previous_output: { tx_hash: input.previousOutput.txHash, - index: input.previousOutput.index.toString(), + index: '0x' + input.previousOutput.index.toString(16), }, - since: input.since.toString(), + since: '0x' + input.since.toString(16), }, output: { - capacity: cell.cellOutput.capacity.toString(), + capacity: '0x' + cell.cellOutput.capacity.toString(16), lock: toMockScript(cell.cellOutput.lock)!, type: toMockScript(cell.cellOutput.type), }, @@ -196,7 +213,7 @@ export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: D }); const txJson = JSON.parse(fs.readFileSync(txJsonFilePath, 'utf-8')); - const tx = ccc.Transaction.from(txJson); + const tx = cccA.JsonRpcTransformers.transactionTo(txJson); const [cell_deps, inputs] = await Promise.all([ resolveCellDeps(client, tx.cellDeps), @@ -210,11 +227,11 @@ export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: D header_deps: tx.headerDeps.map((h) => h.toString()), }, tx: { - version: tx.version.toString(), + version: '0x' + tx.version.toString(16), cell_deps: tx.cellDeps.map((dep) => ({ out_point: { tx_hash: dep.outPoint.txHash, - index: dep.outPoint.index.toString(), + index: '0x' + dep.outPoint.index.toString(16), }, dep_type: toDepType(dep.depType), })), @@ -222,12 +239,12 @@ export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: D inputs: tx.inputs.map((input) => ({ previous_output: { tx_hash: input.previousOutput.txHash, - index: input.previousOutput.index.toString(), + index: '0x' + input.previousOutput.index.toString(16), }, - since: input.since.toString(), + since: '0x' + input.since.toString(16), })), outputs: tx.outputs.map((output) => ({ - capacity: output.capacity.toString(), + capacity: '0x' + output.capacity.toString(16), lock: toMockScript(output.lock)!, type: toMockScript(output.type), })), @@ -236,6 +253,7 @@ export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: D }, }; + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); fs.writeFileSync(outputFilePath, JSON.stringify(mockTx, null, 2)); logger.debug('Dump transaction successfully'); } catch (error: unknown) { From f72d68b040c7e3ea25e34226c695643a50cf3320 Mon Sep 17 00:00:00 2001 From: RetricSu Date: Mon, 30 Mar 2026 16:43:07 +0800 Subject: [PATCH 5/5] fix: address Copilot PR review comments for ccc-based tx dumper - Select ClientPublicTestnet/Mainnet based on RPC URL pattern - Use proper error type checking with instanceof Error --- src/tools/ckb-tx-dumper.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/tools/ckb-tx-dumper.ts b/src/tools/ckb-tx-dumper.ts index 16b96de..c97ffaf 100644 --- a/src/tools/ckb-tx-dumper.ts +++ b/src/tools/ckb-tx-dumper.ts @@ -207,10 +207,16 @@ async function resolveInputs(client: ccc.Client, inputs: ccc.CellInput[]): Promi export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: DumpOption) { try { - const client = new ccc.ClientPublicTestnet({ - url: rpc, - fallbacks: [], - }); + const isTestnet = /testnet/i.test(rpc); + const client = isTestnet + ? new ccc.ClientPublicTestnet({ + url: rpc, + fallbacks: [], + }) + : new ccc.ClientPublicMainnet({ + url: rpc, + fallbacks: [], + }); const txJson = JSON.parse(fs.readFileSync(txJsonFilePath, 'utf-8')); const tx = cccA.JsonRpcTransformers.transactionTo(txJson); @@ -257,7 +263,7 @@ export async function dumpTransaction({ rpc, txJsonFilePath, outputFilePath }: D fs.writeFileSync(outputFilePath, JSON.stringify(mockTx, null, 2)); logger.debug('Dump transaction successfully'); } catch (error: unknown) { - logger.error('Failed to dump transaction:', (error as Error).message); + logger.error('Failed to dump transaction:', error instanceof Error ? error.message : String(error)); throw error; } }