@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
407 lines • 15 kB
JavaScript
import { AddressMap } from '../deterministic/AddressMap.js';
import { ExtendedAddressMap } from '../deterministic/ExtendedAddressMap.js';
import { Address } from '../keypair/Address.js';
import { BufferHelper } from '../utils/BufferHelper.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';
import { BinaryReader } from './BinaryReader.js';
export class BinaryWriter {
currentOffset = 0;
buffer;
constructor(length = 0) {
this.buffer = this.getDefaultBuffer(length);
}
static estimateArrayOfBufferLength(values) {
if (values.length > 65535)
throw new Error('Array size is too large');
let totalLength = U16_BYTE_LENGTH;
for (let i = 0; i < values.length; i++) {
totalLength += U32_BYTE_LENGTH + values[i].length; // each entry has a u32 length prefix
}
return totalLength;
}
writeU8(value) {
if (value > 255)
throw new Error('u8 value is too large.');
this.allocSafe(U8_BYTE_LENGTH);
this.buffer.setUint8(this.currentOffset++, value);
}
writeU16(value, be = true) {
if (value > 65535)
throw new Error('u16 value is too large.');
this.allocSafe(U16_BYTE_LENGTH);
this.buffer.setUint16(this.currentOffset, value, !be);
this.currentOffset += 2;
}
writeU32(value, be = true) {
if (value > 4294967295)
throw new Error('u32 value is too large.');
this.allocSafe(U32_BYTE_LENGTH);
this.buffer.setUint32(this.currentOffset, value, !be);
this.currentOffset += 4;
}
writeU64(value, be = true) {
if (value > 18446744073709551615n)
throw new Error('u64 value is too large.');
this.allocSafe(U64_BYTE_LENGTH);
this.buffer.setBigUint64(this.currentOffset, value, !be);
this.currentOffset += 8;
}
// ------------------- Signed Integer Writers ------------------- //
/**
* Writes a signed 8-bit integer.
*/
writeI8(value) {
if (value < -128 || value > 127)
throw new Error('i8 value is out of range.');
this.allocSafe(I8_BYTE_LENGTH);
this.buffer.setInt8(this.currentOffset, value);
this.currentOffset += I8_BYTE_LENGTH;
}
/**
* Writes a signed 16-bit integer. By default big-endian (be = true).
*/
writeI16(value, be = true) {
if (value < -32768 || value > 32767)
throw new Error('i16 value is out of range.');
this.allocSafe(I16_BYTE_LENGTH);
this.buffer.setInt16(this.currentOffset, value, !be);
this.currentOffset += I16_BYTE_LENGTH;
}
/**
* Writes a signed 32-bit integer. By default big-endian (be = true).
*/
writeI32(value, be = true) {
if (value < -2147483648 || value > 2147483647)
throw new Error('i32 value is out of range.');
this.allocSafe(I32_BYTE_LENGTH);
this.buffer.setInt32(this.currentOffset, value, !be);
this.currentOffset += I32_BYTE_LENGTH;
}
/**
* Writes a signed 64-bit integer. By default big-endian (be = true).
*/
writeI64(value, be = true) {
if (value < -9223372036854775808n || value > 9223372036854775807n) {
throw new Error('i64 value is out of range.');
}
this.allocSafe(I64_BYTE_LENGTH);
this.buffer.setBigInt64(this.currentOffset, value, !be);
this.currentOffset += I64_BYTE_LENGTH;
}
// ---------------------------------------------------------------- //
writeSelector(value) {
this.writeU32(value, true);
}
writeBoolean(value) {
this.writeU8(value ? 1 : 0);
}
writeI128(bigIntValue, be = true) {
if (bigIntValue > 170141183460469231731687303715884105727n ||
bigIntValue < -170141183460469231731687303715884105728n) {
throw new Error('i128 value is too large.');
}
this.allocSafe(I128_BYTE_LENGTH);
const bytesToHex = BufferHelper.valueToUint8Array(bigIntValue, I128_BYTE_LENGTH);
if (bytesToHex.byteLength !== I128_BYTE_LENGTH) {
throw new Error(`Invalid i128 value: ${bigIntValue}`);
}
if (be) {
for (let i = 0; i < bytesToHex.byteLength; i++) {
this.writeU8(bytesToHex[i]);
}
}
else {
for (let i = bytesToHex.byteLength - 1; i >= 0; i--) {
this.writeU8(bytesToHex[i]);
}
}
}
writeU256(bigIntValue, be = true) {
if (bigIntValue >
115792089237316195423570985008687907853269984665640564039457584007913129639935n &&
bigIntValue < 0n) {
throw new Error('u256 value is too large or negative.');
}
this.allocSafe(U256_BYTE_LENGTH);
const bytesToHex = BufferHelper.valueToUint8Array(bigIntValue);
if (bytesToHex.byteLength !== U256_BYTE_LENGTH) {
throw new Error(`Invalid u256 value: ${bigIntValue}`);
}
if (be) {
for (let i = 0; i < bytesToHex.byteLength; i++) {
this.writeU8(bytesToHex[i]);
}
}
else {
for (let i = bytesToHex.byteLength - 1; i >= 0; i--) {
this.writeU8(bytesToHex[i]);
}
}
}
writeU128(bigIntValue, be = true) {
if (bigIntValue > 340282366920938463463374607431768211455n && bigIntValue < 0n) {
throw new Error('u128 value is too large or negative.');
}
this.allocSafe(U128_BYTE_LENGTH);
const bytesToHex = BufferHelper.valueToUint8Array(bigIntValue, U128_BYTE_LENGTH);
if (bytesToHex.byteLength !== U128_BYTE_LENGTH) {
throw new Error(`Invalid u128 value: ${bigIntValue}`);
}
if (be) {
for (let i = 0; i < bytesToHex.byteLength; i++) {
this.writeU8(bytesToHex[i]);
}
}
else {
for (let i = bytesToHex.byteLength - 1; i >= 0; i--) {
this.writeU8(bytesToHex[i]);
}
}
}
writeBytes(value) {
this.allocSafe(value.byteLength);
for (let i = 0; i < value.byteLength; i++) {
this.writeU8(value[i]);
}
}
writeString(value) {
const encoder = new TextEncoder();
const bytes = encoder.encode(value);
this.allocSafe(bytes.length);
this.writeBytes(bytes);
}
writeStringWithLength(value) {
const encoder = new TextEncoder();
const bytes = encoder.encode(value);
this.allocSafe(U32_BYTE_LENGTH + bytes.length);
this.writeU32(bytes.length);
this.writeBytes(bytes);
}
/**
* Writes an address (32 bytes MLDSA key hash only).
*/
writeAddress(value) {
this.verifyAddress(value);
this.writeBytes(value);
}
/**
* Writes the tweaked public key from an Address (32 bytes).
* @param value - The Address containing the tweaked public key
*/
writeTweakedPublicKey(value) {
const tweakedKey = value.tweakedPublicKeyToBuffer();
this.allocSafe(ADDRESS_BYTE_LENGTH);
this.writeBytes(tweakedKey);
}
/**
* Writes a full address with both tweaked public key and MLDSA key hash (64 bytes total).
* Format: [32 bytes tweakedPublicKey][32 bytes MLDSA key hash]
*
* This is the equivalent of btc-runtime's writeExtendedAddress().
*
* @param value - The Address containing both keys
*/
writeExtendedAddress(value) {
this.allocSafe(EXTENDED_ADDRESS_BYTE_LENGTH);
// Write tweaked public key first (32 bytes)
this.writeTweakedPublicKey(value);
// Write MLDSA key hash (32 bytes)
this.writeBytes(value);
}
/**
* Writes a Schnorr signature with its associated full Address.
* Format: [64 bytes full Address][64 bytes signature]
*
* Used for serializing signed data where both the signer's address
* and their Schnorr signature need to be stored together.
*
* @param address - The signer's Address (with both MLDSA and tweaked keys)
* @param signature - The 64-byte Schnorr signature
* @throws {Error} If signature is not exactly 64 bytes
*/
writeSchnorrSignature(address, signature) {
if (signature.length !== SCHNORR_SIGNATURE_BYTE_LENGTH) {
throw new Error(`Invalid Schnorr signature length: expected ${SCHNORR_SIGNATURE_BYTE_LENGTH}, got ${signature.length}`);
}
this.allocSafe(EXTENDED_ADDRESS_BYTE_LENGTH + SCHNORR_SIGNATURE_BYTE_LENGTH);
this.writeExtendedAddress(address);
this.writeBytes(signature);
}
getBuffer(clear = true) {
const buf = new Uint8Array(this.buffer.byteLength);
for (let i = 0; i < this.buffer.byteLength; i++) {
buf[i] = this.buffer.getUint8(i);
}
if (clear)
this.clear();
return buf;
}
reset() {
this.currentOffset = 0;
this.buffer = this.getDefaultBuffer(4);
}
toBytesReader() {
return new BinaryReader(this.getBuffer());
}
getOffset() {
return this.currentOffset;
}
setOffset(offset) {
this.currentOffset = offset;
}
clear() {
this.currentOffset = 0;
this.buffer = this.getDefaultBuffer();
}
[Symbol.dispose]() {
this.clear();
}
allocSafe(size) {
if (this.currentOffset + size > this.buffer.byteLength) {
this.resize(size);
}
}
writeAddressValueTuple(map, be = true) {
if (map.size > 65535)
throw new Error('Map size is too large');
this.writeU16(map.size, be);
const keys = Array.from(map.keys());
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = map.get(key);
if (value === null || value === undefined)
throw new Error('Value not found');
this.writeAddress(key);
this.writeU256(value, be);
}
}
/**
* Writes 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 writeExtendedAddressMapU256().
*/
writeExtendedAddressMapU256(map, be = true) {
if (map.size > 65535)
throw new Error('Map size is too large');
this.writeU16(map.size, be);
for (const [key, value] of map.entries()) {
this.writeExtendedAddress(key);
this.writeU256(value, be);
}
}
writeBytesWithLength(value) {
this.writeU32(value.length);
this.writeBytes(value);
}
writeArrayOfBuffer(values, be = true) {
const totalLength = BinaryWriter.estimateArrayOfBufferLength(values);
this.allocSafe(totalLength);
this.writeU16(values.length, be);
for (let i = 0; i < values.length; i++) {
this.writeU32(values[i].length, be);
this.writeBytes(values[i]);
}
}
writeAddressArray(value) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length);
for (let i = 0; i < value.length; i++) {
this.writeAddress(value[i]);
}
}
/**
* Writes an array of full addresses (64 bytes each).
* Format: [u16 length][FullAddress 0][FullAddress 1]...
*/
writeExtendedAddressArray(value) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.allocSafe(U16_BYTE_LENGTH + value.length * EXTENDED_ADDRESS_BYTE_LENGTH);
this.writeU16(value.length);
for (let i = 0; i < value.length; i++) {
this.writeExtendedAddress(value[i]);
}
}
writeU32Array(value, be = true) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length, be);
for (let i = 0; i < value.length; i++) {
this.writeU32(value[i], be);
}
}
writeU256Array(value, be = true) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length, be);
for (let i = 0; i < value.length; i++) {
this.writeU256(value[i], be);
}
}
writeU128Array(value, be = true) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length, be);
for (let i = 0; i < value.length; i++) {
this.writeU128(value[i], be);
}
}
writeStringArray(value) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length);
for (let i = 0; i < value.length; i++) {
this.writeStringWithLength(value[i]);
}
}
writeU16Array(value, be = true) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length, be);
for (let i = 0; i < value.length; i++) {
this.writeU16(value[i], be);
}
}
writeU8Array(value) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length);
for (let i = 0; i < value.length; i++) {
this.writeU8(value[i]);
}
}
writeU64Array(value, be = true) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length, be);
for (let i = 0; i < value.length; i++) {
this.writeU64(value[i], be);
}
}
writeBytesArray(value) {
if (value.length > 65535)
throw new Error('Array size is too large');
this.writeU16(value.length);
for (let i = 0; i < value.length; i++) {
this.writeBytesWithLength(value[i]);
}
}
verifyAddress(pubKey) {
if (pubKey.byteLength > ADDRESS_BYTE_LENGTH) {
throw new Error(`Address is too long ${pubKey.byteLength} > ${ADDRESS_BYTE_LENGTH} bytes`);
}
}
resize(size) {
const buf = new Uint8Array(this.buffer.byteLength + size);
for (let i = 0; i < this.buffer.byteLength; i++) {
buf[i] = this.buffer.getUint8(i);
}
this.buffer = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
}
getDefaultBuffer(length = 0) {
return new DataView(new ArrayBuffer(length));
}
}
//# sourceMappingURL=BinaryWriter.js.map