polkadart_scale_codec
A Dart library for encoding and decoding data using the SCALE (Simple Concatenated Aggregate Little-Endian) codec used in Polkadot and Substrate-based blockchains.
Features
- Full support for all SCALE primitive types
- Composite types (structs, tuples, enums)
- Collection types (arrays, sequences, maps, sets)
- Optional and Result types
- Compact integer encoding
- Bit sequences with custom bit ordering
- Zero-copy encoding/decoding where possible
Installation
Add this to your pubspec.yaml:
dependencies:
polkadart_scale_codec: ^latest_version
Quick Start
import 'package:polkadart_scale_codec/polkadart_scale_codec.dart';
void main() {
// Create an output buffer
final output = HexOutput();
// Encode a value
U8Codec.codec.encodeTo(42, output);
print(output.toString()); // 0x2a
// Decode it back
final input = Input.fromHex('0x2a');
final decoded = U8Codec.codec.decode(input);
print(decoded); // 42
}
Core Concepts
Input and Output
All encoding and decoding operations use Input and Output interfaces:
-
Input- For reading encoded dataInput.fromHex(String)- Create from hex stringInput.fromBytes(List<int>)- Create from bytes
-
Output- For writing encoded dataHexOutput()- Outputs hex stringByteOutput()- Outputs raw bytes
Codecs
Every type has a corresponding Codec that provides:
encodeTo(value, output)- Encode a valuedecode(input)- Decode a valueencode(value)- Encode to bytes (convenience method)sizeHint(value)- Get encoded size in bytes
Supported Types
Integer Types
Unsigned Integers
| Type | Size | Dart Type | Example |
|---|---|---|---|
u8 |
1 byte | int |
0 to 255 |
u16 |
2 bytes | int |
0 to 65,535 |
u32 |
4 bytes | int |
0 to 4,294,967,295 |
u64 |
8 bytes | BigInt |
0 to 2^64-1 |
u128 |
16 bytes | BigInt |
0 to 2^128-1 |
u256 |
32 bytes | BigInt |
0 to 2^256-1 |
// Small integers (u8, u16, u32)
final output = HexOutput();
U8Codec.codec.encodeTo(69, output);
print(output); // 0x45
// Large integers (u64, u128, u256)
final bigInt = BigInt.parse('115792089237316195423570985008687907853269984665640564039457584007913129639935');
final output2 = HexOutput();
U256Codec.codec.encodeTo(bigInt, output2);
print(output2); // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
Signed Integers
| Type | Size | Dart Type | Range |
|---|---|---|---|
i8 |
1 byte | int |
-128 to 127 |
i16 |
2 bytes | int |
-32,768 to 32,767 |
i32 |
4 bytes | int |
-2^31 to 2^31-1 |
i64 |
8 bytes | BigInt |
-2^63 to 2^63-1 |
i128 |
16 bytes | BigInt |
-2^127 to 2^127-1 |
i256 |
32 bytes | BigInt |
-2^255 to 2^255-1 |
final output = HexOutput();
I8Codec.codec.encodeTo(-128, output);
print(output); // 0x80
final input = Input.fromHex('0x80');
final decoded = I8Codec.codec.decode(input);
print(decoded); // -128
Compact Encoding
Compact encoding is a variable-length encoding for integers that uses fewer bytes for smaller values.
// CompactCodec for regular integers
final output = HexOutput();
CompactCodec.codec.encodeTo(69, output);
print(output); // 0x1501
// CompactBigIntCodec for BigInt values
final bigValue = BigInt.parse('18446744073709551615');
final output2 = HexOutput();
CompactBigIntCodec.codec.encodeTo(bigValue, output2);
print(output2); // 0x13ffffffffffffffff
Compact encoding rules:
- 0-63: Single byte mode (value << 2)
- 64-16383: Two-byte mode
- 16384-1073741823: Four-byte mode
- Larger: Multi-byte mode with length prefix
Basic Types
String
final output = HexOutput();
StrCodec.codec.encodeTo('hello', output);
print(output); // 0x1468656c6c6f (length prefix + UTF-8 bytes)
final input = Input.fromHex('0x1468656c6c6f');
final decoded = StrCodec.codec.decode(input);
print(decoded); // hello
Boolean
final output = HexOutput();
BoolCodec.codec.encodeTo(true, output);
print(output); // 0x01
final output2 = HexOutput();
BoolCodec.codec.encodeTo(false, output2);
print(output2); // 0x00
Option Type
Represents an optional value (Some or None).
// Using nullable syntax
final codec = OptionCodec(BoolCodec.codec);
final output = HexOutput();
codec.encodeTo(true, output);
print(output); // 0x0101 (Some(true))
final output2 = HexOutput();
codec.encodeTo(null, output2);
print(output2); // 0x00 (None)
// Using Option class
final value = Option.some(true);
final output3 = HexOutput();
NestedOptionCodec(BoolCodec.codec).encodeTo(value, output3);
print(output3); // 0x0101
Result Type
Represents a value that can be either Ok or Err.
final codec = ResultCodec(U8Codec.codec, BoolCodec.codec);
// Ok variant
final okValue = Result<int, bool>.ok(42);
final output = HexOutput();
codec.encodeTo(okValue, output);
print(output); // 0x002a
final input = Input.fromHex('0x002a');
final decoded = codec.decode(input);
print(decoded.isOk); // true
print(decoded.okValue); // 42
// Err variant
final errValue = Result<int, bool>.err(false);
final output2 = HexOutput();
codec.encodeTo(errValue, output2);
print(output2); // 0x0100
Collections
Array (Fixed Length)
Arrays have a fixed size known at compile time.
// Array of 4 u8 values
final codec = ArrayCodec(U8Codec.codec, 4);
final output = HexOutput();
codec.encodeTo([1, 2, 3, 4], output);
print(output); // 0x01020304
final input = Input.fromHex('0x01020304');
final decoded = codec.decode(input);
print(decoded); // [1, 2, 3, 4]
Sequence (Variable Length)
Sequences have a variable length with a compact-encoded length prefix.
final codec = SequenceCodec(U8Codec.codec);
final output = HexOutput();
codec.encodeTo([1, 2, 3, 4], output);
print(output); // 0x1001020304 (length prefix + data)
final input = Input.fromHex('0x1001020304');
final decoded = codec.decode(input);
print(decoded); // [1, 2, 3, 4]
BTreeMap
Maps with compact-encoded length prefix.
final codec = BTreeMapCodec(
keyCodec: U32Codec.codec,
valueCodec: BoolCodec.codec,
);
final output = HexOutput();
codec.encodeTo({632: false}, output);
print(output); // 0x047802000000
final input = Input.fromHex('0x047802000000');
final decoded = codec.decode(input);
print(decoded); // {632: false}
Set
Bit-flag based sets for enum-like values.
final codec = SetCodec(8, ['Read', 'Write', 'Execute']);
final output = HexOutput();
codec.encodeTo(['Read', 'Execute'], output);
print(output); // 0x05 (binary: 00000101)
final input = Input.fromHex('0x05');
final decoded = codec.decode(input);
print(decoded); // ['Read', 'Execute']
Composite Types
Tuple
Fixed-size collection of values of different types.
final codec = TupleCodec([
CompactCodec.codec,
BoolCodec.codec,
StrCodec.codec,
]);
final output = HexOutput();
codec.encodeTo([3, true, 'hi'], output);
print(output); // 0x0c01086869
final input = Input.fromHex('0x0c01086869');
final decoded = codec.decode(input);
print(decoded); // [3, true, 'hi']
Composite (Struct)
Named fields with different types.
final codec = CompositeCodec({
'index': U8Codec.codec,
'name': StrCodec.codec,
'active': BoolCodec.codec,
});
final output = HexOutput();
codec.encodeTo({
'index': 1,
'name': 'polkadart',
'active': true,
}, output);
final input = Input.fromHex(output.toString());
final decoded = codec.decode(input);
print(decoded); // {index: 1, name: polkadart, active: true}
Enums
Simple Enum
Enum with unit variants (no data).
final codec = SimpleEnumCodec.fromList(['Red', 'Green', 'Blue']);
final output = HexOutput();
codec.encodeTo('Green', output);
print(output); // 0x01
final input = Input.fromHex('0x01');
final decoded = codec.decode(input);
print(decoded); // Green
Complex Enum
Enum with variants that carry data.
final codec = ComplexEnumCodec.sparse({
0: const MapEntry<String, Codec>('Plain', StrCodec.codec),
1: MapEntry<String, Codec>('Fancy', CompositeCodec({
'index': U8Codec.codec,
'name': StrCodec.codec,
})),
});
final output = HexOutput();
codec.encodeTo(MapEntry('Fancy', {
'index': 1,
'name': 'polkadart',
}), output);
final input = Input.fromHex(output.toString());
final decoded = codec.decode(input);
print(decoded.key); // Fancy
print(decoded.value); // {index: 1, name: polkadart}
Bit Sequences
Compact representation of bit arrays with configurable bit ordering.
final codec = BitSequenceCodec(BitStore.U8, BitOrder.LSB);
final bitArray = BitArray.parseBinary('11111');
final output = HexOutput();
codec.encodeTo(bitArray, output);
print(output); // 0x141f (length + packed bits)
final input = Input.fromHex('0x141f');
final decoded = codec.decode(input);
print(decoded.toBinaryString()); // 11111
Advanced Features
Length-Prefixed Codec
Wraps any codec with a compact length prefix.
final codec = LengthPrefixedCodec(StrCodec.codec);
final output = HexOutput();
codec.encodeTo('hello', output);
// Outputs: compact(length) + compact(str_length) + bytes
Proxy Codec
Used for recursive type definitions.
final proxyCodec = ProxyCodec();
// Define recursive structure
proxyCodec.codec = ComplexEnumCodec.sparse({
0: MapEntry('Leaf', U8Codec.codec),
1: MapEntry('Branch', proxyCodec), // Self-reference
});
Scale Raw Bytes
For including pre-encoded SCALE data without re-encoding.
final preEncoded = U8Codec.codec.encode(42);
final rawBytes = ScaleRawBytes(preEncoded);
// Use in a larger structure without decode/re-encode
final codec = CompositeCodec({
'data': ScaleRawBytes.codec,
});
Null Codec
Empty codec that encodes/decodes nothing (zero bytes).
final codec = NullCodec.codec;
final output = HexOutput();
codec.encodeTo(null, output);
print(output); // 0x (empty)
Working with Bytes
Encoding to Different Formats
final value = 42;
// Hex string
final hexOutput = HexOutput();
U8Codec.codec.encodeTo(value, hexOutput);
print(hexOutput.toString()); // 0x2a
// Raw bytes
final byteOutput = ByteOutput();
U8Codec.codec.encodeTo(value, byteOutput);
final bytes = byteOutput.toBytes();
print(bytes); // Uint8List [42]
// Using encode() helper
final encoded = U8Codec.codec.encode(value);
print(encoded); // Uint8List [42]
Decoding from Different Sources
// From hex string
final input1 = Input.fromHex('0x2a');
final decoded1 = U8Codec.codec.decode(input1);
// From bytes
final input2 = Input.fromBytes([42]);
final decoded2 = U8Codec.codec.decode(input2);
// From Uint8List
final uint8list = Uint8List.fromList([42]);
final input3 = Input.fromBytes(uint8list);
final decoded3 = U8Codec.codec.decode(input3);
Size Hints
Get the encoded size of a value without actually encoding it:
final value = 'hello';
final size = StrCodec.codec.sizeHint(value);
print(size); // Number of bytes when encoded
// Useful for pre-allocating buffers
final buffer = ByteOutput(size);
StrCodec.codec.encodeTo(value, buffer);
Common Patterns
Encoding Multiple Values
final output = HexOutput();
U8Codec.codec.encodeTo(1, output);
BoolCodec.codec.encodeTo(true, output);
StrCodec.codec.encodeTo('test', output);
print(output); // All values concatenated
Decoding Multiple Values
final input = Input.fromHex('0x0101107465737402');
final value1 = U8Codec.codec.decode(input);
final value2 = BoolCodec.codec.decode(input);
final value3 = StrCodec.codec.decode(input);
final value4 = U8Codec.codec.decode(input);
print([value1, value2, value3, value4]); // [1, true, test, 2]
Checking Remaining Data
final input = Input.fromHex('0x2a');
print(input.hasBytes()); // true
final value = U8Codec.codec.decode(input);
print(input.hasBytes()); // false
// Assert all data consumed
input.assertEndOfDataReached(); // Throws if data remaining
Type Reference
Integer Sequence/Array Codecs
For convenience, specialized codecs exist for integer sequences:
// Sequences (with length prefix)
U8SequenceCodec.codec
U16SequenceCodec.codec
U32SequenceCodec.codec
U64SequenceCodec.codec
I8SequenceCodec.codec
I16SequenceCodec.codec
I32SequenceCodec.codec
I64SequenceCodec.codec
// Arrays (fixed length)
U8ArrayCodec(length)
U16ArrayCodec(length)
U32ArrayCodec(length)
U64ArrayCodec(length)
I8ArrayCodec(length)
I16ArrayCodec(length)
I32ArrayCodec(length)
I64ArrayCodec(length)
Resources
License
See the LICENSE file for details.