UNPKG

@btc-vision/transaction

Version:

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

513 lines (406 loc) 16.5 kB
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 type { i16, i32, i64, i8, Selector, u16, u32, u64, u8 } from '../utils/types.js'; import { BinaryReader } from './BinaryReader.js'; export class BinaryWriter implements Disposable { private currentOffset: u32 = 0; private buffer: DataView; constructor(length: number = 0) { this.buffer = this.getDefaultBuffer(length); } public static estimateArrayOfBufferLength(values: Uint8Array[]): u32 { if (values.length > 65535) throw new Error('Array size is too large'); let totalLength: u32 = U16_BYTE_LENGTH; for (let i = 0; i < values.length; i++) { totalLength += U32_BYTE_LENGTH + (values[i] as Uint8Array).length; // each entry has a u32 length prefix } return totalLength; } public writeU8(value: u8): void { if (value > 255) throw new Error('u8 value is too large.'); this.allocSafe(U8_BYTE_LENGTH); this.buffer.setUint8(this.currentOffset++, value); } public writeU16(value: u16, be: boolean = true): void { 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; } public writeU32(value: u32, be: boolean = true): void { 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; } public writeU64(value: u64, be: boolean = true): void { 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. */ public writeI8(value: i8): void { 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). */ public writeI16(value: i16, be: boolean = true): void { 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). */ public writeI32(value: i32, be: boolean = true): void { 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). */ public writeI64(value: i64, be: boolean = true): void { 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; } // ---------------------------------------------------------------- // public writeSelector(value: Selector): void { this.writeU32(value, true); } public writeBoolean(value: boolean): void { this.writeU8(value ? 1 : 0); } public writeI128(bigIntValue: bigint, be: boolean = true): void { 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] as number); } } else { for (let i = bytesToHex.byteLength - 1; i >= 0; i--) { this.writeU8(bytesToHex[i] as number); } } } public writeU256(bigIntValue: bigint, be: boolean = true): void { 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] as number); } } else { for (let i = bytesToHex.byteLength - 1; i >= 0; i--) { this.writeU8(bytesToHex[i] as number); } } } public writeU128(bigIntValue: bigint, be: boolean = true): void { 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] as number); } } else { for (let i = bytesToHex.byteLength - 1; i >= 0; i--) { this.writeU8(bytesToHex[i] as number); } } } public writeBytes(value: Uint8Array): void { this.allocSafe(value.byteLength); for (let i = 0; i < value.byteLength; i++) { this.writeU8(value[i] as number); } } public writeString(value: string): void { const encoder = new TextEncoder(); const bytes = encoder.encode(value); this.allocSafe(bytes.length); this.writeBytes(bytes); } public writeStringWithLength(value: string): void { 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). */ public writeAddress(value: Address): void { 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 */ public writeTweakedPublicKey(value: Address): void { 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 */ public writeExtendedAddress(value: Address): void { 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 */ public writeSchnorrSignature(address: Address, signature: Uint8Array): void { 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); } public getBuffer(clear: boolean = true): Uint8Array { const buf = new Uint8Array(this.buffer.byteLength); for (let i: u32 = 0; i < this.buffer.byteLength; i++) { buf[i] = this.buffer.getUint8(i); } if (clear) this.clear(); return buf; } public reset(): void { this.currentOffset = 0; this.buffer = this.getDefaultBuffer(4); } public toBytesReader(): BinaryReader { return new BinaryReader(this.getBuffer()); } public getOffset(): u32 { return this.currentOffset; } public setOffset(offset: u32): void { this.currentOffset = offset; } public clear(): void { this.currentOffset = 0; this.buffer = this.getDefaultBuffer(); } public [Symbol.dispose](): void { this.clear(); } public allocSafe(size: u32): void { if (this.currentOffset + size > this.buffer.byteLength) { this.resize(size); } } public writeAddressValueTuple(map: AddressMap<bigint>, be: boolean = true): void { 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] as Address; 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(). */ public writeExtendedAddressMapU256(map: ExtendedAddressMap<bigint>, be: boolean = true): void { 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); } } public writeBytesWithLength(value: Uint8Array): void { this.writeU32(value.length); this.writeBytes(value); } public writeArrayOfBuffer(values: Uint8Array[], be: boolean = true): void { 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] as Uint8Array).length, be); this.writeBytes(values[i] as Uint8Array); } } public writeAddressArray(value: Address[]): void { 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] as Address); } } /** * Writes an array of full addresses (64 bytes each). * Format: [u16 length][FullAddress 0][FullAddress 1]... */ public writeExtendedAddressArray(value: Address[]): void { 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] as Address); } } public writeU32Array(value: u32[], be: boolean = true): void { 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] as u32, be); } } public writeU256Array(value: bigint[], be: boolean = true): void { 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] as bigint, be); } } public writeU128Array(value: bigint[], be: boolean = true): void { 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] as bigint, be); } } public writeStringArray(value: string[]): void { 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] as string); } } public writeU16Array(value: u16[], be: boolean = true): void { 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] as u16, be); } } public writeU8Array(value: u8[]): void { 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] as u8); } } public writeU64Array(value: bigint[], be: boolean = true): void { 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] as bigint, be); } } public writeBytesArray(value: Uint8Array[]): void { 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] as Uint8Array); } } private verifyAddress(pubKey: Address): void { if (pubKey.byteLength > ADDRESS_BYTE_LENGTH) { throw new Error( `Address is too long ${pubKey.byteLength} > ${ADDRESS_BYTE_LENGTH} bytes`, ); } } private resize(size: u32): void { const buf: Uint8Array = new Uint8Array(this.buffer.byteLength + size); for (let i: number = 0; i < this.buffer.byteLength; i++) { buf[i] = this.buffer.getUint8(i); } this.buffer = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); } private getDefaultBuffer(length: number = 0): DataView { return new DataView(new ArrayBuffer(length)); } }