diff --git a/AGENTS.md b/AGENTS.md index 4a16412b1..c8367944f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,9 +30,15 @@ npm run build-mainnet npm run lint # eslint src --max-warnings 0 # E2E tests (Cypress, runs against integration-explorer.multiversx.com) -node scripts/cypress.ts # or: npm run cy:run +node scripts/cypress.ts # or: npm run cy:run — full suite + mochawesome report + +# Single spec / single test (bypasses the report wrapper): +npx cypress run --spec cypress/e2e/Search/Search.cy.ts +npx cypress open # interactive runner ``` +Cypress hits the deployed `baseUrl` in `cypress.config.ts`, not your local dev server — no local build is needed to run E2E, but a network connection is. + `src/config/index.ts` **must exist** before starting. The `start-*` scripts create it automatically via the `copy-*-config` step, but if you run `npm run start` directly you need it manually. HTTPS is enabled by default (self-signed cert via `@vitejs/plugin-basic-ssl`). Set `VITE_APP_USE_HTTPS=false` to disable. @@ -45,7 +51,7 @@ HTTPS is enabled by default (self-signed cert via `@vitejs/plugin-basic-ssl`). S `src/index.tsx` → `App.tsx` wraps the app in Redux `` + `` + ``. -Routes are defined in `src/routes/routes.tsx` using React Router v6 `createBrowserRouter`. Every network has its routes prefixed with `/:network/` (e.g. `/devnet/blocks/...`). `generateNetworkRoutes` in `src/routes/helpers/` iterates `networks` from config and wraps routes per network. The `Layout` component (`src/layouts/Layout/`) is the shell that renders the header, hero stats widgets, and footer around page content. +The router itself is created in `src/App.tsx` with React Router v7 `createBrowserRouter`; the route definitions it consumes live in `src/routes/routes.tsx`. Every network has its routes prefixed with `/:network/` (e.g. `/devnet/blocks/...`). `generateNetworkRoutes` in `src/routes/helpers/` iterates `networks` from config and wraps routes per network. The `Layout` component (`src/layouts/Layout/`) is the shell that renders the header, hero stats widgets, and footer around page content. ### Network Configuration diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c63207ea..062d27307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- ## [[2.3.6](https://github.com/multiversx/mx-explorer-dapp/pull/222)] - 2026-08-14 + +- [searchAAfter feature](https://github.com/multiversx/mx-explorer-dapp/pull/221) + - ## [[2.3.5](https://github.com/multiversx/mx-explorer-dapp/pull/219)] - 2026-05-04 - [600ms updates, avoid cached api/websocket updates](https://github.com/multiversx/mx-explorer-dapp/pull/218) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..7066f0b1f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +The guidance for this repository lives in [AGENTS.md](./AGENTS.md) — commands, architecture, and conventions. +Read it before making changes, and record any new learnings there rather than here, so the two never drift apart. diff --git a/cypress/constants/enums.ts b/cypress/constants/enums.ts index 134fc0754..b90df015d 100644 --- a/cypress/constants/enums.ts +++ b/cypress/constants/enums.ts @@ -1,6 +1,7 @@ export enum AssertionEnum { contain = 'contain', include = 'include', + notInclude = 'not.include', beChecked = 'be.checked', exist = 'exist' } diff --git a/cypress/e2e/SearchAfter/SearchAfter.cy.ts b/cypress/e2e/SearchAfter/SearchAfter.cy.ts new file mode 100644 index 000000000..c49d64274 --- /dev/null +++ b/cypress/e2e/SearchAfter/SearchAfter.cy.ts @@ -0,0 +1,186 @@ +/// + +// cursor requests take longer +const CURSOR_TIMEOUT = 15000; + +const firstTxHash = () => + cy + .get('[data-testid="transactionLink"]', { timeout: CURSOR_TIMEOUT }) + .first() + .invoke('text'); + +const visitTransactions = (query: string) => { + cy.intercept('GET', 'https://devnet-api.multiversx.com/transactions?*').as( + 'txs' + ); + cy.visit(`/devnet/transactions${query}`); +}; + +describe('searchAfter cursor pagination', () => { + it('keeps offset pagination below the ceiling and sends no cursor', () => { + visitTransactions('?page=399'); + + cy.wait('@txs').then(({ request }) => { + expect(request.url).to.include('from=9950'); + expect(request.url).to.not.include('searchAfter='); + }); + }); + + it('crosses the wall: page 400 Next hands off to a cursor', () => { + visitTransactions('?page=400'); + + // the last offset page: from + size = 10000 + cy.wait('@txs').its('request.url').should('include', 'from=9975'); + cy.get('[data-testid="transactionsTable"] tr').should('have.length.gt', 1); + + // the cursor is already in hand at the wall, so 401 is a live page button + cy.get('[aria-label="401st Page"]').first().should('not.be.disabled'); + + firstTxHash().then((hashAtWall) => { + cy.get('[data-testid="nextPageButton"]').first().click(); + + cy.url().should('include', 'page=401'); + cy.url().should('include', 'searchAfter='); + + // a cursor request carrying `from` is a 400 from the api + cy.wait('@txs').then(({ request }) => { + expect(request.url).to.include('searchAfter='); + expect(request.url).to.not.include('from='); + }); + + // page 401 must be new rows, not a repeat of the wall or of page 1 + firstTxHash().should('not.equal', hashAtWall); + }); + }); + + it('walks forward then back across cursor pages', () => { + visitTransactions('?page=400'); + cy.wait('@txs'); + + cy.get('[data-testid="nextPageButton"]').first().click(); + cy.url().should('include', 'page=401'); + cy.wait('@txs'); + + firstTxHash().then((hashAt401) => { + cy.get('[data-testid="nextPageButton"]').first().click(); + cy.url().should('include', 'page=402'); + cy.wait('@txs', { timeout: CURSOR_TIMEOUT }); + firstTxHash().should('not.equal', hashAt401); + + // back to 401 using the cursor remembered on the way out + cy.get('[data-testid="previousPageButton"]').first().click(); + cy.url().should('include', 'page=401'); + cy.wait('@txs', { timeout: CURSOR_TIMEOUT }); + firstTxHash().should('equal', hashAt401); + }); + }); + + it('drops the cursor when stepping back below the ceiling', () => { + visitTransactions('?page=400'); + cy.wait('@txs'); + cy.get('[data-testid="nextPageButton"]').first().click(); + cy.wait('@txs'); + + cy.get('[data-testid="previousPageButton"]').first().click(); + cy.url().should('include', 'page=400'); + cy.url().should('not.include', 'searchAfter'); + + cy.wait('@txs').then(({ request }) => { + expect(request.url).to.include('from=9975'); + expect(request.url).to.not.include('searchAfter='); + }); + }); + + it('snaps back when the api ignores the cursor and serves page 1 anyway', () => { + visitTransactions('?page=400'); + cy.wait('@txs'); + + const cursorlessBody = Array.from({ length: 25 }, (_, index) => ({ + txHash: `${index}`.padStart(64, 'a'), + sender: 'erd1qqqqqqqqqqqqqpgqvg8r5yavkyhu6rmmkgqzgsduzheg2fk7v5ysrypdex', + receiver: + 'erd1qqqqqqqqqqqqqpgqvg8r5yavkyhu6rmmkgqzgsduzheg2fk7v5ysrypdex', + senderShard: 1, + receiverShard: 1, + status: 'success', + value: '0', + timestamp: 1783695296, + round: 1 + })); + + cy.intercept( + 'GET', + 'https://devnet-api.multiversx.com/transactions?*searchAfter*', + { statusCode: 200, body: cursorlessBody } + ).as('blindTxs'); + + cy.get('[data-testid="nextPageButton"]').first().click(); + + cy.wait('@blindTxs'); + cy.url().should('not.include', 'page=401'); + cy.url().should('include', 'page=400'); + cy.url().should('not.include', 'searchAfter'); + }); + + it('crosses the wall on blocks too', () => { + cy.intercept('GET', 'https://devnet-api.multiversx.com/blocks?*').as( + 'blocks' + ); + cy.visit('/devnet/blocks?page=400'); + cy.wait('@blocks').its('request.url').should('include', 'from=9975'); + + cy.get('[data-testid="blockLink0"]') + .invoke('text') + .then((nonceAtWall) => { + cy.get('[data-testid="nextPageButton"]').first().click(); + + cy.url().should('include', 'page=401'); + cy.wait('@blocks').then(({ request }) => { + expect(request.url).to.include('searchAfter='); + expect(request.url).to.not.include('from='); + }); + + cy.get('[data-testid="blockLink0"]') + .invoke('text') + .should('not.equal', nonceAtWall); + }); + }); + + it('crosses the wall on accounts too', () => { + cy.intercept('GET', 'https://devnet-api.multiversx.com/accounts?*').as( + 'accounts' + ); + cy.visit('/devnet/accounts?page=400'); + cy.wait('@accounts').its('request.url').should('include', 'from=9975'); + + const firstAddress = () => + cy + .get('[data-testid="accountsTable"] tr', { timeout: CURSOR_TIMEOUT }) + .first() + .invoke('text'); + + firstAddress().then((addressAtWall) => { + cy.get('[data-testid="nextPageButton"]').first().click(); + + cy.url().should('include', 'page=401'); + cy.wait('@accounts').then(({ request }) => { + expect(request.url).to.include('searchAfter='); + expect(request.url).to.not.include('from='); + }); + + firstAddress().should('not.equal', addressAtWall); + }); + }); + + it('ignores a cursor left in the url once page falls back below the ceiling', () => { + // clear cursor + visitTransactions( + '?searchAfter=WzE3ODE4NjM2NzYwMDAsMTc4MTg2MzY3NjAwMCw0MTk4OSwiMktmVW5iMF9RdnVPQVlYaTIyWlRIZz09Il0=' + ); + + cy.wait('@txs').then(({ request }) => { + expect(request.url).to.include('from=0'); + expect(request.url).to.not.include('searchAfter='); + }); + }); +}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 949818fda..2914ba78b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -46,19 +46,35 @@ Cypress.Commands.add('coveredElementHandler', (selector) => { }); }); +// Clicks a pager page button and waits for the pager to re-render with that +// page active. Without this the next command can click a button still holding +// the previous render's handler, sending the app to the wrong page. +const goToPage = (ordinal: string) => { + cy.get(`[aria-label="${ordinal} Page"]`).first().click(); + cy.get(`[aria-label="${ordinal} Page"]`) + .first() + .should('have.attr', 'aria-current', 'page'); +}; + Cypress.Commands.add('paginationHandler', (route) => { cy.get('header').invoke('css', { display: 'none' }); - cy.get('[aria-label="2nd Page"]').first().click(); + goToPage('2nd'); cy.checkUrl('page=2'); - cy.get('[aria-label="3rd Page"]').first().click(); + goToPage('3rd'); cy.checkUrl('page=3'); - cy.get('[aria-label="1st Page"]').first().click(); + goToPage('1st'); + cy.contains('button', 'Next').last().click(); cy.checkUrl('page=2'); + cy.get('[aria-label="2nd Page"]') + .first() + .should('have.attr', 'aria-current', 'page'); + cy.contains('button', 'Prev').click(); cy.checkUrl(route); + cy.url().should(AssertionEnum.notInclude, 'page='); }); Cypress.Commands.add('checkTableHead', (payload: string[]) => { diff --git a/package.json b/package.json index 5562dca17..ae92293a5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mx-explorer-dapp", "description": "MultiversX Blockchain Explorer", - "version": "2.3.5", + "version": "2.3.6", "author": "MultiversX", "license": "GPL-3.0-or-later", "repository": "multiversx/mx-explorer-dapp", diff --git a/src/appConstants/apiFields.constants.ts b/src/appConstants/apiFields.constants.ts index 920c12e81..86e4479bd 100644 --- a/src/appConstants/apiFields.constants.ts +++ b/src/appConstants/apiFields.constants.ts @@ -19,7 +19,8 @@ export const TRANSACTIONS_TABLE_FIELDS = [ 'guardianSignature', 'relayer', 'isRelayed', - 'relayedVersion' + 'relayedVersion', + 'searchAfter' ]; export const IDENTITIES_FIELDS = [ @@ -80,7 +81,8 @@ export const BLOCKS_FIELDS = [ 'gasPenalized', 'maxGasLimit', 'proposer', - 'proposerIdentity' + 'proposerIdentity', + 'searchAfter' ]; export const LATEST_BLOCKS_FIELDS = [ diff --git a/src/appConstants/general.constants.ts b/src/appConstants/general.constants.ts index 88ddf7432..cfc7be9fb 100644 --- a/src/appConstants/general.constants.ts +++ b/src/appConstants/general.constants.ts @@ -34,6 +34,10 @@ export const TEMP_LOCAL_NOTIFICATION_DISMISSED = 'barnardGovernance'; export const CUSTOM_NETWORK_ID = 'custom-network'; export const NEW_VERSION_NOTIFICATION = 'newExplorerVersion'; export const NAVIGATION_SEARCH_STATE = 'fromSearch'; +export const CURSOR_HISTORY_STORAGE_KEY = 'explorerCursors'; + +export const MAX_CURSOR_HISTORY_PAGES = 500; +export const MAX_CURSOR_HISTORY_LISTS = 5; export const SC_INIT_CHARACTERS_LENGTH = 13; diff --git a/src/components/AccountsTable/AccountsTable.tsx b/src/components/AccountsTable/AccountsTable.tsx index 3bd432321..b42c2c687 100644 --- a/src/components/AccountsTable/AccountsTable.tsx +++ b/src/components/AccountsTable/AccountsTable.tsx @@ -56,6 +56,7 @@ export const AccountsTable = ({ total={accountsCount} show={accounts.length > 0} className='d-flex ms-auto me-auto me-sm-0' + items={accounts} /> @@ -147,7 +148,11 @@ export const AccountsTable = ({
- 0} /> + 0} + items={accounts} + />
) : ( diff --git a/src/components/BlocksTable/BlocksTable.tsx b/src/components/BlocksTable/BlocksTable.tsx index a8cd7c4a3..f31673944 100644 --- a/src/components/BlocksTable/BlocksTable.tsx +++ b/src/components/BlocksTable/BlocksTable.tsx @@ -63,7 +63,11 @@ export const BlocksTable = ({ )} - 0} /> + 0} + items={blocks} + /> @@ -195,7 +199,7 @@ export const BlocksTable = ({
- 0} /> + 0} items={blocks} />
diff --git a/src/components/DataDecode/dataDecode.styles.scss b/src/components/DataDecode/dataDecode.styles.scss index 15a3476af..91c0e6eab 100644 --- a/src/components/DataDecode/dataDecode.styles.scss +++ b/src/components/DataDecode/dataDecode.styles.scss @@ -4,7 +4,7 @@ &.has-decode { --data-decode-padding: 0.375rem 9rem 0.375rem 0.75rem; - .copy-button { + .button-holder .copy-button { right: 6.75rem; } } diff --git a/src/components/EventsTable/EventsTable.tsx b/src/components/EventsTable/EventsTable.tsx index 81cae6c70..fee513ffa 100644 --- a/src/components/EventsTable/EventsTable.tsx +++ b/src/components/EventsTable/EventsTable.tsx @@ -46,6 +46,7 @@ export const EventsTable = ({ total={totalEvents} show={events.length > 0} className='d-flex ms-auto me-auto me-sm-0' + items={events} /> @@ -104,7 +105,7 @@ export const EventsTable = ({
- 0} /> + 0} items={events} />
diff --git a/src/components/Pager/Pager.tsx b/src/components/Pager/Pager.tsx index 698c1ae5c..57c52b048 100644 --- a/src/components/Pager/Pager.tsx +++ b/src/components/Pager/Pager.tsx @@ -1,16 +1,17 @@ -import { Fragment } from 'react'; +import { useEffect } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useSearchParams } from 'react-router-dom'; import { ELLIPSIS, PAGE_SIZE, MAX_RESULTS } from 'appConstants'; import { stringIsInteger, formatOrdinals } from 'helpers'; +import { useGetCursorHistory } from 'hooks'; import { faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight } from 'icons/solid'; -import { pagerHelper } from './helpers/pagerHelper'; +import { generatePaginationArray, pagerHelper } from './helpers/pagerHelper'; export interface PagerUIType { total?: number | typeof ELLIPSIS; @@ -19,6 +20,7 @@ export interface PagerUIType { showFirstAndLast?: boolean; className?: string; hasTestId?: boolean; + items?: { searchAfter?: string }[]; } export const Pager = ({ @@ -27,47 +29,123 @@ export const Pager = ({ itemsPerPage = PAGE_SIZE, showFirstAndLast, className = '', - hasTestId = true + hasTestId = true, + items = [] }: PagerUIType) => { const [searchParams, setSearchParams] = useSearchParams(); const params = Object.fromEntries(searchParams); + const { getCursor, setCursor } = useGetCursorHistory(); - const { page, size, ...rest } = params; + const nextCursor = items[items.length - 1]?.searchAfter; + const itemsCount = items.length; + + const { page, size, searchAfter, ...rest } = params; const processedSize = stringIsInteger(String(size)) ? parseInt(String(size)) : itemsPerPage; const processedTotal = total !== ELLIPSIS ? Math.min(total, MAX_RESULTS) : 0; - const { processedPage, lastPage, end, paginationArray } = pagerHelper({ + const { + processedPage, + lastPage, + end, + lastOffsetPage, + isCursorMode, + paginationArray + } = pagerHelper({ total: processedTotal, itemsPerPage: processedSize, page: Number(page) }); + const hasResultsPastCeiling = total === ELLIPSIS || total > MAX_RESULTS; + const isCursorNext = + processedPage >= lastOffsetPage && + Boolean(nextCursor) && + hasResultsPastCeiling; + const isLastCursorPage = isCursorMode && itemsCount < processedSize; + + const baseUrlParams = { ...rest, ...(size ? { size } : {}) }; + const previousCursor = getCursor(processedPage - 1); + const previousPage = processedPage - 1; + const nextUrlParams = { - ...params, - page: `${processedPage + 1}` + ...baseUrlParams, + page: `${processedPage + 1}`, + ...(isCursorNext && nextCursor ? { searchAfter: nextCursor } : {}) }; const firstUrlParams = { ...rest }; const prevUrlParams = { - ...params, - page: `${processedPage - 1}` + ...baseUrlParams, + page: `${previousPage}`, + ...(previousPage > lastOffsetPage && previousCursor + ? { searchAfter: previousCursor } + : {}) }; + const lastUrlParams = { - ...params, + ...baseUrlParams, page: `${lastPage}` }; - const updatePage = (nextUrlParams: any) => { - setSearchParams(nextUrlParams); + const offsetPages = + isCursorNext && !paginationArray.includes(processedPage + 1) + ? [...paginationArray, processedPage + 1] + : paginationArray; + + const pages = isCursorMode + ? generatePaginationArray({ + currentPage: processedPage, + totalPages: processedPage + (nextCursor ? 1 : 0) + }) + : offsetPages; + + const getPageUrlParams = (page: number) => { + if (page <= lastOffsetPage) { + return { ...baseUrlParams, page: `${page}` }; + } + if (page === processedPage + 1 && nextCursor) { + return { ...baseUrlParams, page: `${page}`, searchAfter: nextCursor }; + } + const cursor = getCursor(page); + + return cursor + ? { ...baseUrlParams, page: `${page}`, searchAfter: cursor } + : undefined; }; - const leftBtnActive = processedPage !== 1; - const rightBtnsActive = end < processedTotal; + const updatePage = (urlParams: Record) => { + setSearchParams(urlParams); + }; + + useEffect(() => { + if (isCursorMode && searchAfter) { + setCursor(processedPage, searchAfter); + } + }, [isCursorMode, searchAfter, processedPage]); + + useEffect(() => { + if (isCursorMode && itemsCount && !nextCursor) { + setSearchParams( + { ...rest, page: `${lastOffsetPage}` }, + { replace: true } + ); + } + }, [isCursorMode, nextCursor, itemsCount, lastOffsetPage]); + + const canGoPrevious = isCursorMode + ? previousPage <= lastOffsetPage || Boolean(previousCursor) + : processedPage !== 1; + const canGoNext = isCursorMode + ? Boolean(nextCursor) && !isLastCursorPage + : total === ELLIPSIS || end < processedTotal || isCursorNext; + + const leftBtnActive = canGoPrevious; + const rightBtnsActive = canGoNext; return show ? (
@@ -99,7 +177,7 @@ export const Pager = ({ )} - {processedPage === 1 ? ( + {!canGoPrevious ? (
- {paginationArray.map((page, index) => { - const currentUrlParams = { - ...params, - page: String(page) - }; + {pages.map((page, index) => { + if (page === ELLIPSIS) { + return {ELLIPSIS}; + } + + const pageNumber = Number(page); + const isActive = pageNumber === processedPage; + const pageUrlParams = getPageUrlParams(pageNumber); return ( - - {page !== ELLIPSIS ? ( - - ) : ( - ... - )} - + ); })}
@@ -163,7 +239,7 @@ export const Pager = ({ rightBtnsActive ? '' : 'inactive' }`} > - {total === ELLIPSIS || end < processedTotal ? ( + {canGoNext ? (
@@ -66,7 +67,11 @@ export const ScResultsTable = ({
- 0} /> + 0} + items={scResults} + />
diff --git a/src/components/TransactionsTable/TransactionsTable.tsx b/src/components/TransactionsTable/TransactionsTable.tsx index 784c1495c..c0837e146 100644 --- a/src/components/TransactionsTable/TransactionsTable.tsx +++ b/src/components/TransactionsTable/TransactionsTable.tsx @@ -65,6 +65,7 @@ export const TransactionsTable = ({ total={totalTransactions} show={transactions.length > 0} className='d-flex ms-auto me-auto me-sm-0' + items={transactions} /> @@ -137,7 +138,11 @@ export const TransactionsTable = ({
- 0} /> + 0} + items={transactions} + />
diff --git a/src/helpers/getValue/getLastOffsetPage.ts b/src/helpers/getValue/getLastOffsetPage.ts new file mode 100644 index 000000000..fd8129736 --- /dev/null +++ b/src/helpers/getValue/getLastOffsetPage.ts @@ -0,0 +1,16 @@ +import BigNumber from 'bignumber.js'; +import { MAX_RESULTS, PAGE_SIZE } from 'appConstants'; + +// last page reachable through offset pagination +export const getLastOffsetPage = (size = PAGE_SIZE) => { + const processedSize = new BigNumber(size); + + if (processedSize.isLessThanOrEqualTo(0)) { + return 1; + } + + return new BigNumber(MAX_RESULTS) + .dividedBy(processedSize) + .integerValue(BigNumber.ROUND_FLOOR) + .toNumber(); +}; diff --git a/src/helpers/getValue/index.ts b/src/helpers/getValue/index.ts index a25372ef0..b0a1e1cb2 100644 --- a/src/helpers/getValue/index.ts +++ b/src/helpers/getValue/index.ts @@ -6,6 +6,7 @@ export * from './getColors'; export * from './getCustomPageName'; export * from './getDisplayReceiver'; export * from './getItemsPage'; +export * from './getLastOffsetPage'; export * from './getNftText'; export * from './getNodeIcon'; export * from './getNodeIssue'; diff --git a/src/helpers/isCondition/index.ts b/src/helpers/isCondition/index.ts index d9c451bff..e112e38ec 100644 --- a/src/helpers/isCondition/index.ts +++ b/src/helpers/isCondition/index.ts @@ -1,5 +1,6 @@ export * from './addressIsBech32'; export * from './isContract'; +export * from './isCursorPage'; export * from './isEgldToken'; export * from './isEllipsisActive'; export * from './isHash'; diff --git a/src/helpers/isCondition/isCursorPage.ts b/src/helpers/isCondition/isCursorPage.ts new file mode 100644 index 000000000..efa4dbb67 --- /dev/null +++ b/src/helpers/isCondition/isCursorPage.ts @@ -0,0 +1,10 @@ +import { PAGE_SIZE } from 'appConstants'; +import { getLastOffsetPage } from 'helpers/getValue/getLastOffsetPage'; + +interface IsCursorPageType { + page: number; + size?: number; +} + +export const isCursorPage = ({ page, size = PAGE_SIZE }: IsCursorPageType) => + page > getLastOffsetPage(size); diff --git a/src/hooks/adapter/helpers.ts b/src/hooks/adapter/helpers.ts index ad583e143..55b8a8d14 100644 --- a/src/hooks/adapter/helpers.ts +++ b/src/hooks/adapter/helpers.ts @@ -31,6 +31,7 @@ export const getAccountParams = (address?: string) => export function getTransactionsParams({ page, size, + searchAfter, order, fields = TRANSACTIONS_TABLE_FIELDS.join(','), @@ -67,7 +68,7 @@ export function getTransactionsParams({ ...(isCount ? {} : { - ...getPageParams({ page, size }), + ...getPageParams({ page, size, searchAfter }), ...(fields ? { fields } : {}), ...(order ? { order } : {}), ...(withScResults ? { withScResults } : {}), @@ -102,6 +103,7 @@ export function getTransactionsParams({ export function getEventsParams({ page, size, + searchAfter, address, identifier, @@ -114,7 +116,7 @@ export function getEventsParams({ isCount = false }: GetEventsType) { const params: AdapterProviderPropsType['params'] = { - ...(isCount ? {} : getPageParams({ page, size })), + ...(isCount ? {} : getPageParams({ page, size, searchAfter })), ...(address ? { address } : {}), ...(identifier ? { identifier } : {}), ...(txHash ? { txHash } : {}), @@ -209,6 +211,7 @@ export function getNodeParams({ export function getBlocksParams({ page, size, + searchAfter, fields = BLOCKS_FIELDS.join(','), shard, @@ -224,7 +227,7 @@ export function getBlocksParams({ ...(isCount ? {} : { - ...getPageParams({ page, size }), + ...getPageParams({ page, size, searchAfter }), ...(withProposerIdentity ? { withProposerIdentity } : {}), ...(fields !== undefined ? { fields } : {}) }), @@ -252,6 +255,7 @@ export function getProviderParams({ export function getTokensParams({ page, size, + searchAfter, sort, order, fields, @@ -272,7 +276,7 @@ export function getTokensParams({ ...(isCount ? {} : { - ...getPageParams({ page, size }), + ...getPageParams({ page, size, searchAfter }), ...(sort !== undefined ? { sort } : {}), ...(order !== undefined ? { order } : {}), ...(fields !== undefined ? { fields } : {}), @@ -291,6 +295,7 @@ export function getTokensParams({ export function getCollectionsParams({ page, size, + searchAfter, sort, order, fields, @@ -310,7 +315,7 @@ export function getCollectionsParams({ ...(isCount ? {} : { - ...getPageParams({ page, size }), + ...getPageParams({ page, size, searchAfter }), ...(sort !== undefined ? { sort } : {}), ...(order !== undefined ? { order } : {}), ...(fields !== undefined ? { fields } : {}), @@ -328,6 +333,7 @@ export function getCollectionsParams({ export function getNftsParams({ page, size, + searchAfter, sort, order, fields, @@ -356,7 +362,7 @@ export function getNftsParams({ ...(isCount ? {} : { - ...getPageParams({ page, size }), + ...getPageParams({ page, size, searchAfter }), ...(sort !== undefined ? { sort } : {}), ...(order !== undefined ? { order } : {}), ...(fields !== undefined ? { fields } : {}), @@ -397,7 +403,16 @@ export const getShardAndEpochParams = ( return result; }; -export const getPageParams = ({ page = 1, size = PAGE_SIZE }: BaseApiType) => { +export const getPageParams = ({ + page = 1, + size = PAGE_SIZE, + searchAfter +}: BaseApiType) => { + // the api rejects the request unless `from` is absent alongside a cursor + if (searchAfter) { + return { size, searchAfter }; + } + const from = new BigNumber(page).minus(1).times(size); const isMoreThanMax = from.plus(size).isGreaterThan(MAX_RESULTS); diff --git a/src/hooks/adapter/requests/useAccountRequests.ts b/src/hooks/adapter/requests/useAccountRequests.ts index 9cd27eeef..7008346a0 100644 --- a/src/hooks/adapter/requests/useAccountRequests.ts +++ b/src/hooks/adapter/requests/useAccountRequests.ts @@ -41,6 +41,7 @@ export const useAccountRequests = () => { signal, page, size, + searchAfter, isSmartContract, withOwnerAssets = false, withDeployInfo = false, @@ -53,7 +54,7 @@ export const useAccountRequests = () => { timeout, signal, params: { - ...getPageParams({ page, size }), + ...getPageParams({ page, size, searchAfter }), ...(isSmartContract !== undefined ? { isSmartContract } : {}), ...(withOwnerAssets ? { withOwnerAssets } : {}), ...(withDeployInfo ? { withDeployInfo } : {}), @@ -153,6 +154,7 @@ export const useAccountRequests = () => { address, page, size, + searchAfter, timeout, signal }: BaseApiType & GetAccountResourceType) => @@ -160,7 +162,7 @@ export const useAccountRequests = () => { url: `/accounts/${address}/contracts`, timeout, signal, - params: getPageParams({ page, size }) + params: getPageParams({ page, size, searchAfter }) }), getAccountContractsCount: ( @@ -261,6 +263,7 @@ export const useAccountRequests = () => { type, page, size, + searchAfter, timeout, signal }: BaseApiType & GetAccountResourceType & { type: AccountRolesTypeEnum }) => @@ -268,7 +271,7 @@ export const useAccountRequests = () => { url: `/accounts/${address}/roles/${type}`, timeout, signal, - params: getPageParams({ page, size }) + params: getPageParams({ page, size, searchAfter }) }), getAccountRolesCount: ({ diff --git a/src/hooks/adapter/requests/useTransactionRequests.ts b/src/hooks/adapter/requests/useTransactionRequests.ts index d6c160eed..b9d479004 100644 --- a/src/hooks/adapter/requests/useTransactionRequests.ts +++ b/src/hooks/adapter/requests/useTransactionRequests.ts @@ -88,12 +88,12 @@ export const useTransactionRequests = () => { getScResult: (hash: string, { signal, timeout }: AxiosParamsApiType = {}) => provider({ url: `/results/${hash}`, signal, timeout }), - getScResults: ({ page, size, signal, timeout }: BaseApiType) => + getScResults: ({ page, size, searchAfter, signal, timeout }: BaseApiType) => provider({ url: '/results', signal, timeout, - params: getPageParams({ page, size }) + params: getPageParams({ page, size, searchAfter }) }), getScResultsCount: ({ signal, timeout }: AxiosParamsApiType = {}) => diff --git a/src/hooks/fetch/useFetchApiData.ts b/src/hooks/fetch/useFetchApiData.ts index 4568f9ce4..381fded5d 100644 --- a/src/hooks/fetch/useFetchApiData.ts +++ b/src/hooks/fetch/useFetchApiData.ts @@ -41,13 +41,16 @@ export const useFetchApiData = ({ isCustomUpdate, isRefreshPaused = false }: FetchApiDataProps) => { - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const [dataChanged, setDataChanged] = useState(false); let isCalled = false; const hasUrlParams = - Object.keys(urlParams).length > 0 || page !== 1 || size !== PAGE_SIZE; + Object.keys(urlParams).length > 0 || + page !== 1 || + size !== PAGE_SIZE || + searchAfter !== undefined; const isPaused = Boolean(hasUrlParams || isRefreshPaused); diff --git a/src/hooks/fetch/useFetchBlocks.ts b/src/hooks/fetch/useFetchBlocks.ts index a9d18baa1..f6b0bef2d 100644 --- a/src/hooks/fetch/useFetchBlocks.ts +++ b/src/hooks/fetch/useFetchBlocks.ts @@ -15,7 +15,7 @@ interface BlocksWebsocketResponseType { export const useFetchBlocks = (props: Omit) => { const dispatch = useDispatch(); const blockFilters = useGetBlockFilters(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { dataCountPromise, filters, websocketConfig } = props; const { blocks, blocksCount, isDataReady, isRefreshPaused } = @@ -57,6 +57,7 @@ export const useFetchBlocks = (props: Omit) => { filters: { page, size, + searchAfter, ...blockFilters, ...filters }, diff --git a/src/hooks/fetch/useFetchCustomTransfers.ts b/src/hooks/fetch/useFetchCustomTransfers.ts index 123165b33..1c3d9aa3b 100644 --- a/src/hooks/fetch/useFetchCustomTransfers.ts +++ b/src/hooks/fetch/useFetchCustomTransfers.ts @@ -21,7 +21,7 @@ export interface CustomTransfersWebsocketResponseType { export const useFetchCustomTransfers = (props: FetchCustomTransfersProps) => { const dispatch = useDispatch(); const transactionFilters = useGetTransactionFilters(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { dataCountPromise, filters, websocketConfig } = props; @@ -83,6 +83,7 @@ export const useFetchCustomTransfers = (props: FetchCustomTransfersProps) => { filters: { page, size, + searchAfter, ...transactionFilters, ...filters }, diff --git a/src/hooks/fetch/useFetchEvents.ts b/src/hooks/fetch/useFetchEvents.ts index c9f5cfa7a..51fae21e4 100644 --- a/src/hooks/fetch/useFetchEvents.ts +++ b/src/hooks/fetch/useFetchEvents.ts @@ -15,7 +15,7 @@ interface EventsWebsocketResponseType { export const useFetchEvents = (props: Omit) => { const dispatch = useDispatch(); const eventFilters = useGetEventFilters(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { dataCountPromise, filters } = props; const { events, eventsCount, isDataReady, isRefreshPaused } = @@ -57,6 +57,7 @@ export const useFetchEvents = (props: Omit) => { filters: { page, size, + searchAfter, ...eventFilters, ...filters }, diff --git a/src/hooks/fetch/useFetchTransactions.ts b/src/hooks/fetch/useFetchTransactions.ts index cf8ee858b..bd12317cc 100644 --- a/src/hooks/fetch/useFetchTransactions.ts +++ b/src/hooks/fetch/useFetchTransactions.ts @@ -21,7 +21,7 @@ export interface TransactionsWebsocketResponseType { export const useFetchTransactions = (props: FetchTransactionsProps) => { const dispatch = useDispatch(); const transactionFilters = useGetTransactionFilters(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { hasMaxTransactionsSize, dataCountPromise, filters, websocketConfig } = props; @@ -69,6 +69,7 @@ export const useFetchTransactions = (props: FetchTransactionsProps) => { filters: { page, size: maxTransactionsSize, + searchAfter, ...transactionFilters, ...filters }, diff --git a/src/hooks/urlFilters/index.ts b/src/hooks/urlFilters/index.ts index cbaab1107..006d052ab 100644 --- a/src/hooks/urlFilters/index.ts +++ b/src/hooks/urlFilters/index.ts @@ -1,4 +1,5 @@ export * from './useGetBlockFilters'; +export * from './useGetCursorHistory'; export * from './useGetEventFilters'; export * from './useGetNodeFilters'; export * from './useGetPage'; diff --git a/src/hooks/urlFilters/useGetCursorHistory.ts b/src/hooks/urlFilters/useGetCursorHistory.ts new file mode 100644 index 000000000..ad3303e33 --- /dev/null +++ b/src/hooks/urlFilters/useGetCursorHistory.ts @@ -0,0 +1,100 @@ +import { useCallback } from 'react'; +import { useLocation, useSearchParams } from 'react-router-dom'; + +import { + CURSOR_HISTORY_STORAGE_KEY, + MAX_CURSOR_HISTORY_LISTS, + MAX_CURSOR_HISTORY_PAGES +} from 'appConstants'; + +interface CursorListType { + updatedAt: number; + cursors: Record; +} +type CursorHistoryType = Record; + +const readHistory = (): CursorHistoryType => { + try { + const entry = sessionStorage.getItem(CURSOR_HISTORY_STORAGE_KEY); + + return entry ? JSON.parse(entry) : {}; + } catch { + return {}; + } +}; + +const writeHistory = (history: CursorHistoryType) => { + try { + sessionStorage.setItem(CURSOR_HISTORY_STORAGE_KEY, JSON.stringify(history)); + } catch {} +}; + +const clearOldestLists = (history: CursorHistoryType) => { + const keys = Object.keys(history); + + if (keys.length <= MAX_CURSOR_HISTORY_LISTS) { + return history; + } + + const keep = keys + .sort((a, b) => history[b].updatedAt - history[a].updatedAt) + .slice(0, MAX_CURSOR_HISTORY_LISTS); + + return Object.fromEntries(keep.map((key) => [key, history[key]])); +}; + +const clearLowestPages = (cursors: Record) => { + const pages = Object.keys(cursors); + + if (pages.length <= MAX_CURSOR_HISTORY_PAGES) { + return cursors; + } + + const keep = pages + .sort((a, b) => Number(b) - Number(a)) + .slice(0, MAX_CURSOR_HISTORY_PAGES); + + return Object.fromEntries(keep.map((page) => [page, cursors[page]])); +}; + +export const useGetCursorHistory = () => { + const { pathname } = useLocation(); + const [searchParams] = useSearchParams(); + + const listKey = useCallback(() => { + const params = new URLSearchParams(searchParams); + params.delete('page'); + params.delete('searchAfter'); + params.sort(); + + return `${pathname}?${params.toString()}`; + }, [pathname, searchParams]); + + const getCursor = useCallback( + (page: number) => readHistory()[listKey()]?.cursors?.[page], + [listKey] + ); + + const setCursor = useCallback( + (page: number, cursor: string) => { + const history = readHistory(); + const key = listKey(); + + writeHistory( + clearOldestLists({ + ...history, + [key]: { + updatedAt: Date.now(), + cursors: clearLowestPages({ + ...history[key]?.cursors, + [page]: cursor + }) + } + }) + ); + }, + [listKey] + ); + + return { getCursor, setCursor }; +}; diff --git a/src/hooks/urlFilters/useGetPage.ts b/src/hooks/urlFilters/useGetPage.ts index fb3ff69e6..dfedd1f37 100644 --- a/src/hooks/urlFilters/useGetPage.ts +++ b/src/hooks/urlFilters/useGetPage.ts @@ -2,6 +2,7 @@ import { useSelector } from 'react-redux'; import { useSearchParams } from 'react-router-dom'; import { PAGE_SIZE } from 'appConstants'; +import { isCursorPage } from 'helpers'; import { stringIsInteger } from 'lib'; import { refreshSelector } from 'redux/selectors'; @@ -9,16 +10,23 @@ export const useGetPage = () => { const { timestamp } = useSelector(refreshSelector); const [searchParams] = useSearchParams(); - const { page: urlPage, size: urlSize } = Object.fromEntries(searchParams); + const { + page: urlPage, + size: urlSize, + searchAfter: urlSearchAfter + } = Object.fromEntries(searchParams); const page = stringIsInteger(urlPage) ? parseInt(urlPage) : 1; const size = stringIsInteger(urlSize) ? parseInt(urlSize) : PAGE_SIZE; + const searchAfter = isCursorPage({ page, size }) ? urlSearchAfter : undefined; + const firstPageRefreshTrigger = page === 1 ? timestamp : 0; return { page, size, + searchAfter, firstPageRefreshTrigger }; }; diff --git a/src/pages/Accounts/Accounts.tsx b/src/pages/Accounts/Accounts.tsx index 80fe58dde..daaecd672 100644 --- a/src/pages/Accounts/Accounts.tsx +++ b/src/pages/Accounts/Accounts.tsx @@ -36,7 +36,7 @@ export const Accounts = () => { const sort = useGetSort(); const { search } = useGetSearch(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { getAccounts, getAccountsCount } = useAdapter(); const [accounts, setAccounts] = useState([]); @@ -53,6 +53,7 @@ export const Accounts = () => { getAccounts({ page, size, + searchAfter, search, ...sort }), @@ -108,6 +109,7 @@ export const Accounts = () => { total={totalAccounts} show={accounts.length > 0} className='d-flex ms-auto me-auto me-sm-0' + items={accounts} /> @@ -156,7 +158,11 @@ export const Accounts = () => {
- 0} /> + 0} + items={accounts} + />
diff --git a/src/pages/Applications/Applications.tsx b/src/pages/Applications/Applications.tsx index 40f6dab69..eb7ab23f9 100644 --- a/src/pages/Applications/Applications.tsx +++ b/src/pages/Applications/Applications.tsx @@ -56,7 +56,7 @@ export const Applications = () => { const sort = useGetSort(); const { search } = useGetSearch(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { getAccounts, getAccountsCount } = useAdapter(); const [accounts, setAccounts] = useState([]); @@ -82,6 +82,7 @@ export const Applications = () => { Promise.all([ getAccounts({ page, + searchAfter, search, isSmartContract: true, withOwnerAssets: true, @@ -141,6 +142,7 @@ export const Applications = () => { itemsPerPage={is24hCountAvailable ? PAGE_SIZE : minSize} show={accounts.length > 0} className='d-flex ms-auto me-auto me-sm-0' + items={accounts} /> @@ -291,6 +293,7 @@ export const Applications = () => { total={totalAccounts} itemsPerPage={is24hCountAvailable ? PAGE_SIZE : minSize} show={accounts.length > 0} + items={accounts} /> diff --git a/src/pages/CollectionDetails/CollectionNfts.tsx b/src/pages/CollectionDetails/CollectionNfts.tsx index 2399eeb26..dced6865a 100644 --- a/src/pages/CollectionDetails/CollectionNfts.tsx +++ b/src/pages/CollectionDetails/CollectionNfts.tsx @@ -25,7 +25,7 @@ export const CollectionNfts = () => { const { collectionState } = useSelector(collectionSelector); const { type } = collectionState; const { getCollectionNfts, getCollectionNftsCount } = useAdapter(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { search } = useGetSearch(); const { hash: collection } = useParams() as any; @@ -41,6 +41,7 @@ export const CollectionNfts = () => { search, page, size, + searchAfter, collection, ...(type === NftTypeEnum.NonFungibleESDT ? { withOwner: true } : {}), ...(type === NftTypeEnum.SemiFungibleESDT ? { withSupply: true } : {}) @@ -72,6 +73,7 @@ export const CollectionNfts = () => { total={totalCollectionNfts} show={collectionNfts.length > 0} className='d-flex ms-auto me-auto me-sm-0' + items={collectionNfts} /> @@ -158,6 +160,7 @@ export const CollectionNfts = () => { 0} + items={collectionNfts} /> diff --git a/src/pages/Collections/Collections.tsx b/src/pages/Collections/Collections.tsx index 5b811d379..8d41ba11a 100644 --- a/src/pages/Collections/Collections.tsx +++ b/src/pages/Collections/Collections.tsx @@ -34,7 +34,7 @@ export const Collections = () => { const hasGrowthWidgets = useHasGrowthWidgets(); const isMainnet = useIsMainnet(); const activeRoute = useActiveRoute(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { search } = useGetSearch(); const { search: searchLocation, pathname } = useLocation(); const { getCollections, getCollectionsCount } = useAdapter(); @@ -67,6 +67,7 @@ export const Collections = () => { search, page, size, + searchAfter, type, ...(isMainnet ? { sort: CollectionSortEnum.verifiedAndHolderCount } @@ -156,6 +157,7 @@ export const Collections = () => { total={totalCollections} show={collections.length > 0} className='d-flex ms-auto me-auto me-sm-0' + items={collections} /> @@ -222,7 +224,11 @@ export const Collections = () => {
- 0} /> + 0} + items={collections} + />
diff --git a/src/pages/NativeToken/NativeTokenAccounts.tsx b/src/pages/NativeToken/NativeTokenAccounts.tsx index 2f2276b6e..cb27a288b 100644 --- a/src/pages/NativeToken/NativeTokenAccounts.tsx +++ b/src/pages/NativeToken/NativeTokenAccounts.tsx @@ -13,7 +13,7 @@ export const NativeTokenAccounts = () => { const { id: activeNetworkId } = useSelector(activeNetworkSelector); const { price, marketCap, supply, decimals } = useGetNativeTokenDetails(); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { getAccounts, getAccountsCount } = useAdapter(); const [accounts, setAccounts] = useState([]); @@ -21,17 +21,18 @@ export const NativeTokenAccounts = () => { const [isDataReady, setIsDataReady] = useState(); const fetchAccounts = () => { - Promise.all([getAccounts({ page, size }), getAccountsCount({})]).then( - ([tokenAccountsData, tokenAccountsCountData]) => { - if (tokenAccountsData.success && tokenAccountsCountData.success) { - setAccounts(tokenAccountsData.data); - setAccountsCount(tokenAccountsCountData.data); - } - setIsDataReady( - tokenAccountsData.success && tokenAccountsCountData.success - ); + Promise.all([ + getAccounts({ page, size, searchAfter }), + getAccountsCount({}) + ]).then(([tokenAccountsData, tokenAccountsCountData]) => { + if (tokenAccountsData.success && tokenAccountsCountData.success) { + setAccounts(tokenAccountsData.data); + setAccountsCount(tokenAccountsCountData.data); } - ); + setIsDataReady( + tokenAccountsData.success && tokenAccountsCountData.success + ); + }); }; useEffect(() => { diff --git a/src/pages/TokenDetails/TokenAccounts.tsx b/src/pages/TokenDetails/TokenAccounts.tsx index e2b9f39f2..fe3a5d28d 100644 --- a/src/pages/TokenDetails/TokenAccounts.tsx +++ b/src/pages/TokenDetails/TokenAccounts.tsx @@ -14,7 +14,7 @@ export const TokenDetailsAccounts = () => { const { token } = useSelector(tokenSelector); const { id: activeNetworkId } = useSelector(activeNetworkSelector); - const { page, size } = useGetPage(); + const { page, size, searchAfter } = useGetPage(); const { getTokenAccounts, getTokenAccountsCount } = useAdapter(); const { @@ -31,7 +31,7 @@ export const TokenDetailsAccounts = () => { const fetchAccounts = () => { Promise.all([ - getTokenAccounts({ token: identifier, page, size }), + getTokenAccounts({ token: identifier, page, size, searchAfter }), getTokenAccountsCount({ token: identifier }) ]).then(([tokenAccountsData, tokenAccountsCountData]) => { if (tokenAccountsData.success && tokenAccountsCountData.success) { diff --git a/src/types/account.types.ts b/src/types/account.types.ts index 2ccabb8e0..299293e72 100644 --- a/src/types/account.types.ts +++ b/src/types/account.types.ts @@ -38,6 +38,7 @@ export interface AccountType { activeGuardianServiceUid?: string; ownerAssets?: AccountAssetType; transfersLast24h?: number; + searchAfter?: string; } export interface AccountSliceType extends SliceType { diff --git a/src/types/adapter.types.ts b/src/types/adapter.types.ts index 784f203db..0b157268b 100644 --- a/src/types/adapter.types.ts +++ b/src/types/adapter.types.ts @@ -17,6 +17,8 @@ export interface BaseApiType extends AxiosParamsApiType { extract?: string; // not on api isCount?: boolean; + // cursor taken from the last item of the previous response + searchAfter?: string; } export interface SortableApiType extends BaseApiType { @@ -249,6 +251,7 @@ export interface AdapterProviderPropsType { withScrCount?: boolean; withIdentityInfo?: boolean; owner?: string; + searchAfter?: string; }; timeout: number; timestamp?: number; diff --git a/src/types/block.types.ts b/src/types/block.types.ts index 79aae8a61..b529cf4f1 100644 --- a/src/types/block.types.ts +++ b/src/types/block.types.ts @@ -27,6 +27,7 @@ export interface BlockType { reserved?: string; lastExecutionResultHash?: string; lastExecutionResultNonce?: number; + searchAfter?: string; } export interface UIBlockType extends BlockType { diff --git a/src/types/collection.types.ts b/src/types/collection.types.ts index a30829101..40d2c11bc 100644 --- a/src/types/collection.types.ts +++ b/src/types/collection.types.ts @@ -30,6 +30,7 @@ export interface CollectionType { isVerified?: boolean; nftCount?: number; holderCount?: number; + searchAfter?: string; } export interface CollectionSliceType extends SliceType { diff --git a/src/types/events.types.ts b/src/types/events.types.ts index fe7521a13..295b69cc7 100644 --- a/src/types/events.types.ts +++ b/src/types/events.types.ts @@ -13,6 +13,7 @@ export interface EventType { txOrder: number; order: number; timestamp: number; + searchAfter?: string; } export interface UIEventType extends EventType { diff --git a/src/types/nft.types.ts b/src/types/nft.types.ts index 5aee0139a..1d89d9590 100644 --- a/src/types/nft.types.ts +++ b/src/types/nft.types.ts @@ -79,6 +79,7 @@ export interface NftType { }; scamInfo?: ScamInfoType; isVerified?: boolean; + searchAfter?: string; } export interface NftAccountType { diff --git a/src/types/token.types.ts b/src/types/token.types.ts index 7f8104f7d..5dfad2a45 100644 --- a/src/types/token.types.ts +++ b/src/types/token.types.ts @@ -76,6 +76,7 @@ export interface TokenLockedAccountType { name: string; balance: string; assets?: AccountAssetType; + searchAfter?: string; } export interface TokenSupplyType { diff --git a/src/types/transaction.types.ts b/src/types/transaction.types.ts index 1642640ae..5825118fa 100644 --- a/src/types/transaction.types.ts +++ b/src/types/transaction.types.ts @@ -43,6 +43,7 @@ export interface TransactionType { results?: TransactionSCResultType[]; operations?: TransactionOperationType[]; innerTransactions?: TransactionInnerType[]; + searchAfter?: string; } // TRANSACTION SC RESULTS @@ -65,6 +66,7 @@ export interface TransactionSCResultType { senderAssets?: AccountAssetType; receiverAssets?: AccountAssetType; miniBlockHash?: string; + searchAfter?: string; } export interface TransactionSCResultLogType {