UNPKG

@btc-vision/btc-runtime

Version:

Bitcoin Smart Contract Runtime

359 lines (310 loc) 11.8 kB
import { i128, u128, u256 } from '@btc-vision/as-bignum/assembly'; import { AddressMap } from '../generic/AddressMap'; import { Selector } from '../math/abi'; import { Address } from '../types/Address'; import { Revert } from '../types/Revert'; import { ADDRESS_BYTE_LENGTH, I128_BYTE_LENGTH, U128_BYTE_LENGTH, U16_BYTE_LENGTH, U256_BYTE_LENGTH, U32_BYTE_LENGTH, U64_BYTE_LENGTH, U8_BYTE_LENGTH, } from '../utils'; import { BytesReader } from './BytesReader'; @final export class BytesWriter { private currentOffset: u32 = 0; private buffer: DataView; private readonly typedArray: Uint8Array; constructor(length: i32) { const typedArray = (this.typedArray = new Uint8Array(length)); this.buffer = new DataView(typedArray.buffer); } public bufferLength(): u32 { return this.buffer.byteLength; } public write<T>(value: T): void { if (isInteger<T>()) { const size = sizeof<T>(); if (size === 1) { this.writeU8(<u8>value); return; } if (isSigned<T>()) { switch (size) { case 2: this.writeI16(<i16>value); break; case 4: this.writeI32(<i32>value); break; case 8: this.writeI64(<i64>value); break; default: throw new Revert(`Unsupported integer size: ${size}`); } } else { switch (size) { case 2: this.writeU16(<u16>value); break; case 4: this.writeU32(<u32>value); break; case 8: this.writeU64(<u64>value); break; default: throw new Revert(`Unsupported integer size: ${size}`); } } } else if (isBoolean<T>()) { this.writeBoolean(<boolean>value); } else if (isString<T>()) { this.writeStringWithLength(<string>value); } else if (value instanceof Uint8Array) { this.writeBytesWithLength(<Uint8Array>value); } else if (value instanceof Address) { this.writeAddress(<Address>value); } else if (value instanceof u128) { this.writeU128(<u128>value); } else if (value instanceof u256) { this.writeU256(<u256>value); } else if (value instanceof i128) { this.writeI128(<i128>value); } else { throw new Revert(`Unsupported type: ${typeof value}`); } } public writeU8(value: u8): void { this.allocSafe(U8_BYTE_LENGTH); this.buffer.setUint8(this.currentOffset, value); this.currentOffset += U8_BYTE_LENGTH; } /** * Writes a 16-bit unsigned integer. By default big-endian (be = true). * If be=false, writes little-endian. */ public writeU16(value: u16, be: boolean = true): void { this.allocSafe(U16_BYTE_LENGTH); this.buffer.setUint16(this.currentOffset, value, !be); this.currentOffset += U16_BYTE_LENGTH; } /** * Writes a 32-bit unsigned integer. By default big-endian (be = true). */ public writeU32(value: u32, be: boolean = true): void { this.allocSafe(U32_BYTE_LENGTH); this.buffer.setUint32(this.currentOffset, value, !be); this.currentOffset += U32_BYTE_LENGTH; } /** * Writes a 64-bit unsigned integer. By default big-endian (be = true). */ public writeU64(value: u64, be: boolean = true): void { this.allocSafe(U64_BYTE_LENGTH); this.buffer.setUint64(this.currentOffset, value || 0, !be); this.currentOffset += U64_BYTE_LENGTH; } public writeI64(value: i64, be: boolean = true): void { this.allocSafe(U64_BYTE_LENGTH); this.buffer.setInt64(this.currentOffset, value, !be); this.currentOffset += U64_BYTE_LENGTH; } public writeI32(value: i32, be: boolean = true): void { this.allocSafe(U32_BYTE_LENGTH); this.buffer.setInt32(this.currentOffset, value, !be); this.currentOffset += U32_BYTE_LENGTH; } public writeI16(value: i16, be: boolean = true): void { this.allocSafe(U16_BYTE_LENGTH); this.buffer.setInt16(this.currentOffset, value, !be); this.currentOffset += U16_BYTE_LENGTH; } /** * Writes a 32-bit selector. * @param value */ public writeSelector(value: Selector): void { this.writeU32(value, true); } public writeBoolean(value: boolean): void { this.writeU8(value ? 1 : 0); } /** * Writes a 256-bit unsigned integer. By default big-endian (be = true). */ public writeU256(value: u256, be: boolean = true): void { this.allocSafe(U256_BYTE_LENGTH); const bytes = value.toUint8Array(be); for (let i: i32 = 0; i < U256_BYTE_LENGTH; i++) { this.writeU8(bytes[i]); } } /** * Writes a 128-bit signed integer. By default big-endian (be = true). */ public writeI128(value: i128, be: boolean = true): void { this.allocSafe(I128_BYTE_LENGTH); const bytes = value.toUint8Array(be); for (let i: i32 = 0; i < I128_BYTE_LENGTH; i++) { this.writeU8(bytes[i]); } } /** * Writes a 128-bit unsigned integer. By default big-endian (be = true). */ public writeU128(value: u128, be: boolean = true): void { this.allocSafe(U128_BYTE_LENGTH); const bytes = value.toUint8Array(be); for (let i: i32 = 0; i < U128_BYTE_LENGTH; i++) { this.writeU8(bytes[i]); } } // ------------------ Array Writers ------------------ // public writeU16Array(value: u16[], be: boolean = true): void { if (value.length > 65535) throw new Revert('Array size is too large'); this.allocSafe(U16_BYTE_LENGTH + value.length * U16_BYTE_LENGTH); this.writeU16(u16(value.length), be); for (let i = 0; i < value.length; i++) { this.writeU16(value[i], be); } } public writeU32Array(value: u32[], be: boolean = true): void { if (value.length > 65535) throw new Revert('Array size is too large'); this.allocSafe(U16_BYTE_LENGTH + value.length * U32_BYTE_LENGTH); this.writeU16(u16(value.length), be); for (let i = 0; i < value.length; i++) { this.writeU32(value[i], be); } } public writeU64Array(value: u64[], be: boolean = true): void { if (value.length > 65535) throw new Revert('Array size is too large'); this.allocSafe(U16_BYTE_LENGTH + value.length * U64_BYTE_LENGTH); this.writeU16(u16(value.length), be); for (let i = 0; i < value.length; i++) { this.writeU64(value[i], be); } } public writeU128Array(value: u128[], be: boolean = true): void { if (value.length > 65535) throw new Revert('Array size is too large'); this.allocSafe(U16_BYTE_LENGTH + value.length * U128_BYTE_LENGTH); this.writeU16(u16(value.length), be); for (let i = 0; i < value.length; i++) { this.writeU128(value[i], be); } } public writeU256Array(value: u256[], be: boolean = true): void { if (value.length > 65535) throw new Revert('Array size is too large'); this.allocSafe(U16_BYTE_LENGTH + value.length * U256_BYTE_LENGTH); this.writeU16(u16(value.length), be); for (let i = 0; i < value.length; i++) { this.writeU256(value[i], be); } } public writeAddressArray(value: Address[]): void { if (value.length > 65535) throw new Revert('Array size is too large'); this.writeU16(u16(value.length)); for (let i: i32 = 0; i < value.length; i++) { this.writeAddress(value[i]); } } // --------------------------------------------------- // public writeBytes(value: Uint8Array): void { this.allocSafe(value.length); for (let i = 0; i < value.length; i++) { this.writeU8(value[i]); } } public writeBytesU8Array(value: u8[]): void { this.allocSafe(value.length); for (let i = 0; i < value.length; i++) { this.writeU8(value[i]); } } /** * Writes [u32 length][raw bytes]. * By default big-endian, so length is stored with `writeU32(length, true)`. */ public writeBytesWithLength(value: Uint8Array): void { const length: u32 = u32(value.length); this.allocSafe(length + U32_BYTE_LENGTH); this.writeU32(length); // default be = true => big-endian for (let i: u32 = 0; i < length; i++) { this.writeU8(value[i]); } } public writeString(value: string): void { const bytes = String.UTF8.encode(value); this.writeBytes(Uint8Array.wrap(bytes)); } public writeStringWithLength(value: string): void { const bytes = String.UTF8.encode(value); this.writeU32(bytes.byteLength); this.writeBytes(Uint8Array.wrap(bytes)); } public writeAddress(value: Address): void { const bytes = this.fromAddress(value); this.writeBytes(bytes); } /** * Equivalent to TS’s writeAddressValueTuple, except specialized for u256 values. */ public writeAddressMapU256(value: AddressMap<u256>, be: boolean = true): void { const keys: Address[] = value.keys(); if (keys.length > 65535) throw new Revert('Map size is too large'); this.writeU16(u16(keys.length), be); for (let i: i32 = 0; i < keys.length; i++) { this.writeAddress(keys[i]); this.writeU256(value.get(keys[i]), be); } } // --------------------------------------------------- // public getBuffer(): Uint8Array { return this.typedArray; } public toBytesReader(): BytesReader { return new BytesReader(this.getBuffer()); } public getOffset(): u32 { return this.currentOffset; } public setOffset(offset: u32): void { this.currentOffset = offset; } /** * Ensures we have space for `size` more bytes without going past the current buffer. * If not, calls `resize()` which by default throws a Revert. */ public allocSafe(size: u32): void { const needed = this.currentOffset + size; if (needed > u32(this.buffer.byteLength)) { const sizeDiff: u32 = needed - u32(this.buffer.byteLength); this.resize(sizeDiff); } } private fromAddress(pubKey: Address): Uint8Array { if (pubKey.byteLength > ADDRESS_BYTE_LENGTH) { throw new Revert( `Address is too long ${pubKey.byteLength} > ${ADDRESS_BYTE_LENGTH} bytes`, ); } return pubKey; } /** * This implementation always throws rather than actually resizing, * which is consistent with the original approach. If you need * dynamic resizing, remove the `throw` and implement accordingly. */ private resize(size: u32): void { throw new Revert( `Buffer is getting resized. This is bad for performance. ` + `Expected size: ${this.buffer.byteLength + size} - ` + `Current size: ${this.buffer.byteLength}`, ); } }