Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ckb-devrel/ccc/llms.txt
Use this file to discover all available pages before exploring further.
Transaction structure
A CKB transaction consumes existing cells (inputs) and creates new cells (outputs). The Transaction class in CCC represents a fully-formed transaction:
// From packages/core/src/ckb/transaction.ts
export class Transaction {
constructor(
public version: Num,
public cellDeps: CellDep[], // Script code dependencies
public headerDeps: Hex[], // Block header dependencies
public inputs: CellInput[], // Cells being consumed
public outputs: CellOutput[], // New cells being created
public outputsData: Hex[], // Data for each output cell
public witnesses: Hex[], // Signatures and proofs
) {}
}
Each input references an existing on-chain cell:
// From packages/core/src/ckb/transaction.ts
export class CellInput {
constructor(
public previousOutput: OutPoint, // Which cell to consume
public since: Num, // Time-lock (0 = no constraint)
public cellOutput?: CellOutput, // Populated by completeExtraInfos()
public outputData?: Hex, // Populated by completeExtraInfos()
) {}
}
CellOutput
Each output defines a new cell to create:
// From packages/core/src/ckb/transaction.ts
export class CellOutput {
constructor(
public capacity: Num, // Storage space in Shannon
public lock: Script, // Ownership script
public type?: Script, // Asset type script (optional)
) {}
}
CellDep and DepType
Cell dependencies tell the CKB VM where to find the script code that needs to run:
// From packages/core/src/ckb/transaction.ts
export type DepType = "depGroup" | "code";
export class CellDep {
constructor(
public outPoint: OutPoint, // Where the script code lives
public depType: DepType, // "code" = raw code; "depGroup" = group of deps
) {}
}
Creating transactions
Use Transaction.from() to build a transaction from a plain object. Capacity values can be omitted and will be calculated automatically from the cell’s occupied size:
import { ccc } from "@ckb-ccc/ccc";
const tx = ccc.Transaction.from({
outputs: [
{
lock: recipientLockScript,
capacity: ccc.fixedPointFrom("100"), // 100 CKB in Shannon
},
],
});
You can also use Transaction.default() for an empty transaction:
const tx = ccc.Transaction.default();
tx.addOutput({ lock: recipientLock }, "0x"); // capacity auto-calculated
Working with CKB amounts
Capacity is always stored in Shannon (bigint). Use fixedPointFrom() to convert:
import { ccc } from "@ckb-ccc/ccc";
// 61 CKB — the minimum capacity for a plain cell
const minCapacity = ccc.fixedPointFrom("61"); // 6_100_000_000n
// From a number
const amount = ccc.fixedPointFrom(100); // 10_000_000_000n
// Zero constant
const zero = ccc.Zero; // 0n
Completing a transaction
After defining outputs, call these two methods to automatically collect inputs and calculate fees:
Searches the signer’s cells and adds enough inputs to cover the total output capacity:
// From packages/core/src/ckb/transaction.ts
async completeInputsByCapacity(
from: Signer,
capacityTweak?: NumLike,
filter?: ClientCollectableSearchKeyFilterLike,
): Promise<number>
completeFeeBy(signer, feeRate?)
Calculates the transaction fee based on the serialized transaction size, adds inputs if needed, and creates a change output back to the signer’s address:
// From packages/core/src/ckb/transaction.ts
async completeFeeBy(
from: Signer,
feeRate?: NumLike,
filter?: ClientCollectableSearchKeyFilterLike,
options?: {
feeRateBlockRange?: NumLike;
maxFeeRate?: NumLike;
shouldAddInputs?: boolean;
},
): Promise<[number, boolean]>
If feeRate is omitted, the current network fee rate is fetched from the client automatically.
The completeInputsByCapacity and completeFeeBy methods handle all the
complexity of finding UTXOs and calculating fees. For most transfer
transactions, you only need these two calls before sending.
Full transfer example
This is the canonical pattern from the CCC README for transferring CKB:
import { ccc } from "@ckb-ccc/ccc";
async function transferCkb(
signer: ccc.Signer,
toLock: ccc.Script,
amount: string, // e.g. "100" for 100 CKB
) {
// 1. Define what you want to create
const tx = ccc.Transaction.from({
outputs: [
{
lock: toLock,
capacity: ccc.fixedPointFrom(amount),
},
],
});
// 2. Find and add input cells to cover the output capacity
await tx.completeInputsByCapacity(signer);
// 3. Calculate fee and add a change output automatically
await tx.completeFeeBy(signer);
// 4. Sign and broadcast
const txHash = await signer.sendTransaction(tx);
console.log("Transaction sent:", txHash);
return txHash;
}
Transaction lifecycle
The full lifecycle of a transaction goes through three stages:
// Stage 1: Prepare — adds cell deps and dummy witnesses
const prepared = await signer.prepareTransaction(tx);
// Stage 2: Sign — fills in real witnesses/signatures
const signed = await signer.signTransaction(tx);
// Stage 3: Send — broadcasts and returns the tx hash
const txHash = await signer.sendTransaction(tx);
// sendTransaction() calls signTransaction() internally,
// so you only need to call sendTransaction() directly.
Computing hashes
Use hashCkb() to compute CKB-style Blake2b hashes, and transaction.hash() to get the transaction hash:
import { ccc } from "@ckb-ccc/ccc";
// Compute the transaction hash (excludes witnesses)
const txHash: ccc.Hex = tx.hash();
// Compute the full transaction hash (includes witnesses)
const fullHash: ccc.Hex = tx.hashFull();
// Compute a raw CKB hash of arbitrary bytes
const hash = ccc.hashCkb(someBytes);
Advanced: custom change logic
For more control over how change capacity is handled, use completeFee() directly with a custom change function:
const [addedInputs, hasChange] = await tx.completeFee(
signer,
(tx, capacity) => {
// capacity = excess Shannon available for change
const minCellCapacity = ccc.fixedPointFrom("61"); // 61 CKB minimum
if (capacity >= minCellCapacity) {
tx.addOutput({ capacity, lock: changeScript });
return 0; // 0 = done
}
return minCellCapacity; // Return how much more is needed
},
);
Adding cell dependencies manually
When using custom scripts, add their cell dependencies with addCellDepsOfKnownScripts():
// Add deps for a known script
await tx.addCellDepsOfKnownScripts(client, ccc.KnownScript.XUdt);
// Add a raw cell dep
tx.addCellDeps({
outPoint: { txHash: "0x...", index: 0 },
depType: "depGroup",
});