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
Transactionstructure using@ckb-js-std/core::mol - Use
moleculec-esto generate JS code from.molfiles
Each approach has its pros and cons:
- Contract size:
moleculec-estends to produce larger codebases (moremoltypes = bigger output) - Runtime efficiency:
moleculec-esdefers parsing until invocation, whereascore::molparses everything on creation. The performance difference can be significant depending on the scenario. - Developer experience:
moleculec-esauto-generates code, whilecore::molrequires manual implementation.
This section uses silent-berry as an example.
Originally a Rust project, a small portion has been rewritten in TypeScript, including parts involving Molecule.
silent_berry.mol: The.molschema used in the examples below.types/silent_berry.ts: Code auto-generated bymoleculec-es.types.ts: A manual implementation usingcore::mol.
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);