Tighten block transaction drilldown paging

This commit is contained in:
defiQUG
2026-04-16 14:27:44 -07:00
parent b6e74eb5bd
commit 2e59b9d19c
3 changed files with 51 additions and 12 deletions

View File

@@ -21,6 +21,8 @@ export default function BlockDetailPage() {
const [blockTransactions, setBlockTransactions] = useState<Transaction[]>([])
const [loading, setLoading] = useState(true)
const [transactionsLoading, setTransactionsLoading] = useState(true)
const [transactionsError, setTransactionsError] = useState(false)
const [hasNextTransactionsPage, setHasNextTransactionsPage] = useState(false)
const [transactionPage, setTransactionPage] = useState(1)
const blockTransactionPageSize = 25
@@ -39,12 +41,17 @@ export default function BlockDetailPage() {
const loadBlockTransactions = useCallback(async () => {
setTransactionsLoading(true)
setTransactionsError(false)
try {
const { ok, data } = await transactionsApi.listByBlockSafe(chainId, blockNumber, transactionPage, blockTransactionPageSize)
setBlockTransactions(ok ? data : [])
const { ok, data, hasNextPage } = await transactionsApi.listByBlockSafe(chainId, blockNumber, transactionPage, blockTransactionPageSize)
setBlockTransactions(data)
setHasNextTransactionsPage(hasNextPage)
setTransactionsError(!ok)
} catch (error) {
console.error('Failed to load block transactions:', error)
setBlockTransactions([])
setHasNextTransactionsPage(false)
setTransactionsError(true)
} finally {
setTransactionsLoading(false)
}
@@ -57,6 +64,8 @@ export default function BlockDetailPage() {
if (!isValidBlock) {
setLoading(false)
setTransactionsLoading(false)
setTransactionsError(false)
setHasNextTransactionsPage(false)
setBlock(null)
setBlockTransactions([])
return
@@ -81,8 +90,6 @@ export default function BlockDetailPage() {
const gasUtilization = block && block.gas_limit > 0
? Math.round((block.gas_used / block.gas_limit) * 100)
: null
const canGoNextTransactionsPage = blockTransactions.length === blockTransactionPageSize
const transactionColumns = [
{
header: 'Hash',
@@ -232,7 +239,9 @@ export default function BlockDetailPage() {
columns={transactionColumns}
data={blockTransactions}
emptyMessage={
block.transaction_count > 0
transactionsError
? 'Unable to load indexed block transactions right now. Please retry from this page in a moment.'
: block.transaction_count > 0
? 'No indexed block transactions were returned for this page yet.'
: 'This block does not contain any indexed transactions.'
}
@@ -255,7 +264,7 @@ export default function BlockDetailPage() {
<button
type="button"
onClick={() => setTransactionPage((current) => current + 1)}
disabled={transactionsLoading || !canGoNextTransactionsPage}
disabled={transactionsLoading || !hasNextTransactionsPage}
className="rounded bg-gray-200 px-4 py-2 text-gray-900 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-gray-100"
>
Next tx page

View File

@@ -27,6 +27,7 @@ describe('transactionsApi.listByBlockSafe', () => {
timestamp: '2026-04-16T09:40:12.000000Z',
},
],
next_page_params: { page: 2, page_size: 10 },
}),
})
@@ -36,12 +37,25 @@ describe('transactionsApi.listByBlockSafe', () => {
expect(result.ok).toBe(true)
expect(result.data).toHaveLength(1)
expect(result.hasNextPage).toBe(true)
expect(result.data[0]?.hash).toBe('0xabc')
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock.mock.calls[0]?.[0]).toEqual(
expect.stringContaining('/api/v2/blocks/123/transactions?page=1&page_size=10'),
)
})
it('returns a non-throwing failure result when the block transaction request fails', async () => {
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
const result = await transactionsApi.listByBlockSafe(138, 123, 1, 10)
expect(result).toEqual({
ok: false,
data: [],
hasNextPage: false,
})
})
})
describe('transactionsApi.diagnoseMissing', () => {

View File

@@ -76,6 +76,11 @@ export interface TransactionLookupDiagnostic {
rpc_url?: string
}
export interface BlockTransactionListPage {
data: Transaction[]
next_page_params: Record<string, unknown> | null
}
const CHAIN_138_PUBLIC_RPC_URL = 'https://rpc-http-pub.d-bis.org'
function resolvePublicRpcUrl(chainId: number): string | null {
@@ -227,26 +232,37 @@ export const transactionsApi = {
return { ok: false, data: [] }
}
},
listByBlock: async (chainId: number, blockNumber: number, page = 1, pageSize = 25): Promise<ApiResponse<Transaction[]>> => {
listByBlock: async (chainId: number, blockNumber: number, page = 1, pageSize = 25): Promise<ApiResponse<BlockTransactionListPage>> => {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
})
const raw = await fetchBlockscoutJson<{ items?: unknown[] }>(`/api/v2/blocks/${blockNumber}/transactions?${params.toString()}`)
const raw = await fetchBlockscoutJson<{ items?: unknown[]; next_page_params?: Record<string, unknown> | null }>(
`/api/v2/blocks/${blockNumber}/transactions?${params.toString()}`
)
const data = Array.isArray(raw?.items) ? raw.items.map((item) => normalizeTransaction(item as never, chainId)) : []
return { data }
return {
data: {
data,
next_page_params: raw?.next_page_params ?? null,
},
}
},
listByBlockSafe: async (
chainId: number,
blockNumber: number,
page = 1,
pageSize = 25,
): Promise<{ ok: boolean; data: Transaction[] }> => {
): Promise<{ ok: boolean; data: Transaction[]; hasNextPage: boolean }> => {
try {
const { data } = await transactionsApi.listByBlock(chainId, blockNumber, page, pageSize)
return { ok: true, data }
return {
ok: true,
data: data.data,
hasNextPage: data.next_page_params != null,
}
} catch {
return { ok: false, data: [] }
return { ok: false, data: [], hasNextPage: false }
}
},
}