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 objects
intoMyStruct
type objects
to deal with vector
deserialization
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 array
intoMyStruct[]
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 array
intoT0[]
vector type objects whereT0
is the typescript type where you pass intoset_struct_t
function
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);
});