Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

feat(PE-5550): support sortKey and blockHeight for read interactions #105

Merged
merged 3 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GATEWAY_HOST=localhost
GATEWAY_HOST=arlocal
GATEWAY_PORT=1984
GATEWAY_PROTOCOL=http
LOG_LEVEL=debug
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 40 additions & 10 deletions src/api/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import {
DEFAULT_EVALUATION_OPTIONS,
DEFAULT_PAGES_PER_BATCH,
DEFAULT_STATE_EVALUATION_TIMEOUT_MS,
allowedContractTypes,
} from '../constants';
import { ContractType, EvaluatedContractState } from '../types';
Expand Down Expand Up @@ -107,16 +108,19 @@
constructor(
public readonly contractTxId: string,
public readonly functionName: string,
public readonly input: any,

Check warning on line 111 in src/api/warp.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
public readonly warp: Warp,
public readonly evaluationOptions: Partial<EvaluationOptions>,
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
Expand Down Expand Up @@ -309,8 +313,8 @@
return new NotFoundError(error.message);
} else if (
(error instanceof Error &&
(error as any).type &&

Check warning on line 316 in src/api/warp.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
['TX_NOT_FOUND', 'TX_INVALID'].includes((error as any).type)) ||

Check warning on line 317 in src/api/warp.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
(typeof error === 'string' && (error as string).includes('TX_INVALID'))
) {
return new NotFoundError(`Contract not found. ${error}`);
Expand Down Expand Up @@ -368,14 +372,23 @@
cacheKey: ContractReadInteractionCacheKey,
): Promise<{
result: unknown;
sortKey: string | undefined;
evaluationOptions: Partial<EvaluationOptions>;
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();

Expand All @@ -386,11 +399,13 @@
logger?.debug('Deduplicating in flight requests for read interaction...', {
contractTxId,
cacheKey: cacheKey.toString(),
sortKey,
});
const { result } = await inFlightRequest;
return {
result,
input,
sortKey,
evaluationOptions,
};
}
Expand All @@ -406,18 +421,25 @@
.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
.catch((error: unknown) => {
logger?.debug('Failed to evaluate read interaction on contract!', {
contractTxId,
cacheKey: cacheKey.toString(),
sortKey,
error: error instanceof Error ? error.message : 'Unknown error',
});
})
Expand All @@ -436,6 +458,7 @@
if (!readInteractionResult) {
logger?.error('Read interaction did not return a result!', {
contractTxId,
sortKey,
cacheKey: cacheKey.toString(),
input,
});
Expand All @@ -451,6 +474,7 @@
contractTxId,
cacheKey: cacheKey.toString(),
input,
sortKey,
error,
errorMessage,
});
Expand All @@ -459,12 +483,15 @@

logger?.debug('Successfully evaluated read interaction on contract.', {
contractTxId,
sortKey,

cacheKey: cacheKey.toString(),
});

return {
result,
input,
sortKey,
evaluationOptions,
};
}
Expand All @@ -475,14 +502,16 @@
logger,
functionName,
input,
sortKey,
}: {
contractTxId: string;
warp: Warp;
logger: winston.Logger;
functionName: string;
input: ParsedUrlQuery;
sortKey?: string | undefined;
}): Promise<{
result: any;

Check warning on line 514 in src/api/warp.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
evaluationOptions: Partial<EvaluationOptions>;
}> {
try {
Expand All @@ -498,6 +527,7 @@
input,
warp,
evaluationOptions,
sortKey,
logger,
),
);
Expand Down
28 changes: 27 additions & 1 deletion src/routes/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,8 @@

const associatedRecords = Object.entries(records).reduce(
(
filteredRecords: { [x: string]: any },

Check warning on line 476 in src/routes/contract.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
[record, recordObj]: [string, any],

Check warning on line 477 in src/routes/contract.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
) => {
if (
!filteredContractTxIdsArray.length ||
Expand Down Expand Up @@ -541,7 +541,12 @@

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;

Expand All @@ -550,8 +555,23 @@
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]) => {

Check warning on line 574 in src/routes/contract.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
// parse known integer values for parameters we care about
if (
queryParamsCastedToNumbers.includes(key) &&
Expand All @@ -561,6 +581,10 @@
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;
},
Expand All @@ -571,13 +595,15 @@
contractTxId,
warp,
logger,
sortKey: evaluateWithSortKey,
functionName,
input: parsedInput,
});

ctx.body = {
contractTxId,
result,
sortKey: evaluateWithSortKey,
evaluationOptions,
};
}
44 changes: 44 additions & 0 deletions tests/integration/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 19 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3040,10 +3040,20 @@ arweave-stream-tx@^1.1.0:
dependencies:
exponential-backoff "^3.1.0"

[email protected], 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==
[email protected], 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"
Expand Down Expand Up @@ -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"
Expand Down
Loading