Binary Canonical Serialization (BCS)
BCS defines the way the data is serialized, and the serialized results contains no type information.
To be able to serialize the data and later deserialize it, a schema has to be created (based on the built-in primitives, such as string or u64).
However the schema definitions are not quite simple especially when dealing with move generics type.
deepmove auto generation BCS framework make it easy and convenient
Basic types
Unboxed
move function
public fun set_int(num: u64): u64 {
num
}deepmove auto generation typescript function
import { bcs as bcs_import} from "@mysten/sui/bcs";
// type u64 = string | number | bigint;
import { u64 as u64_import} from "@deepmove/sui";
function set_int(arg0: u64_import): [u64_import] {
let wasm = get_wasm();
let args: any[] = [
wasm.new_bytes(bcs_import.u64().serialize(arg0).toBytes(), "")
]
let [r0] = wasm.call_return_bcs(PACKAGE_ADDRESS, MODULE_NAME, "set_int", [], args);
return [
bcs_import.u64().parse(new Uint8Array(r0.Raw[0]))
];
}the unboxed basic type u64 will translate to bcs_import.u64().serialize(arg0) and bcs_import.u64().parse()
Boxed
when dealing with generic structs or generic functions, basic types will be boxed implements StructClass
public struct Foo2<T0, T1> {
num1: u16,
x: T0,
num2: u16,
y: T0,
z: T0,
num3: bool,
v: vector<vector<T1>>
}
public fun set_foo5<T>(a: Foo2<T, u16>): Foo2<T, u16> {
a
}function set_foo5 < T0 extends StructClass > (type_args: string[], arg0: Foo2 < T0, U16 > ): [Uint8Array] {
let wasm = get_wasm();
let args: any[] = [
wasm.new_bytes(arg0.serialize(arg0.into_value()).toBytes(), "")
]
let [r0] = wasm.call_return_bcs(PACKAGE_ADDRESS, MODULE_NAME, "set_foo5", type_args, args);
return [
new Uint8Array(r0.Raw[0])
];
}as we can see, base type u16 in Foo2<T, u16> will translate to Foo2 < T0, U16 >
Compound types
Compund types are move structs
public struct MyUrl has store, copy, drop {
url: String,
}
public struct MyStruct has drop {
id: u8,
name: String,
urls: vector<MyUrl>
}
public fun get_struct(): MyStruct {
MyStruct {
id: 100,
name: string::utf8(b"get_struct"),
urls: vector[]
}
}
public fun set_struct(v: MyStruct): vector<MyStruct> {
let mut r = vector[];
vector::push_back(&mut r, v);
r
}function get_struct(): [MyStruct] {
let wasm = get_wasm();
let args: any[] = []
let [r0] = wasm.call_return_bcs(PACKAGE_ADDRESS, MODULE_NAME, "get_struct", [], args);
return [
MyStruct.from_bcs(MyStruct.bcs.parse(new Uint8Array(r0.Raw[0])))
];
}
function set_struct(arg0: MyStruct): [MyStruct[]] {
let wasm = get_wasm();
let args: any[] = [
wasm.new_bytes(MyStruct.bcs.serialize(arg0).toBytes(), "")
]
let [r0] = wasm.call_return_bcs(PACKAGE_ADDRESS, MODULE_NAME, "set_struct", [], args);
return [
MyStruct.from_bcs_vector(bcs_import.vector(MyStruct.bcs).parse(new Uint8Array(r0.Raw[0])))
];
}serialization is translated into MyStruct.bcs.serialize(arg0)deserialization will have two steps:
- first
MyStruct.bcs.parse()translate intoplain typescript objects - second
MyStruct.from_bcs()translateplain typescript objectsintoMyStructtype objects
to deal with vectordeserialization will have the following two steps:
- first
bcs_import.vector(MyStruct.bcs).parse()translate intoplain typescript objects array - second
MyStruct.from_bcs_vector()translateplain typescript objects arrayintoMyStruct[]vector type objects
Generics
simple generic struct
public fun set_struct_t<T>(id: T): vector<T> {
let mut v = vector[];
vector::push_back(&mut v, id);
v
}function set_struct_t < T0 extends StructClass > (type_args: string[], arg0: T0): [Uint8Array] {
let wasm = get_wasm();
let args: any[] = [
wasm.new_bytes(arg0.serialize(arg0.into_value()).toBytes(), "")
]
let [r0] = wasm.call_return_bcs(PACKAGE_ADDRESS, MODULE_NAME, "set_struct_t", type_args, args);
return [
new Uint8Array(r0.Raw[0])
];
}serialization is translated into arg0.serialize(arg0.into_value())deserialization in this scene will be handled by developers in two steps:
- first
bcs_import.vector(T0.bcs).parse()translate intoplain typescript objects array - second
T0.from_bcs_vector()translateplain typescript objects arrayintoT0[]vector type objects whereT0is the typescript type where you pass intoset_struct_tfunction
generic struct array
public fun set_foo_vector2<T>(a: vector<Foo<T>>): vector<Foo<T>> {
a
}import {
StructClass,
copy_arr_value,
get_wasm,
has_arr,
into_arr_bcs_vector,
into_arr_value,
to_arr_value,
} from "@deepmove/sui";
function set_foo_vector2 < T0 extends StructClass > (type_args: string[], arg0: Foo < T0 > []): [Uint8Array] {
let wasm = get_wasm();
let args: any[] = [
wasm.new_bytes(has_arr(arg0) ? into_arr_bcs_vector(arg0).serialize(into_arr_value(arg0)).toBytes() : new Uint8Array([0]), "")
]
let [r0] = wasm.call_return_bcs(PACKAGE_ADDRESS, MODULE_NAME, "set_foo_vector2", type_args, args);
return [
new Uint8Array(r0.Raw[0])
];
}for array args can be empty array, so when dealing with empty generic struct array it will use new Uint8Array([0])
generic struct field
as generic struct field T can be generic vector type, especially the passing argument can be empty array.
therefor, deepmove auto generation framework provide T0_bcs field to pass in bcs type for T0 field.
public struct Foo<T> {
x: T,
`for`: T,
}export class Foo < T0 extends TypeArgument > implements StructClass {
$type: string = `${do_get_package_address()}::${MODULE_NAME}::Foo`;
x: T0;
for_: T0;
T0_bcs: any;
constructor(x: T0, for_: T0) {
this.x = x;
this.for_ = for_;
}
into_value() {
return {
x: (this.x as StructClass).into_value ? (this.x as StructClass).into_value() : this.x,
for_: (this.for_ as StructClass).into_value ? (this.for_ as StructClass).into_value() : this.for_
}
}
from_bcs_vector_t(bytes: Uint8Array) {
let args = this.from_bcs_vector(bcs_import.vector(this.get_bcs()(
this.T0_bcs ? this.T0_bcs : (to_arr_value(this.x) as StructClass).return_bcs()
)).parse(bytes));
var self = this;
return args.map(function(arg) {
arg.$type = self.$type;
return arg;
})
}
from_bcs_t(bytes: Uint8Array) {
let result = this.from_bcs(this.get_bcs()(
this.T0_bcs ? this.T0_bcs : (to_arr_value(this.x) as StructClass).return_bcs()
).parse(bytes));
result.$type = this.$type;
return result;
}
serialize(arg: any) {
return this.get_bcs()(
this.T0_bcs ? this.T0_bcs : (to_arr_value(this.x) as StructClass).return_bcs()
).serialize(arg);
}
serialize_bcs() {
return this.get_bcs()(
this.T0_bcs ? this.T0_bcs : (to_arr_value(this.x) as StructClass).return_bcs()
)
}
return_bcs() {
return this.get_bcs()((to_arr_value(this.x) as StructClass).get_bcs())
}
from_bcs(arg: any) {
return Foo.from_bcs(arg)
}
from_bcs_vector(args: any) {
return Foo.from_bcs_vector(args)
}
get_bcs() {
return Foo.bcs
}
get_value() {
return this
}
static $type() {
return `${do_get_package_address()}::${MODULE_NAME}::Foo`
}
from(arg: Foo < T0 > ) {
this.x = arg.x;
this.for_ = arg.for_;
}
static from_bcs < T0 extends TypeArgument > (arg: {
x: T0,
for_: T0
}): Foo < T0 > {
return new Foo(arg.x, arg.for_)
}
static from_bcs_vector < T0 extends TypeArgument > (args: {
x: T0,
for_: T0
} []): Foo < T0 > [] {
return args.map(function(arg) {
return new Foo(arg.x, arg.for_)
})
}
static get bcs() {
return < T0 extends TypeArgument, input0 > (T0: BcsType < T0, input0 > ) =>
bcs_import.struct(`Foo<${T0.name}>`, {
x: T0,
for_: T0,
}).transform({
input: (val: any) => {
return val
},
output: (val) => new Foo(val.x, val.for_),
});
};
}the following is the real example for use when dealing with Move empty option type value
it('test fun is_none', () => {
let a0 = new U8(12);
let [r0] = option.some([U8.$type()], a0);
let b0 = option.Option.from_bcs<U8>(option.Option.bcs(U8.bcs).parse(r0));
let [r1] = option.is_none([U8.$type()], b0);
expect(bcs_import.bool().parse(r1)).toEqual(false);
let [r2] = option.none([U8.$type()]);
let b1 = option.Option.from_bcs<U8>(option.Option.bcs(U8.bcs).parse(r2));
b1.T0_bcs = U8.bcs;
let [r3] = option.is_none([U8.$type()], b1);
expect(bcs_import.bool().parse(r3)).toEqual(true);
});
