@btc-vision/btc-runtime
Version:
Bitcoin Smart Contract Runtime
359 lines (310 loc) • 11.8 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,
U128_BYTE_LENGTH,
U16_BYTE_LENGTH,
U256_BYTE_LENGTH,
U32_BYTE_LENGTH,
U64_BYTE_LENGTH,
U8_BYTE_LENGTH,
} from '../utils';
import { BytesReader } from './BytesReader';
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}`,
);
}
}