@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
432 lines • 14.9 kB
JavaScript
import { AddressMap } from '../deterministic/AddressMap.js';
import { ExtendedAddressMap } from '../deterministic/ExtendedAddressMap.js';
import { Address } from '../keypair/Address.js';
import { ADDRESS_BYTE_LENGTH, EXTENDED_ADDRESS_BYTE_LENGTH, I128_BYTE_LENGTH, I16_BYTE_LENGTH, I32_BYTE_LENGTH, I64_BYTE_LENGTH, I8_BYTE_LENGTH, SCHNORR_SIGNATURE_BYTE_LENGTH, U128_BYTE_LENGTH, U16_BYTE_LENGTH, U256_BYTE_LENGTH, U32_BYTE_LENGTH, U64_BYTE_LENGTH, U8_BYTE_LENGTH, } from '../utils/lengths.js';
export class BinaryReader {
buffer;
currentOffset = 0;
constructor(bytes) {
this.buffer = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}
get byteLength() {
return this.buffer.byteLength;
}
// Helpers for comparisons; unchanged
static stringCompare(a, b) {
return a.localeCompare(b);
}
static bigintCompare(a, b) {
if (a < b)
return -1;
if (a > b)
return 1;
return 0;
}
static numberCompare(a, b) {
if (a < b)
return -1;
if (a > b)
return 1;
return 0;
}
setBuffer(bytes) {
this.buffer = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
this.currentOffset = 0;
}
length() {
return this.buffer.byteLength;
}
bytesLeft() {
return this.buffer.byteLength - this.currentOffset;
}
// ------------------- Signed Integer Readers ------------------- //
/**
* Reads a single signed byte (i8).
*/
readI8() {
this.verifyEnd(this.currentOffset + I8_BYTE_LENGTH);
const value = this.buffer.getInt8(this.currentOffset);
this.currentOffset += I8_BYTE_LENGTH;
return value;
}
/**
* Reads a signed 16-bit integer. By default, big-endian.
* @param be - Endianness; true means big-endian (the default).
*/
readI16(be = true) {
this.verifyEnd(this.currentOffset + I16_BYTE_LENGTH);
const value = this.buffer.getInt16(this.currentOffset, !be);
this.currentOffset += I16_BYTE_LENGTH;
return value;
}
/**
* Reads a signed 32-bit integer. By default, big-endian.
* @param be - Endianness; true means big-endian (the default).
*/
readI32(be = true) {
this.verifyEnd(this.currentOffset + I32_BYTE_LENGTH);
const value = this.buffer.getInt32(this.currentOffset, !be);
this.currentOffset += I32_BYTE_LENGTH;
return value;
}
/**
* Reads a signed 64-bit integer. By default, big-endian.
* @param be - Endianness; true means big-endian (the default).
*/
readI64(be = true) {
this.verifyEnd(this.currentOffset + I64_BYTE_LENGTH);
const value = this.buffer.getBigInt64(this.currentOffset, !be);
this.currentOffset += I64_BYTE_LENGTH;
return value;
}
// ------------------- Unsigned Integer Readers ------------------- //
/**
* Reads a single unsigned byte (u8).
*/
readU8() {
this.verifyEnd(this.currentOffset + U8_BYTE_LENGTH);
const value = this.buffer.getUint8(this.currentOffset);
this.currentOffset += U8_BYTE_LENGTH;
return value;
}
/**
* Reads an unsigned 16-bit integer. By default, big-endian.
* @param be - Endianness; true means big-endian (the default).
*/
readU16(be = true) {
this.verifyEnd(this.currentOffset + U16_BYTE_LENGTH);
const value = this.buffer.getUint16(this.currentOffset, !be);
this.currentOffset += U16_BYTE_LENGTH;
return value;
}
/**
* Reads an unsigned 32-bit integer. By default, big-endian.
* @param be - Endianness; true means big-endian (the default).
*/
readU32(be = true) {
this.verifyEnd(this.currentOffset + U32_BYTE_LENGTH);
const value = this.buffer.getUint32(this.currentOffset, !be);
this.currentOffset += U32_BYTE_LENGTH;
return value;
}
/**
* Reads an unsigned 64-bit integer. By default, big-endian.
* @param be - Endianness; true means big-endian (the default).
*/
readU64(be = true) {
this.verifyEnd(this.currentOffset + U64_BYTE_LENGTH);
const value = this.buffer.getBigUint64(this.currentOffset, !be);
this.currentOffset += U64_BYTE_LENGTH;
return value;
}
/**
* Reads a 128-bit unsigned integer. By default, read big-endian.
* @param be - Endianness; true => big-endian (default).
*/
readU128(be = true) {
const raw = this.readBytes(U128_BYTE_LENGTH);
let bytes = raw;
// If data was written in little-endian, we reverse before interpreting
if (!be) {
bytes = this.reverseBytes(raw);
}
return BigInt('0x' + this.toHexString(bytes));
}
/**
* Reads a 256-bit unsigned integer. Same approach as readU128.
* @param be - Endianness; true => big-endian (default).
*/
readU256(be = true) {
const raw = this.readBytes(U256_BYTE_LENGTH);
let bytes = raw;
if (!be) {
bytes = this.reverseBytes(raw);
}
return BigInt('0x' + this.toHexString(bytes));
}
/**
* Reads a 128-bit signed integer. Interpret the sign bit if big-endian.
* @param be - Endianness; true => big-endian (default).
*/
readI128(be = true) {
const raw = this.readBytes(I128_BYTE_LENGTH);
let bytes = raw;
if (!be) {
bytes = this.reverseBytes(raw);
}
// Construct as a 128-bit two's complement
let value = BigInt('0x' + this.toHexString(bytes));
// If the top bit is set (sign bit in big-endian), interpret negative
const signBitMask = 0x80;
if (bytes[0] & signBitMask) {
// (1 << 128)
const twoTo128 = BigInt(1) << BigInt(128);
// 2's complement
value = value - twoTo128;
}
return value;
}
/**
* Read a boolean (u8 != 0).
*/
readBoolean() {
return this.readU8() !== 0;
}
/**
* Reads 32 bits
*/
readSelector() {
return this.readU32(true);
}
/**
* Reads a raw sequence of bytes (length must be known).
* If zeroStop = true, stops if we encounter 0x00 early.
*/
readBytes(length, zeroStop = false) {
this.verifyEnd(this.currentOffset + length);
let bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
const b = this.buffer.getUint8(this.currentOffset++);
if (zeroStop && b === 0) {
bytes = new Uint8Array(bytes.subarray(0, i));
break;
}
bytes[i] = b;
}
return bytes;
}
/**
* Reads a string of the given length in raw bytes. By default, do NOT zero-stop
* (matching how we wrote the raw bytes).
*/
readString(length) {
const textDecoder = new TextDecoder();
const bytes = this.readBytes(length, false);
return textDecoder.decode(bytes);
}
/**
* Reads a string that was written as [u16 length][raw bytes].
*/
readStringWithLength(be = true) {
const length = this.readU32(be);
return this.readString(length);
}
/**
* Reads an address (32 bytes MLDSA key hash only).
*/
readAddress() {
const bytes = Array.from(this.readBytes(ADDRESS_BYTE_LENGTH));
return new Address(bytes);
}
/**
* Reads the tweaked public key portion (32 bytes) and returns it as a raw Uint8Array.
* Use this when you only need the tweaked key without the full Address object.
*/
readTweakedPublicKey() {
this.verifyEnd(this.currentOffset + ADDRESS_BYTE_LENGTH);
return this.readBytes(ADDRESS_BYTE_LENGTH);
}
/**
* Reads a full address with both MLDSA key hash and tweaked public key (64 bytes total).
* Format: [32 bytes tweakedPublicKey][32 bytes MLDSA key hash]
*
* This is the equivalent of btc-runtime's readExtendedAddress().
*
* @returns An Address instance with both keys set
*/
readExtendedAddress() {
this.verifyEnd(this.currentOffset + EXTENDED_ADDRESS_BYTE_LENGTH);
// Read tweaked public key first (32 bytes)
const tweakedPublicKey = Array.from(this.readBytes(ADDRESS_BYTE_LENGTH));
// Read MLDSA key hash (32 bytes)
const mldsaKeyHash = Array.from(this.readBytes(ADDRESS_BYTE_LENGTH));
return new Address(mldsaKeyHash, tweakedPublicKey);
}
/**
* Reads a Schnorr signature with its associated full Address.
* Format: [64 bytes full Address][64 bytes signature]
*
* Used for deserializing signed data where both the signer's address
* and their Schnorr signature are stored together.
*
* @returns A SchnorrSignature containing the address and signature
*/
readSchnorrSignature() {
this.verifyEnd(this.currentOffset + EXTENDED_ADDRESS_BYTE_LENGTH + SCHNORR_SIGNATURE_BYTE_LENGTH);
const address = this.readExtendedAddress();
const signature = this.readBytes(SCHNORR_SIGNATURE_BYTE_LENGTH);
return { address, signature };
}
/**
* Reads bytes written as [u32 length][bytes].
* @param maxLength if > 0, enforces an upper bound
* @param be
*/
readBytesWithLength(maxLength = 0, be = true) {
const length = this.readU32(be);
if (maxLength > 0 && length > maxLength) {
throw new Error('Data length exceeds maximum length.');
}
return this.readBytes(length);
}
// ------------------ Array readers ------------------ //
readArrayOfBuffer(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readBytesWithLength();
}
return result;
}
readAddressArray(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readAddress();
}
return result;
}
readU256Array(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readU256(be);
}
return result;
}
readU128Array(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readU128(be);
}
return result;
}
readU64Array(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readU64(be);
}
return result;
}
readU32Array(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readU32(be);
}
return result;
}
readU16Array(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readU16(be);
}
return result;
}
readU8Array() {
const length = this.readU16(true); // by default big-endian
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readU8();
}
return result;
}
readStringArray(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readStringWithLength(be);
}
return result;
}
readBytesArray(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readBytesWithLength(0, be);
}
return result;
}
/**
* Reads [u16 length][ (address, u256) pairs ].
*/
readAddressValueTuple(be = true) {
const length = this.readU16(be);
const result = new AddressMap();
for (let i = 0; i < length; i++) {
const address = this.readAddress();
const value = this.readU256(be);
if (result.has(address)) {
throw new Error('Duplicate address found in map');
}
result.set(address, value);
}
return result;
}
/**
* Reads an array of full addresses (64 bytes each).
* Format: [u16 length][FullAddress 0][FullAddress 1]...
*/
readExtendedAddressArray(be = true) {
const length = this.readU16(be);
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this.readExtendedAddress();
}
return result;
}
/**
* Reads a map of full Address -> u256 using the tweaked key for map lookup.
* Format: [u16 length][FullAddress key][u256 value]...
*
* This is the equivalent of btc-runtime's readExtendedAddressMapU256().
*/
readExtendedAddressMapU256(be = true) {
const length = this.readU16(be);
const result = new ExtendedAddressMap();
for (let i = 0; i < length; i++) {
const address = this.readExtendedAddress();
const value = this.readU256(be);
if (result.has(address)) {
throw new Error('Duplicate tweaked address found in map');
}
result.set(address, value);
}
return result;
}
// --------------------------------------------------- //
getOffset() {
return this.currentOffset;
}
setOffset(offset) {
this.currentOffset = offset;
}
/**
* Verifies we have enough bytes in the buffer to read up to `size`.
*/
verifyEnd(size) {
if (size > this.buffer.byteLength) {
throw new Error(`Attempt to read beyond buffer length: requested up to byte offset ${size}, but buffer is only ${this.buffer.byteLength} bytes.`);
}
}
/**
* Utility: reverses a byte array in-place or returns a reversed copy.
*/
reverseBytes(bytes) {
const out = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
out[i] = bytes[bytes.length - 1 - i];
}
return out;
}
/**
* Utility: turn bytes into a hex string without `0x` prefix.
*/
toHexString(bytes) {
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
}
//# sourceMappingURL=BinaryReader.js.map