From 305a9e6150cabae7e6772fca314590da7374e083 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Wed, 29 Nov 2023 19:18:03 -0800 Subject: [PATCH] fix(grapqhl): update graphql to use sorter from warp This allows our interactions endpoint to sort interactions using sorts logic before we compare with their evaluation results. --- src/api/graphql.ts | 74 +++++++++++++++++++------------- src/api/warp.ts | 19 ++++++-- src/routes/contract.ts | 9 +--- src/types.ts | 2 +- tests/integration/routes.test.ts | 28 ++++++------ 5 files changed, 80 insertions(+), 52 deletions(-) diff --git a/src/api/graphql.ts b/src/api/graphql.ts index 3e70459..dba2553 100644 --- a/src/api/graphql.ts +++ b/src/api/graphql.ts @@ -16,7 +16,11 @@ */ import Arweave from 'arweave'; import { ArNSInteraction } from '../types.js'; -import { LexicographicalInteractionsSorter, TagsParser } from 'warp-contracts'; +import { + GQLResultInterface, + LexicographicalInteractionsSorter, + TagsParser, +} from 'warp-contracts'; import logger from '../logger'; export const MAX_REQUEST_SIZE = 100; @@ -102,8 +106,9 @@ export async function getDeployedContractsByWallet( export async function getWalletInteractionsForContract( arweave: Arweave, params: { - address: string | undefined; + address?: string; contractTxId: string; + sortKey: string | undefined; blockHeight: number | undefined; }, ): Promise<{ @@ -114,7 +119,12 @@ export async function getWalletInteractionsForContract( }> { const parser = new TagsParser(); const interactionSorter = new LexicographicalInteractionsSorter(arweave); - const { address, contractTxId, blockHeight } = params; + const { + address, + contractTxId, + blockHeight: blockHeightFilter, + sortKey: sortKeyFilter, + } = params; let hasNextPage = false; let cursor: string | undefined; const interactions = new Map< @@ -123,23 +133,21 @@ export async function getWalletInteractionsForContract( >(); do { const ownerFilter = address ? `owners: ["${address}"]` : ''; - const blockHeightFilter = - blockHeight !== null && blockHeight !== undefined - ? `block: { min: 0 max: ${blockHeight} }` - : ''; - const queryObject = { query: ` { transactions ( ${ownerFilter} - ${blockHeightFilter} tags:[ { name:"Contract", values:["${contractTxId}"] } ], + block: { + min: 0, + max: ${blockHeightFilter ?? null} + }, sort: HEIGHT_DESC, first: ${MAX_REQUEST_SIZE}, bundledIn: null, @@ -170,27 +178,37 @@ export async function getWalletInteractionsForContract( }`, }; - const { status, ...response } = await arweave.api.post( + const { status, data } = await arweave.api.post( '/graphql', queryObject, ); + if (status !== 200) { throw Error( `Failed to fetch contracts for wallet. Status code: ${status}`, ); } - if (!response.data.data?.transactions?.edges?.length) { + if (!data.data.transactions?.edges?.length) { continue; } - for (const e of response.data.data.transactions.edges) { + + console.log(JSON.stringify(data.data.transactions.edges, null, 2)); + + // remove interactions without block data + const validInteractions = data.data.transactions.edges.filter( + (i) => i.node.block && i.node.block.height && i.node.block.id, + ); + // sort them using warps sort logic and adds sort keys + const sortedInteractions = await interactionSorter.sort(validInteractions); + for (const i of sortedInteractions) { // basic validation for smartweave tags - const inputTag = parser.getInputTag(e.node, contractTxId); - const contractTag = parser.getContractTag(e.node); + const inputTag = parser.getInputTag(i.node, contractTxId); + const contractTag = parser.getContractTag(i.node); if (!inputTag || !contractTag) { logger.debug('Invalid tags for interaction via GQL, ignoring...', { contractTxId, - interactionId: e.node.id, + interactionId: i.node.id, inputTag, contractTag, }); @@ -199,24 +217,21 @@ export async function getWalletInteractionsForContract( const parsedInput = inputTag?.value ? JSON.parse(inputTag.value) : undefined; - const sortKey = await interactionSorter.createSortKey( - e.node.block.id, - e.node.id, - e.node.block.height, - ); - interactions.set(e.node.id, { - height: e.node.block.height, - timestamp: e.node.block.timestamp, - sortKey, + interactions.set(i.node.id, { + height: i.node.block.height, + timestamp: i.node.block.timestamp, input: parsedInput, - owner: e.node.owner.address, + owner: i.node.owner.address, + sortKey: i.node.sortKey, }); + // if we have a sort key filter, we can stop here + if (i.node.sortKey === sortKeyFilter) { + break; + } } cursor = - response.data.data.transactions.edges[MAX_REQUEST_SIZE - 1]?.cursor ?? - undefined; - hasNextPage = - response.data.data.transactions.pageInfo?.hasNextPage ?? false; + data.data.transactions.edges[MAX_REQUEST_SIZE - 1]?.cursor ?? undefined; + hasNextPage = data.data.transactions.pageInfo?.hasNextPage ?? false; } while (hasNextPage); return { interactions, @@ -229,6 +244,7 @@ export async function getContractsTransferredToOrControlledByWallet( ): Promise<{ ids: string[] }> { const { address } = params; let hasNextPage = false; + []; let cursor: string | undefined; const ids = new Set(); do { diff --git a/src/api/warp.ts b/src/api/warp.ts index 05f3ff6..4164e1f 100644 --- a/src/api/warp.ts +++ b/src/api/warp.ts @@ -60,9 +60,10 @@ class ContractStateCacheKey { ) {} toString(): string { - return `${this.contractTxId}-${createQueryParamHash( - this.evaluationOptions, - )}`; + return `${this.contractTxId}-${createSortKeyBlockHeightHash({ + sortKey: this.sortKey, + blockHeight: this.blockHeight, + })}-${createQueryParamHash(this.evaluationOptions)}`; } // Facilitate ReadThroughPromiseCache key derivation @@ -154,6 +155,18 @@ function createQueryParamHash(evalOptions: Partial): string { return hash.digest('hex'); } +function createSortKeyBlockHeightHash({ + sortKey, + blockHeight, +}: { + sortKey: string | undefined; + blockHeight: number | undefined; +}) { + const hash = createHash('sha256'); + hash.update(`${sortKey}-${blockHeight}`); + return hash.digest('hex'); +} + async function readThroughToContractState( cacheKey: ContractStateCacheKey, ): Promise { diff --git a/src/routes/contract.ts b/src/routes/contract.ts index 216cfb8..acf86c6 100644 --- a/src/routes/contract.ts +++ b/src/routes/contract.ts @@ -22,7 +22,7 @@ import { } from '../types'; import { getContractReadInteraction, getContractState } from '../api/warp'; import { getWalletInteractionsForContract } from '../api/graphql'; -import { BadRequestError, NotFoundError } from '../errors'; +import { NotFoundError } from '../errors'; import { mismatchedInteractionCount } from '../metrics'; export async function contractHandler(ctx: KoaContext) { @@ -54,12 +54,6 @@ export async function contractInteractionsHandler(ctx: KoaContext) { const { arweave, logger, warp, sortKey, blockHeight } = ctx.state; const { contractTxId, address } = ctx.params; - if (sortKey) { - throw new BadRequestError( - 'Sort key is not supported for contract interactions', - ); - } - logger.debug('Fetching all contract interactions', { contractTxId, }); @@ -75,6 +69,7 @@ export async function contractInteractionsHandler(ctx: KoaContext) { getWalletInteractionsForContract(arweave, { address, contractTxId, + sortKey, blockHeight, }), ]); diff --git a/src/types.ts b/src/types.ts index f8e0bda..51827f9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,7 +54,7 @@ export type ArNSInteraction = { input: PstInput | undefined; height: number; owner: string; - sortKey: string; + sortKey?: string; timestamp: number; errorMessage?: string; }; diff --git a/tests/integration/routes.test.ts b/tests/integration/routes.test.ts index edd313c..5655434 100644 --- a/tests/integration/routes.test.ts +++ b/tests/integration/routes.test.ts @@ -182,11 +182,13 @@ describe('Integration tests', () => { expect(status).to.equal(400); }); - it.only('should return contract state evaluated up to a given block height', async () => { - // block height before interactions - const blockHeight = 1; + it('should return contract state evaluated up to a given block height', async () => { + const { height: previousBlockHeight } = + await arweave.blocks.getCurrent(); + // mine a block height to ensure the contract is evaluated at previous one + await arweave.api.get('mine'); const { status, data } = await axios.get( - `/v1/contract/${id}?blockHeight=${blockHeight}`, + `/v1/contract/${id}?blockHeight=${previousBlockHeight}`, ); const { contractTxId, state, evaluationOptions, sortKey } = data; expect(status).to.equal(200); @@ -195,7 +197,7 @@ describe('Integration tests', () => { expect(state).not.to.be.undefined; expect(sortKey).not.be.undefined; expect(sortKey.split(',')[0]).to.equal( - `${blockHeight}`.padStart(12, '0'), + `${previousBlockHeight}`.padStart(12, '0'), ); }); }); @@ -268,14 +270,16 @@ describe('Integration tests', () => { expect(interactions).to.deep.equal([]); }); - it('should throw a bad request error when trying to use sortKey for interactions endpoint', async () => { - // block height before the interactions were created - const sortKey = 'test-sort-key'; - - const { status } = await axios.get( - `/v1/contract/${id}/interactions?sortKey=${sortKey}`, + it('should only return interactions up to a provided sort key height', async () => { + const knownSortKey = contractInteractions[0].sortKey; + const { status, data } = await axios.get( + `/v1/contract/${id}/interactions?sortKey=${knownSortKey}`, ); - expect(status).to.equal(400); + expect(status).to.equal(200); + expect(data).to.not.be.undefined; + const { contractTxId, interactions } = data; + expect(contractTxId).to.equal(id); + expect(interactions).to.deep.equal([contractInteractions[0]]); }); });