Files
warp/src/legacy/smartweave-global.ts
Michał Szynwelski 5da88ff8c8
Some checks failed
CI / build (push) Has been cancelled
v1.5.0-rc.0
2023-12-11 13:47:54 +01:00

401 lines
12 KiB
TypeScript

/* eslint-disable */
import Arweave from 'arweave';
import { EvaluationOptions } from '../core/modules/StateEvaluator';
import { GQLNodeInterface, GQLTagInterface, VrfData } from './gqlResult';
import { CacheKey, SortKeyCache } from '../cache/SortKeyCache';
import { SortKeyCacheRangeOptions } from '../cache/SortKeyCacheRangeOptions';
import { InteractionState } from '../contract/states/InteractionState';
import { safeGet } from '../utils/utils';
import { Buffer } from 'warp-isomorphic';
/**
*
* This class is exposed as a global for contracts
* as 'SmartWeave' and provides an API for getting further
* information or using utility and crypto functions from
* inside the contracts execution.
*
* It provides an api:
*
* - SmartWeave.transaction.id
* - SmartWeave.transaction.reward
* - SmartWeave.block.height
* - SmartWeave.block.timestamp
* - etc
*
* and access to some of the arweave utils:
* - SmartWeave.arweave.utils
* - SmartWeave.arweave.crypto
* - SmartWeave.arweave.wallets
* - SmartWeave.arweave.ar
*
* as well as access to the potentially non-deterministic full client:
* - SmartWeave.unsafeClient
*
*/
export type TransactionOrigin = 'L1' | 'L2';
export class SmartWeaveGlobal {
gasUsed: number;
gasLimit: number;
transaction: SWTransaction;
block: SWBlock;
vrf: SWVrf;
evaluationOptions: EvaluationOptions;
arweave: Pick<Arweave, 'ar' | 'wallets' | 'utils' | 'crypto'>;
contract: {
id: string;
owner: string;
};
unsafeClient: Arweave;
baseArweaveUrl: string;
safeArweaveGet: (input: RequestInfo | URL, init?: RequestInit) => Promise<unknown>;
contracts: {
readContractState: (contractId: string) => Promise<any>;
viewContractState: (contractId: string, input: any) => Promise<any>;
write: (contractId: string, input: any) => Promise<any>;
refreshState: () => Promise<any>;
};
extensions: any;
_activeTx?: GQLNodeInterface;
caller?: string;
kv: KV;
constructor(
arweave: Arweave,
contract: { id: string; owner: string },
evaluationOptions: EvaluationOptions,
interactionState: InteractionState,
storage: SortKeyCache<any> | null
) {
this.gasUsed = 0;
this.gasLimit = Number.MAX_SAFE_INTEGER;
this.unsafeClient = arweave;
this.arweave = {
ar: arweave.ar,
utils: arweave.utils,
wallets: arweave.wallets,
crypto: arweave.crypto
};
this.baseArweaveUrl = `${arweave.api.config.protocol}://${arweave.api.config.host}:${arweave.api.config.port}`;
this.safeArweaveGet = async function (query: string) {
return safeGet(`${this.baseArweaveUrl}${query}`);
};
this.evaluationOptions = evaluationOptions;
this.contract = contract;
this.transaction = new SWTransaction(this);
this.block = new SWBlock(this);
this.contracts = {
readContractState: (contractId: string, height?: number, returnValidity?: boolean) => {
throw new Error('Not implemented - should be set by HandlerApi implementor');
},
viewContractState: (contractId: string, input: any) => {
throw new Error('Not implemented - should be set by HandlerApi implementor');
},
write: (contractId: string, input: any, throwOnError?: boolean) => {
throw new Error('Not implemented - should be set by HandlerApi implementor');
},
refreshState: () => {
throw new Error('Not implemented - should be set by HandlerApi implementor');
}
};
this.vrf = new SWVrf(this);
this.useGas = this.useGas.bind(this);
this.getBalance = this.getBalance.bind(this);
this.extensions = {};
this.kv = new KV(storage, interactionState, this.transaction, this.contract.id);
}
useGas(gas: number) {
if (gas < 0) {
throw new Error(`[RE:GNE] Gas number exception - gas < 0.`);
}
this.gasUsed += gas;
if (this.gasUsed > this.gasLimit) {
throw new Error(`[RE:OOG] Out of gas! Used: ${this.gasUsed}, limit: ${this.gasLimit}`);
}
}
async getBalance(address: string, height?: number): Promise<string> {
if (!this._activeTx) {
throw new Error('Cannot read balance - active tx is not set.');
}
if (!this.block.height) {
throw new Error('Cannot read balance - block height not set.');
}
const effectiveHeight = height || this.block.height;
// http://nyc-1.dev.arweave.net:1984/block/height/914387/wallet/M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI/balance
return await fetch(
`${this.evaluationOptions.walletBalanceUrl}block/height/${effectiveHeight}/wallet/${address}/balance`
)
.then((res) => {
return res.ok ? res.text() : Promise.reject(res);
})
.catch((error) => {
throw new Error(`Unable to read wallet balance. ${error.status}. ${error.body?.message}`);
});
}
/**
* Returns a random but deterministic integer within the inclusive range [1, maxValue]
*/
randomNumber(maxValue: number): number {
if (!this._activeTx?.random) {
return this.vrf.randomInt(maxValue);
}
if (!Number.isInteger(maxValue)) {
throw new Error('Integer max value required for random integer generation');
}
if (maxValue < 1 || maxValue > Number.MAX_SAFE_INTEGER) {
throw new Error(`Integer max value must be in the range [1, ${Number.MAX_SAFE_INTEGER}]`);
}
const base64 = this._activeTx.random.replace(/-/g, '+').replace(/_/g, '/');
const array = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
const bigInt = Buffer.from(array).readBigUInt64BE();
const result = (bigInt % BigInt(maxValue)) + BigInt(1);
return Number(result);
}
}
// tslint:disable-next-line: max-classes-per-file
export class SWTransaction {
constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {}
get id() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.id;
}
get owner() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.owner.address;
}
get target() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.recipient;
}
get tags(): GQLTagInterface[] {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.tags;
}
get sortKey(): string {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.sortKey;
}
get dryRun(): boolean {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.dry === true;
}
get quantity() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.quantity.winston;
}
get reward() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.fee.winston;
}
get origin(): TransactionOrigin {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.source === 'redstone-sequencer' ? 'L2' : 'L1';
}
}
// tslint:disable-next-line: max-classes-per-file
export class SWBlock {
constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {}
get height() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.block.height;
}
get indep_hash() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.block.id;
}
get timestamp() {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current tx');
}
return this.smartWeaveGlobal._activeTx.block.timestamp;
}
}
/**
* @deprecated VRF mechanism will not be used after the decentralized sequencer is launched
*/
export class SWVrf {
constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {}
get data(): VrfData {
return this.smartWeaveGlobal._activeTx.vrf;
}
// returns the original generated random number as a BigInt string;
get value(): string {
return this.smartWeaveGlobal._activeTx.vrf.bigint;
}
/**
* @deprecated use SmartWeave.randomNumber function instead
*/
randomInt(maxValue: number): number {
if (this.smartWeaveGlobal._activeTx?.random) {
return this.smartWeaveGlobal.randomNumber(maxValue);
}
if (!Number.isInteger(maxValue)) {
throw new Error('Integer max value required for random integer generation');
}
const result = (BigInt(this.smartWeaveGlobal._activeTx.vrf.bigint) % BigInt(maxValue)) + BigInt(1);
if (result > Number.MAX_SAFE_INTEGER || result < Number.MIN_SAFE_INTEGER) {
throw new Error('Random int cannot be cast to number');
}
return Number(result);
}
}
export class KV {
constructor(
private readonly _storage: SortKeyCache<any> | null,
private readonly _interactionState: InteractionState,
private readonly _transaction: SWTransaction,
private readonly _contractTxId: string
) {}
async put(key: string, value: any): Promise<void> {
this.checkStorageAvailable();
await this._storage.put(new CacheKey(key, this._transaction.sortKey), value);
}
async get(key: string): Promise<unknown | null> {
this.checkStorageAvailable();
const sortKey = this._transaction.sortKey;
// then we're checking if the values exists in the interactionState
const interactionStateValue = await this._interactionState.getKV(
this._contractTxId,
new CacheKey(key, this._transaction.sortKey)
);
if (interactionStateValue != null) {
return interactionStateValue;
}
const result = await this._storage.getLessOrEqual(key, this._transaction.sortKey);
return result?.cachedValue ?? null;
}
async del(key: string): Promise<void> {
this.checkStorageAvailable();
const sortKey = this._transaction.sortKey;
// then we're checking if the values exists in the interactionState
const interactionStateValue = await this._interactionState.delKV(
this._contractTxId,
new CacheKey(key, this._transaction.sortKey)
);
if (interactionStateValue != null) {
return interactionStateValue;
}
await this._storage.del(new CacheKey(key, this._transaction.sortKey));
}
async keys(options?: SortKeyCacheRangeOptions): Promise<string[]> {
const sortKey = this._transaction.sortKey;
return await this._storage.keys(sortKey, options);
}
async kvMap<V>(options?: SortKeyCacheRangeOptions): Promise<Map<string, V>> {
const sortKey = this._transaction.sortKey;
return this._storage.kvMap(sortKey, options);
}
async begin() {
if (this._storage) {
return this._storage.begin();
}
}
async commit(): Promise<void> {
if (this._storage) {
if (this._transaction.dryRun) {
await this._storage.rollback();
} else {
await this._storage.commit();
}
}
}
async rollback(): Promise<void> {
if (this._storage) {
await this._storage.rollback();
}
}
open(): Promise<void> {
if (this._storage) {
return this._storage.open();
}
}
close(): Promise<void> {
if (this._storage) {
return this._storage.close();
}
}
private checkStorageAvailable() {
if (!this._storage) {
throw new Error('KV Storage not available');
}
}
}