diff --git a/src/api/warp.ts b/src/api/warp.ts index f8e92d8..24bc79b 100644 --- a/src/api/warp.ts +++ b/src/api/warp.ts @@ -14,19 +14,21 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { EvaluationManifest, EvaluationOptions, Warp } from 'warp-contracts'; +import { + EvalStateResult, + EvaluationManifest, + EvaluationOptions, + InteractionResult, + SortKeyCacheResult, + Warp, +} from 'warp-contracts'; import { DEFAULT_EVALUATION_OPTIONS, EVALUATION_TIMEOUT_MS, allowedContractTypes, } from '../constants'; import { ContractType, EvaluatedContractState } from '../types'; -import { - BadRequestError, - EvaluationError, - NotFoundError, - UnknownError, -} from '../errors'; +import { EvaluationError, NotFoundError, UnknownError } from '../errors'; import * as _ from 'lodash'; import { EvaluationTimeoutError } from '../errors'; import { createHash } from 'crypto'; @@ -37,7 +39,14 @@ import winston from 'winston'; import { ParsedUrlQuery } from 'querystring'; // cache duplicate requests on the same instance within a short period of time -const requestMap: Map | undefined> = new Map(); +const stateRequestMap: Map< + string, + Promise>> +> = new Map(); +const readRequestMap: Map< + string, + Promise> +> = new Map(); // Convenience class for read through caching class ContractStateCacheKey { @@ -112,7 +121,8 @@ class ContractReadInteractionCacheKey { const contractReadInteractionCache: ReadThroughPromiseCache< ContractReadInteractionCacheKey, { - result: any; + result: unknown; + input: unknown; evaluationOptions: Partial; } > = new ReadThroughPromiseCache({ @@ -154,7 +164,7 @@ async function readThroughToContractState( // Prevent multiple in-flight requests for the same contract state // This could be needed if the read through cache gets overwhelmed - const inFlightRequest = requestMap.get(cacheId); + const inFlightRequest = stateRequestMap.get(cacheId); if (inFlightRequest) { logger?.debug('Deduplicating in flight requests for contract state...', { contractTxId, @@ -180,7 +190,7 @@ async function readThroughToContractState( // set cached value for multiple requests during initial promise const readStatePromise = contract.readState(); - requestMap.set(cacheId, readStatePromise); + stateRequestMap.set(cacheId, readStatePromise); readStatePromise .catch((error: unknown) => { @@ -196,11 +206,20 @@ async function readThroughToContractState( cacheId, }); // remove the cached request whether it completes or fails - requestMap.delete(cacheId); + stateRequestMap.delete(cacheId); }); // await the response - const { cachedValue, sortKey } = await requestMap.get(cacheId); + const stateEvaluationResult = await stateRequestMap.get(cacheId); + if (!stateEvaluationResult) { + logger?.error('Contract state did not return a result!', { + contractTxId, + cacheKey: cacheKey.toString(), + }); + throw new UnknownError(`Unknown error occurred evaluating contract state.`); + } + + const { cachedValue, sortKey } = stateEvaluationResult; logger?.debug('Successfully evaluated contract state.', { contractTxId, cacheKey: cacheKey.toString(), @@ -285,8 +304,9 @@ export async function getContractState({ export async function readThroughToContractReadInteraction( cacheKey: ContractReadInteractionCacheKey, ): Promise<{ - result: any; + result: unknown; evaluationOptions: Partial; + input: unknown; }> { const { contractTxId, evaluationOptions, warp, logger, functionName, input } = cacheKey; @@ -298,7 +318,7 @@ export async function readThroughToContractReadInteraction( // Prevent multiple in-flight requests for the same contract state // This could be needed if the read through cache gets overwhelmed - const inFlightRequest = requestMap.get(cacheId); + const inFlightRequest = readRequestMap.get(cacheId); if (inFlightRequest) { logger?.debug('Deduplicating in flight requests for read interaction...', { contractTxId, @@ -307,6 +327,7 @@ export async function readThroughToContractReadInteraction( const { result } = await inFlightRequest; return { result, + input, evaluationOptions, }; } @@ -326,7 +347,7 @@ export async function readThroughToContractReadInteraction( function: functionName, ...input, }); - requestMap.set(cacheId, readInteractionPromise); + readRequestMap.set(cacheId, readInteractionPromise); readInteractionPromise .catch((error: unknown) => { @@ -342,24 +363,37 @@ export async function readThroughToContractReadInteraction( cacheId, }); // remove the cached request whether it completes or fails - requestMap.delete(cacheId); + readRequestMap.delete(cacheId); }); // await the response - const { result } = await requestMap.get(cacheId); + const readInteractionResult = await readRequestMap.get(cacheId); // we shouldn't return read interactions that don't have a result - if (!result) { + if (!readInteractionResult) { logger?.error('Read interaction did not return a result!', { contractTxId, cacheKey: cacheKey.toString(), input, }); - throw new BadRequestError( - `Invalid read interaction for contract ${contractTxId}.`, + throw new UnknownError( + `Failed to evaluate read interaction for ${contractTxId}.`, ); } + const { result, error, errorMessage } = readInteractionResult; + + if (error || errorMessage) { + logger?.error('Read interaction failed!', { + contractTxId, + cacheKey: cacheKey.toString(), + input, + error, + errorMessage, + }); + throw new EvaluationError(errorMessage); + } + logger?.debug('Successfully evaluated read interaction on contract.', { contractTxId, cacheKey: cacheKey.toString(), @@ -367,6 +401,7 @@ export async function readThroughToContractReadInteraction( return { result, + input, evaluationOptions, }; } diff --git a/src/middleware/warp.ts b/src/middleware/warp.ts index c139178..9ff49dd 100644 --- a/src/middleware/warp.ts +++ b/src/middleware/warp.ts @@ -41,7 +41,7 @@ const warp = WarpFactory.forMainnet( ).useStateCache( new LmdbCache(defaultCacheOptions, { maxEntriesPerContract: 1000, - minEntriesPerContract: 10, + minEntriesPerContract: 0, }), );