@btc-vision/btc-runtime
Version:
Bitcoin Smart Contract Runtime
227 lines (185 loc) • 7.34 kB
text/typescript
import { Blockchain } from '../../env';
import { BytesWriter } from '../../buffer/BytesWriter';
import { BytesReader } from '../../buffer/BytesReader';
import { encodePointerUnknownLength } from '../../math/abi';
import { i128, u128, u256 } from '@btc-vision/as-bignum/assembly';
import { idOfAddress, idOfI128, idOfString, idOfU128, idOfU256, idOfUint8Array } from '../codecs/Ids';
import { AddressCodec } from '../codecs/AddressCodec';
import { BooleanCodec } from '../codecs/BooleanCodec';
import { StringCodec } from '../codecs/StringCodec';
import { VariableBytesCodec } from '../codecs/VariableBytesCodec';
import { U256Codec } from '../codecs/U256Codec';
import { Address } from '../../types/Address';
import { Revert } from '../../types/Revert';
/**
* A reflection-based StorageMap<K, V>.
* - `pointer` => a u16 "namespace"
* - `set(key, value)` => store bytes
* - `get(key)` => decode bytes
*
* This version uses a chain of `if/else` for each possible type T,
* so the compiler doesn't complain about mismatched returns.
*/
export class StorageMap<K, V> {
private readonly pointer: u16;
constructor(pointer: u16) {
this.pointer = pointer;
}
// ----------------------------------------------------------
// PUBLIC API
// ----------------------------------------------------------
public set(key: K, value: V): this {
const storageKey = this.getStorageKey(key);
this.storeValue<V>(storageKey, value);
return this;
}
public get(key: K): V {
const storageKey = this.getStorageKey(key);
return this.decodeValue<V>(storageKey);
}
public has(key: K): bool {
const storageKey = this.getStorageKey(key);
return Blockchain.hasStorageAt(storageKey);
}
public delete(key: K): bool {
const storageKey = this.getStorageKey(key);
if (!Blockchain.hasStorageAt(storageKey)) {
return false;
}
Blockchain.setStorageAt(storageKey, new Uint8Array(0));
return true;
}
public clear(): void {
// Not implemented: we'd need key-tracking to remove them all
throw new Error('clear() not implemented; no key-tracking logic here.');
}
// ----------------------------------------------------------
// INTERNAL: Derive the final storage key
// ----------------------------------------------------------
private getStorageKey(k: K): Uint8Array {
// 1) encode the key
const keyBytes = this.encodeValue<K>(k);
// 2) transform with pointer
return encodePointerUnknownLength(this.pointer, keyBytes);
}
// ----------------------------------------------------------
// ENCODE / DECODE
// ----------------------------------------------------------
/**
* Retrieve the value of type P from the given storage pointer (32 bytes).
* If it's a variable-length type, we decode from pointer-based approach
* (like VariableBytesCodec or StringCodec).
* Otherwise, we get the raw from storage and decode.
*/
private decodeValue<P>(pointer: Uint8Array): P {
// For some types like `Uint8Array` or `string`, we might want
// to treat the pointer as an allocated "variable" location.
const typeId = idof<P>();
// For variable-length things:
if (typeId == idOfUint8Array) {
// decode variable bytes from pointer
const arr = VariableBytesCodec.decode(pointer);
return changetype<P>(arr);
}
if (typeId == idOfString) {
const str = StringCodec.decode(pointer);
return changetype<P>(str);
}
// For everything else, we read raw from storage
const raw = Blockchain.getStorageAt(pointer);
return this.decodeBytesAsType<P>(raw);
}
/**
* Store a value of type P at the given storage key.
* - Some types (uint8array, string) -> chunked (pointer-based).
* - Others -> direct bytes in that slot.
*/
private storeValue<P>(storageKey: Uint8Array, value: P): void {
const raw = this.encodeValue<P>(value);
Blockchain.setStorageAt(storageKey, raw);
}
// ----------------------------------------------------------
// decodeBytesAsType: interpret raw bytes as type P
// ----------------------------------------------------------
private decodeBytesAsType<P>(raw: Uint8Array): P {
// isInteger => built-in numeric types (i32, u32, etc.)
if (isInteger<P>()) {
if (raw.length < <i32>sizeof<P>()) {
return changetype<P>(0);
}
const reader = new BytesReader(raw);
return reader.read<P>();
}
// isBoolean => decode with BooleanCodec
if (isBoolean<P>()) {
const boolVal = BooleanCodec.decode(raw);
return changetype<P>(boolVal);
}
const typeId = idof<P>();
// u256
if (typeId == idOfU256) {
const decoded = U256Codec.decode(raw);
return changetype<P>(decoded);
}
// i128
if (typeId == idOfI128) {
const val = i128.fromUint8ArrayBE(raw);
return changetype<P>(val);
}
// u128
if (typeId == idOfU128) {
const val = u128.fromUint8ArrayBE(raw);
return changetype<P>(val);
}
// Address
if (typeId == idOfAddress) {
const addr = AddressCodec.decode(raw);
return changetype<P>(addr);
}
throw new Revert(`Unsupported type ${typeId}`);
}
// ----------------------------------------------------------
// encodeValue<P>: convert a value of type P into raw bytes
// ----------------------------------------------------------
private encodeValue<P>(value: P): Uint8Array {
// built-in integer => write with BytesWriter
if (isInteger<P>()) {
const writer = new BytesWriter(sizeof<P>());
writer.write<P>(value);
return writer.getBuffer();
}
// bool => BooleanCodec
if (isBoolean<P>()) {
return BooleanCodec.encode(changetype<bool>(value));
}
// Check reflection for bigger types
if (value instanceof Uint8Array) {
return VariableBytesCodec.encode(value);
}
if (isString<P>()) {
const strVal = changetype<string>(value);
return StringCodec.encode(strVal);
}
if (value instanceof u256) {
return U256Codec.encode(changetype<u256>(value));
}
if (value instanceof u128) {
// store big-endian
const uval = changetype<u128>(value);
return uval.toUint8Array(true);
}
if (value instanceof i128) {
const ival = changetype<i128>(value);
return ival.toUint8Array(true);
}
if (value instanceof Address) {
return AddressCodec.encode(changetype<Address>(value));
}
// If nested map => handle in a custom branch (not shown here):
// if (value instanceof StorageMap<...>) { ... }
throw new Revert('encodeValue: Unsupported type');
}
}