BCS - Binary Canonical Serialization
This library implements Binary Canonical Serialization (BCS) in Dart.
Quickstart
import 'package:bcs/bcs.dart';
// define UID as a 32-byte array, then add a transform to/from hex strings
final UID = Bcs.fixedArray(32, Bcs.u8()).transform(
input: (id) => fromHEX(id.toString()),
output: (id) => toHEX(Uint8List.fromList(id)),
);
final Coin = Bcs.struct('Coin', {
"id": UID,
"value": Bcs.u64(),
});
// deserialization: BCS bytes into Coin
final bcsBytes = Coin.serialize({
"id": '0000000000000000000000000000000000000000000000000000000000000001',
"value": BigInt.from(1000000),
}).toBytes();
final coin = Coin.parse(bcsBytes);
// serialization: Object into bytes - an Option with <T = Coin>
final hex = Bcs.option(Coin).serialize(coin).toHex();
print(hex);
Description
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
). There are no type hints in the serialized bytes on
what they mean, so the schema used for decoding must match the schema used to encode the data.
The bcs
library can be used to define schemas that can serialize and deserialize BCS
encoded data.
Basic types
bcs supports a number of built in base types that can be combined to create more complex types. The following table lists the primitive types available:
Method | Dart Type | Dart Input Type | Description |
---|---|---|---|
bool |
bool |
bool |
Boolean type (converts to true / false ) |
u8 , u16 , u32 |
int |
int |
Unsigned Integer types |
u64 , u128 , u256 |
BigInt |
BigInt |
Unsigned Integer types, decoded as string to allow for JSON serialization |
uleb128 |
int |
int |
Unsigned LEB128 integer type |
string |
String |
String |
UTF-8 encoded string |
bytes(size) |
Uint8List |
Uint8List |
Fixed length bytes |
import 'package:bcs/bcs.dart';
final u8 = Bcs.u8().serialize(100).toBytes();
final u64 = Bcs.u64().serialize(BigInt.from(1000000)).toBytes();
final u128 = Bcs.u128().serialize('100000010000001000000').toBytes();
final str = Bcs.string().serialize('this is an ascii string').toBytes();
final bytes = Bcs.bytes(4).serialize(Uint8List.fromList([1, 2, 3, 4])).toBytes();
final parsedU8 = Bcs.u8().parse(u8);
final parsedU64 = Bcs.u64().parse(u64);
final parsedU128 = Bcs.u128().parse(u128);
final parsedStr = Bcs.string().parse(str);
final parsedBytes = Bcs.bytes(4).parse(bytes);
Compound types
For most use-cases you'll want to combine primitive types into more complex types like vectors
,
structs
and enums
. The following table lists methods available for creating compound types:
Method | Description |
---|---|
vector(T type) |
A variable length list of values of type T |
fixedArray(size, T) |
A fixed length array of values of type T |
option(T type) |
A value of type T or null |
enumeration(name, values) |
An enum value representing one of the provided values |
struct(name, fields) |
A struct with named fields of the provided types |
tuple(types) |
A tuple of the provided types |
map(K, V) |
A map of keys of type K to values of type V |
import 'package:bcs/bcs.dart';
// Vectors
final intList = Bcs.vector(Bcs.u8()).serialize([1, 2, 3, 4, 5]).toBytes();
final stringList = Bcs.vector(Bcs.string()).serialize(['a', 'b', 'c']).toBytes();
// Arrays
final intArray = Bcs.fixedArray(4, Bcs.u8()).serialize([1, 2, 3, 4]).toBytes();
final stringArray = Bcs.fixedArray(3, Bcs.string()).serialize(['a', 'b', 'c']).toBytes();
// Option
final option = Bcs.option(Bcs.string()).serialize('some value').toBytes();
final nullOption = Bcs.option(Bcs.string()).serialize(null).toBytes();
// Enum
final MyEnum = Bcs.enumeration('MyEnum', {
"NoType": null,
"Int": Bcs.u8(),
"String": Bcs.string(),
"Array": Bcs.fixedArray(3, Bcs.u8()),
});
final noTypeEnum = MyEnum.serialize({ "NoType": null }).toBytes();
final intEnum = MyEnum.serialize({ "Int": 100 }).toBytes();
final stringEnum = MyEnum.serialize({ "String": 'string' }).toBytes();
final arrayEnum = MyEnum.serialize({ "Array": [1, 2, 3] }).toBytes();
// Struct
final MyStruct = Bcs.struct('MyStruct', {
"id": Bcs.u8(),
"name": Bcs.string(),
});
final struct = MyStruct.serialize({ "id": 1, "name": 'name' }).toBytes();
// Tuple
final tuple = Bcs.tuple([Bcs.u8(), Bcs.string()]).serialize([1, 'name']).toBytes();
// Map
final map = Bcs
.map(Bcs.u8(), Bcs.string())
.serialize(
{
1: 'one',
2: 'two',
}).toBytes();
// Parsing data back into original types
// Vectors
final parsedIntList = Bcs.vector(Bcs.u8()).parse(intList);
final parsedStringList = Bcs.vector(Bcs.string()).parse(stringList);
// Arrays
final parsedIntArray = Bcs.fixedArray(4, Bcs.u8()).parse(intArray);
// Option
final parsedOption = Bcs.option(Bcs.string()).parse(option);
final parsedNullOption = Bcs.option(Bcs.string()).parse(nullOption);
// Enum
final parsedNoTypeEnum = MyEnum.parse(noTypeEnum);
final parsedIntEnum = MyEnum.parse(intEnum);
final parsedStringEnum = MyEnum.parse(stringEnum);
final parsedArrayEnum = MyEnum.parse(arrayEnum);
// Struct
final parsedStruct = MyStruct.parse(struct);
// Tuple
final parsedTuple = Bcs.tuple([Bcs.u8(), Bcs.string()]).parse(tuple);
// Map
final parsedMap = Bcs.map(Bcs.u8(), Bcs.string()).parse(map);
Generics
To define a generic struct or an enum, you can define a generic typescript function helper
import 'package:bcs/bcs.dart';
import 'package:bcs/bcs_type.dart';
// The T typescript generic is a placeholder for the typescript type of the generic value
// The T argument will be the bcs type passed in when creating a concrete instance of the Container type
BcsType Container<T>(BcsType<T, T> T) {
return Bcs.struct('Container<T>', {
"contents": T,
});
}
// When serializing, we have to pass the type to use for `T`
final bytes = Container(Bcs.u8()).serialize({ "contents": 100 }).toBytes();
// Alternatively we can save the concrete type as a variable
// final U8Container = Container(Bcs.u8());
// final bytes = U8Container.serialize({ "contents": 100 }).toBytes();
// Using multiple generics
BcsType VecMap<K, V>(BcsType<K, K> K, BcsType<V, V> V) {
// You can use the names of the generic params in the type name to
return Bcs.struct(
// You can use the names of the generic params to give your type a more useful name
"VecMap<${K.name}, ${V.name}>",
{
"keys": Bcs.vector(K),
"values": Bcs.vector(V),
}
);
}
// To serialize VecMap, we can use:
VecMap(Bcs.string(), Bcs.string())
.serialize({
"keys": ['key1', 'key2', 'key3'],
"values": ['value1', 'value2', 'value3'],
})
.toBytes();
Transforms
If you the format you use in your code is different from the format expected for BCS serialization,
you can use the transform
API to map between the types you use in your application, and the types
needed for serialization.
The address
type used by Move code is a good example of this. In many cases, you'll want to
represent an address as a hex string, but the BCS serialization format for addresses is a 32 byte
array. To handle this, you can use the transform
API to map between the two formats:
final Address = Bcs.bytes(32).transform(
input: (val) => fromHEX(val.toString()),
output: (val) => toHEX(val),
);
final serialized = Address.serialize('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef').toBytes();
final parsed = Address.parse(serialized);
Formats for serialized bytes
When you call serialize
on a BcsType
, you will receive a SerializedBcs
instance. This wrapper
preserves type information for the serialized bytes, and can be used to get raw data in various
formats.
final serializedString = Bcs.string().serialize('this is a string');
// SerializedBcs.toBytes() returns a Uint8List
final bytes = serializedString.toBytes();
// You can get the serialized bytes encoded as hex, base64 or base58
final hex = serializedString.toHex();
final base64 = serializedString.toBase64();
final base58 = serializedString.toBase58();
// To parse a BCS value from bytes, the bytes need to be a Uint8List
final str1 = Bcs.string().parse(bytes);
// If your data is encoded as string, you need to convert it to Uint8List first
final str2 = Bcs.string().parse(fromHEX(hex));
final str3 = Bcs.string().parse(fromB64(base64));
final str4 = Bcs.string().parse(fromB58(base58));
expect((str1 == str2) == (str3 == str4), true);