diff --git a/.env.test b/.env.test index 7be8736..3adfddc 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,4 @@ -GATEWAY_HOST=localhost +GATEWAY_HOST=arlocal GATEWAY_PORT=1984 GATEWAY_PROTOCOL=http LOG_LEVEL=debug diff --git a/package.json b/package.json index b23be48..3b6a248 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,14 @@ "@aws-sdk/client-s3": "^3.490.0", "@koa/cors": "^4.0.0", "@koa/router": "^12.0.0", - "arweave": "^1.13.7", + "arweave": "^1.14.4", "koa": "^2.14.2", "koa-bodyparser": "^4.4.0", "koa-router": "^12.0.0", "koa2-swagger-ui": "^5.10.0", "lodash": "^4.17.21", "prom-client": "^14.2.0", - "warp-contracts": "^1.4.32", + "warp-contracts": "^1.4.37", "warp-contracts-lmdb": "^1.1.10", "warp-contracts-sqlite": "^1.0.2", "winston": "^3.8.2", diff --git a/src/api/warp.ts b/src/api/warp.ts index 9f4bef4..9d1a6ae 100644 --- a/src/api/warp.ts +++ b/src/api/warp.ts @@ -25,6 +25,7 @@ import { import { DEFAULT_EVALUATION_OPTIONS, DEFAULT_PAGES_PER_BATCH, + DEFAULT_STATE_EVALUATION_TIMEOUT_MS, allowedContractTypes, } from '../constants'; import { ContractType, EvaluatedContractState } from '../types'; @@ -110,13 +111,16 @@ class ContractReadInteractionCacheKey { public readonly input: any, public readonly warp: Warp, public readonly evaluationOptions: Partial, + public readonly sortKey?: string, public readonly logger?: winston.Logger, ) {} toString(): string { - return `${this.contractTxId}-${this.functionName}-${createQueryParamHash( - this.input, - )}-${createQueryParamHash(this.evaluationOptions)}`; + return `${this.contractTxId}-${this.functionName}-${ + this.sortKey || 'latest' + }-${createQueryParamHash(this.input)}-${createQueryParamHash( + this.evaluationOptions, + )}`; } // Facilitate ReadThroughPromiseCache key derivation @@ -368,14 +372,23 @@ export async function readThroughToContractReadInteraction( cacheKey: ContractReadInteractionCacheKey, ): Promise<{ result: unknown; + sortKey: string | undefined; evaluationOptions: Partial; input: unknown; }> { - const { contractTxId, evaluationOptions, warp, logger, functionName, input } = - cacheKey; + const { + contractTxId, + evaluationOptions, + sortKey, + warp, + logger, + functionName, + input, + } = cacheKey; logger?.debug('Reading through to contract read interaction...', { contractTxId, cacheKey: cacheKey.toString(), + sortKey, }); const cacheId = cacheKey.toString(); @@ -386,11 +399,13 @@ export async function readThroughToContractReadInteraction( logger?.debug('Deduplicating in flight requests for read interaction...', { contractTxId, cacheKey: cacheKey.toString(), + sortKey, }); const { result } = await inFlightRequest; return { result, input, + sortKey, evaluationOptions, }; } @@ -406,11 +421,17 @@ export async function readThroughToContractReadInteraction( .setEvaluationOptions(evaluationOptions); // set cached value for multiple requests during initial promise - // TODO: add abort signal when view state supports it - const readInteractionPromise = contract.viewState({ - function: functionName, - ...input, - }); + const readInteractionPromise = contract.viewState( + { + function: functionName, + ...input, + }, + undefined, // tags + undefined, // transfer + undefined, // caller + AbortSignal.timeout(DEFAULT_STATE_EVALUATION_TIMEOUT_MS), + sortKey, + ); readRequestMap.set(cacheId, readInteractionPromise); readInteractionPromise @@ -418,6 +439,7 @@ export async function readThroughToContractReadInteraction( logger?.debug('Failed to evaluate read interaction on contract!', { contractTxId, cacheKey: cacheKey.toString(), + sortKey, error: error instanceof Error ? error.message : 'Unknown error', }); }) @@ -436,6 +458,7 @@ export async function readThroughToContractReadInteraction( if (!readInteractionResult) { logger?.error('Read interaction did not return a result!', { contractTxId, + sortKey, cacheKey: cacheKey.toString(), input, }); @@ -451,6 +474,7 @@ export async function readThroughToContractReadInteraction( contractTxId, cacheKey: cacheKey.toString(), input, + sortKey, error, errorMessage, }); @@ -459,12 +483,15 @@ export async function readThroughToContractReadInteraction( logger?.debug('Successfully evaluated read interaction on contract.', { contractTxId, + sortKey, + cacheKey: cacheKey.toString(), }); return { result, input, + sortKey, evaluationOptions, }; } @@ -475,12 +502,14 @@ export async function getContractReadInteraction({ logger, functionName, input, + sortKey, }: { contractTxId: string; warp: Warp; logger: winston.Logger; functionName: string; input: ParsedUrlQuery; + sortKey?: string | undefined; }): Promise<{ result: any; evaluationOptions: Partial; @@ -498,6 +527,7 @@ export async function getContractReadInteraction({ input, warp, evaluationOptions, + sortKey, logger, ), ); diff --git a/src/routes/contract.ts b/src/routes/contract.ts index f3c8edf..c16f16e 100644 --- a/src/routes/contract.ts +++ b/src/routes/contract.ts @@ -541,7 +541,12 @@ export async function contractReservedHandler(ctx: KoaContext) { const queryParamsCastedToNumbers = ['qty', 'years', 'height']; export async function contractReadInteractionHandler(ctx: KoaContext) { - const { warp, logger: _logger } = ctx.state; + const { + warp, + logger: _logger, + sortKey: requestedSortKey, + blockHeight: requestedBlockHeight, + } = ctx.state; const { contractTxId, functionName } = ctx.params; const { query: input } = ctx.request; @@ -550,6 +555,21 @@ export async function contractReadInteractionHandler(ctx: KoaContext) { functionName, }); + let evaluateWithSortKey = requestedSortKey; + if (!requestedSortKey && requestedBlockHeight) { + const { sortKey } = await getContractState({ + contractTxId, + warp, + logger, + blockHeight: requestedBlockHeight, + }); + logger.info('Using sort key from block height', { + blockHeight: requestedBlockHeight, + sortKey, + }); + evaluateWithSortKey = sortKey; + } + const parsedInput = Object.entries(input).reduce( (parsedParams: { [x: string]: any }, [key, value]) => { // parse known integer values for parameters we care about @@ -561,6 +581,10 @@ export async function contractReadInteractionHandler(ctx: KoaContext) { parsedParams[key] = +value; return parsedParams; } + // exclude sortKey and blockHeight from input as they are used to evaluate the contract state + if (key === 'sortKey' || key === 'blockHeight') { + return parsedParams; + } parsedParams[key] = value; return parsedParams; }, @@ -571,6 +595,7 @@ export async function contractReadInteractionHandler(ctx: KoaContext) { contractTxId, warp, logger, + sortKey: evaluateWithSortKey, functionName, input: parsedInput, }); @@ -578,6 +603,7 @@ export async function contractReadInteractionHandler(ctx: KoaContext) { ctx.body = { contractTxId, result, + sortKey: evaluateWithSortKey, evaluationOptions, }; } diff --git a/tests/integration/routes.test.ts b/tests/integration/routes.test.ts index 4c955e3..668f01f 100644 --- a/tests/integration/routes.test.ts +++ b/tests/integration/routes.test.ts @@ -649,6 +649,50 @@ describe('Integration tests', () => { }); }); + describe('/:contractTxId/read/:readInteraction', () => { + it('should return the read interaction for the provided contract and read interaction id', async () => { + const { status, data } = await axios.get( + `/v1/contract/${id}/read/priceForInteraction`, + ); + expect(status).to.equal(200); + expect(data).to.not.be.undefined; + const { contractTxId, result } = data; + expect(contractTxId).to.equal(id); + expect(result).not.to.be.undefined; + }); + + it('should return a 400 for an invalid read interaction id', async () => { + const { status } = await axios.get( + `/v1/contract/${id}/read/non-existent-read-api`, + ); + expect(status).to.equal(400); + }); + + it('should properly evaluate state for a read interaction at a provided sortKey', async () => { + const { status, data } = await axios.get( + `/v1/contract/${id}/read/priceForInteraction?sortKey=${contractInteractions[0].sortKey}`, + ); + expect(status).to.equal(200); + expect(data).to.not.be.undefined; + const { contractTxId, sortKey, result } = data; + expect(contractTxId).to.equal(id); + expect(result).not.to.be.undefined; + expect(sortKey).to.equal(contractInteractions[0].sortKey); + }); + + it('should properly evaluate state for a read interaction at a provided block height', async () => { + const { status, data } = await axios.get( + `/v1/contract/${id}/read/priceForInteraction?blockHeight=${contractInteractions[0].height}`, + ); + expect(status).to.equal(200); + expect(data).to.not.be.undefined; + const { contractTxId, sortKey, result } = data; + expect(contractTxId).to.equal(id); + expect(result).not.to.be.undefined; + expect(sortKey).to.equal(contractInteractions[0].sortKey); + }); + }); + describe('/:contractTxId/state/:nestedPath', () => { for (const nestedPath of [ 'owner', diff --git a/yarn.lock b/yarn.lock index 418f3f8..2b6517b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3040,10 +3040,20 @@ arweave-stream-tx@^1.1.0: dependencies: exponential-backoff "^3.1.0" -arweave@1.13.7, arweave@^1.10.13, arweave@^1.10.5, arweave@^1.11.4, arweave@^1.13.7: - version "1.13.7" - resolved "https://registry.yarnpkg.com/arweave/-/arweave-1.13.7.tgz#cda8534c833baec372a7052c61f53b4e39a886d7" - integrity sha512-Hv+x2bSI6UyBHpuVbUDMMpMje1ETfpJWj52kKfz44O0IqDRi/LukOkkDUptup1p6OT6KP1/DdpnUnsNHoskFeA== +arweave@1.14.4, arweave@^1.14.4: + version "1.14.4" + resolved "https://registry.yarnpkg.com/arweave/-/arweave-1.14.4.tgz#5ba22136aa0e7fd9495258a3931fb770c9d6bf21" + integrity sha512-tmqU9fug8XAmFETYwgUhLaD3WKav5DaM4p1vgJpEj/Px2ORPPMikwnSySlFymmL2qgRh2ZBcZsg11+RXPPGLsA== + dependencies: + arconnect "^0.4.2" + asn1.js "^5.4.1" + base64-js "^1.5.1" + bignumber.js "^9.0.2" + +arweave@^1.10.13, arweave@^1.10.5, arweave@^1.11.4, arweave@^1.13.7: + version "1.14.0" + resolved "https://registry.yarnpkg.com/arweave/-/arweave-1.14.0.tgz#a4424455a4137b7708cdc627b5bda1881d6881b5" + integrity sha512-P2g9FjbJZQfk0Q3a5R2aCyPP3jen3ZN6Oxh6p6BlwEGHn5M5O0KvZSaiNV4X/PENgnZA4+afOf9MR3ySGcR3JQ== dependencies: arconnect "^0.4.2" asn1.js "^5.4.1" @@ -8223,13 +8233,13 @@ warp-contracts-sqlite@^1.0.2: better-sqlite3 "^8.3.0" safe-stable-stringify "^2.4.3" -warp-contracts@^1.4.32: - version "1.4.32" - resolved "https://registry.yarnpkg.com/warp-contracts/-/warp-contracts-1.4.32.tgz#3d5c1ce973dde9d481618b1e060fac2de2870049" - integrity sha512-33OGr5xmarXFAfdmnJvAOrFLIT/Z6mp5BJEXw3dfaffM7mOGDsegWMDxNRoqWC2LFQFvvqnmuxq/FhEC6QRNgg== +warp-contracts@^1.4.37: + version "1.4.37" + resolved "https://registry.yarnpkg.com/warp-contracts/-/warp-contracts-1.4.37.tgz#d22d34750bb50638188ca40d53015207688d1f58" + integrity sha512-jDQQF4FqRCy6OAnsb87d9LMzVp18eRTFyDQp9QirLD2C7IwBAH2DVrfpVcyPeZuxw4bNz3CSVemdMS+oFJIldQ== dependencies: archiver "^5.3.0" - arweave "1.13.7" + arweave "1.14.4" async-mutex "^0.4.0" bignumber.js "9.1.1" events "3.3.0"