Skip to content

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

rust
public fun set_int(num: u64): u64 {
    num
}

deepmove auto generation typescript function

typescript
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

rust
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
}
typescript
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

rust
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
}
typescript
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 into plain typescript objects
  • second MyStruct.from_bcs() translate plain typescript objects into MyStruct type objects

to deal with vectordeserialization will have the following two steps:

  • first bcs_import.vector(MyStruct.bcs).parse() translate into plain typescript objects array
  • second MyStruct.from_bcs_vector() translate plain typescript objects array into MyStruct[] vector type objects

Generics

simple generic struct

rust
public fun set_struct_t<T>(id: T): vector<T> {
    let mut v = vector[];
    vector::push_back(&mut v, id);
    v
}
typescript
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 into plain typescript objects array
  • second T0.from_bcs_vector() translate plain typescript objects array into T0[] vector type objects where T0 is the typescript type where you pass into set_struct_t function

generic struct array

rust
public fun set_foo_vector2<T>(a: vector<Foo<T>>): vector<Foo<T>> {
    a
}
typescript
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.

rust
public struct Foo<T> {
    x: T,
    `for`: T,
}
typescript
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

typescript
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);
});

References

https://sdk.mystenlabs.com/bcs

Released under the MIT License.