BCS - Binary Canonical Serialization
This small and lightweight library implements Binary Canonical Serialization (BCS) in JavaScript, making BCS available in both Browser and NodeJS environments.
Install
To install, add the @mysten/bcs
package to your project:
npm i @mysten/bcs
Quickstart
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
bcs.registerAlias("UID", BCS.ADDRESS);
bcs.registerEnumType("Option<T>", {
none: null,
some: "T",
});
bcs.registerStructType("Coin", {
id: "UID",
value: BCS.U64,
});
let bcsBytes = bcs
.ser("Coin", {
id: "0000000000000000000000000000000000000000000000000000000000000001",
value: 1000000n,
})
.toBytes();
let coin = bcs.de("Coin", bcsBytes, "hex");
let data = bcs.ser("Option<Coin>", { some: coin }).toString("hex");
console.log(data);
Description
BCS defines the way the data is serialized, and the result contains no type information. To be able to serialize the data and later deserialize, a schema has to be created (based on the built-in primitives, such as address
or u64
). There's no tip in the serialized bytes on what they mean, so the receiving part needs to know how to treat it.
Configuration
BCS constructor is configurable for the target. The following parameters are available for custom configuration:
parameter | required | description |
---|
vectorType | + | Defines the type of the vector (vector<T> in SuiMove, Vec<T> in Rust) |
addressLength | + | Length of the built-in address type. 20 for SuiMove, 32 for Core Move |
addressEncoding | - | Custom encoding for addresses - "hex" or "base64" |
genericSeparators | - | Generic type parameters syntax, default is ['<', '>'] |
types | - | Define enums, structs and aliases at initialization stage |
withPrimitives | - | Whether to register primitive types (true by default) |
import { BCS } from "@mysten/bcs";
const SUI_ADDRESS_LENGTH = 32;
const bcs = new BCS({
vectorType: "vector<T>",
addressLength: SUI_ADDRESS_LENGTH,
addressEncoding: "hex",
genericSeparators: ["<", ">"],
types: {
structs: {
User: {
name: BCS.STRING,
age: BCS.U8,
},
},
enums: {},
aliases: { hex: BCS.HEX },
},
withPrimitives: true,
});
let bytes = bcs.ser("User", { name: "Adam", age: "30" }).toString("base64");
console.log(bytes);
For Sui Move there's already a pre-built configuration which can be used through the getSuiMoveConfig()
call.
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
const val = [1, 2, 3, 4];
const ser = bcs.ser(["vector", BCS.U8], val).toBytes();
const res = bcs.de(["vector", BCS.U8], ser);
console.assert(res.toString() === val.toString());
Similar configuration exists for Rust, the difference is the Vec<T>
for vectors and address
(being a special Move type) is not needed:
import { BCS, getRustConfig } from "@mysten/bcs";
const bcs = new BCS(getRustConfig());
const val = [1, 2, 3, 4];
const ser = bcs.ser(["Vec", BCS.U8], val).toBytes();
const res = bcs.de(["Vec", BCS.U8], ser);
console.assert(res.toString() === val.toString());
Built-in types
By default, BCS will have a set of built-in type definitions and handy abstractions; all of them are supported in Move.
Supported integer types are: u8, u16, u32, u64, u128 and u256. Constants BCS.U8
to BCS.U256
are provided by the library.
Type | Constant | Description |
---|
'bool' | BCS.BOOL | Boolean type (converts to true / false ) |
'u8'...'u256' | BCS.U8 ... BCS.U256 | Integer types |
'address' | BCS.ADDRESS | Address type (also used for IDs in Sui Move) |
'vector<T>' | 'Vec<T>' | Only custom use, requires T | Generic vector of any element |
'string' | BCS.STRING | vector<u8> that (de)serializes to/from ASCII string |
'hex-string' | BCS.HEX | vector<u8> that (de)serializes to/from HEX string |
'base64-string' | BCS.BASE64 | vector<u8> that (de)serializes to/from Base64 string |
'base58-string' | BCS.BASE58 | vector<u8> that (de)serializes to/from Base58 string |
All of the type usage examples below can be used for bcs.de(<type>, ...)
as well.
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
let _u8 = bcs.ser(BCS.U8, 100).toBytes();
let _u64 = bcs.ser(BCS.U64, 1000000n).toString("hex");
let _u128 = bcs.ser(BCS.U128, "100000010000001000000").toString("base64");
let _bool = bcs.ser(BCS.BOOL, true).toString("hex");
let _addr = bcs
.ser(BCS.ADDRESS, "0000000000000000000000000000000000000001")
.toBytes();
let _str = bcs.ser(BCS.STRING, "this is an ascii string").toBytes();
let _u8_vec = bcs.ser(["vector", BCS.U8], [1, 2, 3, 4, 5, 6, 7]).toBytes();
let _bool_vec = bcs.ser(["vector", BCS.BOOL], [true, true, false]).toBytes();
let _str_vec = bcs
.ser("vector<bool>", ["string1", "string2", "string3"])
.toBytes();
let _matrix = bcs
.ser("vector<vector<u8>>", [
[0, 0, 0],
[1, 1, 1],
[2, 2, 2],
])
.toBytes();
Ser/de and formatting
To serialize and deserialize data to and from BCS there are two methods: bcs.ser()
and bcs.de()
.
import { BCS, getSuiMoveConfig, BcsWriter } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
let bcsWriter: BcsWriter = bcs.ser(BCS.STRING, "this is a string");
let bytes: Uint8Array = bcsWriter.toBytes();
let hex: string = bcsWriter.toString("hex");
let base64: string = bcsWriter.toString("base64");
let base58: string = bcsWriter.toString("base58");
let str1 = bcs.de(BCS.STRING, bytes);
let str2 = bcs.de(BCS.STRING, hex, "hex");
let str3 = bcs.de(BCS.STRING, base64, "base64");
let str4 = bcs.de(BCS.STRING, base58, "base58");
console.assert((str1 == str2) == (str3 == str4), "Result is the same");
Registering new types
Tip: all registering methods start with bcs.register*
(eg bcs.registerStructType
).
Alias
Alias is a way to create custom name for a registered type. It is helpful for fine-tuning a predefined schema without making changes deep in the tree.
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
bcs.registerAlias("ObjectDigest", BCS.BASE58);
let _b58 = bcs.ser("ObjectDigest", "Ldp").toBytes();
bcs.registerAlias("ObjectDigest", BCS.HEX);
let _hex = bcs.ser("ObjectDigest", "C0FFEE").toBytes();
Struct
Structs are the most common way of working with data; in BCS, a struct is simply a sequence of base types.
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
bcs.registerStructType("Balance", {
value: BCS.U64,
});
bcs.registerStructType("Coin", {
id: BCS.ADDRESS,
balance: "Balance",
});
let _bytes = bcs
.ser("Coin", {
id: "0x0000000000000000000000000000000000000000000000000000000000000005",
balance: {
value: 100000000n,
},
})
.toBytes();
Using Generics
To define a generic struct or an enum, pass the type parameters. It can either be done as a part of a string or as an Array. See below:
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
bcs.registerStructType(["Container", "T"], {
contents: "T"
});
bcs.ser(["Container", BCS.U8], {
contents: 100
}).toString("hex");
bcs.ser(["Container", [ "vector", BCS.BOOL ]], {
contents: [ true, false, true ]
}).toString("hex");
bcs.registerStructType(["VecMap", "Key", "Val"], {
keys: ["vector", "Key"],
values: ["vector", "Val"]
});
bcs.ser(["VecMap", BCS.STRING, BCS.STRING], {
keys: [ "key1", "key2", "key3" ],
values: [ "value1", "value2", "value3" ]
});
Enum
In BCS enums are encoded in a special way - first byte marks the order and then the value. Enum is an object, only one property of which is used; if an invariant is empty, null
should be used to mark it (see Option<T>
below).
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
bcs.registerEnumType("Option<T>", {
none: null,
some: "T",
});
bcs.registerEnumType("TransactionType", {
single: "vector<u8>",
batch: "vector<vector<u8>>",
});
let _optionNone = bcs.ser("Option<TransactionType>", {
none: true,
});
let _optionTx = bcs.ser("Option<TransactionType>", {
some: {
single: [1, 2, 3, 4, 5, 6],
},
});
let _optionTxBatch = bcs.ser("Option<TransactionType>", {
some: {
batch: [
[1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6],
],
},
});
Inline (de)serialization
Sometimes it is useful to get a value without registering a new struct. For that inline struct definition can be used.
Nested struct definitions are not yet supported, only first level properties can be used (but they can reference any type, including other struct types).
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
const coin = {
id: "0000000000000000000000000000000000000000000000000000000000000005",
value: 1111333333222n,
};
let coin_bytes = bcs.ser({ id: BCS.ADDRESS, value: BCS.U64 }, coin).toBytes();
let coin_restored = bcs.de({ id: BCS.ADDRESS, value: BCS.U64 }, coin_bytes);
console.assert(coin.id == coin_restored.id, "`id` must match");
console.assert(coin.value == coin_restored.value, "`value` must match");
Aligning schema with Move
Currently, main applications of this library are:
- Serializing transactions and data passed into a transaction
- Deserializing onchain data for performance and formatting reasons
- Deserializing events
In this library, all of the primitive Move types are present as built-ins, however, there's a set of special types in Sui which can be simplified to a primitive.
module me::example {
struct Metadata has store {
name: std::ascii::String,
}
struct ChainObject has key {
id: sui::object::UID,
owner: address,
meta: Metadata
}
}
Definition for the above should be the following:
import { BCS, getSuiMoveConfig } from "@mysten/bcs";
const bcs = new BCS(getSuiMoveConfig());
bcs.registerAlias("UID", BCS.ADDRESS);
bcs.registerStructType("Metadata", {
name: BCS.STRING,
});
bcs.registerStructType("ChainObject", {
id: "UID",
owner: BCS.ADDRESS,
meta: "Metadata",
});
See definition of the UID here
struct UID has store {
id: ID
}
struct ID has store, copy, drop {
bytes: address
}
// { id: { bytes: '0x.....' } }