npm package: @mgonetwork/bcs
Description: BCS - Canonical Binary Serialization implementation for JavaScript
Author: Mango
Latest version: 1.0.0

# BCS - Binary Canonical Serialization

This small and lightweight library implements
[Binary Canonical Serialization (BCS)](https://github.com/zefchain/bcs) in JavaScript, making BCS
available in both Browser and NodeJS environments.

## Install

To install, add the [`@mango/bcs`](https://www.npmjs.com/package/@mango/bcs) package to your
project:

```sh
npm i @mango/bcs
```

## Quickstart

```ts
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// registering types so we can use them
bcs.registerAlias('UID', BCS.ADDRESS);
bcs.registerEnumType('Option<T>', {
	none: null,
	some: 'T',
});
bcs.registerStructType('Coin', {
	id: 'UID',
	value: BCS.U64,
});

// deserialization: BCS bytes into Coin
let bcsBytes = bcs
	.ser('Coin', {
		id: '0000000000000000000000000000000000000000000000000000000000000001',
		value: 1000000n,
	})
	.toBytes();
let coin = bcs.de('Coin', bcsBytes, 'hex');

// serialization: Object into bytes - an Option with <T = Coin>
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 MgoMove, `Vec<T>` in Rust) |
| `addressLength`     | +        | Length of the built-in `address` type. 20 for MgoMove, 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)                   |

```ts
// Example: All options used
import { BCS } from '@mango/bcs';

const MGO_ADDRESS_LENGTH = 32;
const bcs = new BCS({
	vectorType: 'vector<T>',
	addressLength: MGO_ADDRESS_LENGTH,
	addressEncoding: 'hex',
	genericSeparators: ['<', '>'],
	types: {
		// define schema in the initializer
		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 Mgo Move there's already a pre-built configuration which can be used through the
`getMgoMoveConfig()` call.

```ts
// Example: Mgo Move Config
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// use bcs.ser() to serialize data
const val = [1, 2, 3, 4];
const ser = bcs.ser(['vector', BCS.U8], val).toBytes();

// use bcs.de() to deserialize data
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:

```ts
// Example: Rust Config
import { BCS, getRustConfig } from '@mango/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 Mgo 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.

```ts
// Example: Primitive types
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// Integers
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');

// Other types
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();

// Vectors (vector<T>)
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();

// Even vector of vector (...of vector) is an option
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()`.

```ts
// Example: Ser/de and Encoding
import { BCS, BcsWriter, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// bcs.ser() returns an instance of BcsWriter which can be converted to bytes or a string
let bcsWriter: BcsWriter = bcs.ser(BCS.STRING, 'this is a string');

// writer.toBytes() returns a Uint8Array
let bytes: Uint8Array = bcsWriter.toBytes();

// custom encodings can be chosen when needed (just like Buffer)
let hex: string = bcsWriter.toString('hex');
let base64: string = bcsWriter.toString('base64');
let base58: string = bcsWriter.toString('base58');

// bcs.de() reads BCS data and returns the value
// by default it expects data to be `Uint8Array`
let str1 = bcs.de(BCS.STRING, bytes);

// alternatively, an encoding of input can be specified
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.

```ts
// Example: Alias
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

bcs.registerAlias('ObjectDigest', BCS.BASE58);

// ObjectDigest is now treated as base58 string
let _b58 = bcs.ser('ObjectDigest', 'Ldp').toBytes();

// we can override already existing definition to make it a HEX string
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.

```ts
// Example: Struct
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// register a custom type (it becomes available for using)
bcs.registerStructType('Balance', {
	value: BCS.U64,
});

bcs.registerStructType('Coin', {
	id: BCS.ADDRESS,
	// reference another registered type
	balance: 'Balance',
});

// value passed into ser function has to have the same
// structure as the definition
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:

```ts
// Example: Generics
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// Container -> the name of the type
// T -> type parameter which has to be passed in `ser()` or `de()` methods
// If you're not familiar with generics, treat them as type Templates
bcs.registerStructType(['Container', 'T'], {
	contents: 'T',
});

// When serializing, we have to pass the type to use for `T`
bcs
	.ser(['Container', BCS.U8], {
		contents: 100,
	})
	.toString('hex');

// Reusing the same Container type with different contents.
// Mind that generics need to be passed as Array after the main type.
bcs
	.ser(['Container', ['vector', BCS.BOOL]], {
		contents: [true, false, true],
	})
	.toString('hex');

// Using multiple generics - you can use any string for convenience and
// readability. See how we also use array notation for a field definition.
bcs.registerStructType(['VecMap', 'Key', 'Val'], {
	keys: ['vector', 'Key'],
	values: ['vector', 'Val'],
});

// To serialize VecMap, we can use:
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).

```ts
// Example: Enum
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

bcs.registerEnumType('Option<T>', {
	none: null,
	some: 'T',
});

bcs.registerEnumType('TransactionType', {
	single: 'vector<u8>',
	batch: 'vector<vector<u8>>',
});

// any truthy value marks empty in struct value
let _optionNone = bcs.ser('Option<TransactionType>', {
	none: true,
});

// some now contains a value of type TransactionType
let _optionTx = bcs.ser('Option<TransactionType>', {
	some: {
		single: [1, 2, 3, 4, 5, 6],
	},
});

// same type signature but a different enum invariant - batch
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).

```ts
// Example: Inline Struct
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// Some value we want to serialize
const coin = {
	id: '0000000000000000000000000000000000000000000000000000000000000005',
	value: 1111333333222n,
};

// Instead of defining a type we pass struct schema as the first argument
let coin_bytes = bcs.ser({ id: BCS.ADDRESS, value: BCS.U64 }, coin).toBytes();

// Same with deserialization
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:

1. Serializing transactions and data passed into a transaction
2. Deserializing onchain data for performance and formatting reasons
3. 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 Mgo which can be simplified to a primitive.

```rust
// Definition in Move which we want to read in JS
module me::example {
    struct Metadata has store {
        name: std::ascii::String,
    }

    struct ChainObject has key {
        id: mgo::object::UID,
        owner: address,
        meta: Metadata
    }
    // ...
}
```

Definition for the above should be the following:

```ts
// Example: Simplifying UID
import { BCS, getMgoMoveConfig } from '@mango/bcs';

const bcs = new BCS(getMgoMoveConfig());

// If there's a deep nested struct we can ignore Move type
// structure and use only the value.
bcs.registerAlias('UID', BCS.ADDRESS);

// Simply follow the definition onchain
bcs.registerStructType('Metadata', {
	name: BCS.STRING,
});

// Same for the main object that we intend to read
bcs.registerStructType('ChainObject', {
	id: 'UID',
	owner: BCS.ADDRESS,
	meta: 'Metadata',
});
```

<details><summary>See definition of the UID here</summary>
<pre>
struct UID has store {
    id: ID
}

struct ID has store, copy, drop { bytes: address }

// { id: { bytes: '0x.....' } }

</pre>
</details>