@btc-vision/btc-runtime
Version:
Bitcoin Smart Contract Runtime
356 lines (314 loc) • 11.1 kB
text/typescript
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,
I64_BYTE_LENGTH,
U128_BYTE_LENGTH,
U16_BYTE_LENGTH,
U256_BYTE_LENGTH,
U32_BYTE_LENGTH,
U64_BYTE_LENGTH,
U8_BYTE_LENGTH,
} from '../utils';
import { sizeof } from 'builtins';
export class BytesReader {
private readonly buffer: DataView;
private currentOffset: i32 = 0;
constructor(bytes: Uint8Array) {
this.buffer = new DataView(bytes.buffer);
}
public get byteLength(): i32 {
return this.buffer.byteLength;
}
public read<T>(): T {
const id = idof<T>();
if (isBoolean<T>()) {
return this.readBoolean() as T;
} else if (isString<T>()) {
return this.readStringWithLength() as T;
} else if (isInteger<T>()) {
if (isSigned<T>()) {
const size = sizeof<T>();
switch (size) {
case 8:
return this.readU8() as T;
case 16:
return this.readI16() as T;
case 32:
return this.readI32() as T;
case 64:
return this.readI64() as T;
default:
throw new Error(`Invalid size ${size}`);
}
} else {
const size = sizeof<T>();
switch (size) {
case 8:
return this.readU8() as T;
case 16:
return this.readU16() as T;
case 32:
return this.readU32() as T;
case 64:
return this.readU64() as T;
default:
throw new Error(`Invalid size ${size}`);
}
}
} else if (id === idof<u256>()) {
return this.readU256() as T;
} else if (id === idof<u128>()) {
return this.readU128() as T;
} else if (id === idof<i128>()) {
return this.readI128() as T;
} else if (id === idof<Address>()) {
return this.readAddress() as T;
} else if (id === idof<Uint8Array>()) {
return this.readBytesWithLength() as T;
} else if (id === idof<string>()) {
return this.readStringWithLength() as T;
} else {
throw new Error(`Unsupported type ${id}`);
}
}
public readI16(): i16 {
this.verifyEnd(this.currentOffset + 2);
const value = this.buffer.getInt16(this.currentOffset, true);
this.currentOffset += 2;
return value;
}
public readI32(): i32 {
this.verifyEnd(this.currentOffset + 4);
const value = this.buffer.getInt32(this.currentOffset, true);
this.currentOffset += 4;
return value;
}
public readU8(): u8 {
this.verifyEnd(this.currentOffset + U8_BYTE_LENGTH);
const value = this.buffer.getUint8(this.currentOffset);
this.currentOffset += U8_BYTE_LENGTH;
return value;
}
/**
* By default, big-endian (be = true).
*/
public readU16(be: boolean = true): u16 {
this.verifyEnd(this.currentOffset + U16_BYTE_LENGTH);
const value = this.buffer.getUint16(this.currentOffset, !be);
this.currentOffset += U16_BYTE_LENGTH;
return value;
}
/**
* By default, big-endian (be = true).
*/
public readU32(be: boolean = true): u32 {
this.verifyEnd(this.currentOffset + U32_BYTE_LENGTH);
const value = this.buffer.getUint32(this.currentOffset, !be);
this.currentOffset += U32_BYTE_LENGTH;
return value;
}
/**
* By default, big-endian (be = true).
*/
public readU64(be: boolean = true): u64 {
this.verifyEnd(this.currentOffset + U64_BYTE_LENGTH);
const value = this.buffer.getUint64(this.currentOffset, !be);
this.currentOffset += U64_BYTE_LENGTH;
return value;
}
/**
* Reads 256 bits. The writer calls `writeU256(value, be)`.
* If be=true, we do big-endian; if be=false, little-endian.
*/
public readU256(be: boolean = true): u256 {
this.verifyEnd(this.currentOffset + U256_BYTE_LENGTH);
const raw: u8[] = this.readBytesArray(U256_BYTE_LENGTH);
return be ? u256.fromBytesBE(raw) : u256.fromBytesLE(raw);
}
public readI64(be: boolean = true): i64 {
this.verifyEnd(this.currentOffset + I64_BYTE_LENGTH);
const value = this.buffer.getInt64(this.currentOffset, !be);
this.currentOffset += I64_BYTE_LENGTH;
return value;
}
public readU128(be: boolean = true): u128 {
this.verifyEnd(this.currentOffset + U128_BYTE_LENGTH);
const raw: u8[] = this.readBytesArray(U128_BYTE_LENGTH);
return be ? u128.fromBytesBE(raw) : u128.fromBytesLE(raw);
}
public readI128(be: boolean = true): i128 {
this.verifyEnd(this.currentOffset + I128_BYTE_LENGTH);
const raw: u8[] = this.readBytesArray(I128_BYTE_LENGTH);
return be ? i128.fromBytesBE(raw) : i128.fromBytesLE(raw);
}
/**
* Reads `length` bytes, optionally stopping early if a 0x00 is seen.
*/
public readBytes(length: u32, zeroStop: boolean = false): Uint8Array {
let bytes = new Uint8Array(length);
for (let i: u32 = 0; i < length; i++) {
const b: u8 = this.readU8();
if (zeroStop && b === 0) {
bytes = bytes.subarray(0, i);
break;
}
bytes[i] = b;
}
return bytes;
}
/**
* Convenience for reading a fixed number of bytes into a plain u8[] array.
*/
public readBytesArray(count: i32): u8[] {
const arr = new Array<u8>(count);
for (let i = 0; i < count; i++) {
arr[i] = this.readU8();
}
return arr;
}
/**
* [u32 length][raw bytes]. By default big-endian for the length,
* to match AS BytesWriter's `writeBytesWithLength`.
*/
public readBytesWithLength(be: boolean = true): Uint8Array {
const length = this.readU32(be);
return this.readBytes(length);
}
/**
* Reads a string of `length` raw bytes, zeroStop = true for convenience.
* (Or the writer may not have used zeroStop.)
*/
public readString(length: u32): string {
const bytes = this.readBytes(length, true);
return String.UTF8.decode(bytes.buffer);
}
/**
* [u16 length][raw bytes].
* The AS writer calls `writeStringWithLength(value: string)` => writes length big-endian by default.
*/
public readStringWithLength(be: boolean = true): string {
const length = this.readU32(be);
return this.readString(length);
}
public readBoolean(): boolean {
return this.readU8() !== 0;
}
public readSelector(): Selector {
return this.readU32(true);
}
/**
* Reads an Address (32 bytes).
*/
public readAddress(): Address {
this.verifyEnd(this.currentOffset + ADDRESS_BYTE_LENGTH);
const addr = new Address();
for (let i: i32 = 0; i < ADDRESS_BYTE_LENGTH; i++) {
addr[i] = this.readU8();
}
return addr;
}
// ------------------- Arrays ------------------- //
/**
* The AS writer does `writeU32(length)` for U256 arrays, so we read a u32.
* If you changed it to a `u16`, then do readU16() here.
*/
public readU256Array(be: boolean = true): u256[] {
// The AS writer currently writes a u32 length for U256 arrays
const length = this.readU32();
const result = new Array<u256>(length);
for (let i: u32 = 0; i < length; i++) {
result[i] = this.readU256(be);
}
return result;
}
/**
* The AS writer uses a [u16 length] for U64 arrays.
*/
public readU64Array(be: boolean = true): u64[] {
const length = this.readU16(be);
const result = new Array<u64>(length);
for (let i: u32 = 0; i < length; i++) {
result[i] = this.readU64(be);
}
return result;
}
public readU32Array(be: boolean = true): u32[] {
const length = this.readU16(be);
const result = new Array<u32>(length);
for (let i: u16 = 0; i < length; i++) {
result[i] = this.readU32(be);
}
return result;
}
public readU16Array(be: boolean = true): u16[] {
const length = this.readU16(be);
const result = new Array<u16>(length);
for (let i: u16 = 0; i < length; i++) {
result[i] = this.readU16(be);
}
return result;
}
public readU128Array(be: boolean = true): u128[] {
const length = this.readU16(be);
const result = new Array<u128>(length);
for (let i: u16 = 0; i < length; i++) {
result[i] = this.readU128(be);
}
return result;
}
/**
* The AS writer uses a [u8 length] for transaction inputs/outputs in the example,
* but for an "AddressArray" we use [u16 length].
*/
public readAddressArray(be: boolean = true): Address[] {
const length = this.readU16(be);
const result = new Array<Address>(length);
for (let i: u16 = 0; i < length; i++) {
result[i] = this.readAddress();
}
return result;
}
/**
* Map of [u16 length] entries, each entry = [Address, U256], consistent with the writer’s `writeAddressMapU256`.
*/
public readAddressMapU256(be: boolean = true): AddressMap<u256> {
const length = this.readU16(be);
const result = new AddressMap<u256>();
for (let i: u16 = 0; i < length; i++) {
const address = this.readAddress();
const value = this.readU256(be);
if (result.has(address)) {
throw new Revert('Duplicate address found in map');
}
result.set(address, value);
}
return result;
}
public getOffset(): i32 {
return this.currentOffset;
}
public setOffset(offset: i32): void {
this.currentOffset = offset;
}
/**
* Checks if we have enough bytes left in the buffer.
*/
public verifyEnd(size: i32): void {
if (size > this.buffer.byteLength) {
throw new Error(
`Attempt to read beyond buffer length. Requested up to offset ${size}, ` +
`but buffer is only ${this.buffer.byteLength} bytes.`,
);
}
}
public toString(): string {
return Uint8Array.wrap(this.buffer.buffer).toString();
}
}