Analysis Date: 2026-02-17
Tooling:
ghCLI is NOT available and must NOT be installed. PR and review workflows are handled inline by the AI Coworker (see AGENTS.md).
Legacy vs. New code:
@ickb/lumos-utils@1.4.2and@ickb/v1-core@1.4.2are LEGACY and DEPRECATED npm packages. The apps (apps/bot,apps/tester,apps/interface) still depend on them.- The
packages/directory contains the NEW replacement libraries built on CCC (ckb-ccc), which will eventually replace the legacy packages in the apps. - All
@ckb-lumos/*packages are DEPRECATED -- Lumos is being replaced by CCC. - CCC PRs for UDT and Epochs have been MERGED upstream -- those features now exist in CCC itself.
SmartTransactionwas DELETED in Phase 1 in favor of CCC's client cache for header caching. Headers are now fetched inline via CCC client calls (client.getTransactionWithHeader(),client.getHeaderByNumber()). All manager method signatures now acceptccc.TransactionLikeand returnccc.Transactiondirectly.- CCC is sometimes overridden locally via
bash forks/forker/record.shand.pnpmfile.cjsfor testing unpublished changes.
When writing new code: Use CCC (@ckb-ccc/core) types and patterns exclusively in packages/. Never introduce new Lumos dependencies.
Files:
- Use
snake_casefor multi-word source files:owned_owner.ts - Use single lowercase words when possible:
cells.ts,entities.ts,logic.ts,codec.ts,utils.ts,heap.ts,udt.ts - Every package has an
index.tsbarrel file that re-exports everything - Config files at root use dot-prefix convention:
prettier.config.cjs,eslint.config.mjs,vitest.config.mts
Functions:
- Use
camelCasefor all functions:binarySearch,asyncBinarySearch,hexFrom,isHex,collect - Prefix boolean-returning functions with
is:isHex(),isDeposit(),isUdt(),isReceipt(),isCkb2Udt(),isMatchable(),isFulfilled() - Use
tryFromfor fallible constructors that returnundefinedon failure:OrderCell.tryFrom(),OrderGroup.tryFrom() - Use
mustFromfor throwing constructors:OrderCell.mustFrom() - Use
fromfor static factory methods:Ratio.from(),Info.from(),Epoch.from(),MasterCell.from() - Use
validate()for throwing validation andisValid()for boolean validation -- always as a pair
Variables:
- Use
camelCasefor variables and parameters:ckbScale,udtScale,tipHeader,feeRate - Use
UPPER_SNAKE_CASEfor constants:ICKB_SOFT_CAP_PER_DEPOSIT,ICKB_DEPOSIT_CAP - Prefix private module-level mutable state with underscore:
_knownHeaders,_knownTxsOutputs
Types/Interfaces:
- Use
PascalCasefor types, interfaces, and classes:Ratio,Info,OrderData,OrderCell,Epoch - Suffix data-transfer / input interfaces with
Like:InfoLike,RelativeLike,OrderDataLike,MasterLike,EpochLike - The
Liketype is the "encodable" or input representation; the plain name is the decoded/validated form - Use
ValueComponentsinterface for anything withckbValueandudtValueproperties
Classes:
- Use
PascalCase:MinHeap,BufferedGenerator,UdtManager,DaoManager,LogicManager,OrderManager,OwnedOwnerManager,IckbSdk - Manager classes implement
ScriptDepsinterface and containscriptandcellDepsproperties - Generic type parameters use single capital letters:
<T>,<K>
Formatting:
- Prettier with
prettier-plugin-organize-imports - Double quotes (not single quotes):
singleQuote: false - Trailing commas everywhere:
trailingComma: "all" - Config:
prettier.config.cjs - Interface app additionally uses
prettier-plugin-tailwindcssviaapps/interface/.prettierrc
Linting:
- ESLint with
typescript-eslintstrict type-checked config - Root config:
eslint.config.mjs - Interface has its own ESLint config:
apps/interface/eslint.config.mjs(adds React plugins) - Key enforced rule:
@typescript-eslint/explicit-function-return-type: "error"-- every function must have an explicit return type annotation - Strict type checking enabled (
tseslint.configs.strictTypeChecked)
TypeScript Compiler:
- Root
tsconfig.jsontargetsES2020withNodeNextmodule resolution strict: truewith additional strict checks:noUnusedLocals: truenoUnusedParameters: truenoFallthroughCasesInSwitch: truenoUncheckedIndexedAccess: truenoImplicitOverride: truenoImplicitAny: truenoEmitOnError: true
verbatimModuleSyntax: true-- useimport typefor type-only importsdeclaration: true,declarationMap: true,sourceMap: trueremoveComments: true,stripInternal: true-- comments stripped from output,@internalmembers excluded from .d.ts- Required Node.js version:
>=24
Order (enforced by prettier-plugin-organize-imports):
- External dependencies (
@ckb-ccc/core,@ckb-lumos/*,crypto,process) - Internal workspace packages (
@ickb/utils,@ickb/core,@ickb/dao,@ickb/order) - Relative imports (
./entities.js,./cells.js,./utils.js)
Style:
- Always use
.jsextension in relative imports (required byNodeNextresolution):import { gcd } from "./utils.js"; - Use
import typefor type-only imports:import type { UdtHandler } from "./udt.js"; - Mixed imports combine values and types:
import { unique, type ValueComponents } from "./utils.js"; - Destructure imports at the top:
import { ccc, mol } from "@ckb-ccc/core"; - Named exports only -- no default exports anywhere in the codebase
Path Aliases:
- None. All imports use bare specifiers for packages and relative paths within packages.
Patterns:
- Throw
Errordirectly -- never custom error classes exceptErrorTransactionInsufficientCoininpackages/utils/src/udt.ts:
throw Error("Ratio invalid: not empty, not populated");
throw Error("iCKB deposit minimum is 1082 CKB");
throw Error("Header not found");- validate() / isValid() pair -- consistent throughout the codebase. Use this pair on all domain entities:
validate(): void {
if (/* invalid condition */) {
throw Error("Description of what is wrong");
}
}
isValid(): boolean {
try {
this.validate();
return true;
} catch {
return false;
}
}This pattern appears in: Ratio (packages/order/src/entities.ts), Info (same file), Relative (same file), OrderData (same file), OrderCell (packages/order/src/cells.ts), OrderGroup (same file), MasterCell (same file)
- tryFrom / mustFrom factory pair for parsing from raw blockchain data:
// packages/order/src/cells.ts
static tryFrom(cell: ccc.Cell): OrderCell | undefined {
try {
return OrderCell.mustFrom(cell);
} catch {
return undefined;
}
}
static mustFrom(cell: ccc.Cell): OrderCell {
const data = OrderData.decode(cell.outputData);
data.validate();
// ... construct and return
}- Env var validation at app entry -- check and throw immediately:
// apps/bot/src/index.ts, apps/tester/src/index.ts
if (!CHAIN) {
throw Error("Invalid env CHAIN: Empty");
}
if (!isChain(CHAIN)) {
throw Error("Invalid env CHAIN: " + CHAIN);
}- Async error handling in app loops -- catch, log structured JSON, continue:
// apps/bot/src/index.ts, apps/tester/src/index.ts, apps/faucet/src/main.ts
try {
// ... main logic
} catch (e) {
if (e instanceof Object && "stack" in e) {
/* eslint-disable-next-line @typescript-eslint/no-misused-spread */
executionLog.error = { ...e, stack: e.stack ?? "" };
} else {
executionLog.error = e ?? "Empty Error";
}
}
console.log(JSON.stringify(executionLog, replacer, " "));Framework: console only -- no logging framework.
Patterns:
- Apps log structured JSON via
console.log(JSON.stringify(executionLog, replacer, " "))wherereplacerconvertsbiginttonumber - The sampler app (
apps/sampler/src/index.ts) logs CSV output directly - Library packages (
packages/*) do not log -- they only throw errors
BigInt serialization helper used in apps:
function replacer(_: unknown, value: unknown): unknown {
return typeof value === "bigint" ? Number(value) : value;
}When to Comment:
- Use JSDoc (
/** ... */) for all public functions, methods, classes, and interfaces inpackages/ - Include
@param,@returns,@throws,@example,@remarkstags as appropriate - Use
@internaltag for functions that should be excluded from generated declarations - Use
@creditsfor code ported from other languages (Go standard library translations inpackages/utils/src/utils.ts,packages/utils/src/heap.ts) - Use
@linkfor referencing external resources - Inline comments (
//) explain non-obvious logic like bit operations or mathematical formulas - The sampler app uses
@packageDocumentationat the module level
TypeDoc / Documentation:
- TypeDoc generates API documentation from JSDoc, configured via
typedoc.base.json - Sort order:
source-order, alphabetical, kind - Source links point to GitHub master branch
- Each package has its own
typedoc.jsonextending the base
eslint-disable comments are used sparingly for known-safe patterns:
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
/* eslint-disable-next-line @typescript-eslint/no-misused-spread */
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */Size: Functions are generally compact (under 50 lines). Longer functions exist in the apps for transaction orchestration but are organized into clear named sub-functions.
Parameters:
- Use object destructuring for multi-field inputs:
{ ckbScale, udtScale },{ cell, data, ckbUnoccupied, ... } - Use
options?objects for optional parameters with defaults:
// packages/dao/src/dao.ts
async *findDeposits(
client: ccc.Client,
locks: ccc.Script[],
options?: {
tip?: ccc.ClientBlockHeader;
onChain?: boolean;
minLockUp?: Epoch;
maxLockUp?: Epoch;
limit?: number;
},
): AsyncGenerator<DaoCell> { ... }- Use variadic args:
sum(res: bigint, ...rest: bigint[]),isOwner(...locks: ccc.Script[])
Return Values:
- Use tuples for multi-value returns:
Promise<[number, boolean]>,[ccc.FixedPoint, ccc.FixedPoint] - Use
| undefinedinstead ofnullfor missing values:OrderCell | undefined - Use explicit
voidreturn for side-effect-only functions - All return types must be explicit (enforced by ESLint)
Exports:
- Every package uses barrel exports via
index.ts:export * from "./cells.js"; - All exports are named, never default
- Type-only exports use
export typein conjunction withverbatimModuleSyntax
Barrel Files:
- Located at
packages/*/src/index.ts - Re-export everything from each source module
- No logic in barrel files
- Example (
packages/utils/src/index.ts):
export * from "./codec.js";
export * from "./heap.js";
export * from "./udt.js";
export * from "./utils.js";Package structure (identical for all packages/*):
src/index.ts- barrel exportssrc/*.ts- source filespackage.json- with"type": "module","sideEffects": false,"main": "dist/index.js","types": "dist/index.d.ts"tsconfig.json- extends root../../tsconfig.json, setsrootDir: "src",outDir: "dist"vitest.config.mts- test configuration (includessrc/**/*.test.ts)
TS codecs must match the Molecule schema at forks/contracts/schemas/encoding.mol. The on-chain contracts use Molecule for serialization; the TS packages must produce byte-identical encodings.
Entity classes use CCC's ccc.Entity.Base with decorator-based codec definition:
// packages/order/src/entities.ts
@ccc.codec(
mol.struct({
ckbScale: mol.Uint64,
udtScale: mol.Uint64,
}),
)
export class Ratio extends ccc.Entity.Base<ExchangeRatio, Ratio>() {
constructor(
public ckbScale: ccc.Num,
public udtScale: ccc.Num,
) {
super();
}
static override from(ratio: ExchangeRatio): Ratio {
if (ratio instanceof Ratio) {
return ratio;
}
const { ckbScale, udtScale } = ratio;
return new Ratio(ckbScale, udtScale);
}
}Key conventions for entity classes:
- Two generic parameters:
<LikeType, SelfType> - Constructor takes decoded/validated fields with
publicmodifier - Static
from()method overrides base, short-circuits oninstanceofcheck validate()andisValid()pair for validation- Static helper constructors:
Ratio.empty(),Info.create()
- Use
bigintfor all blockchain numeric values (capacity, amounts, block numbers, epochs) - Use
0nfor zero,1nfor one - Use
ccc.Num(alias forbigint) andccc.FixedPoint(alias forbigint) for type clarity - Bit operations on bigints:
1n << BigInt(this.ckbMinMatchLog),n >> 1 - Use
Number()only when interfacing with JS APIs that require it - Formatting for display:
ccc.fixedPointToString()or customfmtCkb()in apps
- Use
Object.freeze()extensively for shared data:
// apps/bot/src/index.ts
const frozenResult = Object.freeze(result);
Object.freeze(tx.outputs.map(...));
let origins: readonly I8Cell[] = Object.freeze([]);- Use
readonlyon class fields and interface members where appropriate - Use
Readonly<T>wrapper type for maps and objects:Readonly<Map<string, readonly Cell[]>>
Finder methods throughout the library use async * generators for lazy iteration:
// packages/utils/src/udt.ts
async *findUdts(
client: ccc.Client,
locks: ccc.Script[],
options?: { onChain?: boolean; limit?: number },
): AsyncGenerator<UdtCell> {
const limit = options?.limit ?? defaultFindCellsLimit;
for (const lock of unique(locks)) {
// ... RPC query setup ...
for await (const cell of client.findCells(...findCellsArgs)) {
if (!this.isUdt(cell) || !cell.cellOutput.lock.eq(lock)) {
continue;
}
yield { cell, ckbValue: cell.cellOutput.capacity, udtValue: ccc.udtBalanceFrom(cell.outputData), [isUdtSymbol]: true };
}
}
}To collect results into an array, use the collect() helper from packages/utils/src/utils.ts:
const udts = await collect(udtManager.findUdts(client, locks));- Apps use top-level
awaitat the end of the module:
// apps/sampler/src/index.ts
await main();
process.exit(0);- Legacy apps (
apps/bot,apps/tester) usefor (;;)infinite loops with sleep - New apps (
apps/faucet,apps/sampler) also usefor (;;)or run-once patterns - All apps are ESM (
"type": "module"in package.json) - Build command for all:
tsc(compile only, no bundler except forapps/interfacewhich uses Vite)
Convention analysis: 2026-02-17