CCC Molecule: JavaScript SDK with serialization
When working with your dApp development, the JavaScript SDK CCC provide the all-in-one Molecule module to help you serialize and deserialize data in Molecule format.
You can import the Molecule module from the SDK and use it to serialize and deserialize data.
import { mol } from "@ckb-ccc/core";
In the following sections, we will show you how to use the CCC Molecule module with basic examples and advanced examples. The full code of this tutorial is available on GitHub
Basic Usages
Working with Molecule basic data types are quite simple. CCC Molecule provides straightforward APIs for such types.
import { ccc } from "@ckb-ccc/core";
// Molecule Basic Types
// There is only one built-in primitive type in Molecule: byte.
console.log(ccc.mol.Bytes.encode(Buffer.from("hello")).toString());
console.log(ccc.mol.Bytes.decode(Buffer.from("68656c6c6f", "hex")).toString());
// Composite Types
// array: An array consists of an item type and an unsigned integer.
console.log(
ccc.mol.array(ccc.mol.Bytes, 2).encode(["hello", "world"]).toString()
);
console.log(
ccc.mol
.array(ccc.mol.Bytes, 2)
.decode(Buffer.from("68656c6c6f776f726c64", "hex"))
.toString()
);
// struct: A struct consists of a set of named and typed fields.
console.log(
ccc.mol
.struct({ a: ccc.mol.Bytes, b: ccc.mol.Bytes })
.encode({ a: "hello", b: "world" })
.toString()
);
console.log(
ccc.mol
.struct({ a: ccc.mol.Bytes, b: ccc.mol.Bytes })
.decode(Buffer.from("68656c6c6f776f726c64", "hex"))
.toString()
);
// vector: A vector contains only one item type.
console.log(
ccc.mol
.table({ a: ccc.mol.Bytes, b: ccc.mol.Bytes })
.encode({ a: "hello", b: "world" })
.toString()
);
console.log(
ccc.mol
.table({ a: ccc.mol.Bytes, b: ccc.mol.Bytes })
.decode(Buffer.from("68656c6c6f776f726c64", "hex"))
.toString()
);
// table: A table consists of a set of named and typed fields, same as struct.
console.log(
ccc.mol.vector(ccc.mol.Bytes).encode(["hello", "world"]).toString()
);
console.log(
ccc.mol
.vector(ccc.mol.Bytes)
.decode(Buffer.from("68656c6c6f776f726c64", "hex"))
.toString()
);
// option: An option contains only an item type.
console.log(ccc.mol.option(ccc.mol.Bytes).encode("hello").toString());
console.log(
ccc.mol
.option(ccc.mol.Bytes)
.decode(Buffer.from("68656c6c6f", "hex"))
?.toString()
);
// union: A union contains a set of item types.
console.log(
ccc.mol
.union({ a: ccc.mol.Bytes, b: ccc.mol.Bytes })
.encode({ type: "a", value: "hello" })
.toString()
);
console.log(
ccc.mol
.union({ a: ccc.mol.Bytes, b: ccc.mol.Bytes })
.decode(Buffer.from("68656c6c6f", "hex"))
.toString()
);
Besides the basic data types, CCC Molecule also provides some common data structures:
console.log(ccc.mol.Bool.decode("0x01")); // true
console.log(ccc.mol.Bool.encode(true).toString()); // 1
// All kinds of bytes
console.log(ccc.mol.Byte16.encode(Buffer.from("hello")).toString());
console.log(ccc.mol.Byte16.decode("0x68656c6c6f").toString());
console.log(ccc.mol.Byte32.encode(Buffer.from("hello")).toString());
console.log(ccc.mol.Byte32.decode("0x68656c6c6f").toString());
// ...
// All kinds of numbers
console.log(ccc.mol.Uint8.encode(1).toString()); // 0x01
console.log(ccc.mol.Uint8.decode("0x01")); // 1
console.log(ccc.mol.Uint128LE.decode("0x01000000000000000000000000000000")); // 1
console.log(ccc.mol.Uint128LE.encode(1).toString()); // 0x01000000000000000000000000000000
// ...
Advanced Examples
We have provide a full Molecule example of a role-playing game consisting of 4 schema files.
In the advance usage of CCC Molecule, we will show you how to use the Molecule module to serialize and deserialize the full data in the role-playing game example.
The basic data type in the game example is a AttrValue
:
// AttrValue is an alias of `byte`.
//
// Since Molecule data are strongly-typed, it can gives compile time guarantees
// that the right type of value is supplied to a method.
//
// In this example, we use this alias to define an unsigned integer which
// has an upper limit: 100.
// So it's easy to distinguish between this type and a real `byte`.
// Of course, the serialization wouldn't do any checks for this upper limit
// automatically. You have to implement it by yourself.
//
// **NOTE**:
// - This feature is dependent on the exact implementation.
// In official Rust generated code, we use new type to implement this feature.
array AttrValue [byte; 1];
With CCC, we can define the AttrValue
type with simple codec bindings:
import { Bytes, bytesFrom, BytesLike, mol, NumLike } from "@ckb-ccc/core";
export type AttrValue = number;
export type AttrValueLike = NumLike;
export const AttrValueCodec: mol.Codec<AttrValueLike, AttrValue> =
mol.Codec.from({
byteLength: 1,
encode: attrValueToBytes,
decode: attrValueFromBytes,
});
export function attrValueFrom(val: AttrValueLike): AttrValue {
if (typeof val === "number") {
if (val > 100) {
throw new Error(`Invalid attr value ${val}`);
}
return val;
}
if (typeof val === "bigint") {
if (val > BigInt(100)) {
throw new Error(`Invalid attr value ${val}`);
}
return Number(val);
}
if (typeof val === "string") {
const num = parseInt(val);
if (num > 100) {
throw new Error(`Invalid attr value ${val}`);
}
return num;
}
throw new Error(`Invalid attr value ${val}`);
}
export function attrValueToBytes(val: AttrValueLike): Bytes {
return bytesFrom([attrValueFrom(val)]);
}
export function attrValueFromBytes(bytes: BytesLike): AttrValue {
return attrValueFrom(bytesFrom(bytes)[0]);
}
With the codec bindings, we can easily serialize and deserialize the AttrValue
type:
AttrValueCodec.encode(80).toString();
AttrValueCodec.decode(Buffer.from("50", "hex")).toString();
Since we implement the upper limit check in the codec bindings, it is also possible to validate the input data before de/serialization.
AttrValueCodec.encode(101).toString();
// this will throw an error
In the same way, we can define the rest data types in the role-playing game example:
import { Bytes, bytesFrom, BytesLike, mol, NumLike } from "@ckb-ccc/shell";
export type AttrValue = number;
export type AttrValueLike = NumLike;
export const AttrValueCodec: mol.Codec<AttrValueLike, AttrValue> =
mol.Codec.from({
byteLength: 1,
encode: attrValueToBytes,
decode: attrValueFromBytes,
});
export function attrValueFrom(val: AttrValueLike): AttrValue {
if (typeof val === "number") {
if (val > 100) {
throw new Error(`Invalid attr value ${val}`);
}
return val;
}
if (typeof val === "bigint") {
if (val > BigInt(100)) {
throw new Error(`Invalid attr value ${val}`);
}
return Number(val);
}
if (typeof val === "string") {
const num = parseInt(val);
if (num > 100) {
throw new Error(`Invalid attr value ${val}`);
}
return num;
}
throw new Error(`Invalid attr value ${val}`);
}
export function attrValueToBytes(val: AttrValueLike): Bytes {
return bytesFrom([attrValueFrom(val)]);
}
export function attrValueFromBytes(bytes: BytesLike): AttrValue {
return attrValueFrom(bytesFrom(bytes)[0]);
}
export type SkillLevel = number;
export type SkillLevelLike = NumLike;
export const SkillLevelCodec: mol.Codec<SkillLevelLike, SkillLevel> =
mol.Codec.from({
byteLength: 1,
encode: skillLevelToBytes,
decode: skillLevelFromBytes,
});
export function skillLevelFrom(val: SkillLevelLike): SkillLevel {
if (typeof val === "number") {
if (val > 10) {
throw new Error(`Invalid skill level ${val}`);
}
return val;
}
if (typeof val === "bigint") {
if (val > BigInt(10)) {
throw new Error(`Invalid skill level ${val}`);
}
return Number(val);
}
if (typeof val === "string") {
const num = parseInt(val);
if (num > 10) {
throw new Error(`Invalid skill level ${val}`);
}
return num;
}
throw new Error(`Invalid skill level ${val}`);
}
export function skillLevelToBytes(val: SkillLevelLike): Bytes {
return bytesFrom([skillLevelFrom(val)]);
}
export function skillLevelFromBytes(bytes: BytesLike): SkillLevel {
return skillLevelFrom(bytesFrom(bytes)[0]);
}
export interface Attributes {
strength: AttrValue;
dexterity: AttrValue;
endurance: AttrValue;
speed: AttrValue;
intelligence: AttrValue;
wisdom: AttrValue;
perception: AttrValue;
concentration: AttrValue;
}
export interface AttributesLike {
strength: AttrValueLike;
dexterity: AttrValueLike;
endurance: AttrValueLike;
speed: AttrValueLike;
intelligence: AttrValueLike;
wisdom: AttrValueLike;
perception: AttrValueLike;
concentration: AttrValueLike;
}
export const AttributesCodec: mol.Codec<AttributesLike, Attributes> =
mol.struct({
strength: AttrValueCodec,
dexterity: AttrValueCodec,
endurance: AttrValueCodec,
speed: AttrValueCodec,
intelligence: AttrValueCodec,
wisdom: AttrValueCodec,
perception: AttrValueCodec,
concentration: AttrValueCodec,
});
export type ArmorLight = SkillLevel;
export type ArmorHeavy = SkillLevel;
export type ArmorShields = SkillLevel;
export type WeaponSwords = SkillLevel;
export type WeaponBows = SkillLevel;
export type WeaponBlunt = SkillLevel;
export type Dodge = SkillLevel;
export type PickLocks = SkillLevel;
export type Mercantile = SkillLevel;
export type Survival = SkillLevel;
export type ArmorLightLike = SkillLevelLike;
export type ArmorHeavyLike = SkillLevelLike;
export type ArmorShieldsLike = SkillLevelLike;
export type WeaponSwordsLike = SkillLevelLike;
export type WeaponBowsLike = SkillLevelLike;
export type WeaponBluntLike = SkillLevelLike;
export type DodgeLike = SkillLevelLike;
export type PickLocksLike = SkillLevelLike;
export type MercantileLike = SkillLevelLike;
export type SurvivalLike = SkillLevelLike;
export type Skill =
| {type: "ArmorLight"; value: ArmorLight | undefined | null}
| {type: "ArmorHeavy"; value: ArmorHeavy | undefined | null}
| {type: "ArmorShields"; value: ArmorShields | undefined | null}
| {type: "WeaponSwords"; value: WeaponSwords | undefined | null}
| {type: "WeaponBows"; value: WeaponBows | undefined | null}
| {type: "WeaponBlunt"; value: WeaponBlunt | undefined | null}
| {type: "Dodge"; value: Dodge | undefined | null}
| {type: "PickLocks"; value: PickLocks | undefined | null}
| {type: "Mercantile"; value: Mercantile | undefined | null}
| {type: "Survival"; value: Survival | undefined | null};
export type SkillLike =
| {type: "ArmorLight"; value: ArmorLightLike | undefined | null}
| {type: "ArmorHeavy"; value: ArmorHeavyLike | undefined | null}
| {type: "ArmorShields"; value: ArmorShieldsLike | undefined | null}
| {type: "WeaponSwords"; value: WeaponSwordsLike | undefined | null}
| {type: "WeaponBows"; value: WeaponBowsLike | undefined | null}
| {type: "WeaponBlunt"; value: WeaponBluntLike | undefined | null}
| {type: "Dodge"; value: DodgeLike | undefined | null}
| {type: "PickLocks"; value: PickLocksLike | undefined | null}
| {type: "Mercantile"; value: MercantileLike | undefined | null}
| {type: "Survival"; value: SurvivalLike | undefined | null};
export type Skills = Skill[];
export type SkillsLike = SkillLike[];
export const ArmorLightCodec: mol.Codec<
ArmorLightLike | undefined | null,
ArmorLight | undefined | null
> = mol.option(SkillLevelCodec);
export const ArmorHeavyCodec: mol.Codec<
ArmorHeavyLike | undefined | null,
ArmorHeavy | undefined | null
> = mol.option(SkillLevelCodec);
export const ArmorShieldsCodec: mol.Codec<
ArmorShieldsLike | undefined | null,
ArmorShields | undefined | null
> = mol.option(SkillLevelCodec);
export const WeaponSwordsCodec: mol.Codec<
WeaponSwordsLike | undefined | null,
WeaponSwords | undefined | null
> = mol.option(SkillLevelCodec);
export const WeaponBowsCodec: mol.Codec<
WeaponBowsLike | undefined | null,
WeaponBows | undefined | null
> = mol.option(SkillLevelCodec);
export const WeaponBluntCodec: mol.Codec<
WeaponBluntLike | undefined | null,
WeaponBlunt | undefined | null
> = mol.option(SkillLevelCodec);
export const DodgeCodec: mol.Codec<
DodgeLike | undefined | null,
Dodge | undefined | null
> = mol.option(SkillLevelCodec);
export const PickLocksCodec: mol.Codec<
PickLocksLike | undefined | null,
PickLocks | undefined | null
> = mol.option(SkillLevelCodec);
export const MercantileCodec: mol.Codec<
MercantileLike | undefined | null,
Mercantile | undefined | null
> = mol.option(SkillLevelCodec);
export const SurvivalCodec: mol.Codec<
SurvivalLike | undefined | null,
Survival | undefined | null
> = mol.option(SkillLevelCodec);
export const SkillCodec: mol.Codec<SkillLike, Skill> = mol.union({
ArmorLight: ArmorLightCodec,
ArmorHeavy: ArmorHeavyCodec,
ArmorShields: ArmorShieldsCodec,
WeaponSwords: WeaponSwordsCodec,
WeaponBows: WeaponBowsCodec,
WeaponBlunt: WeaponBluntCodec,
Dodge: DodgeCodec,
PickLocks: PickLocksCodec,
Mercantile: MercantileCodec,
Survival: SurvivalCodec,
});
export const SkillsCodec: mol.Codec<SkillsLike, Skills> = mol.vector(SkillCodec);
export type Class = number;
export type ClassLike = NumLike;
export const ClassCodec: mol.Codec<ClassLike, Class> = mol.Codec.from({
byteLength: 1,
encode: classToBytes,
decode: classFromBytes,
});
export function classFrom(val: ClassLike): Class {
if (typeof val === "number") {
return val;
}
if (typeof val === "bigint") {
return Number(val);
}
if (typeof val === "string") {
return parseInt(val);
}
throw new Error(`Invalid class ${val}`);
}
export function classToBytes(val: ClassLike): Bytes {
return bytesFrom([classFrom(val)]);
}
export function classFromBytes(bytes: BytesLike): Class {
return classFrom(bytesFrom(bytes)[0]);
}
export interface Hero {
class: Class;
level: number;
experiences: number;
hp: number;
mp: number;
baseDamage: number;
attrs: Attributes;
skills: Skills;
}
export interface HeroLike {
class: ClassLike;
level: NumLike;
experiences: NumLike;
hp: NumLike;
mp: NumLike;
baseDamage: NumLike;
attrs: AttributesLike;
skills: SkillsLike;
}
export const HeroCodec: mol.Codec<HeroLike, Hero> = mol.table({
class: ClassCodec,
level: mol.Uint8,
experiences: mol.Uint32,
hp: mol.Uint16,
mp: mol.Uint16,
baseDamage: mol.Uint16,
attrs: AttributesCodec,
skills: SkillsCodec
});
export interface Monster {
hp: number;
damage: number;
}
export interface MonsterLike {
hp: NumLike;
damage: NumLike;
}
export const MonsterCodec: mol.Codec<MonsterLike, Monster> = mol.table({
hp: mol.Uint16,
damage: mol.Uint16,
});
// Basic Usage
const myHero: Hero = {
class: 1,
level: 1,
experiences: 0,
hp: 100,
mp: 100,
baseDamage: 10,
attrs: {
strength: 10,
dexterity: 10,
endurance: 10,
speed: 10,
intelligence: 10,
wisdom: 10,
perception: 10,
concentration: 10,
},
skills: [{type: "ArmorLight", value: 1}],
};
const myHeroBytes = HeroCodec.encode(myHero);
console.log(HeroCodec.decode(myHeroBytes));
Another util CCC provides is the decorator @mol.codec
to simplify the codec bindings when working with javascript Objects. You can add custom methods to manipulate the data structure as you like.
// advance usage
@mol.codec(MonsterCodec)
export class Monster extends mol.Entity.Base<MonsterLike, Monster>() {
constructor(public monster: MonsterLike){
super();
}
customMethod(){
console.log("calling custom method");
}
}
const myMonster = new Monster({hp: 100, damage: 10});
myMonster.customMethod();
console.log(myMonster.toBytes())