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 (moremol
types = bigger output) - Runtime efficiency:
moleculec-es
defers parsing until invocation, whereascore::mol
parses everything on creation. The performance difference can be significant depending on the scenario. - Developer experience:
moleculec-es
auto-generates code, whilecore::mol
requires 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.mol
schema 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);