UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

527 lines (389 loc) 18.4 kB
# BinaryReader Type-safe binary deserialization for decoding OPNet calldata, contract return values, and event data. ## Overview `BinaryReader` is the counterpart to `BinaryWriter`. It reads structured binary data from a `Uint8Array` buffer, advancing an internal offset as each value is consumed. It is used to decode contract return values, event data, and any binary payload produced by `BinaryWriter` or the OPNet runtime. The reader performs bounds checking on every read operation, throwing an `Error` if a read would exceed the buffer length. This ensures corrupted or truncated data is caught early rather than producing silently wrong results. **Source:** `src/buffer/BinaryReader.ts` ## Table of Contents - [Constructor and Initialization](#constructor-and-initialization) - [Endianness](#endianness) - [Primitive Read Methods](#primitive-read-methods) - [Unsigned Integers](#unsigned-integers) - [Signed Integers](#signed-integers) - [Boolean and Selector](#boolean-and-selector) - [String and Bytes Read Methods](#string-and-bytes-read-methods) - [Address Read Methods](#address-read-methods) - [Array Read Methods](#array-read-methods) - [Map Read Methods](#map-read-methods) - [Utility Methods and Properties](#utility-methods-and-properties) - [Static Comparators](#static-comparators) - [Examples](#examples) --- ## Constructor and Initialization ```typescript import { BinaryReader } from '@btc-vision/transaction'; // Create from a Uint8Array const reader = new BinaryReader(data); // Create from the output of a BinaryWriter const writer = new BinaryWriter(); writer.writeU256(42n); const reader = writer.toBytesReader(); ``` | Parameter | Type | Description | |-----------|------|-------------| | `bytes` | `BufferLike` (`Uint8Array`) | The binary data to read from. The reader wraps a `DataView` over the underlying `ArrayBuffer`. | The reader starts at offset 0 and advances through the buffer as values are read. The buffer itself is not copied; the reader references the original memory. --- ## Endianness Many read methods accept a `be` (big-endian) parameter: | Value | Byte Order | Description | |-------|-----------|-------------| | `true` (default) | Big-endian | Most significant byte first. This is the default for OPNet calldata. | | `false` | Little-endian | Least significant byte first. | The `be` parameter must match the endianness used when the data was written. Mismatched endianness will produce incorrect values without any error. --- ## Primitive Read Methods ### Unsigned Integers | Method | Return Type | Size | Value Range | |--------|------------|------|-------------| | `readU8()` | `u8` (number) | 1 byte | 0 to 255 | | `readU16(be?)` | `u16` (number) | 2 bytes | 0 to 65,535 | | `readU32(be?)` | `u32` (number) | 4 bytes | 0 to 4,294,967,295 | | `readU64(be?)` | `bigint` | 8 bytes | 0 to 2^64 - 1 | | `readU128(be?)` | `bigint` | 16 bytes | 0 to 2^128 - 1 | | `readU256(be?)` | `bigint` | 32 bytes | 0 to 2^256 - 1 | ```typescript const a = reader.readU8(); // number const b = reader.readU16(); // number const c = reader.readU32(); // number const d = reader.readU64(); // bigint const e = reader.readU128(); // bigint const f = reader.readU256(); // bigint // Read in little-endian const g = reader.readU32(false); ``` All methods throw an `Error` if reading would go past the end of the buffer. ### Signed Integers | Method | Return Type | Size | Value Range | |--------|------------|------|-------------| | `readI8()` | `i8` (number) | 1 byte | -128 to 127 | | `readI16(be?)` | `i16` (number) | 2 bytes | -32,768 to 32,767 | | `readI32(be?)` | `i32` (number) | 4 bytes | -2,147,483,648 to 2,147,483,647 | | `readI64(be?)` | `i64` (bigint) | 8 bytes | -2^63 to 2^63 - 1 | | `readI128(be?)` | `bigint` | 16 bytes | -2^127 to 2^127 - 1 | ```typescript const a = reader.readI8(); // number const b = reader.readI16(); // number const c = reader.readI32(); // number const d = reader.readI64(); // bigint const e = reader.readI128(); // bigint ``` `readI128` interprets the raw 16 bytes as a two's complement signed integer: if the most significant bit is set, the value is treated as negative. ### Boolean and Selector | Method | Return Type | Size | Description | |--------|------------|------|-------------| | `readBoolean()` | `boolean` | 1 byte | Returns `true` if the byte is non-zero, `false` if zero | | `readSelector()` | `Selector` (number) | 4 bytes | Reads a big-endian `u32` function selector | ```typescript const flag = reader.readBoolean(); // true or false const selector = reader.readSelector(); // e.g. 0x1a2b3c4d ``` The selector is always read in big-endian byte order. --- ## String and Bytes Read Methods | Method | Parameters | Return Type | Description | |--------|-----------|------------|-------------| | `readBytes(length, zeroStop?)` | `length: u32`, `zeroStop?: boolean` | `Uint8Array` | Reads exactly `length` bytes. If `zeroStop` is `true`, reading stops at the first `0x00` byte and the returned array is truncated. | | `readBytesWithLength(maxLength?, be?)` | `maxLength?: number`, `be?: boolean` | `Uint8Array` | Reads a `u32` length prefix, then that many bytes. Throws if length exceeds `maxLength` (when `maxLength > 0`). | | `readString(length)` | `length: u32` | `string` | Reads `length` bytes and decodes them as UTF-8. | | `readStringWithLength(be?)` | `be?: boolean` | `string` | Reads a `u32` length prefix, then decodes that many bytes as UTF-8. | ```typescript // Read exactly 10 raw bytes const raw = reader.readBytes(10); // Read raw bytes, stopping early at 0x00 const nullTerminated = reader.readBytes(256, true); // Read length-prefixed bytes const data = reader.readBytesWithLength(); // Read length-prefixed bytes with a safety limit const bounded = reader.readBytesWithLength(1024); // throws if length > 1024 // Read a fixed-length string const name = reader.readString(5); // reads exactly 5 bytes as UTF-8 // Read a length-prefixed string const label = reader.readStringWithLength(); ``` **Wire format for `readBytesWithLength` / `readStringWithLength`:** ``` [u32 byte length][raw bytes...] ``` --- ## Address Read Methods | Method | Return Type | Size | Description | |--------|------------|------|-------------| | `readAddress()` | `Address` | 32 bytes | Reads a 32-byte MLDSA key hash and returns an `Address` instance | | `readTweakedPublicKey()` | `Uint8Array` | 32 bytes | Reads a 32-byte tweaked public key as raw bytes | | `readExtendedAddress()` | `Address` | 64 bytes | Reads both tweaked public key (32 bytes) and MLDSA key hash (32 bytes), returns an `Address` with both keys | | `readSchnorrSignature()` | `SchnorrSignature` | 128 bytes | Reads a full extended address (64 bytes) followed by a 64-byte Schnorr signature | ```typescript import { Address } from '@btc-vision/transaction'; // Read a standard 32-byte address const addr: Address = reader.readAddress(); // Read just the tweaked public key as raw bytes const tweakedKey: Uint8Array = reader.readTweakedPublicKey(); // Read a full 64-byte extended address const extAddr: Address = reader.readExtendedAddress(); // Read a Schnorr signature with its signer address const sig = reader.readSchnorrSignature(); console.log(sig.address); // Address instance console.log(sig.signature); // Uint8Array (64 bytes) ``` ### SchnorrSignature Interface ```typescript interface SchnorrSignature { readonly address: Address; // The signer's Address (both keys) readonly signature: Uint8Array; // The 64-byte Schnorr signature } ``` **Wire format for `readExtendedAddress`:** ``` [32 bytes tweakedPublicKey][32 bytes MLDSA key hash] ``` **Wire format for `readSchnorrSignature`:** ``` [32 bytes tweakedPublicKey][32 bytes MLDSA key hash][64 bytes signature] ``` --- ## Array Read Methods All array methods read a `u16` length prefix, then that many elements. The maximum array length is 65,535 elements. | Method | Return Type | Element Size | Description | |--------|------------|-------------|-------------| | `readU8Array()` | `u8[]` | 1 byte each | Array of unsigned 8-bit integers | | `readU16Array(be?)` | `u16[]` | 2 bytes each | Array of unsigned 16-bit integers | | `readU32Array(be?)` | `u32[]` | 4 bytes each | Array of unsigned 32-bit integers | | `readU64Array(be?)` | `bigint[]` | 8 bytes each | Array of unsigned 64-bit integers | | `readU128Array(be?)` | `bigint[]` | 16 bytes each | Array of unsigned 128-bit integers | | `readU256Array(be?)` | `bigint[]` | 32 bytes each | Array of unsigned 256-bit integers | | `readStringArray(be?)` | `string[]` | variable | Each string has a `u32` length prefix | | `readBytesArray(be?)` | `Uint8Array[]` | variable | Each byte array has a `u32` length prefix | | `readAddressArray(be?)` | `Address[]` | 32 bytes each | Array of standard addresses | | `readExtendedAddressArray(be?)` | `Address[]` | 64 bytes each | Array of extended addresses | | `readArrayOfBuffer(be?)` | `Uint8Array[]` | variable | Each buffer has a `u32` length prefix | ```typescript // Numeric arrays const bytes = reader.readU8Array(); // u8[] const shorts = reader.readU16Array(); // u16[] const ints = reader.readU32Array(); // u32[] const longs = reader.readU64Array(); // bigint[] const big128 = reader.readU128Array(); // bigint[] const big256 = reader.readU256Array(); // bigint[] // String and bytes arrays const strings = reader.readStringArray(); // string[] const blobs = reader.readBytesArray(); // Uint8Array[] const buffers = reader.readArrayOfBuffer(); // Uint8Array[] // Address arrays const addrs = reader.readAddressArray(); // Address[] const extAddrs = reader.readExtendedAddressArray(); // Address[] ``` **Wire format for arrays:** ``` [u16 element count][element 0][element 1]...[element N-1] ``` For variable-length elements (strings, bytes, buffers), each element includes its own `u32` length prefix: ``` [u16 count][u32 len0][bytes0][u32 len1][bytes1]... ``` > **Note:** `readU8Array()` always reads its count prefix as big-endian. Other array methods accept the `be` parameter which controls endianness for both the count prefix and the element values. --- ## Map Read Methods | Method | Return Type | Key Size | Value Size | Description | |--------|------------|---------|-----------|-------------| | `readAddressValueTuple(be?)` | `AddressMap<bigint>` | 32 bytes | 32 bytes (u256) | Reads a map of standard addresses to u256 values | | `readExtendedAddressMapU256(be?)` | `ExtendedAddressMap<bigint>` | 64 bytes | 32 bytes (u256) | Reads a map of extended addresses to u256 values | ```typescript import { AddressMap, ExtendedAddressMap } from '@btc-vision/transaction'; // Read a standard address -> u256 map const balances: AddressMap<bigint> = reader.readAddressValueTuple(); // Read an extended address -> u256 map const extBalances: ExtendedAddressMap<bigint> = reader.readExtendedAddressMapU256(); // Iterate over the map for (const [address, value] of balances.entries()) { console.log(address, value); } ``` **Wire format for `readAddressValueTuple`:** ``` [u16 count][32 bytes address][32 bytes u256 value]...[repeated] ``` **Wire format for `readExtendedAddressMapU256`:** ``` [u16 count][64 bytes extended address][32 bytes u256 value]...[repeated] ``` Both methods throw an `Error` if a duplicate address is encountered in the map data. --- ## Utility Methods and Properties | Method / Property | Type | Description | |-------------------|------|-------------| | `byteLength` (getter) | `number` | Total byte length of the underlying buffer | | `length()` | `number` | Same as `byteLength` -- total byte length of the buffer | | `bytesLeft()` | `number` | Number of bytes remaining from the current offset to the end of the buffer | | `getOffset()` | `u16` | Returns the current read offset | | `setOffset(offset)` | `void` | Manually sets the read offset. Use with caution. | | `setBuffer(bytes)` | `void` | Replaces the underlying buffer and resets the offset to 0 | | `verifyEnd(size)` | `void` | Throws if `size` exceeds the buffer byte length. Used internally before every read. | ```typescript // Check buffer dimensions console.log('Total bytes:', reader.byteLength); console.log('Bytes read so far:', reader.getOffset()); console.log('Bytes remaining:', reader.bytesLeft()); // Peek at data without consuming it const savedOffset = reader.getOffset(); const peekValue = reader.readU32(); reader.setOffset(savedOffset); // rewind // Replace the buffer entirely reader.setBuffer(newData); // Offset is now 0 and reads come from newData // Ensure we have consumed everything if (reader.bytesLeft() !== 0) { throw new Error('Unexpected trailing data'); } ``` --- ## Static Comparators `BinaryReader` provides three static comparison functions suitable for use with `Array.prototype.sort()`: | Method | Parameters | Description | |--------|-----------|-------------| | `BinaryReader.stringCompare(a, b)` | `a: string`, `b: string` | Locale-aware string comparison via `localeCompare` | | `BinaryReader.bigintCompare(a, b)` | `a: bigint`, `b: bigint` | Numeric comparison for `bigint` values | | `BinaryReader.numberCompare(a, b)` | `a: number`, `b: number` | Numeric comparison for `number` values | All return `-1`, `0`, or `1` following the standard comparator contract. ```typescript const values = [300n, 100n, 200n]; values.sort(BinaryReader.bigintCompare); // [100n, 200n, 300n] const names = ['charlie', 'alice', 'bob']; names.sort(BinaryReader.stringCompare); // ['alice', 'bob', 'charlie'] ``` --- ## Examples ### Reading Contract Return Values When a contract function returns data, decode it with `BinaryReader`: ```typescript import { BinaryReader } from '@btc-vision/transaction'; // Assume `returnData` is the Uint8Array returned by the contract const reader = new BinaryReader(returnData); // Decode a balanceOf(address) return value const balance: bigint = reader.readU256(); console.log('Balance:', balance); ``` ### Decoding Event Data ```typescript import { BinaryReader } from '@btc-vision/transaction'; // Event: Transfer(address from, address to, uint256 value) const reader = new BinaryReader(eventData); const from = reader.readAddress(); const to = reader.readAddress(); const value = reader.readU256(); console.log(`Transfer: ${value} from ${from} to ${to}`); ``` ### Decoding Complex Return Data ```typescript const reader = new BinaryReader(returnData); // A function that returns (string name, string symbol, u8 decimals, u256 totalSupply) const name = reader.readStringWithLength(); const symbol = reader.readStringWithLength(); const decimals = reader.readU8(); const totalSupply = reader.readU256(); console.log(`${name} (${symbol}), decimals: ${decimals}, supply: ${totalSupply}`); ``` ### Round-Trip Encode / Decode ```typescript import { BinaryWriter, BinaryReader } from '@btc-vision/transaction'; // Encode const writer = new BinaryWriter(); writer.writeSelector(0xa9059cbb); writer.writeAddress(recipientAddress); writer.writeU256(1000000000000000000n); writer.writeBoolean(true); writer.writeStringWithLength('memo: payment'); writer.writeU64Array([1n, 2n, 3n]); // Decode const reader = writer.toBytesReader(); const selector = reader.readSelector(); // 0xa9059cbb const addr = reader.readAddress(); // recipientAddress const amount = reader.readU256(); // 1000000000000000000n const flag = reader.readBoolean(); // true const memo = reader.readStringWithLength(); // 'memo: payment' const ids = reader.readU64Array(); // [1n, 2n, 3n] // Verify all data was consumed console.log('Bytes remaining:', reader.bytesLeft()); // 0 ``` ### Usage with ABICoder The `ABICoder` class uses `BinaryReader` internally to decode return values based on ABI type definitions: ```typescript import { ABICoder, BinaryReader } from '@btc-vision/transaction'; import { ABIDataTypes } from '@btc-vision/transaction'; const coder = new ABICoder(); // Decode return data using ABI type descriptors const decoded = coder.decodeData(returnData, [ ABIDataTypes.ADDRESS, ABIDataTypes.UINT256, ABIDataTypes.BOOL, ABIDataTypes.STRING, ]); const [address, amount, success, message] = decoded; ``` You can also use `ABICoder.decodeSingleValue` for fine-grained control: ```typescript const reader = new BinaryReader(data); // Skip the 4-byte selector reader.readSelector(); // Decode individual values const addr = coder.decodeSingleValue(reader, ABIDataTypes.ADDRESS); const value = coder.decodeSingleValue(reader, ABIDataTypes.UINT256); const balances = coder.decodeSingleValue(reader, ABIDataTypes.ADDRESS_UINT256_TUPLE); ``` ### Reading Maps and Iterating ```typescript import { BinaryReader, AddressMap } from '@btc-vision/transaction'; const reader = new BinaryReader(data); // Read an address -> u256 map (e.g., token allowances) const allowances: AddressMap<bigint> = reader.readAddressValueTuple(); // Check a specific address if (allowances.has(spenderAddress)) { const allowance = allowances.get(spenderAddress); console.log('Allowance:', allowance); } // Iterate all entries for (const [addr, value] of allowances.entries()) { console.log(`${addr}: ${value}`); } ``` ### Rewinding and Re-reading ```typescript const reader = new BinaryReader(data); // Read the selector to determine which decoder to use const selector = reader.readSelector(); if (selector === 0xa9059cbb) { // transfer(address, u256) const to = reader.readAddress(); const amount = reader.readU256(); } else if (selector === 0x095ea7b3) { // approve(address, u256) const spender = reader.readAddress(); const amount = reader.readU256(); } else { // Unknown selector -- rewind and read raw reader.setOffset(0); const raw = reader.readBytes(reader.byteLength); } ``` ### Verifying Buffer Consumption After decoding, it is good practice to check that all bytes were consumed: ```typescript const reader = new BinaryReader(data); const value1 = reader.readU256(); const value2 = reader.readU64(); if (reader.bytesLeft() !== 0) { console.warn(`Warning: ${reader.bytesLeft()} unexpected trailing bytes`); } ```