Files
warp/src/contract/HandlerBasedContract.ts
2023-01-23 15:46:57 +01:00

908 lines
31 KiB
TypeScript

import { TransactionStatusResponse } from 'arweave/node/transactions';
import stringify from 'safe-stable-stringify';
import * as crypto from 'crypto';
import { SortKeyCacheResult } from '../cache/SortKeyCache';
import { ContractCallRecord, InteractionCall } from '../core/ContractCallRecord';
import { ExecutionContext } from '../core/ExecutionContext';
import {
ContractInteraction,
HandlerApi,
InteractionData,
InteractionResult
} from '../core/modules/impl/HandlerExecutorFactory';
import { LexicographicalInteractionsSorter } from '../core/modules/impl/LexicographicalInteractionsSorter';
import { InteractionsSorter } from '../core/modules/InteractionsSorter';
import { DefaultEvaluationOptions, EvalStateResult, EvaluationOptions } from '../core/modules/StateEvaluator';
import { SmartWeaveTags } from '../core/SmartWeaveTags';
import { Warp } from '../core/Warp';
import { createDummyTx, createInteractionTx } from '../legacy/create-interaction-tx';
import { GQLNodeInterface } from '../legacy/gqlResult';
import { Benchmark } from '../logging/Benchmark';
import { LoggerFactory } from '../logging/LoggerFactory';
import { Evolve } from '../plugins/Evolve';
import { ArweaveWrapper } from '../utils/ArweaveWrapper';
import { sleep } from '../utils/utils';
import { BenchmarkStats, Contract, InnerCallData, WriteInteractionOptions, WriteInteractionResponse } from './Contract';
import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract';
import { InnerWritesEvaluator } from './InnerWritesEvaluator';
import { generateMockVrf } from '../utils/vrf';
import { Signature, CustomSignature } from './Signature';
import { ContractDefinition } from '../core/ContractDefinition';
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
import { Mutex } from 'async-mutex';
/**
* An implementation of {@link Contract} that is backwards compatible with current style
* of writing SW contracts (ie. using the "handle" function).
*
* It requires {@link ExecutorFactory} that is using {@link HandlerApi} generic type.
*/
export class HandlerBasedContract<State> implements Contract<State> {
private readonly logger = LoggerFactory.INST.create('HandlerBasedContract');
// TODO: refactor: extract execution context logic to a separate class
private readonly ecLogger = LoggerFactory.INST.create('ExecutionContext');
private _callStack: ContractCallRecord;
private _evaluationOptions: EvaluationOptions;
private _eoEvaluator: EvaluationOptionsEvaluator; // this is set after loading Contract Definition for the root contract
private readonly _innerWritesEvaluator = new InnerWritesEvaluator();
private readonly _callDepth: number;
private _benchmarkStats: BenchmarkStats = null;
private readonly _arweaveWrapper: ArweaveWrapper;
private _sorter: InteractionsSorter;
private _rootSortKey: string;
private signature: Signature;
private warpFetchWrapper: WarpFetchWrapper;
private _children: HandlerBasedContract<any>[] = [];
private _uncommittedStates = new Map<string, EvalStateResult<unknown>>();
private readonly mutex = new Mutex();
constructor(
private readonly _contractTxId: string,
protected readonly warp: Warp,
private readonly _parentContract: Contract<any> = null,
private readonly _innerCallData: InnerCallData = null
) {
this.waitForConfirmation = this.waitForConfirmation.bind(this);
this._arweaveWrapper = new ArweaveWrapper(warp.arweave);
this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
if (_parentContract != null) {
this._evaluationOptions = this.getRoot().evaluationOptions();
if (_parentContract.evaluationOptions().useKVStorage) {
throw new Error('Foreign writes or reads are forbidden for kv storage contracts');
}
this._callDepth = _parentContract.callDepth() + 1;
const callingInteraction: InteractionCall = _parentContract
.getCallStack()
.getInteraction(_innerCallData.callingInteraction.id);
if (this._callDepth > this._evaluationOptions.maxCallDepth) {
throw new Error(
`Max call depth of ${this._evaluationOptions.maxCallDepth} has been exceeded for interaction ${JSON.stringify(
callingInteraction.interactionInput
)}`
);
}
this.logger.debug('Calling interaction', {
id: _innerCallData.callingInteraction.id,
sortKey: _innerCallData.callingInteraction.sortKey,
type: _innerCallData.callType
});
// if you're reading a state of the contract, on which you've just made a write - you're doing it wrong.
// the current state of the callee contract is always in the result of an internal write.
// following is a protection against naughty developers who might be doing such crazy things ;-)
if (
callingInteraction.interactionInput?.foreignContractCalls[_contractTxId]?.innerCallType === 'write' &&
_innerCallData.callType === 'read'
) {
throw new Error(
'Calling a readContractState after performing an inner write is wrong - instead use a state from the result of an internal write.'
);
}
const callStack = new ContractCallRecord(_contractTxId, this._callDepth, _innerCallData?.callType);
callingInteraction.interactionInput.foreignContractCalls[_contractTxId] = callStack;
this._callStack = callStack;
this._rootSortKey = _parentContract.rootSortKey;
(_parentContract as HandlerBasedContract<unknown>)._children.push(this);
} else {
this._callDepth = 0;
this._callStack = new ContractCallRecord(_contractTxId, 0);
this._rootSortKey = null;
this._evaluationOptions = new DefaultEvaluationOptions();
this._children = [];
}
this.getCallStack = this.getCallStack.bind(this);
this.warpFetchWrapper = new WarpFetchWrapper(this.warp);
}
async readState(
sortKeyOrBlockHeight?: string | number,
caller?: string,
interactions?: GQLNodeInterface[]
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
this.logger.info('Read state for', {
contractTxId: this._contractTxId,
sortKeyOrBlockHeight
});
if (!this.isRoot() && sortKeyOrBlockHeight == null) {
throw new Error('SortKey MUST be always set for non-root contract calls');
}
const { stateEvaluator } = this.warp;
const sortKey =
typeof sortKeyOrBlockHeight == 'number'
? this._sorter.generateLastSortKey(sortKeyOrBlockHeight)
: sortKeyOrBlockHeight;
if (sortKey && !this.isRoot() && this.hasUncommittedState(this.txId())) {
const result = this.getUncommittedState(this.txId());
return {
sortKey,
cachedValue: result as EvalStateResult<State>
};
}
// TODO: not sure if we should synchronize on a contract instance or contractTxId
// in the latter case, the warp instance should keep a map contractTxId -> mutex
const releaseMutex = await this.mutex.acquire();
try {
const initBenchmark = Benchmark.measure();
this.maybeResetRootContract();
const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions);
this.logger.info('Execution Context', {
srcTxId: executionContext.contractDefinition?.srcTxId,
missingInteractions: executionContext.sortedInteractions?.length,
cachedSortKey: executionContext.cachedState?.sortKey
});
initBenchmark.stop();
const stateBenchmark = Benchmark.measure();
const result = await stateEvaluator.eval(executionContext);
stateBenchmark.stop();
const total = (initBenchmark.elapsed(true) as number) + (stateBenchmark.elapsed(true) as number);
this._benchmarkStats = {
gatewayCommunication: initBenchmark.elapsed(true) as number,
stateEvaluation: stateBenchmark.elapsed(true) as number,
total
};
this.logger.info('Benchmark', {
'Gateway communication ': initBenchmark.elapsed(),
'Contract evaluation ': stateBenchmark.elapsed(),
'Total: ': `${total.toFixed(0)}ms`
});
if (sortKey && !this.isRoot()) {
this.setUncommittedState(this.txId(), result.cachedValue);
}
return result;
} finally {
releaseMutex();
}
}
async readStateFor(
sortKey: string,
interactions: GQLNodeInterface[]
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
return this.readState(sortKey, undefined, interactions);
}
async viewState<Input, View>(
input: Input,
tags: Tags = [],
transfer: ArTransfer = emptyTransfer
): Promise<InteractionResult<State, View>> {
this.logger.info('View state for', this._contractTxId);
return await this.callContract<Input, View>(input, undefined, undefined, tags, transfer);
}
async viewStateForTx<Input, View>(
input: Input,
interactionTx: GQLNodeInterface
): Promise<InteractionResult<State, View>> {
this.logger.info(`View state for ${this._contractTxId}`, interactionTx);
return await this.doApplyInputOnTx<Input, View>(input, interactionTx);
}
async dryWrite<Input>(
input: Input,
caller?: string,
tags?: Tags,
transfer?: ArTransfer
): Promise<InteractionResult<State, unknown>> {
this.logger.info('Dry-write for', this._contractTxId);
return await this.callContract<Input>(input, caller, undefined, tags, transfer);
}
async applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`);
return await this.doApplyInputOnTx<Input>(input, transaction);
}
async writeInteraction<Input>(
input: Input,
options?: WriteInteractionOptions
): Promise<WriteInteractionResponse | null> {
this.logger.info('Write interaction', { input, options });
if (!this.signature) {
throw new Error("Wallet not connected. Use 'connect' method first.");
}
const { arweave, interactionsLoader, environment } = this.warp;
// we're calling this to verify whether proper env is used for this contract
// (e.g. test env for test contract)
await this.warp.definitionLoader.load(this._contractTxId);
const effectiveTags = options?.tags || [];
const effectiveTransfer = options?.transfer || emptyTransfer;
const effectiveStrict = options?.strict === true;
const effectiveVrf = options?.vrf === true;
const effectiveDisableBundling = options?.disableBundling === true;
const effectiveReward = options?.reward;
const bundleInteraction = interactionsLoader.type() == 'warp' && !effectiveDisableBundling;
this.signature.checkNonArweaveSigningAvailability(bundleInteraction);
if (
bundleInteraction &&
effectiveTransfer.target != emptyTransfer.target &&
effectiveTransfer.winstonQty != emptyTransfer.winstonQty
) {
throw new Error('Ar Transfers are not allowed for bundled interactions');
}
if (effectiveVrf && !bundleInteraction && environment === 'mainnet') {
throw new Error('Vrf generation is only available for bundle interaction');
}
if (bundleInteraction) {
return await this.bundleInteraction(input, {
tags: effectiveTags,
strict: effectiveStrict,
vrf: effectiveVrf
});
} else {
const interactionTx = await this.createInteraction(
input,
effectiveTags,
effectiveTransfer,
effectiveStrict,
false,
effectiveVrf && environment !== 'mainnet',
effectiveReward
);
const response = await arweave.transactions.post(interactionTx);
if (response.status !== 200) {
this.logger.error('Error while posting transaction', response);
return null;
}
if (this._evaluationOptions.waitForConfirmation) {
this.logger.info('Waiting for confirmation of', interactionTx.id);
const benchmark = Benchmark.measure();
await this.waitForConfirmation(interactionTx.id);
this.logger.info('Transaction confirmed after', benchmark.elapsed());
}
if (this.warp.environment == 'local' && this._evaluationOptions.mineArLocalBlocks) {
await this.warp.testing.mineBlock();
}
return { originalTxId: interactionTx.id };
}
}
private async bundleInteraction<Input>(
input: Input,
options: {
tags: Tags;
strict: boolean;
vrf: boolean;
}
): Promise<WriteInteractionResponse | null> {
this.logger.info('Bundle interaction input', input);
const interactionTx = await this.createInteraction(
input,
options.tags,
emptyTransfer,
options.strict,
true,
options.vrf
);
const response = await this.warpFetchWrapper
.fetch(`${this._evaluationOptions.sequencerUrl}gateway/sequencer/register`, {
method: 'POST',
body: JSON.stringify(interactionTx),
headers: {
'Accept-Encoding': 'gzip, deflate, br',
'Content-Type': 'application/json',
Accept: 'application/json'
}
})
.then((res) => {
this.logger.debug(res);
return res.ok ? res.json() : Promise.reject(res);
})
.catch((error) => {
this.logger.error(error);
if (error.body?.message) {
this.logger.error(error.body.message);
}
throw new Error(`Unable to bundle interaction: ${JSON.stringify(error)}`);
});
return {
bundlrResponse: response,
originalTxId: interactionTx.id
};
}
private async createInteraction<Input>(
input: Input,
tags: Tags,
transfer: ArTransfer,
strict: boolean,
bundle = false,
vrf = false,
reward?: string
) {
if (this._evaluationOptions.internalWrites) {
// Call contract and verify if there are any internal writes:
// 1. Evaluate current contract state
// 2. Apply input as "dry-run" transaction
// 3. Verify the callStack and search for any "internalWrites" transactions
// 4. For each found "internalWrite" transaction - generate additional tag:
// {name: 'InternalWrite', value: callingContractTxId}
const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer, strict, vrf);
if (strict && handlerResult.type !== 'ok') {
throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`);
}
const callStack: ContractCallRecord = this.getCallStack();
const innerWrites = this._innerWritesEvaluator.eval(callStack);
this.logger.debug('Input', input);
this.logger.debug('Callstack', callStack.print());
innerWrites.forEach((contractTxId) => {
tags.push({
name: SmartWeaveTags.INTERACT_WRITE,
value: contractTxId
});
});
this.logger.debug('Tags with inner calls', tags);
}
if (vrf) {
tags.push({
name: SmartWeaveTags.REQUEST_VRF,
value: 'true'
});
}
const interactionTx = await createInteractionTx(
this.warp.arweave,
this.signature.signer,
this._contractTxId,
input,
tags,
transfer.target,
transfer.winstonQty,
bundle,
this.warp.environment === 'testnet',
reward
);
if (!this._evaluationOptions.internalWrites && strict) {
const { arweave } = this.warp;
const caller =
this.signature.type == 'arweave'
? await arweave.wallets.ownerToAddress(interactionTx.owner)
: interactionTx.owner;
const handlerResult = await this.callContract(input, caller, undefined, tags, transfer, strict, vrf);
if (handlerResult.type !== 'ok') {
throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`);
}
}
return interactionTx;
}
txId(): string {
return this._contractTxId;
}
getCallStack(): ContractCallRecord {
return this._callStack;
}
connect(signature: ArWallet | CustomSignature): Contract<State> {
this.signature = new Signature(this.warp, signature);
return this;
}
setEvaluationOptions(options: Partial<EvaluationOptions>): Contract<State> {
if (!this.isRoot()) {
throw new Error('Evaluation options can be set only for the root contract');
}
this._evaluationOptions = {
...this._evaluationOptions,
...options
};
return this;
}
private async waitForConfirmation(transactionId: string): Promise<TransactionStatusResponse> {
const { arweave } = this.warp;
const status = await arweave.transactions.getStatus(transactionId);
if (status.confirmed === null) {
this.logger.info(`Transaction ${transactionId} not yet confirmed. Waiting another 20 seconds before next check.`);
await sleep(20000);
await this.waitForConfirmation(transactionId);
} else {
this.logger.info(`Transaction ${transactionId} confirmed`, status);
return status;
}
}
private async createExecutionContext(
contractTxId: string,
upToSortKey?: string,
forceDefinitionLoad = false,
interactions?: GQLNodeInterface[]
): Promise<ExecutionContext<State, HandlerApi<State>>> {
const { definitionLoader, interactionsLoader, stateEvaluator } = this.warp;
const benchmark = Benchmark.measure();
const cachedState = await stateEvaluator.latestAvailableState<State>(contractTxId, upToSortKey);
this.logger.debug('cache lookup', benchmark.elapsed());
benchmark.reset();
const evolvedSrcTxId = Evolve.evolvedSrcTxId(cachedState?.cachedValue?.state);
let handler, contractDefinition, sortedInteractions;
this.logger.debug('Cached state', cachedState, upToSortKey);
if (cachedState && cachedState.sortKey == upToSortKey) {
this.logger.debug('State fully cached, not loading interactions.');
if (forceDefinitionLoad || evolvedSrcTxId || interactions?.length) {
contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
if (interactions?.length) {
sortedInteractions = this._sorter.sort(interactions.map((i) => ({ node: i, cursor: null })));
}
}
} else {
// if we want to apply some 'external' interactions on top of the state cached at given sort key
// AND we don't have the state cached at the exact requested sort key - throw.
// NOTE: this feature is used by the D.R.E. nodes.
if (interactions?.length) {
throw new Error(`Cannot apply requested interactions at ${upToSortKey}`);
}
[contractDefinition, sortedInteractions] = await Promise.all([
definitionLoader.load<State>(contractTxId, evolvedSrcTxId),
interactions
? Promise.resolve(interactions)
: await interactionsLoader.load(
contractTxId,
cachedState?.sortKey,
// (1) we want to eagerly load dependant contract interactions and put them
// in the interactions' loader cache
// see: https://github.com/warp-contracts/warp/issues/198
this.getToSortKey(upToSortKey),
this._evaluationOptions
)
]);
// (2) ...but we still need to return only interactions up to original "upToSortKey"
if (cachedState?.sortKey) {
sortedInteractions = sortedInteractions.filter((i) => i.sortKey.localeCompare(cachedState?.sortKey) > 0);
}
if (upToSortKey) {
sortedInteractions = sortedInteractions.filter((i) => i.sortKey.localeCompare(upToSortKey) <= 0);
}
this.logger.debug('contract and interactions load', benchmark.elapsed());
if (this.isRoot() && sortedInteractions.length) {
// note: if the root contract has zero interactions, it still should be safe
// - as no other contracts will be called.
this._rootSortKey = sortedInteractions[sortedInteractions.length - 1].sortKey;
}
}
if (this.isRoot()) {
this._eoEvaluator = new EvaluationOptionsEvaluator(
this.evaluationOptions(),
contractDefinition.manifest?.evaluationOptions
);
}
const contractEvaluationOptions = this.isRoot()
? this._eoEvaluator.rootOptions
: this.getEoEvaluator().forForeignContract(contractDefinition.manifest?.evaluationOptions);
if (!this.isRoot() && contractEvaluationOptions.useKVStorage) {
throw new Error('Foreign read/writes cannot be performed on kv storage contracts');
}
this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);
if (contractDefinition) {
handler = (await this.warp.executorFactory.create(
contractDefinition,
contractEvaluationOptions,
this.warp
)) as HandlerApi<State>;
}
return {
warp: this.warp,
contract: this,
contractDefinition,
sortedInteractions,
evaluationOptions: contractEvaluationOptions,
handler,
cachedState,
requestedSortKey: upToSortKey
};
}
private getToSortKey(upToSortKey?: string) {
if (this._parentContract?.rootSortKey) {
if (!upToSortKey) {
return this._parentContract.rootSortKey;
}
return this._parentContract.rootSortKey.localeCompare(upToSortKey) > 0
? this._parentContract.rootSortKey
: upToSortKey;
} else {
return upToSortKey;
}
}
private async createExecutionContextFromTx(
contractTxId: string,
transaction: GQLNodeInterface
): Promise<ExecutionContext<State, HandlerApi<State>>> {
const caller = transaction.owner.address;
const sortKey = transaction.sortKey;
const baseContext = await this.createExecutionContext(contractTxId, sortKey, true);
return {
...baseContext,
caller
};
}
private maybeResetRootContract() {
if (this.isRoot()) {
this.logger.debug('Clearing call stack for the root contract');
this._callStack = new ContractCallRecord(this.txId(), 0);
this._rootSortKey = null;
this.warp.interactionsLoader.clearCache();
this._children = [];
this._uncommittedStates = new Map();
}
}
private async callContract<Input, View = unknown>(
input: Input,
caller?: string,
sortKey?: string,
tags: Tags = [],
transfer: ArTransfer = emptyTransfer,
strict = false,
vrf = false
): Promise<InteractionResult<State, View>> {
this.logger.info('Call contract input', input);
this.maybeResetRootContract();
if (!this.signature) {
this.logger.warn('Wallet not set.');
}
const { arweave, stateEvaluator } = this.warp;
// create execution context
let executionContext = await this.createExecutionContext(this._contractTxId, sortKey, true);
const currentBlockData =
this.warp.environment == 'mainnet' ? await this._arweaveWrapper.warpGwBlock() : await arweave.blocks.getCurrent();
// add caller info to execution context
let effectiveCaller;
if (caller) {
effectiveCaller = caller;
} else if (this.signature) {
// we're creating this transaction just to call the signing function on it
// - and retrieve the caller/owner
const dummyTx = await arweave.createTransaction({
data: Math.random().toString().slice(-4),
reward: '72600854',
last_tx: 'p7vc1iSP6bvH_fCeUFa9LqoV5qiyW-jdEKouAT0XMoSwrNraB9mgpi29Q10waEpO'
});
await this.signature.signer(dummyTx);
effectiveCaller = await arweave.wallets.ownerToAddress(dummyTx.owner);
} else {
effectiveCaller = '';
}
this.logger.info('effectiveCaller', effectiveCaller);
executionContext = {
...executionContext,
caller: effectiveCaller
};
// eval current state
const evalStateResult = await stateEvaluator.eval<State>(executionContext);
this.logger.info('Current state', evalStateResult.cachedValue.state);
// create interaction transaction
const interaction: ContractInteraction<Input> = {
input,
caller: executionContext.caller
};
this.logger.debug('interaction', interaction);
const tx = await createInteractionTx(
arweave,
this.signature?.signer,
this._contractTxId,
input,
tags,
transfer.target,
transfer.winstonQty,
true,
this.warp.environment === 'testnet'
);
const dummyTx = createDummyTx(tx, executionContext.caller, currentBlockData);
this.logger.debug('Creating sortKey for', {
blockId: dummyTx.block.id,
id: dummyTx.id,
height: dummyTx.block.height
});
dummyTx.sortKey = await this._sorter.createSortKey(dummyTx.block.id, dummyTx.id, dummyTx.block.height, true);
dummyTx.strict = strict;
if (vrf) {
dummyTx.vrf = generateMockVrf(dummyTx.sortKey, arweave);
}
const handleResult = await this.evalInteraction<Input, View>(
{
interaction,
interactionTx: dummyTx
},
executionContext,
evalStateResult.cachedValue
);
if (handleResult.type !== 'ok') {
this.logger.fatal('Error while interacting with contract', {
type: handleResult.type,
error: handleResult.errorMessage
});
}
return handleResult;
}
private async doApplyInputOnTx<Input, View = unknown>(
input: Input,
interactionTx: GQLNodeInterface
): Promise<InteractionResult<State, View>> {
this.maybeResetRootContract();
let evalStateResult: SortKeyCacheResult<EvalStateResult<State>>;
const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx);
if (!this.isRoot() && this.hasUncommittedState(this.txId())) {
evalStateResult = {
sortKey: interactionTx.sortKey,
cachedValue: this.getUncommittedState(this.txId()) as EvalStateResult<State>
};
} else {
evalStateResult = await this.warp.stateEvaluator.eval<State>(executionContext);
this.setUncommittedState(this.txId(), evalStateResult.cachedValue);
}
this.logger.debug('callContractForTx - evalStateResult', {
result: evalStateResult.cachedValue.state,
txId: this._contractTxId
});
const interaction: ContractInteraction<Input> = {
input,
caller: this._parentContract.txId()
};
const interactionData: InteractionData<Input> = {
interaction,
interactionTx
};
const result = await this.evalInteraction<Input, View>(
interactionData,
executionContext,
evalStateResult.cachedValue
);
result.originalValidity = evalStateResult.cachedValue.validity;
result.originalErrorMessages = evalStateResult.cachedValue.errorMessages;
return result;
}
private async evalInteraction<Input, View = unknown>(
interactionData: InteractionData<Input>,
executionContext: ExecutionContext<State, HandlerApi<State>>,
evalStateResult: EvalStateResult<State>
) {
const interactionCall: InteractionCall = this.getCallStack().addInteractionData(interactionData);
const benchmark = Benchmark.measure();
const result = await executionContext.handler.handle<Input, View>(
executionContext,
evalStateResult,
interactionData
);
interactionCall.update({
cacheHit: false,
outputState: this._evaluationOptions.stackTrace.saveState ? result.state : undefined,
executionTime: benchmark.elapsed(true) as number,
valid: result.type === 'ok',
errorMessage: result.errorMessage,
gasUsed: result.gasUsed
});
return result;
}
parent(): Contract | null {
return this._parentContract;
}
callDepth(): number {
return this._callDepth;
}
evaluationOptions(): EvaluationOptions {
return this._evaluationOptions;
}
lastReadStateStats(): BenchmarkStats {
return this._benchmarkStats;
}
stateHash(state: State): string {
const jsonState = stringify(state);
// note: cannot reuse:
// "The Hash object can not be used again after hash.digest() method has been called.
// Multiple calls will cause an error to be thrown."
const hash = crypto.createHash('sha256');
hash.update(jsonState);
return hash.digest('hex');
}
async syncState(externalUrl: string, params?: any): Promise<Contract> {
const { stateEvaluator } = this.warp;
const response = await this.warpFetchWrapper
.fetch(
`${externalUrl}?${new URLSearchParams({
id: this._contractTxId,
...params
})}`
)
.then((res) => {
return res.ok ? res.json() : Promise.reject(res);
})
.catch((error) => {
if (error.body?.message) {
this.logger.error(error.body.message);
}
throw new Error(`Unable to retrieve state. ${error.status}: ${error.body?.message}`);
});
await stateEvaluator.syncState(this._contractTxId, response.sortKey, response.state, response.validity);
return this;
}
async evolve(newSrcTxId: string, options?: WriteInteractionOptions): Promise<WriteInteractionResponse | null> {
return await this.writeInteraction<any>({ function: 'evolve', value: newSrcTxId }, options);
}
get rootSortKey(): string {
return this._rootSortKey;
}
getEoEvaluator(): EvaluationOptionsEvaluator {
const root = this.getRoot() as HandlerBasedContract<unknown>;
return root._eoEvaluator;
}
isRoot(): boolean {
return this._parentContract == null;
}
async getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, any>>> {
const lastCached = await this.warp.stateEvaluator.getCache().getLast(this.txId());
if (lastCached == null) {
return {
sortKey: null,
cachedValue: new Map()
};
}
const storage = this.warp.kvStorageFactory(this.txId());
const result: Map<string, any> = new Map();
try {
await storage.open();
for (const key of keys) {
const lastValue = await storage.getLessOrEqual(key, lastCached.sortKey);
result.set(key, lastValue == null ? null : lastValue.cachedValue);
}
return {
sortKey: lastCached.sortKey,
cachedValue: result
};
} finally {
await storage.close();
}
}
getUncommittedState(contractTxId: string): EvalStateResult<unknown> {
return this.getRoot()._uncommittedStates.get(contractTxId);
}
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void {
this.getRoot()._uncommittedStates.set(contractTxId, result);
}
hasUncommittedState(contractTxId: string): boolean {
return this.getRoot()._uncommittedStates.has(contractTxId);
}
resetUncommittedState(): void {
this.getRoot()._uncommittedStates = new Map();
}
async commitStates(interaction: GQLNodeInterface): Promise<void> {
const uncommittedStates = this.getRoot()._uncommittedStates;
try {
// i.e. if more than root contract state is in uncommitted state
// - without this check, we would effectively cache state for each evaluated interaction
// - which is not storage-effective
if (uncommittedStates.size > 1) {
for (const [k, v] of uncommittedStates) {
await this.warp.stateEvaluator.putInCache(k, interaction, v);
}
}
} finally {
this.resetUncommittedState();
}
}
private getRoot(): HandlerBasedContract<unknown> {
let result: Contract = this;
while (!result.isRoot()) {
result = result.parent();
}
return result as HandlerBasedContract<unknown>;
}
}