BytesWriter and BytesReader handle binary serialization and deserialization. They're used for encoding return values, event data, and cross-contract communication.
import { BytesWriter, Address } from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
// Create writer with initial capacity
const writer = new BytesWriter(64);
// Write data
writer.writeAddress(recipient);
writer.writeU256(amount);
writer.writeBoolean(true);
// Get encoded bytes
const data: Uint8Array = writer.getBuffer();classDiagram
class BytesWriter {
-DataView buffer
-Uint8Array typedArray
-u32 currentOffset
+BytesWriter(length)
+write~T~(value) void
+writeU8(value)
+writeU16(value, be)
+writeU32(value, be)
+writeU64(value, be)
+writeU128(value, be)
+writeU256(value, be)
+writeAddress(addr)
+writeExtendedAddress(extAddr)
+writeSchnorrSignature(addr, sig)
+writeString(str)
+writeBytes(data)
+writeBoolean(bool)
+writeExtendedAddressArray(arr)
+writeExtendedAddressMapU256(map)
+getBuffer() Uint8Array
}
class BytesReader {
-DataView buffer
-i32 currentOffset
+BytesReader(bytes)
+read~T~() T
+readU8() u8
+readU16(be) u16
+readU32(be) u32
+readU64(be) u64
+readU128(be) u128
+readU256(be) u256
+readAddress() Address
+readExtendedAddress() ExtendedAddress
+readSchnorrSignature() SchnorrSignature
+readString() string
+readBytes(length) Uint8Array
+readBoolean() bool
+readExtendedAddressArray() ExtendedAddress[]
+readExtendedAddressMapU256() ExtendedAddressMap
+hasMoreData() bool
}
class DataView {
<<built-in>>
+getUint8()
+setUint8()
+getUint32()
+setUint32()
}
BytesWriter --> DataView : uses
BytesReader --> DataView : uses
note for BytesWriter "Sequential write\nfixed-size buffer"
note for BytesReader "Sequential read\nwith position tracking"
// With initial capacity (recommended - buffer does NOT auto-grow)
const writer = new BytesWriter(128);
// Note: Buffer does NOT grow dynamically - will throw Revert if exceeded
// Always pre-calculate required size or use generous initial capacity// Boolean
writer.writeBoolean(true); // 1 byte
// Unsigned integers
writer.writeU8(255); // 1 byte
writer.writeU16(65535); // 2 bytes
writer.writeU32(4294967295); // 4 bytes
writer.writeU64(value); // 8 bytes
writer.writeU128(value); // 16 bytes
writer.writeU256(value); // 32 bytes
// Signed integers
writer.writeI8(-128);
writer.writeI16(-32768);
writer.writeI32(-2147483648);
writer.writeI64(value);// Address (32 bytes)
writer.writeAddress(address);
// ExtendedAddress (64 bytes: 32 tweaked key + 32 ML-DSA key hash)
writer.writeExtendedAddress(extendedAddress);
// Schnorr signature with signer address (128 bytes total)
writer.writeSchnorrSignature(signerAddress, signature);
// String without length prefix
writer.writeString('Hello, World!');
// String with u32 length prefix
writer.writeStringWithLength('Hello, World!');
// Bytes without length prefix
writer.writeBytes(data);
// Bytes with u32 length prefix
writer.writeBytesWithLength(data);
// Selector (4 bytes, big-endian)
writer.writeSelector(selector);The write<T>() method automatically selects the correct write method based on the type:
// Automatically dispatches to the correct write method
writer.write<u64>(12345); // writeU64
writer.write<u256>(amount); // writeU256
writer.write<bool>(true); // writeBoolean
writer.write<Address>(addr); // writeAddress
writer.write<ExtendedAddress>(extAddr); // writeExtendedAddress
writer.write<string>('hello'); // writeStringWithLength
// Supported types:
// - Primitives: bool, u8, u16, u32, u64, i8, i16, i32, i64
// - Big numbers: u128, u256, i128
// - Complex: Address, ExtendedAddress, Selector, string, Uint8ArrayAll array methods use a u16 length prefix (max 65535 elements):
// Address array (u16 length prefix + addresses)
writer.writeAddressArray(addresses);
// ExtendedAddress array (u16 length prefix + 64-byte addresses)
writer.writeExtendedAddressArray(extendedAddresses);
// Numeric arrays (u16 length prefix + values)
writer.writeU8Array(u8Values);
writer.writeU16Array(u16Values);
writer.writeU32Array(u32Values);
writer.writeU64Array(u64Values);
writer.writeU128Array(u128Values);
writer.writeU256Array(u256Values);
// Array of variable-length buffers (u16 count, then u32 length + data for each)
writer.writeArrayOfBuffer(buffers);
// AddressMap<u256> (u16 count, then address + u256 pairs)
writer.writeAddressMapU256(addressMap);
// ExtendedAddressMap<u256> (u16 count, then 64-byte address + u256 pairs)
writer.writeExtendedAddressMapU256(extendedAddressMap);// Get the full buffer
const buffer: Uint8Array = writer.getBuffer();
// Get current write offset (bytes written so far)
const offset: u32 = writer.getOffset();
// Get total buffer capacity
const capacity: u32 = writer.bufferLength();
// Convert to BytesReader for reading
const reader: BytesReader = writer.toBytesReader();import { BytesReader, Address } from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
// Create reader from buffer
const reader = new BytesReader(data);
// Read data in same order it was written
const recipient: Address = reader.readAddress();
const amount: u256 = reader.readU256();
const flag: bool = reader.readBoolean();// From Uint8Array
const reader = new BytesReader(buffer);
// From BytesWriter
const reader = writer.toBytesReader();// Boolean
const flag: bool = reader.readBoolean();
// Unsigned integers
const u8val: u8 = reader.readU8();
const u16val: u16 = reader.readU16();
const u32val: u32 = reader.readU32();
const u64val: u64 = reader.readU64();
const u128val: u128 = reader.readU128();
const u256val: u256 = reader.readU256();
// Signed integers
const i8val: i8 = reader.readI8();
const i16val: i16 = reader.readI16();
const i32val: i32 = reader.readI32();
const i64val: i64 = reader.readI64();// Address (32 bytes)
const addr: Address = reader.readAddress();
// ExtendedAddress (64 bytes: 32 tweaked key + 32 ML-DSA key hash)
const extAddr: ExtendedAddress = reader.readExtendedAddress();
// Schnorr signature with signer address (128 bytes)
const schnorrSig: SchnorrSignature = reader.readSchnorrSignature();
const signer: ExtendedAddress = schnorrSig.address;
const signature: Uint8Array = schnorrSig.signature;
// String with known length (bytes read, zeroStop = true)
const name: string = reader.readString(32); // reads up to 32 bytes, stops at null
// String with u32 length prefix
const name2: string = reader.readStringWithLength();
// Bytes with known length
const data: Uint8Array = reader.readBytes(64);
// Bytes with u32 length prefix
const data2: Uint8Array = reader.readBytesWithLength();
// Selector (4 bytes, big-endian)
const selector: Selector = reader.readSelector();The read<T>() method automatically selects the correct read method based on the type:
// Automatically dispatches to the correct read method
const num: u64 = reader.read<u64>(); // readU64
const amount: u256 = reader.read<u256>(); // readU256
const flag: bool = reader.read<bool>(); // readBoolean
const addr: Address = reader.read<Address>(); // readAddress
const extAddr: ExtendedAddress = reader.read<ExtendedAddress>(); // readExtendedAddress
const text: string = reader.read<string>(); // readStringWithLength
// Supported types:
// - Primitives: bool, u8, u16, u32, u64, i8, i16, i32, i64
// - Big numbers: u128, u256, i128
// - Complex: Address, ExtendedAddress, Selector, stringAll array methods expect a u16 length prefix:
// Address array
const addresses: Address[] = reader.readAddressArray();
// ExtendedAddress array (64 bytes each)
const extAddresses: ExtendedAddress[] = reader.readExtendedAddressArray();
// Numeric arrays
const u8Values: u8[] = reader.readU8Array();
const u16Values: u16[] = reader.readU16Array();
const u32Values: u32[] = reader.readU32Array();
const u64Values: u64[] = reader.readU64Array();
const u128Values: u128[] = reader.readU128Array();
const u256Values: u256[] = reader.readU256Array();
// Array of variable-length buffers
const buffers: Uint8Array[] = reader.readArrayOfBuffer();
// AddressMap<u256>
const addressMap: AddressMap<u256> = reader.readAddressMapU256();
// ExtendedAddressMap<u256> (64-byte addresses as keys)
const extAddressMap: ExtendedAddressMap<u256> = reader.readExtendedAddressMapU256();// Get current read offset
const offset: i32 = reader.getOffset();
// Set read position (must be within buffer bounds)
reader.setOffset(32);
// Get total buffer length
const total: i32 = reader.byteLength;
// Verify enough bytes remain for next read
reader.verifyEnd(offset + 32); // Throws Revert if not enough bytes| Type | Size | Format |
|---|---|---|
bool |
1 | 0 or 1 |
u8/i8 |
1 | Raw byte |
u16/i16 |
2 | Big-endian (default) |
u32/i32 |
4 | Big-endian (default) |
u64/i64 |
8 | Big-endian (default) |
u128/i128 |
16 | Big-endian (default) |
u256 |
32 | Big-endian (default) |
Address |
32 | Raw bytes |
ExtendedAddress |
64 | 32 bytes tweaked key + 32 bytes ML-DSA key hash |
SchnorrSignature |
128 | 64 bytes ExtendedAddress + 64 bytes signature |
Selector |
4 | Big-endian (u32) |
string |
4 + n | Length prefix (u32 BE) + UTF-8 |
bytes |
4 + n | Length prefix (u32 BE) + raw |
arrays |
2 + n | Length prefix (u16 BE) + elements |
Note: All multi-byte integers default to big-endian. Pass be = false for little-endian:
writer.writeU32(value, false); // Little-endian
reader.readU32(false); // Little-endian---
config:
theme: dark
---
flowchart LR
A["Data"] --> B{"Type?"}
B -->|"Primitive"| C["Fixed Size"]
B -->|"String/Array"| D["Length Prefix + Data"]
C --> E["Write to Buffer"]
D --> E
E --> F["Increment Offset"]
F --> G["Final Buffer"]
"Hello" encoded:
| 05 00 00 00 | 48 65 6c 6c 6f |
| length=5 | "Hello" UTF-8 |
[1, 2, 3] as u256 encoded:
| 03 00 00 00 | 01 00...00 | 02 00...00 | 03 00...00 |
| length=3 | 1 (32 bytes)| 2 (32 bytes)| 3 (32 bytes)|
// Simple return
public getValue(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeU256(this._value.value);
return writer;
}
// Multiple return values
public getState(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(96);
writer.writeU256(this._value1.value);
writer.writeU256(this._value2.value);
writer.writeAddress(this._owner.value);
return writer;
}
// No return
public setState(calldata: Calldata): BytesWriter {
// ... do work ...
return new BytesWriter(0); // Empty response
}export class SwapEvent extends NetEvent {
constructor(
public readonly user: Address,
public readonly tokenIn: Address,
public readonly tokenOut: Address,
public readonly amountIn: u256,
public readonly amountOut: u256
) {
super('Swap');
}
protected override encodeData(writer: BytesWriter): void {
writer.writeAddress(this.user);
writer.writeAddress(this.tokenIn);
writer.writeAddress(this.tokenOut);
writer.writeU256(this.amountIn);
writer.writeU256(this.amountOut);
}
}// Define method selectors (sha256 first 4 bytes of method signature)
const TRANSFER_SELECTOR: u32 = 0xa9059cbb; // transfer(address,uint256)
// Encode call to another contract
private encodeTransfer(to: Address, amount: u256): Uint8Array {
const writer = new BytesWriter(68);
writer.writeSelector(TRANSFER_SELECTOR);
writer.writeAddress(to);
writer.writeU256(amount);
return writer.getBuffer();
}
// Make the call
const calldata = this.encodeTransfer(recipient, tokenAmount);
const result = Blockchain.call(tokenContract, calldata, true);
// Decode result
if (result.success) {
const reader = new BytesReader(result.data);
const success: bool = reader.readBoolean();
}sequenceDiagram
participant C1 as Calling Contract
participant W as BytesWriter
participant BC as Blockchain
participant C2 as Target Contract
participant R as BytesReader
C1->>W: new BytesWriter(68)
C1->>W: writeSelector(0xa9059cbb)
Note over W: [4 bytes] Selector
C1->>W: writeAddress(recipient)
Note over W: [4 + 32 bytes] Selector + Address
C1->>W: writeU256(amount)
Note over W: [4 + 32 + 32 bytes] Complete calldata
W-->>C1: getBuffer()
C1->>BC: call(tokenContract, calldata, true)
BC->>C2: Execute with calldata
C2->>R: new BytesReader(calldata)
R->>C2: readSelector() → 0xa9059cbb
R->>C2: readAddress() → recipient
R->>C2: readU256() → amount
C2->>C2: Execute transfer logic
C2->>W: new BytesWriter(1)
C2->>W: writeBoolean(true)
W-->>C2: getBuffer()
C2-->>BC: Return encoded result
BC-->>C1: result.data
C1->>R: new BytesReader(result.data)
R-->>C1: readBoolean() → true
Note over C1,C2: Total calldata: 68 bytes<br/>Selector(4) + Address(32) + u256(32)
// Define a struct-like type
class Order {
maker: Address;
taker: Address;
makerAmount: u256;
takerAmount: u256;
expiry: u64;
}
// Encode a "struct"
private encodeOrder(
maker: Address,
taker: Address,
makerAmount: u256,
takerAmount: u256,
expiry: u64
): Uint8Array {
const writer = new BytesWriter(104);
writer.writeAddress(maker);
writer.writeAddress(taker);
writer.writeU256(makerAmount);
writer.writeU256(takerAmount);
writer.writeU64(expiry);
return writer.getBuffer();
}
// Decode a "struct"
private decodeOrder(data: Uint8Array): Order {
const reader = new BytesReader(data);
const order = new Order();
order.maker = reader.readAddress();
order.taker = reader.readAddress();
order.makerAmount = reader.readU256();
order.takerAmount = reader.readU256();
order.expiry = reader.readU64();
return order;
}graph LR
subgraph Order["Order Struct Encoding (104 bytes)"]
direction LR
A["Offset 0-31<br/>maker Address<br/>32 bytes"]
B["Offset 32-63<br/>taker Address<br/>32 bytes"]
C["Offset 64-95<br/>makerAmount u256<br/>32 bytes"]
D["Offset 96-127<br/>takerAmount u256<br/>32 bytes"]
E["Offset 128-135<br/>expiry u64<br/>8 bytes"]
end
A --> B --> C --> D --> E
Maximum array length: 65,535 elements
// Writing large arrays
if (items.length > 65535) {
throw new Revert('Array too large');
}
writer.writeU256Array(items);Maximum event data: 352 bytes
protected override encodeData(writer: BytesWriter): void {
// Calculate total size
// 32 + 32 + 32 + 32 + 8 = 136 bytes - OK
writer.writeAddress(this.addr1); // 32
writer.writeAddress(this.addr2); // 32
writer.writeU256(this.amount1); // 32
writer.writeU256(this.amount2); // 32
writer.writeU64(this.timestamp); // 8
}| Feature | Solidity | OP_NET |
|---|---|---|
| Encode function | abi.encode(...) |
BytesWriter methods |
| Decode function | abi.decode(data, (types)) |
BytesReader methods |
| Packed encoding | abi.encodePacked(...) |
Default behavior (no padding) |
| Encode with selector | abi.encodeWithSelector(sel, ...) |
writer.writeSelector(sel); writer.write...() |
| Encode with signature | abi.encodeWithSignature(sig, ...) |
Manual selector + params |
| Byte order | Big-endian | Big-endian (default) |
| Padding | 32-byte aligned | No padding (native sizes) |
| Return encoding | Automatic | Manual via BytesWriter |
| Solidity Encoding | OP_NET Encoding |
|---|---|
abi.encode(uint256) |
writer.writeU256(value) |
abi.encode(uint128) |
writer.writeU128(value) |
abi.encode(uint64) |
writer.writeU64(value) |
abi.encode(uint32) |
writer.writeU32(value) |
abi.encode(uint16) |
writer.writeU16(value) |
abi.encode(uint8) |
writer.writeU8(value) |
abi.encode(bool) |
writer.writeBoolean(value) |
abi.encode(address) |
writer.writeAddress(addr) |
abi.encode(bytes32) |
writer.writeBytes(data) |
abi.encode(string) |
writer.writeString(str) |
abi.encode(bytes) |
writer.writeBytes(data) |
abi.encode(address[]) |
writer.writeAddressArray(addrs) |
abi.encode(uint256[]) |
writer.writeU256Array(values) |
// Solidity
bytes memory data = abi.encode(recipient, amount);
bytes memory packed = abi.encodePacked(recipient, amount);// OP_NET
const writer = new BytesWriter(64); // 32 + 32 bytes
writer.writeAddress(recipient);
writer.writeU256(amount);
const data: Uint8Array = writer.getBuffer();
// OP_NET encoding is similar to encodePacked (no padding)// Solidity
(address to, uint256 amount) = abi.decode(data, (address, uint256));// OP_NET
const reader = new BytesReader(data);
const to: Address = reader.readAddress();
const amount: u256 = reader.readU256();// Solidity
function encodeTransferData(
address from,
address to,
uint256 amount,
string memory memo
) public pure returns (bytes memory) {
return abi.encode(from, to, amount, memo);
}// OP_NET
function encodeTransferData(
from: Address,
to: Address,
amount: u256,
memo: string
): Uint8Array {
// Calculate size: 32 + 32 + 32 + (4 + memo.length)
const writer = new BytesWriter(100 + memo.length);
writer.writeAddress(from);
writer.writeAddress(to);
writer.writeU256(amount);
writer.writeString(memo);
return writer.getBuffer();
}// Solidity
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
bytes memory callData = abi.encodeWithSelector(selector, to, amount);
// Or
bytes memory callData = abi.encodeWithSignature("transfer(address,uint256)", to, amount);// OP_NET
const TRANSFER_SELECTOR: Selector = Selector.from("transfer(address,uint256)");
const writer = new BytesWriter(68); // 4 + 32 + 32
writer.writeSelector(TRANSFER_SELECTOR);
writer.writeAddress(to);
writer.writeU256(amount);
const callData: Uint8Array = writer.getBuffer();// Solidity - Automatic return encoding
function getInfo() public view returns (address, uint256, bool) {
return (owner, balance, isActive);
}
// ABI automatically encodes the return tuple// OP_NET - Manual return encoding
public getInfo(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(65); // 32 + 32 + 1
writer.writeAddress(this._owner.value);
writer.writeU256(this._balance.value);
writer.writeBoolean(this._isActive.value);
return writer;
}// Solidity
(bool success, bytes memory returnData) = target.call(callData);
if (success) {
(address addr, uint256 amount) = abi.decode(returnData, (address, uint256));
}// OP_NET
const result = Blockchain.call(target, callData, true);
if (result.success) {
const reader = new BytesReader(result.data);
const addr: Address = reader.readAddress();
const amount: u256 = reader.readU256();
}// Solidity
struct Order {
address maker;
address taker;
uint256 makerAmount;
uint256 takerAmount;
uint64 expiry;
}
function encodeOrder(Order memory order) public pure returns (bytes memory) {
return abi.encode(order.maker, order.taker, order.makerAmount, order.takerAmount, order.expiry);
}// OP_NET
class Order {
maker: Address;
taker: Address;
makerAmount: u256;
takerAmount: u256;
expiry: u64;
}
function encodeOrder(order: Order): Uint8Array {
const writer = new BytesWriter(136); // 32+32+32+32+8
writer.writeAddress(order.maker);
writer.writeAddress(order.taker);
writer.writeU256(order.makerAmount);
writer.writeU256(order.takerAmount);
writer.writeU64(order.expiry);
return writer.getBuffer();
}// Solidity - Events are indexed/non-indexed
event Transfer(address indexed from, address indexed to, uint256 amount);
function _transfer(address from, address to, uint256 amount) internal {
// ... transfer logic
emit Transfer(from, to, amount); // Automatic encoding
}// OP_NET - Custom event class with manual encoding
export class TransferEvent extends NetEvent {
constructor(
public readonly from: Address,
public readonly to: Address,
public readonly amount: u256
) {
super('Transfer');
}
protected override encodeData(writer: BytesWriter): void {
writer.writeAddress(this.from);
writer.writeAddress(this.to);
writer.writeU256(this.amount);
}
}
// Usage
this.emitEvent(new TransferEvent(from, to, amount));// Solidity
function extractSelector(bytes calldata data) public pure returns (bytes4) {
return bytes4(data[:4]);
}
function extractAddress(bytes calldata data, uint offset) public pure returns (address) {
return abi.decode(data[offset:offset+32], (address));
}// OP_NET
function extractSelector(data: Uint8Array): Selector {
const reader = new BytesReader(data);
return reader.readSelector();
}
function extractAddress(data: Uint8Array, offset: u32): Address {
const reader = new BytesReader(data);
// Skip to offset by reading and discarding bytes
for (let i: u32 = 0; i < offset; i++) {
reader.readU8();
}
return reader.readAddress();
}| Data | Solidity abi.encode | Solidity encodePacked | OP_NET |
|---|---|---|---|
bool |
32 bytes | 1 byte | 1 byte |
uint8 |
32 bytes | 1 byte | 1 byte |
uint16 |
32 bytes | 2 bytes | 2 bytes |
uint32 |
32 bytes | 4 bytes | 4 bytes |
uint64 |
32 bytes | 8 bytes | 8 bytes |
uint128 |
32 bytes | 16 bytes | 16 bytes |
uint256 |
32 bytes | 32 bytes | 32 bytes |
address |
32 bytes | 20 bytes | 32 bytes |
(addr, u256, bool) |
96 bytes | 53 bytes | 65 bytes |
| Aspect | Solidity | OP_NET |
|---|---|---|
| Encoding approach | abi.encode() function |
BytesWriter object methods |
| Decoding approach | abi.decode() with type tuple |
BytesReader sequential reads |
| Packed encoding | abi.encodePacked() (separate) |
Default (always packed) |
| Selector encoding | abi.encodeWithSelector() |
writeSelector() + params |
| Dynamic sizing | Automatic | Pre-calculate or auto-grow |
| Return values | Automatic encoding | Manual BytesWriter construction |
| Error on overflow | Reverts | Reverts |
| Endianness | Big-endian | Big-endian (default) |
// Solidity
bytes memory encoded = abi.encode(addr, amount, flag);// OP_NET equivalent
const writer = new BytesWriter(65); // 32 + 32 + 1
writer.writeAddress(addr);
writer.writeU256(amount);
writer.writeBoolean(flag);
const encoded = writer.getBuffer();// Solidity
(address addr, uint256 amount, bool flag) = abi.decode(data, (address, uint256, bool));// OP_NET equivalent
const reader = new BytesReader(data);
const addr = reader.readAddress();
const amount = reader.readU256();
const flag = reader.readBoolean();// Solidity
bytes memory data = abi.encodeWithSelector(
IERC20.transfer.selector,
recipient,
amount
);// OP_NET equivalent
const TRANSFER_SELECTOR: Selector = Selector.from("transfer(address,uint256)");
const writer = new BytesWriter(68);
writer.writeSelector(TRANSFER_SELECTOR);
writer.writeAddress(recipient);
writer.writeU256(amount);
const data = writer.getBuffer();// Calculate required size
const size = 32 + 32 + 4; // address + u256 + u32
const writer = new BytesWriter(size);
// Better than growing the buffer// Always read in the same order as written
// Document the order clearly
/**
* Encoded format:
* - to: Address (32 bytes)
* - amount: u256 (32 bytes)
* - data: bytes (4 + n bytes)
*/// Check if enough data remains
if (reader.remaining < 32) {
throw new Revert('Insufficient data');
}
const value = reader.readU256();// Fixed-size fields first
writer.writeAddress(addr);
writer.writeU256(amount);
writer.writeU64(timestamp);
// Variable-size fields last
writer.writeString(message);
writer.writeBytes(data);Navigation:
- Previous: Calldata
- Next: Stored Primitives