From 8abdd8ac22f3598d38447d99427c8f444b16bce4 Mon Sep 17 00:00:00 2001 From: rodrigopavezi Date: Wed, 4 Dec 2024 09:24:38 -0300 Subject: [PATCH 1/4] refactor: remove node-localstorage dependency and update storage handling in LitProvider - Removed node-localstorage from package.json and yarn.lock. - Updated LitProvider to use a new StorageProvider interface. - Refactored storage handling logic to improve compatibility with Node.js. - Enhanced the RequestNetwork class to include getter and setter for cipherProvider. - Modified GraphQL queries to improve transaction retrieval logic and pagination settings. This change streamlines storage management and enhances the overall architecture of the project. --- packages/lit-protocol-cipher/package.json | 4 +-- .../src/lit-protocol-cipher-provider.ts | 33 ++++++------------- .../src/api/request-network.ts | 19 +++++++++++ packages/thegraph-data-access/src/queries.ts | 15 ++++----- .../src/subgraph-client.ts | 29 ++++++++-------- yarn.lock | 16 +-------- 6 files changed, 50 insertions(+), 66 deletions(-) diff --git a/packages/lit-protocol-cipher/package.json b/packages/lit-protocol-cipher/package.json index 24a55d220..8eda1d014 100644 --- a/packages/lit-protocol-cipher/package.json +++ b/packages/lit-protocol-cipher/package.json @@ -50,12 +50,10 @@ "@requestnetwork/request-client.js": "0.51.0", "@requestnetwork/types": "0.46.0", "@walletconnect/modal": "2.7.0", - "ethers": "5.7.2", - "node-localstorage": "3.0.5" + "ethers": "5.7.2" }, "devDependencies": { "@types/node": "18.11.9", - "@types/node-localstorage": "1.3.3", "jest-junit": "16.0.0", "ts-node": "10.9.1", "typescript": "5.1.3" diff --git a/packages/lit-protocol-cipher/src/lit-protocol-cipher-provider.ts b/packages/lit-protocol-cipher/src/lit-protocol-cipher-provider.ts index d0e980af4..ceaf65006 100644 --- a/packages/lit-protocol-cipher/src/lit-protocol-cipher-provider.ts +++ b/packages/lit-protocol-cipher/src/lit-protocol-cipher-provider.ts @@ -8,6 +8,7 @@ import { AuthSig, LIT_NETWORKS_KEYS, AuthCallbackParams, + StorageProvider, } from '@lit-protocol/types'; import { LitAccessControlConditionResource, @@ -16,7 +17,8 @@ import { } from '@lit-protocol/auth-helpers'; import { Signer } from 'ethers'; import { LIT_ABILITY } from '@lit-protocol/constants'; -import { disconnectWeb3, LitNodeClient, LitNodeClientNodeJs } from '@lit-protocol/lit-node-client'; +import { disconnectWeb3, LitNodeClient } from '@lit-protocol/lit-node-client'; +import type { LitNodeClientNodeJs } from '@lit-protocol/lit-node-client'; /** * @class LitProvider @@ -53,7 +55,7 @@ export default class LitProvider implements CipherProviderTypes.ICipherProvider /** * @property {any} storageProvider - The storage provider for the Node.js Lit client. */ - private storageProvider: any | null = null; + private nodeJsStorageProvider: StorageProvider | undefined; /** * @property {boolean} debug - A boolean indicating if debug mode is enabled. @@ -75,11 +77,13 @@ export default class LitProvider implements CipherProviderTypes.ICipherProvider network: LIT_NETWORKS_KEYS, nodeConnectionConfig: NodeConnectionConfig, debug?: boolean, + nodeJsStorageProvider?: StorageProvider, ) { this.chain = chain; this.network = network; this.dataAccess = new HttpDataAccess({ nodeConnectionConfig }); this.debug = debug || false; + this.nodeJsStorageProvider = nodeJsStorageProvider; } /** @@ -98,27 +102,10 @@ export default class LitProvider implements CipherProviderTypes.ICipherProvider }); await this.client.connect(); } else { - // Evaluate the code in a way that prevents static analysis - const getNodeStorage = new Function( - ` - return import('node-localstorage').then(m => m.LocalStorage); - `, - ); - - const LocalStorage = await getNodeStorage(); - const localStorage = new LocalStorage('./request-network-lit-protocol-cipher'); - - this.storageProvider = { - getItem: (key: string) => localStorage.getItem(key), - setItem: (key: string, value: string) => localStorage.setItem(key, value), - removeItem: (key: string) => localStorage.removeItem(key), - clear: () => localStorage.clear(), - provider: localStorage, - }; - + const { LitNodeClientNodeJs } = await import('@lit-protocol/lit-node-client'); this.client = new LitNodeClientNodeJs({ litNetwork: this.network, - storageProvider: this.storageProvider, + storageProvider: this.nodeJsStorageProvider, debug: this.debug, }); @@ -139,8 +126,8 @@ export default class LitProvider implements CipherProviderTypes.ICipherProvider disconnectWeb3(); } this.sessionSigs = null; - if (this.storageProvider) { - this.storageProvider.clear(); + if (this.nodeJsStorageProvider) { + this.nodeJsStorageProvider.provider.clear(); } } diff --git a/packages/request-client.js/src/api/request-network.ts b/packages/request-client.js/src/api/request-network.ts index 05784a37c..d91f40b60 100644 --- a/packages/request-client.js/src/api/request-network.ts +++ b/packages/request-client.js/src/api/request-network.ts @@ -40,6 +40,8 @@ export default class RequestNetwork { private contentData: ContentDataExtension; private currencyManager: CurrencyTypes.ICurrencyManager; + private cipherProvider?: CipherProviderTypes.ICipherProvider; + /** * @param dataAccess instance of data-access layer * @param signatureProvider module in charge of the signatures @@ -72,6 +74,23 @@ export default class RequestNetwork { this.currencyManager, paymentOptions, ); + this.cipherProvider = cipherProvider; + } + + /** + * Get the cipher provider + * @returns the cipher provider + */ + public getCipherProvider(): CipherProviderTypes.ICipherProvider | undefined { + return this.cipherProvider; + } + + /** + * Set the cipher provider + * @param cipherProvider the cipher provider + */ + public setCipherProvider(cipherProvider: CipherProviderTypes.ICipherProvider): void { + this.cipherProvider = cipherProvider; } /** diff --git a/packages/thegraph-data-access/src/queries.ts b/packages/thegraph-data-access/src/queries.ts index e25e102c9..b4543a5ac 100644 --- a/packages/thegraph-data-access/src/queries.ts +++ b/packages/thegraph-data-access/src/queries.ts @@ -79,17 +79,14 @@ ${TransactionsBodyFragment} query GetTransactionsByTopics($topics: [String!]!, $first: Int!, $skip: Int!) { ${metaQueryBody} - channels( - where: { topics_contains: $topics } + transactions( + where: { topics_contains_nocase: $topics } first: $first skip: $skip - ){ - transactions( - orderBy: blockTimestamp, - orderDirection: asc - ) { - ...TransactionsBody - } + orderBy: blockTimestamp + orderDirection: asc + ) { + ...TransactionsBody } }`; diff --git a/packages/thegraph-data-access/src/subgraph-client.ts b/packages/thegraph-data-access/src/subgraph-client.ts index cae64db29..aa89d0e75 100644 --- a/packages/thegraph-data-access/src/subgraph-client.ts +++ b/packages/thegraph-data-access/src/subgraph-client.ts @@ -19,8 +19,8 @@ export class SubgraphClient implements StorageTypes.IIndexer { private graphql: GraphQLClient; public readonly endpoint: string; - private readonly DEFAULT_PAGE_SIZE = 10; - private readonly MAX_PAGE_SIZE = 100; + private readonly DEFAULT_PAGE_SIZE = 100; + private readonly MAX_PAGE_SIZE = 1000; constructor(endpoint: string, options?: RequestConfig) { this.endpoint = endpoint; @@ -71,28 +71,25 @@ export class SubgraphClient implements StorageTypes.IIndexer { const effectivePage = page ?? 1; const skip = (effectivePage - 1) * effectivePageSize; - const { _meta, channels } = await this.graphql.request< - Meta & { channels: { transactions: Transaction[] }[] } - >(GetTransactionsByTopics, { - topics, - first: effectivePageSize, - skip, - }); + const response = await this.graphql.request( + GetTransactionsByTopics, + { + topics, + first: effectivePageSize, + skip, + }, + ); - const transactionsByChannel = channels - .map(({ transactions }) => transactions) - .flat() - .sort((a, b) => a.blockTimestamp - b.blockTimestamp); + const indexedTransactions = response.transactions.map(this.toIndexedTransaction); - const indexedTransactions = transactionsByChannel.map(this.toIndexedTransaction); return { transactions: indexedTransactions, - blockNumber: _meta.block.number, + blockNumber: response._meta.block.number, pagination: { page: effectivePage, pageSize: effectivePageSize, total: indexedTransactions.length, - hasMore: skip + effectivePageSize < indexedTransactions.length, + hasMore: indexedTransactions.length === effectivePageSize, }, }; } diff --git a/yarn.lock b/yarn.lock index 9754fbc0a..8744c4dcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5921,13 +5921,6 @@ "@types/node" "*" form-data "^4.0.0" -"@types/node-localstorage@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/node-localstorage/-/node-localstorage-1.3.3.tgz#b221f1bd6c61a2cc6b16c9934f2110779568f185" - integrity sha512-Wkn5g4eM5x10UNV9Xvl9K6y6m0zorocuJy4WjB5muUdyMZuPbZpSJG3hlhjGHe1HGxbOQO7RcB+jlHcNwkh+Jw== - dependencies: - "@types/node" "*" - "@types/node@*": version "14.14.35" resolved "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz" @@ -18615,13 +18608,6 @@ node-int64@^0.4.0: resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-localstorage@3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/node-localstorage/-/node-localstorage-3.0.5.tgz#4acda05bb7d3fffaa477f13c028d105866bb43ad" - integrity sha512-GCwtK33iwVXboZWYcqQHu3aRvXEBwmPkAMRBLeaX86ufhqslyUkLGsi4aW3INEfdQYpUB5M9qtYf3eHvAk2VBg== - dependencies: - write-file-atomic "^5.0.1" - node-releases@^2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" @@ -25866,7 +25852,7 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -write-file-atomic@^5.0.0, write-file-atomic@^5.0.1: +write-file-atomic@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== From 455b28c8a323298a51ac9c54b6a86c4006309513 Mon Sep 17 00:00:00 2001 From: rodrigopavezi Date: Wed, 4 Dec 2024 10:57:39 -0300 Subject: [PATCH 2/4] fix: test --- packages/request-node/test/getTransactionsByChannelId.test.ts | 2 +- packages/request-node/test/persistTransaction.test.ts | 2 ++ packages/thegraph-data-access/src/queries.ts | 2 +- packages/thegraph-data-access/src/subgraph-client.ts | 4 +++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/request-node/test/getTransactionsByChannelId.test.ts b/packages/request-node/test/getTransactionsByChannelId.test.ts index 06f10cda3..93f72b012 100644 --- a/packages/request-node/test/getTransactionsByChannelId.test.ts +++ b/packages/request-node/test/getTransactionsByChannelId.test.ts @@ -3,7 +3,7 @@ import request from 'supertest'; import { getRequestNode } from '../src/server'; import { RequestNode } from '../src/requestNode'; -jest.setTimeout(20000); +jest.setTimeout(30000); const time = Date.now(); const channelId = `01aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa${time}`; const anotherChannelId = `01bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb${time}`; diff --git a/packages/request-node/test/persistTransaction.test.ts b/packages/request-node/test/persistTransaction.test.ts index e86469f83..05f45489a 100644 --- a/packages/request-node/test/persistTransaction.test.ts +++ b/packages/request-node/test/persistTransaction.test.ts @@ -18,6 +18,8 @@ const badlyFormattedTransactionData = { not: 'a transaction' }; let requestNodeInstance: RequestNode; let server: any; +jest.setTimeout(30000); + const mockServer = setupServer(); /* eslint-disable no-magic-numbers */ diff --git a/packages/thegraph-data-access/src/queries.ts b/packages/thegraph-data-access/src/queries.ts index b4543a5ac..3e5b9bf98 100644 --- a/packages/thegraph-data-access/src/queries.ts +++ b/packages/thegraph-data-access/src/queries.ts @@ -80,7 +80,7 @@ ${TransactionsBodyFragment} query GetTransactionsByTopics($topics: [String!]!, $first: Int!, $skip: Int!) { ${metaQueryBody} transactions( - where: { topics_contains_nocase: $topics } + where: { topics_contains: $topics } first: $first skip: $skip orderBy: blockTimestamp diff --git a/packages/thegraph-data-access/src/subgraph-client.ts b/packages/thegraph-data-access/src/subgraph-client.ts index aa89d0e75..fe20031fa 100644 --- a/packages/thegraph-data-access/src/subgraph-client.ts +++ b/packages/thegraph-data-access/src/subgraph-client.ts @@ -71,10 +71,12 @@ export class SubgraphClient implements StorageTypes.IIndexer { const effectivePage = page ?? 1; const skip = (effectivePage - 1) * effectivePageSize; + const topicsArray = Array.isArray(topics) ? topics : [topics]; + const response = await this.graphql.request( GetTransactionsByTopics, { - topics, + topics: topicsArray, first: effectivePageSize, skip, }, From cfd2861b447a31f65d935766f9ed2a96f92efaa4 Mon Sep 17 00:00:00 2001 From: rodrigopavezi Date: Wed, 4 Dec 2024 12:11:06 -0300 Subject: [PATCH 3/4] fix: updatedBetween issues on pagination --- packages/data-access/src/data-read.ts | 41 +++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/data-access/src/data-read.ts b/packages/data-access/src/data-read.ts index 4c2bf5c87..704c2b57b 100644 --- a/packages/data-access/src/data-read.ts +++ b/packages/data-access/src/data-read.ts @@ -70,7 +70,6 @@ export class DataAccessRead implements DataAccessTypes.IDataRead { } const pending = this.pendingStore?.findByTopics(topics) || []; - const pendingItems = pending.map((item) => ({ hash: item.storageResult.id, channelId: item.channelId, @@ -113,25 +112,33 @@ export class DataAccessRead implements DataAccessTypes.IDataRead { const transactions = result.transactions.concat(...pendingItems); - // list of channels having at least one tx updated during the updatedBetween boundaries - const channels = ( - updatedBetween - ? transactions.filter( - (tx) => - tx.blockTimestamp >= (updatedBetween.from || 0) && - tx.blockTimestamp <= (updatedBetween.to || Number.MAX_SAFE_INTEGER), - ) - : transactions - ).map((x) => x.channelId); + // Apply timestamp filtering FIRST + const filteredTransactions = updatedBetween + ? transactions.filter( + (tx) => + tx.blockTimestamp >= (updatedBetween.from || 0) && + tx.blockTimestamp <= (updatedBetween.to || Number.MAX_SAFE_INTEGER), + ) + : transactions; + + // Then get unique channels from filtered transactions + const channels = [...new Set(filteredTransactions.map((x) => x.channelId))]; + // Get all transactions for these channels const filteredTxs = transactions.filter((tx) => channels.includes(tx.channelId)); + + // Apply pagination to the filtered results + const start = ((page || 1) - 1) * (pageSize || filteredTxs.length); + const end = start + (pageSize || filteredTxs.length); + const paginatedTxs = filteredTxs.slice(start, end); + return { meta: { - storageMeta: filteredTxs.reduce((acc, tx) => { + storageMeta: paginatedTxs.reduce((acc, tx) => { acc[tx.channelId] = [this.toStorageMeta(tx, result.blockNumber, this.network)]; return acc; }, {} as Record), - transactionsStorageLocation: filteredTxs.reduce((prev, curr) => { + transactionsStorageLocation: paginatedTxs.reduce((prev, curr) => { if (!prev[curr.channelId]) { prev[curr.channelId] = []; } @@ -141,17 +148,15 @@ export class DataAccessRead implements DataAccessTypes.IDataRead { pagination: page && pageSize ? { - total: result.transactions.length + pendingItems.length, + total: filteredTxs.length, page, pageSize, - hasMore: - (page - 1) * pageSize + filteredTxs.length - pendingItemsOnCurrentPage < - result.transactions.length, + hasMore: end < filteredTxs.length, } : undefined, }, result: { - transactions: filteredTxs.reduce((prev, curr) => { + transactions: paginatedTxs.reduce((prev, curr) => { if (!prev[curr.channelId]) { prev[curr.channelId] = []; } From 18103d796bd82f09675d8d051831704c0abc808b Mon Sep 17 00:00:00 2001 From: rodrigopavezi Date: Wed, 4 Dec 2024 18:42:09 -0300 Subject: [PATCH 4/4] chore: update docker-compose and improve data access logic - Modified docker-compose.yml to adjust the build context and dockerfile path for the graph-deploy service. - Enhanced data access logic in data-read.ts to streamline transaction filtering and pagination. - Updated GraphQL queries in queries.ts to correctly retrieve transactions by topics and ensure proper ordering. - Refactored subgraph-client.ts to handle channel-based transaction retrieval, improving data organization and pagination handling. These changes improve the overall structure and functionality of the data access layer. --- docker-compose.yml | 6 +-- packages/data-access/src/data-read.ts | 41 ++++++++----------- packages/thegraph-data-access/src/queries.ts | 17 ++++---- .../src/subgraph-client.ts | 27 ++++++------ 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f99d72117..e62642b4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,8 +52,8 @@ services: restart: on-failure:20 graph-deploy: build: - context: https://github.com/RequestNetwork/docker-images.git#main - dockerfile: request-subgraph-storage/Dockerfile + context: https://github.com/RequestNetwork/docker-images.git#main:request-subgraph-storage + dockerfile: ./Dockerfile depends_on: - ipfs - postgres @@ -61,7 +61,7 @@ services: - ganache environment: GRAPH_NODE: 'http://graph-node:8020' - IPFS_HOST: 'ipfs:5001' + IPFS_HOST: 'http://ipfs:5001' KEEP_ALIVE: 0 SUBGRAPH_FILE: 'subgraph-private.yaml' restart: on-failure:20 diff --git a/packages/data-access/src/data-read.ts b/packages/data-access/src/data-read.ts index 704c2b57b..4c2bf5c87 100644 --- a/packages/data-access/src/data-read.ts +++ b/packages/data-access/src/data-read.ts @@ -70,6 +70,7 @@ export class DataAccessRead implements DataAccessTypes.IDataRead { } const pending = this.pendingStore?.findByTopics(topics) || []; + const pendingItems = pending.map((item) => ({ hash: item.storageResult.id, channelId: item.channelId, @@ -112,33 +113,25 @@ export class DataAccessRead implements DataAccessTypes.IDataRead { const transactions = result.transactions.concat(...pendingItems); - // Apply timestamp filtering FIRST - const filteredTransactions = updatedBetween - ? transactions.filter( - (tx) => - tx.blockTimestamp >= (updatedBetween.from || 0) && - tx.blockTimestamp <= (updatedBetween.to || Number.MAX_SAFE_INTEGER), - ) - : transactions; - - // Then get unique channels from filtered transactions - const channels = [...new Set(filteredTransactions.map((x) => x.channelId))]; + // list of channels having at least one tx updated during the updatedBetween boundaries + const channels = ( + updatedBetween + ? transactions.filter( + (tx) => + tx.blockTimestamp >= (updatedBetween.from || 0) && + tx.blockTimestamp <= (updatedBetween.to || Number.MAX_SAFE_INTEGER), + ) + : transactions + ).map((x) => x.channelId); - // Get all transactions for these channels const filteredTxs = transactions.filter((tx) => channels.includes(tx.channelId)); - - // Apply pagination to the filtered results - const start = ((page || 1) - 1) * (pageSize || filteredTxs.length); - const end = start + (pageSize || filteredTxs.length); - const paginatedTxs = filteredTxs.slice(start, end); - return { meta: { - storageMeta: paginatedTxs.reduce((acc, tx) => { + storageMeta: filteredTxs.reduce((acc, tx) => { acc[tx.channelId] = [this.toStorageMeta(tx, result.blockNumber, this.network)]; return acc; }, {} as Record), - transactionsStorageLocation: paginatedTxs.reduce((prev, curr) => { + transactionsStorageLocation: filteredTxs.reduce((prev, curr) => { if (!prev[curr.channelId]) { prev[curr.channelId] = []; } @@ -148,15 +141,17 @@ export class DataAccessRead implements DataAccessTypes.IDataRead { pagination: page && pageSize ? { - total: filteredTxs.length, + total: result.transactions.length + pendingItems.length, page, pageSize, - hasMore: end < filteredTxs.length, + hasMore: + (page - 1) * pageSize + filteredTxs.length - pendingItemsOnCurrentPage < + result.transactions.length, } : undefined, }, result: { - transactions: paginatedTxs.reduce((prev, curr) => { + transactions: filteredTxs.reduce((prev, curr) => { if (!prev[curr.channelId]) { prev[curr.channelId] = []; } diff --git a/packages/thegraph-data-access/src/queries.ts b/packages/thegraph-data-access/src/queries.ts index 3e5b9bf98..3776f9eb8 100644 --- a/packages/thegraph-data-access/src/queries.ts +++ b/packages/thegraph-data-access/src/queries.ts @@ -79,14 +79,17 @@ ${TransactionsBodyFragment} query GetTransactionsByTopics($topics: [String!]!, $first: Int!, $skip: Int!) { ${metaQueryBody} - transactions( + channels( where: { topics_contains: $topics } - first: $first - skip: $skip - orderBy: blockTimestamp - orderDirection: asc - ) { - ...TransactionsBody + ){ + transactions( + orderBy: blockTimestamp, + orderDirection: asc + first: $first + skip: $skip + ) { + ...TransactionsBody + } } }`; diff --git a/packages/thegraph-data-access/src/subgraph-client.ts b/packages/thegraph-data-access/src/subgraph-client.ts index fe20031fa..2bab69b86 100644 --- a/packages/thegraph-data-access/src/subgraph-client.ts +++ b/packages/thegraph-data-access/src/subgraph-client.ts @@ -71,27 +71,28 @@ export class SubgraphClient implements StorageTypes.IIndexer { const effectivePage = page ?? 1; const skip = (effectivePage - 1) * effectivePageSize; - const topicsArray = Array.isArray(topics) ? topics : [topics]; - - const response = await this.graphql.request( - GetTransactionsByTopics, - { - topics: topicsArray, - first: effectivePageSize, - skip, - }, - ); + const { _meta, channels } = await this.graphql.request< + Meta & { channels: { transactions: Transaction[] }[] } + >(GetTransactionsByTopics, { + topics, + first: effectivePageSize, + skip, + }); - const indexedTransactions = response.transactions.map(this.toIndexedTransaction); + const transactionsByChannel = channels + .map(({ transactions }) => transactions) + .flat() + .sort((a, b) => a.blockTimestamp - b.blockTimestamp); + const indexedTransactions = transactionsByChannel.map(this.toIndexedTransaction); return { transactions: indexedTransactions, - blockNumber: response._meta.block.number, + blockNumber: _meta.block.number, pagination: { page: effectivePage, pageSize: effectivePageSize, total: indexedTransactions.length, - hasMore: indexedTransactions.length === effectivePageSize, + hasMore: skip + effectivePageSize < indexedTransactions.length, }, }; }