Skip to main content

Molecule

Many CKB contracts require inputting certain data. For example, in the default Secp256K1 script, the Args field requires a public key hash, while the Witness contains signature data. These simple scenarios are easy to handle by directly passing the data or processing it in order. However, when dealing with complex data structures, things get messy quickly.

That’s why many CKB contracts use Molecule, a serialization framework. In fact, even the `Transaction` objects used in CKB contracts are serialized using Molecule.

In C and Rust, Molecule has its own dedicated libraries. In JavaScript, however, it’s integrated into the ckb-js-std package. This article focuses on two key areas:

  • Transaction-related structures
  • How to build custom Molecule structures

Transaction Information

The Transaction Structure of CKB uses Molecule for serialization and deserialization. In JavaScript, ckb-js-std provides utilities to work with these structures.

When using let tx = HighLevel.loadTransaction(); to load the entire transaction, it returns a Transaction structure. You can then inspect various parts of the transaction—such as Witnesses, OutputsData, and more—inside your contract.

(If you only need to access specific fields, it's better to use the corresponding syscalls, as loading the full transaction consumes a large number of cycles. Refer to Examples/load-tx-info for more details.)

Building Custom Molecules

In C and Rust, developers typically use the moleculec CLI tool to generate code. However, it doesn't support JavaScript. For JS, you have two main options:

  • Mimic the Transaction structure using @ckb-js-std/core::mol
  • Use moleculec-es to generate JS code from .mol files

Each approach has its pros and cons:

  • Contract size: moleculec-es tends to produce larger codebases (more mol types = bigger output)
  • Runtime efficiency: moleculec-es defers parsing until invocation, whereas core::mol parses everything on creation. The performance difference can be significant depending on the scenario.
  • Developer experience: moleculec-es auto-generates code, while core::mol requires manual implementation.
note

This section uses silent-berry as an example.

Originally a Rust project, a small portion has been rewritten in TypeScript, including parts involving Molecule.

Both approaches are used in the project, as shown in these two commits: 6671b30 and d7573cf.

Using moleculec-es

This is similar to the C/Rust workflow: write a .mol file, convert it to JSON with moleculec, then use moleculec-es to generate JS code.

moleculec --language - --schema-file crate/types/schemas/silent_berry.mol --format json > crate/types/src/silent_berry.json
moleculec-es -inputFile crate/types/src/silent_berry.json -outputFile ts/types/silent_berry.js

Initialization:

new AccountBookData(witness);

Example usage:

let totalIncome = bigintFromBytes(witnessData.getTotalIncomeUdt().raw());

Using core::mol

With @ckb-js-std/core::mol, you need to manually define your parser:

export type AccountBookDataLike = {
proof: BytesLike;
totalIncomeUdt: NumLike;
withdrawnUdt?: NumLike | null;
};

@mol.codec(
mol.table({
proof: mol.Bytes,
totalIncomeUdt: mol.Uint128,
withdrawnUdt: mol.Uint128Opt,
})
)
export class AccountBookData extends mol.Entity.Base<
AccountBookDataLike,
AccountBookData
>() {
constructor(
public proof: Bytes,
public totalIncomeUdt: Num,
public withdrawnUdt: Num | null
) {
super();
}

static from(op: AccountBookDataLike): AccountBookData {
if (op instanceof AccountBookData) {
return op;
}
return new AccountBookData(
op.proof,
op.totalIncomeUdt,
optionToNum(op.withdrawnUdt)
);
}
}

Initialization is slightly different:

AccountBookData.decode(witness);

Example usage:

let totalIncome = BigInt(witnessData.totalIncomeUdt);