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 implements Contract { 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[] = []; private _interactionState; private _dreStates = new Map>>(); 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)._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>> { 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>(sortKey, result as EvalStateResult); } // 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>> { return this.readState(sortKey, undefined, interactions); } async viewState( input: Input, tags: Tags = [], transfer: ArTransfer = emptyTransfer ): Promise> { this.logger.info('View state for', this._contractTxId); return await this.callContract(input, 'view', undefined, undefined, tags, transfer); } async viewStateForTx( input: Input, interactionTx: GQLNodeInterface ): Promise> { this.logger.info(`View state for ${this._contractTxId}`); return await this.doApplyInputOnTx(input, interactionTx, 'view'); } async dryWrite( input: Input, caller?: string, tags?: Tags, transfer?: ArTransfer, vrf?: boolean ): Promise> { this.logger.info('Dry-write for', this._contractTxId); return await this.callContract(input, 'write', caller, undefined, tags, transfer, undefined, vrf); } async applyInput(input: Input, transaction: GQLNodeInterface): Promise> { this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`); return await this.doApplyInputOnTx(input, transaction, 'write'); } async writeInteraction( input: Input, options?: WriteInteractionOptions ): Promise { 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, options: { tags: Tags; strict: boolean; vrf: boolean; } ): Promise { 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, tags: Tags, transfer: ArTransfer, strict: boolean, vrf = false ) { if (this._evaluationOptions.internalWrites) { // it modifies tags await this.discoverInternalWrites(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, tags: Tags, transfer: ArTransfer, strict: boolean, bundle = false, vrf = false, reward?: string ) { if (this._evaluationOptions.internalWrites) { // it modifies tags await this.discoverInternalWrites(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( 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 { this._signature = new Signature(this.warp, signature); return this; } setEvaluationOptions(options: Partial): Contract { 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 { 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>> { const { definitionLoader, interactionsLoader, stateEvaluator } = this.warp; const benchmark = Benchmark.measure(); let cachedState = await stateEvaluator.latestAvailableState(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(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(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(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; } 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>> { 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 | 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>> { 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: Input, interactionType: InteractionType, caller?: string, sortKey?: string, tags: Tags = [], transfer: ArTransfer = emptyTransfer, strict = false, vrf = false, sign = true ): Promise> { 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(executionContext); this.logger.info('Current state', evalStateResult.cachedValue.state); // create interaction transaction const interaction: ContractInteraction = { 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('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( { 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: Input, interactionTx: GQLNodeInterface, interactionType: InteractionType ): Promise> { this.maybeResetRootContract(); let evalStateResult: SortKeyCacheResult>; const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx); if (!this.isRoot() && this.interactionState().has(this.txId())) { evalStateResult = new SortKeyCacheResult>( interactionTx.sortKey, this.interactionState().get(this.txId()) as EvalStateResult ); } else { evalStateResult = await this.warp.stateEvaluator.eval(executionContext); this.interactionState().update(this.txId(), evalStateResult.cachedValue); } this.logger.debug('callContractForTx - evalStateResult', { result: evalStateResult.cachedValue.state, txId: this._contractTxId }); const interaction: ContractInteraction = { input, caller: this._parentContract.txId(), interactionType }; const interactionData: InteractionData = { interaction, interactionTx }; const result = await this.evalInteraction( interactionData, executionContext, evalStateResult.cachedValue ); result.originalValidity = evalStateResult.cachedValue.validity; result.originalErrorMessages = evalStateResult.cachedValue.errorMessages; return result; } private async evalInteraction( interactionData: InteractionData, executionContext: ExecutionContext>, evalStateResult: EvalStateResult ) { const interactionCall: InteractionCall = this.getCallStack().addInteractionData(interactionData); const benchmark = Benchmark.measure(); await executionContext.handler.initState(evalStateResult.state); const result = await executionContext.handler.handle( 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 { 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 { 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 { return await this.writeInteraction({ function: 'evolve', value: newSrcTxId }, options); } get rootSortKey(): string { return this._rootSortKey; } getRootEoEvaluator(): EvaluationOptionsEvaluator { const root = this.getRoot() as HandlerBasedContract; return root._eoEvaluator; } isRoot(): boolean { return this._parentContract == null; } async getStorageValues(keys: string[]): Promise>> { const lastCached = await this.warp.stateEvaluator.getCache().getLast(this.txId()); if (lastCached == null) { return new SortKeyCacheResult>(null, new Map()); } const storage = this.warp.kvStorageFactory(this.txId()); const result: Map = 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>(lastCached.sortKey, result); } finally { await storage.close(); } } interactionState(): InteractionState { return this.getRoot()._interactionState; } getRoot(): HandlerBasedContract { let result: Contract = this; while (!result.isRoot()) { result = result.parent(); } return result as HandlerBasedContract; } private async maybeSyncStateWithRemoteSource( remoteState: SortKeyCacheResult>, upToSortKey: string, cachedState: SortKeyCacheResult> ): Promise>> { const { stateEvaluator } = this.warp; if (this.isStateHigherThanAndUpTo(remoteState, cachedState?.sortKey, upToSortKey)) { return await stateEvaluator.syncState( this._contractTxId, remoteState.sortKey, remoteState.cachedValue.state, remoteState.cachedValue.validity ); } return cachedState; } private isStateHigherThanAndUpTo( remoteState: SortKeyCacheResult>, fromSortKey: string, upToSortKey: string ) { return ( remoteState && (!upToSortKey || upToSortKey >= remoteState.sortKey) && (!fromSortKey || remoteState.sortKey > fromSortKey) ); } setDREState( contractTxId: string, result: DREContractStatusResponse ): SortKeyCacheResult> { const dreCachedState = new SortKeyCacheResult( result.sortKey, new EvalStateResult(result.state, {}, result.errorMessages) ); this.getRoot()._dreStates.set(contractTxId, dreCachedState); return dreCachedState; } getDreState(contractTxId: string): SortKeyCacheResult> { return this.getRoot()._dreStates.get(contractTxId) as SortKeyCacheResult>; } 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, 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); } }