Tighten block transaction drilldown paging
This commit is contained in:
@@ -21,6 +21,8 @@ export default function BlockDetailPage() {
|
|||||||
const [blockTransactions, setBlockTransactions] = useState<Transaction[]>([])
|
const [blockTransactions, setBlockTransactions] = useState<Transaction[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [transactionsLoading, setTransactionsLoading] = useState(true)
|
const [transactionsLoading, setTransactionsLoading] = useState(true)
|
||||||
|
const [transactionsError, setTransactionsError] = useState(false)
|
||||||
|
const [hasNextTransactionsPage, setHasNextTransactionsPage] = useState(false)
|
||||||
const [transactionPage, setTransactionPage] = useState(1)
|
const [transactionPage, setTransactionPage] = useState(1)
|
||||||
const blockTransactionPageSize = 25
|
const blockTransactionPageSize = 25
|
||||||
|
|
||||||
@@ -39,12 +41,17 @@ export default function BlockDetailPage() {
|
|||||||
|
|
||||||
const loadBlockTransactions = useCallback(async () => {
|
const loadBlockTransactions = useCallback(async () => {
|
||||||
setTransactionsLoading(true)
|
setTransactionsLoading(true)
|
||||||
|
setTransactionsError(false)
|
||||||
try {
|
try {
|
||||||
const { ok, data } = await transactionsApi.listByBlockSafe(chainId, blockNumber, transactionPage, blockTransactionPageSize)
|
const { ok, data, hasNextPage } = await transactionsApi.listByBlockSafe(chainId, blockNumber, transactionPage, blockTransactionPageSize)
|
||||||
setBlockTransactions(ok ? data : [])
|
setBlockTransactions(data)
|
||||||
|
setHasNextTransactionsPage(hasNextPage)
|
||||||
|
setTransactionsError(!ok)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load block transactions:', error)
|
console.error('Failed to load block transactions:', error)
|
||||||
setBlockTransactions([])
|
setBlockTransactions([])
|
||||||
|
setHasNextTransactionsPage(false)
|
||||||
|
setTransactionsError(true)
|
||||||
} finally {
|
} finally {
|
||||||
setTransactionsLoading(false)
|
setTransactionsLoading(false)
|
||||||
}
|
}
|
||||||
@@ -57,6 +64,8 @@ export default function BlockDetailPage() {
|
|||||||
if (!isValidBlock) {
|
if (!isValidBlock) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setTransactionsLoading(false)
|
setTransactionsLoading(false)
|
||||||
|
setTransactionsError(false)
|
||||||
|
setHasNextTransactionsPage(false)
|
||||||
setBlock(null)
|
setBlock(null)
|
||||||
setBlockTransactions([])
|
setBlockTransactions([])
|
||||||
return
|
return
|
||||||
@@ -81,8 +90,6 @@ export default function BlockDetailPage() {
|
|||||||
const gasUtilization = block && block.gas_limit > 0
|
const gasUtilization = block && block.gas_limit > 0
|
||||||
? Math.round((block.gas_used / block.gas_limit) * 100)
|
? Math.round((block.gas_used / block.gas_limit) * 100)
|
||||||
: null
|
: null
|
||||||
const canGoNextTransactionsPage = blockTransactions.length === blockTransactionPageSize
|
|
||||||
|
|
||||||
const transactionColumns = [
|
const transactionColumns = [
|
||||||
{
|
{
|
||||||
header: 'Hash',
|
header: 'Hash',
|
||||||
@@ -232,7 +239,9 @@ export default function BlockDetailPage() {
|
|||||||
columns={transactionColumns}
|
columns={transactionColumns}
|
||||||
data={blockTransactions}
|
data={blockTransactions}
|
||||||
emptyMessage={
|
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.'
|
? 'No indexed block transactions were returned for this page yet.'
|
||||||
: 'This block does not contain any indexed transactions.'
|
: 'This block does not contain any indexed transactions.'
|
||||||
}
|
}
|
||||||
@@ -255,7 +264,7 @@ export default function BlockDetailPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTransactionPage((current) => current + 1)}
|
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"
|
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
|
Next tx page
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ describe('transactionsApi.listByBlockSafe', () => {
|
|||||||
timestamp: '2026-04-16T09:40:12.000000Z',
|
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.ok).toBe(true)
|
||||||
expect(result.data).toHaveLength(1)
|
expect(result.data).toHaveLength(1)
|
||||||
|
expect(result.hasNextPage).toBe(true)
|
||||||
expect(result.data[0]?.hash).toBe('0xabc')
|
expect(result.data[0]?.hash).toBe('0xabc')
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||||
expect(fetchMock.mock.calls[0]?.[0]).toEqual(
|
expect(fetchMock.mock.calls[0]?.[0]).toEqual(
|
||||||
expect.stringContaining('/api/v2/blocks/123/transactions?page=1&page_size=10'),
|
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', () => {
|
describe('transactionsApi.diagnoseMissing', () => {
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export interface TransactionLookupDiagnostic {
|
|||||||
rpc_url?: string
|
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'
|
const CHAIN_138_PUBLIC_RPC_URL = 'https://rpc-http-pub.d-bis.org'
|
||||||
|
|
||||||
function resolvePublicRpcUrl(chainId: number): string | null {
|
function resolvePublicRpcUrl(chainId: number): string | null {
|
||||||
@@ -227,26 +232,37 @@ export const transactionsApi = {
|
|||||||
return { ok: false, data: [] }
|
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({
|
const params = new URLSearchParams({
|
||||||
page: page.toString(),
|
page: page.toString(),
|
||||||
page_size: pageSize.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)) : []
|
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 (
|
listByBlockSafe: async (
|
||||||
chainId: number,
|
chainId: number,
|
||||||
blockNumber: number,
|
blockNumber: number,
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 25,
|
pageSize = 25,
|
||||||
): Promise<{ ok: boolean; data: Transaction[] }> => {
|
): Promise<{ ok: boolean; data: Transaction[]; hasNextPage: boolean }> => {
|
||||||
try {
|
try {
|
||||||
const { data } = await transactionsApi.listByBlock(chainId, blockNumber, page, pageSize)
|
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 {
|
} catch {
|
||||||
return { ok: false, data: [] }
|
return { ok: false, data: [], hasNextPage: false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user