This Ruby gem provides an implementation of Binary Canonical Serialization (BCS), a serialization format used in blockchain applications like Move language. BCS provides canonical (deterministic) serialization across different platforms and languages.
Add this line to your application's Gemfile:
gem 'bcs'And then execute:
$ bundle installOr install it yourself as:
$ gem install bcsrequire 'bcs'
# Define a simple struct
Coin = Bcs.struct("Coin", {
id: Bcs.bytes(32),
value: Bcs.u64
})
# Serialize data
coin_data = {
id: Array.new(32, 0),
value: 1000000
}
# Serialize to bytes
bytes = Coin.serialize(coin_data)
puts "Serialized to #{bytes.length} bytes"
# Deserialize back to original format
deserialized = Coin.deserialize(bytes)
puts deserialized.inspect
# => {:id=>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], :value=>1000000}BCS supports several primitive types:
# 8-bit, 16-bit, 32-bit, 64-bit, 128-bit, and 256-bit unsigned integers
u8_value = Bcs.u8.serialize(255)
u64_value = Bcs.u64.serialize(18446744073709551615)
u128_value = Bcs.u128.serialize(340282366920938463463374607431768211455)
# Deserialize
Bcs.u8.deserialize(u8_value) # => 255
Bcs.u64.deserialize(u64_value) # => 18446744073709551615bool_bytes = Bcs.bool.serialize(true)
Bcs.bool.deserialize(bool_bytes) # => truestr_bytes = Bcs.string.serialize("Hello, 世界")
Bcs.string.deserialize(str_bytes) # => "Hello, 世界"# Exactly 32 bytes
bytes_type = Bcs.bytes(32)
data = Array.new(32, 42)
bytes = bytes_type.serialize(data)
deserialized = bytes_type.deserialize(bytes)uleb_bytes = Bcs.uleb128.serialize(1000000)
Bcs.uleb128.deserialize(uleb_bytes) # => 1000000# Vector of u8 values
vector_type = Bcs.vector(Bcs.u8)
data = [1, 2, 3, 4, 5]
bytes = vector_type.serialize(data)
deserialized = vector_type.deserialize(bytes)
# => [1, 2, 3, 4, 5]
# Vector of strings
string_vector = Bcs.vector(Bcs.string)
words = ["hello", "world", "BCS"]
bytes = string_vector.serialize(words)
deserialized = string_vector.deserialize(bytes)
# => ["hello", "world", "BCS"]# Fixed array of 4 u16 values
array_type = Bcs.fixed_array(4, Bcs.u16)
data = [1000, 2000, 3000, 4000]
bytes = array_type.serialize(data)
deserialized = array_type.deserialize(bytes)
# => [1000, 2000, 3000, 4000]# Optional string
option_type = Bcs.option(Bcs.string)
# Some value
some_bytes = option_type.serialize("present")
option_type.deserialize(some_bytes) # => "present"
# None value (nil)
none_bytes = option_type.serialize(nil)
option_type.deserialize(none_bytes) # => nil# Define a Person struct
Person = Bcs.struct("Person", {
name: Bcs.string,
age: Bcs.u8,
email: Bcs.option(Bcs.string),
addresses: Bcs.vector(Bcs.struct("Address", {
street: Bcs.string,
city: Bcs.string,
zip_code: Bcs.string
}))
})
person_data = {
name: "Alice Johnson",
age: 30,
email: "alice@example.com",
addresses: [
{
street: "123 Main St",
city: "San Francisco",
zip_code: "94105"
},
{
street: "456 Oak Ave",
city: "New York",
zip_code: "10001"
}
]
}
bytes = Person.serialize(person_data)
deserialized = Person.deserialize(bytes)
puts deserialized.inspect# Define an enum with different variants
Status = Bcs.enum("Status", {
pending: nil, # Unit variant
approved: Bcs.string, # Variant with data
rejected: Bcs.struct("Reason", { # Variant with complex data
code: Bcs.u16,
message: Bcs.string
})
})
# Different enum values
pending_status = { pending: true }
approved_status = { approved: "John Doe" }
rejected_status = { rejected: { code: 404, message: "Not Found" } }
# Serialize and deserialize
pending_bytes = Status.serialize(pending_status)
approved_bytes = Status.serialize(approved_status)
rejected_bytes = Status.serialize(rejected_status)
Status.deserialize(pending_bytes) # => {:pending=>true}
Status.deserialize(approved_bytes) # => {:approved=>"John Doe"}
Status.deserialize(rejected_bytes) # => {:rejected=>{:code=>404, :message=>"Not Found"}}# Define a tuple type
tuple_type = Bcs.tuple([Bcs.u32, Bcs.string, Bcs.bool])
tuple_data = [42, "answer", true]
bytes = tuple_type.serialize(tuple_data)
deserialized = tuple_type.deserialize(bytes)
# => [42, "answer", true]# Map from strings to u64 values
balance_type = Bcs.map(Bcs.string, Bcs.u64)
balances = {
"BTC" => 1000000,
"ETH" => 2000000000000000000,
"USDC" => 500000000
}
bytes = balance_type.serialize(balances)
deserialized = balance_type.deserialize(bytes)
# => {"BTC"=>1000000, "ETH"=>2000000000000000000, "USDC"=>500000000}You can transform types to handle different input/output formats:
# Transform hex string to 32-byte array
Address = Bcs.bytes(32).transform(
input_proc: ->(hex_str) { [hex_str.gsub(/^0x/, '')].pack('H*') },
output_proc: ->(bytes) { "0x#{bytes.unpack('H*').first}" }
)
# Use hex strings directly
hex_address = "0x" + "a" * 64
bytes = Address.serialize(hex_address)
deserialized = Address.deserialize(bytes)
puts deserialized # => "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
# Transform timestamps between seconds and milliseconds
TimestampMs = Bcs.u64.transform(
input_proc: ->(seconds) { seconds * 1000 },
output_proc: ->(milliseconds) { milliseconds / 1000 }
)
timestamp_seconds = 1640995200 # Unix timestamp
bytes = TimestampMs.serialize(timestamp_seconds)
deserialized = TimestampMs.deserialize(bytes)
puts deserialized # => 1640995200BCS supports multiple output formats:
Coin = Bcs.struct("Coin", {
id: Bcs.bytes(32),
value: Bcs.u64
})
coin_data = {
id: Array.new(32, 0),
value: 1000000
}
# Raw bytes
bytes = Coin.serialize(coin_data)
# Hex string
hex_string = Coin.to_hex(coin_data)
puts hex_string # => "000000000000000000000000000000000000000000000000000000000000000040420f0000000000"
# Base64 string
base64_string = Coin.to_base64(coin_data)
puts base64_string # => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEECDgAAAAA="
# Decode from different formats
from_hex = Coin.from_hex(hex_string)
from_base64 = Coin.from_base64(base64_string)Here's a more complex example for a cryptocurrency transaction:
# Address type (32 bytes as hex string)
Address = Bcs.bytes(32).transform(
input_proc: ->(hex_str) { [hex_str.gsub(/^0x/, '')].pack('H*') },
output_proc: ->(bytes) { "0x#{bytes.unpack('H*').first}" }
)
# Coin type
Coin = Bcs.struct("Coin", {
id: Bcs.bytes(32),
value: Bcs.u64
})
# Transaction type
Transaction = Bcs.struct("Transaction", {
sender: Address,
receiver: Address,
amount: Bcs.u64,
fee: Bcs.u64,
coins: Bcs.vector(Coin),
timestamp: Bcs.u64
})
# Create a transaction
transaction = {
sender: "0x" + "1" * 64,
receiver: "0x" + "2" * 64,
amount: 1000000000,
fee: 1000000,
coins: [
{ id: ["0x" + "3" * 64].pack('H*'), value: 500000000 },
{ id: ["0x" + "4" * 64].pack('H*'), value: 500000001 }
],
timestamp: Time.now.to_i
}
# Serialize transaction
tx_bytes = Transaction.serialize(transaction)
puts "Transaction size: #{tx_bytes.length} bytes"
# Deserialize transaction
deserialized_tx = Transaction.deserialize(tx_bytes)
puts deserialized_tx.inspectBCS provides comprehensive error handling:
begin
Bcs.u8.serialize(256) # Too large for u8
rescue Bcs::ValidationError => e
puts "Validation error: #{e.message}"
end
begin
Bcs.u32.deserialize("short") # Not enough bytes
rescue Bcs::DeserializationError => e
puts "Deserialization error: #{e.message}"
end
begin
Bcs.bool.serialize("not_boolean") # Wrong type
rescue Bcs::ValidationError => e
puts "Validation error: #{e.message}"
endBCS is designed for high performance:
# Large data handling
large_array = Array.new(10000) { |i| i % 256 }
vector_type = Bcs.vector(Bcs.u8)
start_time = Time.now
bytes = vector_type.serialize(large_array)
serialize_time = Time.now - start_time
start_time = Time.now
deserialized = vector_type.deserialize(bytes)
deserialize_time = Time.now - start_time
puts "Serialize time: #{serialize_time}s"
puts "Deserialize time: #{deserialize_time}s"
puts "Data size: #{bytes.length} bytes"This Ruby implementation is compatible with the TypeScript BCS library. Data serialized with one can be deserialized with the other, ensuring cross-platform compatibility.
| Feature | TypeScript BCS | Ruby BCS |
|---|---|---|
| Basic Types | ✅ | ✅ |
| Complex Types | ✅ | ✅ |
| Transformations | ✅ | ✅ |
| Type Inference | ✅ | |
| Hex/Base64 Support | ✅ | ✅ |
| Error Handling | ✅ | ✅ |
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a Pull Request
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
This gem is available as open source under the terms of the Apache-2.0 License.
- BCS specification and reference implementation: https://github.com/zefchain/bcs
- Original TypeScript implementation: https://github.com/MystenLabs/ts-sdks