Files
warp/src/contract/HandlerBasedContract.ts
2023-06-28 16:03:27 +02:00

1048 lines
36 KiB
TypeScript

import stringify from 'safe-stable-stringify';
import { SortKeyCacheResult } from '../cache/SortKeyCache';
import { ContractCallRecord, InteractionCall } from '../core/ContractCallRecord';
import { ExecutionContext } from '../core/ExecutionContext';
import {
ContractInteraction,
HandlerApi,
InteractionData,
InteractionResult,
InteractionType
} 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 { WARP_TAGS } from '../core/KnownTags';
import { Warp } from '../core/Warp';
import { createDummyTx, createInteractionTagsList, 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 { getJsonResponse, isBrowser, sleep, stripTrailingSlash } from '../utils/utils';
import {
BenchmarkStats,
Contract,
DREContractStatusResponse,
InnerCallData,
WriteInteractionOptions,
WriteInteractionResponse
} from './Contract';
import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract';
import { InnerWritesEvaluator } from './InnerWritesEvaluator';
import { CustomSignature, Signature } from './Signature';
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
import { Mutex } from 'async-mutex';
import { Tag, TransactionStatusResponse } from '../utils/types/arweave-types';
import { InteractionState } from './states/InteractionState';
import { ContractInteractionState } from './states/ContractInteractionState';
import { Crypto } from 'warp-isomorphic';
import { VrfPluginFunctions } from '../core/WarpPlugin';
import Arweave from 'arweave';
import { createData, tagsExceedLimit, DataItem, Signer } from 'warp-arbundles';
/**
* 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 readonly _innerWritesEvaluator = new InnerWritesEvaluator();
private readonly _callDepth: number;
private readonly _arweaveWrapper: ArweaveWrapper;
private readonly _mutex = new Mutex();
private _callStack: ContractCallRecord;
private _evaluationOptions: EvaluationOptions;
private _eoEvaluator: EvaluationOptionsEvaluator; // this is set after loading Contract Definition for the root contract
private _benchmarkStats: BenchmarkStats = null;
private _sorter: InteractionsSorter;
private _rootSortKey: string;
private _signature: Signature;
private _warpFetchWrapper: WarpFetchWrapper;
private _children: HandlerBasedContract<unknown>[] = [];
private _interactionState;
private _dreStates = new Map<string, SortKeyCacheResult<EvalStateResult<State>>>();
constructor(
private readonly _contractTxId: string,
protected readonly warp: Warp,
private readonly _parentContract: Contract = null,
private readonly _innerCallData: InnerCallData = null
) {
this.waitForConfirmation = this.waitForConfirmation.bind(this);
this._arweaveWrapper = new ArweaveWrapper(warp);
this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
if (_parentContract != null) {
this._evaluationOptions = this.getRoot().evaluationOptions();
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._interactionState = new ContractInteractionState(warp);
}
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.interactionState().has(this.txId())) {
const result = this.interactionState().get(this.txId());
return new SortKeyCacheResult<EvalStateResult<State>>(sortKey, 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.interactionState().update(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, 'view', undefined, undefined, tags, transfer);
}
async viewStateForTx<Input, View>(
input: Input,
interactionTx: GQLNodeInterface
): Promise<InteractionResult<State, View>> {
this.logger.info(`View state for ${this._contractTxId}`);
return await this.doApplyInputOnTx<Input, View>(input, interactionTx, 'view');
}
async dryWrite<Input>(
input: Input,
caller?: string,
tags?: Tags,
transfer?: ArTransfer,
vrf?: boolean
): Promise<InteractionResult<State, unknown>> {
this.logger.info('Dry-write for', this._contractTxId);
return await this.callContract<Input>(input, 'write', caller, undefined, tags, transfer, undefined, vrf);
}
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, 'write');
}
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);
this._signature.checkBundlerSignerAvailability(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 (!input) {
throw new Error(`Input should be a truthy value: ${JSON.stringify(input)}`);
}
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 interactionDataItem = await this.createInteractionDataItem(
input,
options.tags,
emptyTransfer,
options.strict,
options.vrf
);
const response = this._warpFetchWrapper.fetch(
`${stripTrailingSlash(this._evaluationOptions.sequencerUrl)}/gateway/v2/sequencer/register`,
{
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
Accept: 'application/json'
},
body: interactionDataItem.getRaw()
}
);
const dataItemId = await interactionDataItem.id;
return {
bundlrResponse: await getJsonResponse(response),
originalTxId: dataItemId
};
}
private async createInteractionDataItem<Input>(
input: Input,
tags: Tags,
transfer: ArTransfer,
strict: boolean,
vrf = false
) {
if (this._evaluationOptions.internalWrites) {
// it modifies tags
await this.discoverInternalWrites<Input>(input, tags, transfer, strict, vrf);
}
if (vrf) {
tags.push(new Tag(WARP_TAGS.REQUEST_VRF, 'true'));
}
const interactionTags = createInteractionTagsList(
this._contractTxId,
input,
this.warp.environment === 'testnet',
tags
);
if (tagsExceedLimit(interactionTags)) {
throw new Error(`Interaction tags exceed limit of 4096 bytes.`);
}
const data = Math.random().toString().slice(-4);
const bundlerSigner = this._signature.bundlerSigner;
if (!bundlerSigner) {
throw new Error(
`Signer not set correctly. If you connect wallet through 'use_wallet', please remember that it only works when bundling is disabled.`
);
}
let interactionDataItem: DataItem;
if (isBrowser() && bundlerSigner.signer?.signDataItem) {
interactionDataItem = await bundlerSigner.signDataItem(data, interactionTags);
} else {
interactionDataItem = createData(data, bundlerSigner, { tags: interactionTags });
await interactionDataItem.sign(bundlerSigner);
}
// TODO: for ethereum owner is set to public key and not the address!!
if (!this._evaluationOptions.internalWrites && strict) {
await this.checkInteractionInStrictMode(interactionDataItem.owner, input, tags, transfer, strict, vrf);
}
return interactionDataItem;
}
private async createInteraction<Input>(
input: Input,
tags: Tags,
transfer: ArTransfer,
strict: boolean,
bundle = false,
vrf = false,
reward?: string
) {
if (this._evaluationOptions.internalWrites) {
// it modifies tags
await this.discoverInternalWrites<Input>(input, tags, transfer, strict, vrf);
}
if (vrf) {
tags.push(new Tag(WARP_TAGS.REQUEST_VRF, '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) {
await this.checkInteractionInStrictMode(interactionTx.owner, input, tags, transfer, strict, vrf);
}
return interactionTx;
}
private async checkInteractionInStrictMode<Input>(
owner: string,
input: Input,
tags: Tags,
transfer: ArTransfer,
strict: boolean,
vrf: boolean
) {
const { arweave } = this.warp;
const caller = this._signature.type == 'arweave' ? await arweave.wallets.ownerToAddress(owner) : owner;
const handlerResult = await this.callContract(input, 'write', caller, undefined, tags, transfer, strict, vrf);
if (handlerResult.type !== 'ok') {
throw Error('Cannot create interaction: ' + JSON.stringify(handlerResult.error || handlerResult.errorMessage));
}
}
txId(): string {
return this._contractTxId;
}
getCallStack(): ContractCallRecord {
return this._callStack;
}
connect(signature: ArWallet | CustomSignature | Signer): 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();
let 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, contractEvaluationOptions, remoteState;
let sortedInteractions = interactions || [];
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 = (await this._sorter.sort(interactions.map((i) => ({ node: i, cursor: null })))).map(
(i) => i.node
);
}
}
} 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 = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
contractEvaluationOptions = this.resolveEvaluationOptions(contractDefinition.manifest?.evaluationOptions);
if (contractEvaluationOptions.remoteStateSyncEnabled && !contractEvaluationOptions.useKVStorage) {
remoteState = await this.getRemoteContractState(contractTxId);
cachedState = await this.maybeSyncStateWithRemoteSource(remoteState, upToSortKey, cachedState);
const maybeEvolvedSrcTxId = Evolve.evolvedSrcTxId(cachedState?.cachedValue?.state);
if (maybeEvolvedSrcTxId && maybeEvolvedSrcTxId !== contractDefinition.srcTxId) {
// even though the state will be synced, the CacheableStateEvaluator will
// still try to init it in the WASM module (https://github.com/warp-contracts/warp/issues/372)
// if the state struct definition has changed via evolve - there is a risk of panic in Rust.
// that's why the contract definition has to be updated.
contractDefinition = await definitionLoader.load<State>(contractTxId, maybeEvolvedSrcTxId);
}
}
if (!remoteState && sortedInteractions.length == 0) {
sortedInteractions = await interactionsLoader.load(
contractTxId,
cachedState?.sortKey,
this.getToSortKey(upToSortKey),
contractEvaluationOptions
);
}
// 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 (contractDefinition) {
if (!contractEvaluationOptions) {
contractEvaluationOptions = this.resolveEvaluationOptions(contractDefinition.manifest?.evaluationOptions);
}
this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);
handler = (await this.warp.executorFactory.create(
contractDefinition,
contractEvaluationOptions,
this.warp,
this.interactionState()
)) as HandlerApi<State>;
}
return {
warp: this.warp,
contract: this,
contractDefinition,
sortedInteractions,
evaluationOptions: contractEvaluationOptions || this.evaluationOptions(),
handler,
cachedState,
requestedSortKey: upToSortKey
};
}
private resolveEvaluationOptions(rootManifestEvalOptions: EvaluationOptions) {
if (this.isRoot()) {
this._eoEvaluator = new EvaluationOptionsEvaluator(this.evaluationOptions(), rootManifestEvalOptions);
return this._eoEvaluator.rootOptions;
}
return this.getRootEoEvaluator().forForeignContract(rootManifestEvalOptions);
}
private async getRemoteContractState(contractId: string): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
if (this.hasDreState(contractId)) {
return this.getDreState(contractId);
} else {
const dreResponse = await this.fetchRemoteContractState(contractId);
if (dreResponse != null) {
return this.setDREState(contractId, dreResponse);
}
return null;
}
}
private async fetchRemoteContractState(contractId: string): Promise<DREContractStatusResponse<State> | null> {
return this._warpFetchWrapper
.fetch(`${this._evaluationOptions.remoteStateSyncSource}?id=${contractId}&events=false`)
.then((res) => {
return res.ok ? res.json() : Promise.reject(res);
})
.catch((error) => {
throw new Error(`Unable to read contract state from DRE. ${error.status}. ${error.body?.message}`);
});
}
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._interactionState = new ContractInteractionState(this.warp);
this._dreStates = new Map();
}
}
private async callContract<Input, View = unknown>(
input: Input,
interactionType: InteractionType,
caller?: string,
sortKey?: string,
tags: Tags = [],
transfer: ArTransfer = emptyTransfer,
strict = false,
vrf = false,
sign = true
): 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) {
effectiveCaller = await this._signature.getAddress();
} 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,
interactionType
};
this.logger.debug('interaction', interaction);
const tx = await createInteractionTx(
arweave,
sign ? this._signature?.signer : undefined,
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) {
Arweave.utils;
const vrfPlugin = this.warp.maybeLoadPlugin<void, VrfPluginFunctions>('vrf');
if (vrfPlugin) {
dummyTx.vrf = vrfPlugin.process().generateMockVrf(dummyTx.sortKey);
} else {
this.logger.warn('Cannot generate mock vrf for interaction - no "warp-contracts-plugin-vrf" attached!');
}
}
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,
interactionType: InteractionType
): Promise<InteractionResult<State, View>> {
this.maybeResetRootContract();
let evalStateResult: SortKeyCacheResult<EvalStateResult<State>>;
const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx);
if (!this.isRoot() && this.interactionState().has(this.txId())) {
evalStateResult = new SortKeyCacheResult<EvalStateResult<State>>(
interactionTx.sortKey,
this.interactionState().get(this.txId()) as EvalStateResult<State>
);
} else {
evalStateResult = await this.warp.stateEvaluator.eval<State>(executionContext);
this.interactionState().update(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(),
interactionType
};
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();
await executionContext.handler.initState(evalStateResult.state);
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;
}
async stateHash(state: State): Promise<string> {
const jsonState = stringify(state);
const hash = await Crypto.subtle.digest('SHA-256', Buffer.from(jsonState, 'utf-8'));
return Buffer.from(hash).toString('hex');
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- params can be anything
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<State>(this._contractTxId, response.sortKey, response.state, response.validity);
return this;
}
async evolve(newSrcTxId: string, options?: WriteInteractionOptions): Promise<WriteInteractionResponse | null> {
return await this.writeInteraction({ function: 'evolve', value: newSrcTxId }, options);
}
get rootSortKey(): string {
return this._rootSortKey;
}
getRootEoEvaluator(): 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, unknown>>> {
const lastCached = await this.warp.stateEvaluator.getCache().getLast(this.txId());
if (lastCached == null) {
return new SortKeyCacheResult<Map<string, unknown>>(null, new Map());
}
const storage = this.warp.kvStorageFactory(this.txId());
const result: Map<string, unknown> = 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 new SortKeyCacheResult<Map<string, unknown>>(lastCached.sortKey, result);
} finally {
await storage.close();
}
}
interactionState(): InteractionState {
return this.getRoot()._interactionState;
}
getRoot(): HandlerBasedContract<unknown> {
let result: Contract = this;
while (!result.isRoot()) {
result = result.parent();
}
return result as HandlerBasedContract<unknown>;
}
private async maybeSyncStateWithRemoteSource(
remoteState: SortKeyCacheResult<EvalStateResult<State>>,
upToSortKey: string,
cachedState: SortKeyCacheResult<EvalStateResult<State>>
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
const { stateEvaluator } = this.warp;
if (this.isStateHigherThanAndUpTo(remoteState, cachedState?.sortKey, upToSortKey)) {
return await stateEvaluator.syncState<State>(
this._contractTxId,
remoteState.sortKey,
remoteState.cachedValue.state,
remoteState.cachedValue.validity
);
}
return cachedState;
}
private isStateHigherThanAndUpTo(
remoteState: SortKeyCacheResult<EvalStateResult<State>>,
fromSortKey: string,
upToSortKey: string
) {
return (
remoteState &&
(!upToSortKey || upToSortKey >= remoteState.sortKey) &&
(!fromSortKey || remoteState.sortKey > fromSortKey)
);
}
setDREState(
contractTxId: string,
result: DREContractStatusResponse<State>
): SortKeyCacheResult<EvalStateResult<State>> {
const dreCachedState = new SortKeyCacheResult(
result.sortKey,
new EvalStateResult(result.state, {}, result.errorMessages)
);
this.getRoot()._dreStates.set(contractTxId, dreCachedState);
return dreCachedState;
}
getDreState(contractTxId: string): SortKeyCacheResult<EvalStateResult<State>> {
return this.getRoot()._dreStates.get(contractTxId) as SortKeyCacheResult<EvalStateResult<State>>;
}
hasDreState(contractTxId: string): boolean {
return this.getRoot()._dreStates.has(contractTxId);
}
// 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}
private async discoverInternalWrites<Input>(
input: Input,
tags: Tags,
transfer: ArTransfer,
strict: boolean,
vrf: boolean
) {
const handlerResult = await this.callContract(
input,
'write',
undefined,
undefined,
tags,
transfer,
strict,
vrf,
false
);
if (strict && handlerResult.type !== 'ok') {
throw Error('Cannot create interaction: ' + JSON.stringify(handlerResult.error || 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(new Tag(WARP_TAGS.INTERACT_WRITE, contractTxId));
});
this.logger.debug('Tags with inner calls', tags);
}
}