Block countdown. Infinite loop if 404 block is in the past. (#3130)

* Block countdown. Infinite loop if 404 block is in the past.

Resolves #3124

* fix test
This commit is contained in:
tom goriunov
2025-11-14 18:50:17 +01:00
committed by GitHub
parent dc45b86a64
commit 199f693101
6 changed files with 85 additions and 30 deletions
+18 -9
View File
@@ -4,12 +4,12 @@ import type { PublicRpcSchema } from 'viem';
import { getEnvValue } from 'configs/app/utils';
type Params = PublicRpcSchema[number];
type Params = Array<PublicRpcSchema[number]>;
export type MockRpcResponseFixture = (params: Params) => Promise<void>;
const fixture: TestFixture<MockRpcResponseFixture, { page: Page }> = async({ page }, use) => {
await use(async(rpcMock) => {
await use(async(rpcMocks) => {
const rpcUrl = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL');
if (!rpcUrl) {
@@ -27,14 +27,23 @@ const fixture: TestFixture<MockRpcResponseFixture, { page: Page }> = async({ pag
const json = route.request().postDataJSON();
const id = json?.id;
const payload = {
id,
jsonrpc: '2.0',
method: rpcMock.Method,
...(rpcMock.Parameters ? { params: rpcMock.Parameters } : {}),
};
if (id === undefined) {
route.continue();
return;
}
if (isEqual(json, payload) && id !== undefined) {
const rpcMock = rpcMocks.find((mock) => {
const payload = {
id,
jsonrpc: '2.0',
method: mock.Method,
...(mock.Parameters ? { params: mock.Parameters } : {}),
};
return isEqual(json, payload);
});
if (rpcMock) {
return route.fulfill({
status: 200,
json: {
+17 -1
View File
@@ -19,6 +19,7 @@ type RpcResponseType = GetBlockReturnType<Chain, false, 'latest'> | null;
export type BlockQuery = UseQueryResult<Block, ResourceError<{ status: number }>> & {
isDegradedData: boolean;
isFutureBlock: boolean;
};
interface Params {
@@ -47,6 +48,17 @@ export default function useBlockQuery({ heightOrHash }: Params): BlockQuery {
},
});
const latestBlockQuery = useQuery({
queryKey: [ 'RPC', 'block', 'latest' ],
queryFn: async() => {
if (!publicClient) {
return null;
}
return publicClient.getBlock({ blockTag: 'latest' });
},
enabled: publicClient !== undefined && (apiQuery.isError || apiQuery.errorUpdateCount > 0),
});
const rpcQuery = useQuery<RpcResponseType, unknown, Block | null>({
queryKey: [ 'RPC', 'block', { heightOrHash } ],
queryFn: async() => {
@@ -91,7 +103,7 @@ export default function useBlockQuery({ heightOrHash }: Params): BlockQuery {
};
},
placeholderData: GET_BLOCK,
enabled: publicClient !== undefined && (apiQuery.isError || apiQuery.errorUpdateCount > 0),
enabled: !latestBlockQuery.isPending,
retry: false,
refetchOnMount: false,
});
@@ -120,5 +132,9 @@ export default function useBlockQuery({ heightOrHash }: Params): BlockQuery {
return {
...query,
isDegradedData: isRpcQuery,
isFutureBlock: Boolean(
!heightOrHash.startsWith('0x') &&
latestBlockQuery.data && Number(latestBlockQuery.data.number) < Number(heightOrHash),
),
};
}
+2 -2
View File
@@ -44,11 +44,11 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse
await mockApiResponse('general:address_counters', addressCountersMock.forValidator, { pathParams: { hash: addressMock.hash } });
await mockApiResponse('general:address_tabs_counters', null as never, { pathParams: { hash: addressMock.hash }, status: 500 });
await mockApiResponse('general:address_txs', null as never, { pathParams: { hash: addressMock.hash }, status: 500 });
await mockRpcResponse({
await mockRpcResponse([ {
Method: 'eth_getBalance',
Parameters: [ addressMock.hash, 'latest' ],
ReturnType: numberToHex(1234567890123456),
});
} ]);
const component = await render(<Address/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrls[0]);
+45 -15
View File
@@ -21,11 +21,21 @@ test.beforeEach(async({ mockTextAd }) => {
test('degradation view, details tab', async({ render, mockApiResponse, mockRpcResponse, page }) => {
await mockApiResponse('general:block', null as never, { pathParams: { height_or_hash: height }, status: 500 });
await mockRpcResponse({
Method: 'eth_getBlockByNumber',
Parameters: [ numberToHex(Number(height)), false ],
ReturnType: blockMock.rpcBlockBase,
});
await mockRpcResponse([
{
Method: 'eth_getBlockByNumber',
Parameters: [ 'latest', false ],
ReturnType: {
...blockMock.rpcBlockBase,
number: String(Number(height) + 1_000) as `0x${ string }`,
},
},
{
Method: 'eth_getBlockByNumber',
Parameters: [ numberToHex(Number(height)), false ],
ReturnType: blockMock.rpcBlockBase,
},
]);
const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrls[0]);
@@ -42,11 +52,21 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon
await mockApiResponse('general:block', blockMock.base, { pathParams: { height_or_hash: height } });
await mockApiResponse('general:block_txs', null as never, { pathParams: { height_or_hash: height }, status: 500 });
await mockRpcResponse({
Method: 'eth_getBlockByNumber',
Parameters: [ numberToHex(Number(height)), true ],
ReturnType: blockMock.rpcBlockWithTxsInfo,
});
await mockRpcResponse([
{
Method: 'eth_getBlockByNumber',
Parameters: [ 'latest', false ],
ReturnType: {
...blockMock.rpcBlockWithTxsInfo,
number: String(Number(height) + 1_000) as `0x${ string }`,
},
},
{
Method: 'eth_getBlockByNumber',
Parameters: [ numberToHex(Number(height)), true ],
ReturnType: blockMock.rpcBlockWithTxsInfo,
},
]);
const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrls[0]);
@@ -64,11 +84,21 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR
await mockEnvs(ENVS_MAP.beaconChain);
await mockApiResponse('general:block', blockMock.withWithdrawals, { pathParams: { height_or_hash: height } });
await mockApiResponse('general:block_withdrawals', null as never, { pathParams: { height_or_hash: height }, status: 500 });
await mockRpcResponse({
Method: 'eth_getBlockByNumber',
Parameters: [ numberToHex(Number(height)), false ],
ReturnType: blockMock.rpcBlockBase,
});
await mockRpcResponse([
{
Method: 'eth_getBlockByNumber',
Parameters: [ 'latest', false ],
ReturnType: {
...blockMock.rpcBlockBase,
number: String(Number(height) + 1_000) as `0x${ string }`,
},
},
{
Method: 'eth_getBlockByNumber',
Parameters: [ numberToHex(Number(height)), false ],
ReturnType: blockMock.rpcBlockBase,
},
]);
const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrls[0]);
+1 -1
View File
@@ -148,7 +148,7 @@ const BlockPageContent = () => {
throwOnAbsentParamError(heightOrHash);
if (blockQuery.isError) {
if (!blockQuery.isDegradedData && blockQuery.error.status === 404 && !heightOrHash.startsWith('0x')) {
if (!blockQuery.isDegradedData && blockQuery.error.status === 404 && !heightOrHash.startsWith('0x') && blockQuery.isFutureBlock) {
const url = routeParams({ pathname: '/block/countdown/[height]', query: { height: heightOrHash } }, multichainContext);
router.push(url, undefined, { shallow: true });
return null;
+2 -2
View File
@@ -22,10 +22,10 @@ const testFn = async({ render, mockAssetResponse, mockEnvs, mockRpcResponse, moc
]);
await mockApiResponse('admin:marketplace_dapp', appsMock[0], { pathParams: { chainId: config.chain.id, dappId: appsMock[0].id } });
await mockAssetResponse(appsMock[0].url, './mocks/apps/app.html');
await mockRpcResponse({
await mockRpcResponse([ {
Method: 'eth_chainId',
ReturnType: numberToHex(Number(config.chain.id)),
});
} ]);
const component = await render(
<Flex flexDirection="column" mx={{ base: 4, lg: 6 }} h="100vh">