inodrahq / bcs
A PHP implementation of BCS (Binary Canonical Serialization) — the serialization format used by Sui, Aptos, and other Move-based blockchains
v1.0.0
2026-03-10 22:33 UTC
Requires
- php: >=8.2
- ext-bcmath: *
Requires (Dev)
- phpstan/phpstan: ^2
- phpunit/phpunit: ^11
README
A PHP implementation of Binary Canonical Serialization (BCS) — the serialization format used by Sui, Aptos, and other Move-based blockchains.
Requirements
- PHP 8.2+
- ext-bcmath
Installation
composer require inodrahq/bcs
Quick Start
use Inodrahq\Bcs\Encoder; use Inodrahq\Bcs\Decoder; use Inodrahq\Bcs\U64; use Inodrahq\Bcs\BcsString; use Inodrahq\Bcs\Vector; use Inodrahq\Bcs\Boolean; // Encode a u64 $encoder = new Encoder(); (new U64(1000000))->encode($encoder); $bytes = $encoder->getBytes(); // raw BCS bytes // Decode it back $decoder = new Decoder($bytes); $value = U64::decode($decoder); echo $value->getValue(); // "1000000" // Encode a string $encoder = new Encoder(); (new BcsString('hello'))->encode($encoder); // Encode a vector of u64s $encoder = new Encoder(); $vec = new Vector(U64::class, [new U64(1), new U64(2), new U64(3)]); $vec->encode($encoder);
Supported Types
| BCS Type | PHP Class | Description |
|---|---|---|
bool |
Boolean |
True/false |
u8 |
U8 |
Unsigned 8-bit integer |
u16 |
U16 |
Unsigned 16-bit little-endian |
u32 |
U32 |
Unsigned 32-bit little-endian |
u64 |
U64 |
Unsigned 64-bit little-endian |
u128 |
U128 |
Unsigned 128-bit little-endian |
u256 |
U256 |
Unsigned 256-bit little-endian |
i8 |
I8 |
Signed 8-bit integer |
i16 |
I16 |
Signed 16-bit little-endian |
i32 |
I32 |
Signed 32-bit little-endian |
i64 |
I64 |
Signed 64-bit little-endian |
i128 |
I128 |
Signed 128-bit little-endian |
bytes |
Bytes |
Fixed-length byte array |
string |
BcsString |
ULEB128-prefixed UTF-8 string |
vector<T> |
Vector |
ULEB128-prefixed sequence |
option<T> |
Option |
0x00 (None) or 0x01 + value (Some) |
map<K,V> |
Map |
Sorted key-value pairs |
struct |
Struct |
Ordered named fields |
enum |
Enum |
ULEB128 variant index + data |
tuple |
Tuple |
Ordered unnamed fields |
fixed_array |
FixedArray |
Fixed-length array (no length prefix) |
byte_vector |
ByteVector |
ULEB128-prefixed raw bytes |
Advanced Usage
Structs
use Inodrahq\Bcs\Struct; use Inodrahq\Bcs\U64; use Inodrahq\Bcs\BcsString; // Define and encode a struct $struct = new Struct([ 'name' => new BcsString('Alice'), 'balance' => new U64(1000), ]); $encoder = new Encoder(); $struct->encode($encoder);
Enums
use Inodrahq\Bcs\Enum; use Inodrahq\Bcs\U64; use Inodrahq\Bcs\BcsString; // Variant 0 with a string payload $enum = new Enum(0, new BcsString('hello')); // Variant 1 with a u64 payload $enum = new Enum(1, new U64(42));
Options
use Inodrahq\Bcs\Option; use Inodrahq\Bcs\U64; $some = Option::some(new U64(42)); $none = Option::none();
Maps
use Inodrahq\Bcs\Map; use Inodrahq\Bcs\BcsString; use Inodrahq\Bcs\U64; $map = new Map(BcsString::class, U64::class, [ [new BcsString('alice'), new U64(100)], [new BcsString('bob'), new U64(200)], ]);
Tuples
use Inodrahq\Bcs\Tuple; use Inodrahq\Bcs\U64; use Inodrahq\Bcs\BcsString; // Heterogeneous ordered collection $tuple = new Tuple([U64::class, BcsString::class], [new U64(42), new BcsString('hello')]); $encoder = new Encoder(); $tuple->encode($encoder); // Decode with type context $decoder = new Decoder($encoder->getBytes()); $decoded = Tuple::decodeWithTypes($decoder, [U64::class, BcsString::class]); echo $decoded->get(0)->getValue(); // "42" echo $decoded->get(1)->getValue(); // "hello"
Fixed Arrays
use Inodrahq\Bcs\FixedArray; use Inodrahq\Bcs\U8; // Fixed-size array (no length prefix, unlike Vector) $arr = new FixedArray(U8::class, [new U8(1), new U8(2), new U8(3)], 3); $encoder = new Encoder(); $arr->encode($encoder); // Decode with known size $decoder = new Decoder($encoder->getBytes()); $decoded = FixedArray::decodeWithType($decoder, U8::class, 3);
Decoding
All container types require type context for decoding:
use Inodrahq\Bcs\Decoder; use Inodrahq\Bcs\Vector; use Inodrahq\Bcs\Struct; use Inodrahq\Bcs\Enum; use Inodrahq\Bcs\Option; use Inodrahq\Bcs\Map; // Decode from hex $decoder = Decoder::fromHex('03010203'); $vec = Vector::decodeWithType($decoder, U8::class); // Struct decoding (fields must match encoding order) $decoder = new Decoder($bytes); $struct = Struct::decodeWithTypes($decoder, [ 'name' => BcsString::class, 'balance' => U64::class, ]); echo $struct->getField('name')->getValue(); // Enum decoding (map variant tag => type class, null for no data) $decoder = new Decoder($bytes); $enum = Enum::decodeWithVariants($decoder, [ 0 => BcsString::class, // variant 0 has a string 1 => U64::class, // variant 1 has a u64 2 => null, // variant 2 has no data ]); // Option decoding $decoder = new Decoder($bytes); $opt = Option::decodeWithType($decoder, U64::class); if ($opt->isSome()) { echo $opt->getValue()->getValue(); } // Map decoding $decoder = new Decoder($bytes); $map = Map::decodeWithTypes($decoder, BcsString::class, U64::class);
Custom Types
Implement the BcsType interface:
use Inodrahq\Bcs\BcsType; use Inodrahq\Bcs\Encoder; use Inodrahq\Bcs\Decoder; class MyType implements BcsType { public function encode(Encoder $encoder): void { // Write your bytes } public static function decode(Decoder $decoder): static { // Read and return } }
Hex Helpers
All encoded types support hex output via the Encoder:
$encoder = new Encoder(); (new U64(255))->encode($encoder); echo bin2hex($encoder->getBytes()); // "ff00000000000000"
Testing
composer test
228 tests verified against golden vectors generated by the Rust bcs crate.
License
MIT