Commit 935a7414 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #299 from blockscout/block-sockets

sockets for blocks list and tx page
parents b2cd2daa 888124f2
import type { SocketConnectOption } from 'phoenix';
import { Socket } from 'phoenix';
import React, { useEffect, useState } from 'react';
export const SocketContext = React.createContext<Socket | null>(null);
interface SocketProviderProps {
children: React.ReactNode;
url: string;
options?: Partial<SocketConnectOption>;
}
export function SocketProvider({ children, options, url }: SocketProviderProps) {
const [ socket, setSocket ] = useState<Socket | null>(null);
useEffect(() => {
const socketInstance = new Socket(url, options);
socketInstance.connect();
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
setSocket(null);
};
}, [ options, url ]);
return (
<SocketContext.Provider value={ socket }>
{ children }
</SocketContext.Provider>
);
}
export function useSocket() {
const context = React.useContext(SocketContext);
if (context === undefined) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
}
import type { Channel } from 'phoenix';
import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus |
SocketMessage.TxStatusUpdate;
interface SocketMessageParamsGeneric<Event extends string, Payload extends object> {
channel: Channel | undefined;
event: Event;
handler: (payload: Payload) => void;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
}
import type { Channel } from 'phoenix';
import { useEffect, useRef, useState } from 'react';
import notEmpty from 'lib/notEmpty';
import { useSocket } from './context';
interface Params {
topic: string;
params?: object;
isDisabled: boolean;
onJoin?: (channel: Channel, message: unknown) => void;
onSocketClose?: () => void;
onSocketError?: () => void;
}
export default function useSocketChannel({ topic, params, isDisabled, onJoin, onSocketClose, onSocketError }: Params) {
const socket = useSocket();
const [ channel, setChannel ] = useState<Channel>();
const onCloseRef = useRef<string>();
const onErrorRef = useRef<string>();
const onJoinRef = useRef(onJoin);
onJoinRef.current = onJoin;
useEffect(() => {
const cleanUpRefs = () => {
const refs = [ onCloseRef.current, onErrorRef.current ].filter(notEmpty);
refs.length > 0 && socket?.off(refs);
};
if (!isDisabled) {
onCloseRef.current = onSocketClose && socket?.onClose(onSocketClose);
onErrorRef.current = onSocketError && socket?.onError(onSocketError);
} else {
cleanUpRefs();
}
return cleanUpRefs;
}, [ onSocketClose, onSocketError, socket, isDisabled ]);
useEffect(() => {
if (isDisabled && channel) {
channel.leave();
setChannel(undefined);
}
}, [ channel, isDisabled ]);
useEffect(() => {
if (socket === null || isDisabled) {
return;
}
const ch = socket.channel(topic, params);
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message));
setChannel(ch);
return () => {
ch.leave();
setChannel(undefined);
};
}, [ socket, topic, params, isDisabled ]);
return channel;
}
import { useEffect, useRef } from 'react';
import type { SocketMessageParams } from 'lib/socket/types';
export default function useSocketMessage({ channel, event, handler }: SocketMessageParams) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
if (channel === undefined) {
return;
}
const ref = channel.on(event, (message) => {
handlerRef.current?.(message);
});
return () => {
channel.off(event, ref);
};
}, [ channel, event ]);
}
...@@ -36,7 +36,7 @@ export interface BlocksResponse { ...@@ -36,7 +36,7 @@ export interface BlocksResponse {
next_page_params: { next_page_params: {
block_number: number; block_number: number;
items_count: number; items_count: number;
}; } | null;
} }
export interface BlockTransactionsResponse { export interface BlockTransactionsResponse {
...@@ -47,3 +47,8 @@ export interface BlockTransactionsResponse { ...@@ -47,3 +47,8 @@ export interface BlockTransactionsResponse {
items_count: number; items_count: number;
} | null; } | null;
} }
export interface NewBlockSocketResponse {
average_block_time: string;
block: Block;
}
...@@ -11,7 +11,7 @@ const BlockTxs = () => { ...@@ -11,7 +11,7 @@ const BlockTxs = () => {
return ( return (
<TxsContent <TxsContent
queryName={ QueryKeys.blockTxs } queryName={ QueryKeys.blockTxs }
apiPath={ `/api/blocks/${ router.query.id }/transactions` } apiPath={ `/node-api/blocks/${ router.query.id }/transactions` }
/> />
); );
}; };
......
import { Box, Text, Show, Hide, Skeleton } from '@chakra-ui/react'; import { Box, Text, Show, Hide, Skeleton, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block'; import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries'; import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import BlocksList from 'ui/blocks/BlocksList'; import BlocksList from 'ui/blocks/BlocksList';
import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile'; import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile';
import BlocksTable from 'ui/blocks/BlocksTable'; import BlocksTable from 'ui/blocks/BlocksTable';
...@@ -19,12 +22,48 @@ interface Props { ...@@ -19,12 +22,48 @@ interface Props {
const BlocksContent = ({ type }: Props) => { const BlocksContent = ({ type }: Props) => {
const fetch = useFetch(); const fetch = useFetch();
const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>(
[ QueryKeys.blocks, type ], [ QueryKeys.blocks, type ],
async() => await fetch(`/node-api/blocks${ type ? `?type=${ type }` : '' }`), async() => await fetch(`/node-api/blocks${ type ? `?type=${ type }` : '' }`),
); );
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.blocks, type ], (prevData: BlocksResponse | undefined) => {
const shouldAddToList = !type || type === payload.block.type;
if (!prevData) {
return {
items: shouldAddToList ? [ payload.block ] : [],
next_page_params: null,
};
}
return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData;
});
}, [ queryClient, type ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new blocks.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new blocks. Please click here to refresh the page.');
}, []);
const channel = useSocketChannel({
topic: 'blocks:new_block',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isLoading || isError,
});
useSocketMessage({
channel,
event: 'new_block',
handler: handleNewBlockMessage,
});
if (isLoading) { if (isLoading) {
return ( return (
<> <>
...@@ -50,6 +89,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -50,6 +89,7 @@ const BlocksContent = ({ type }: Props) => {
return ( return (
<> <>
<Text as="span">Total of { data.items[0].height.toLocaleString() } blocks</Text> <Text as="span">Total of { data.items[0].height.toLocaleString() } blocks</Text>
{ socketAlert && <Alert status="warning" mt={ 8 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show> <Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show>
<Hide below="lg" key="content-desktop"><BlocksTable data={ data.items }/></Hide> <Hide below="lg" key="content-desktop"><BlocksTable data={ data.items }/></Hide>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}> <Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
......
...@@ -42,7 +42,7 @@ const TransactionPageContent = () => { ...@@ -42,7 +42,7 @@ const TransactionPageContent = () => {
const { data } = useQuery<unknown, unknown, Transaction>( const { data } = useQuery<unknown, unknown, Transaction>(
[ 'tx', router.query.id ], [ 'tx', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }`), async() => await fetch(`/node-api/transactions/${ router.query.id }`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
}, },
......
...@@ -4,9 +4,11 @@ import React from 'react'; ...@@ -4,9 +4,11 @@ import React from 'react';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection'; import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import ScrollDirectionContext from 'ui/ScrollDirectionContext'; import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
...@@ -36,6 +38,7 @@ const Page = ({ ...@@ -36,6 +38,7 @@ const Page = ({
) : children; ) : children;
return ( return (
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2/websocket?vsn=2.0.0` }>
<ScrollDirectionContext.Provider value={ directionContext }> <ScrollDirectionContext.Provider value={ directionContext }>
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/> <NavigationDesktop/>
...@@ -45,6 +48,7 @@ const Page = ({ ...@@ -45,6 +48,7 @@ const Page = ({
</Flex> </Flex>
</Flex> </Flex>
</ScrollDirectionContext.Provider> </ScrollDirectionContext.Provider>
</SocketProvider>
); );
}; };
......
import { Grid, GridItem, Text, Box, Icon, Link, Spinner, Tag, Flex, Tooltip, chakra } from '@chakra-ui/react'; import { Grid, GridItem, Text, Box, Icon, Link, Spinner, Tag, Flex, Tooltip, chakra } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import type { Transaction } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
...@@ -15,7 +10,6 @@ import errorIcon from 'icons/status/error.svg'; ...@@ -15,7 +10,6 @@ import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg'; import successIcon from 'icons/status/success.svg';
import { WEI, WEI_IN_GWEI } from 'lib/consts'; import { WEI, WEI_IN_GWEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration'; import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
...@@ -28,13 +22,14 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -28,13 +22,14 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
// import PrevNext from 'ui/shared/PrevNext'; // import PrevNext from 'ui/shared/PrevNext';
import RawInputData from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
// import TokenSnippet from 'ui/shared/TokenSnippet';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton'; import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TokenTransferList from 'ui/tx/TokenTransferList'; import TokenTransferList from 'ui/tx/TokenTransferList';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData'; import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TOKEN_TRANSFERS = [ const TOKEN_TRANSFERS = [
{ title: 'Tokens Transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' }, { title: 'Tokens Transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
...@@ -44,16 +39,7 @@ const TOKEN_TRANSFERS = [ ...@@ -44,16 +39,7 @@ const TOKEN_TRANSFERS = [
]; ];
const TxDetails = () => { const TxDetails = () => {
const router = useRouter(); const { data, isLoading, isError, socketStatus } = useFetchTxInfo();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Transaction>(
[ QueryKeys.tx, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
...@@ -87,6 +73,11 @@ const TxDetails = () => { ...@@ -87,6 +73,11 @@ const TxDetails = () => {
return ( return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
{ socketStatus && (
<GridItem colSpan={{ base: undefined, lg: 2 }} mb={ 2 }>
<TxSocketAlert status={ socketStatus }/>
</GridItem>
) }
<DetailsInfoItem <DetailsInfoItem
title="Transaction hash" title="Transaction hash"
hint="Unique character string (TxID) assigned to every verified transaction." hint="Unique character string (TxID) assigned to every verified transaction."
......
...@@ -6,6 +6,7 @@ import React from 'react'; ...@@ -6,6 +6,7 @@ import React from 'react';
import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
...@@ -18,6 +19,9 @@ import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDeskt ...@@ -18,6 +19,9 @@ import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDeskt
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile'; import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
import TxInternalsTable from 'ui/tx/internals/TxInternalsTable'; import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
import type { Sort, SortField } from 'ui/tx/internals/utils'; import type { Sort, SortField } from 'ui/tx/internals/utils';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = { const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
value: [ 'value-desc', 'value-asc', undefined ], value: [ 'value-desc', 'value-asc', undefined ],
...@@ -72,11 +76,12 @@ const TxInternals = () => { ...@@ -72,11 +76,12 @@ const TxInternals = () => {
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]); const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>(''); const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>(); const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ QueryKeys.txInternals, router.query.id ], [ QueryKeys.txInternals, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/internal-transactions`), async() => await fetch(`/node-api/transactions/${ router.query.id }/internal-transactions`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
}, },
); );
...@@ -92,7 +97,11 @@ const TxInternals = () => { ...@@ -92,7 +97,11 @@ const TxInternals = () => {
}; };
}, []); }, []);
if (isLoading) { if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
if (isLoading || txInfo.isLoading) {
return ( return (
<> <>
<Show below="lg"><TxInternalsSkeletonMobile/></Show> <Show below="lg"><TxInternalsSkeletonMobile/></Show>
...@@ -101,7 +110,7 @@ const TxInternals = () => { ...@@ -101,7 +110,7 @@ const TxInternals = () => {
); );
} }
if (isError) { if (isError || txInfo.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -6,28 +6,37 @@ import React from 'react'; ...@@ -6,28 +6,37 @@ import React from 'react';
import type { LogsResponse } from 'types/api/log'; import type { LogsResponse } from 'types/api/log';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxLogItem from 'ui/tx/logs/TxLogItem'; import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton'; import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => { const TxLogs = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch(); const fetch = useFetch();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ QueryKeys.txLog, router.query.id ], [ QueryKeys.txLog, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/logs`), async() => await fetch(`/node-api/transactions/${ router.query.id }/logs`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
}, },
); );
if (isError) { if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
if (isError || txInfo.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading) { if (isLoading || txInfo.isLoading) {
return ( return (
<Box> <Box>
<TxLogSkeleton/> <TxLogSkeleton/>
......
import { Alert, Spinner } from '@chakra-ui/react';
import React from 'react';
const TxPendingAlert = () => {
return (
<Alert>
<Spinner size="sm" mr={ 2 }/>
This transaction is pending confirmation.
</Alert>
);
};
export default TxPendingAlert;
...@@ -6,27 +6,36 @@ import React from 'react'; ...@@ -6,27 +6,36 @@ import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => { const TxRawTrace = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch(); const fetch = useFetch();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>(
[ QueryKeys.txRawTrace, router.query.id ], [ QueryKeys.txRawTrace, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/raw-trace`), async() => await fetch(`/node-api/transactions/${ router.query.id }/raw-trace`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
}, },
); );
if (isError) { if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
if (isError || txInfo.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading) { if (isLoading || txInfo.isLoading) {
return ( return (
<> <>
<Flex justifyContent="end" mb={ 2 }> <Flex justifyContent="end" mb={ 2 }>
......
import { Alert } from '@chakra-ui/react';
import React from 'react';
interface Props {
status: 'error' | 'close';
}
const TxSocketAlert = ({ status }: Props) => {
const text = status === 'close' ?
'Connection is lost. Please click here to update transaction info.' :
'An error has occurred while fetching transaction info. Please click here to update.';
return <Alert status="warning" as="a" href={ window.document.location.href }>{ text }</Alert>;
};
export default React.memo(TxSocketAlert);
import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react'; import { Tr, Td, Tag, Icon, Box, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -19,12 +19,14 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -19,12 +19,14 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
<Td> <Td>
<Flex rowGap={ 2 } flexWrap="wrap">
{ typeTitle && ( { typeTitle && (
<Box w="126px" display="inline-block"> <Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag> <Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
</Box> </Box>
) } ) }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
</Td> </Td>
<Td> <Td>
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
......
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Transaction } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import delay from 'lib/delay';
import useFetch from 'lib/hooks/useFetch';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
interface Params {
onTxStatusUpdate?: () => void;
updateDelay?: number;
}
type ReturnType = UseQueryResult<Transaction, unknown> & {
socketStatus: 'close' | 'error' | undefined;
}
export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params | undefined = {}): ReturnType {
const router = useRouter();
const fetch = useFetch();
const queryClient = useQueryClient();
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const queryResult = useQuery<unknown, unknown, Transaction>(
[ QueryKeys.tx, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
refetchOnMount: false,
},
);
const { data, isError, isLoading } = queryResult;
const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => {
updateDelay && await delay(updateDelay);
queryClient.invalidateQueries({ queryKey: [ QueryKeys.tx, router.query.id ] });
onTxStatusUpdate?.();
}, [ onTxStatusUpdate, queryClient, router.query.id, updateDelay ]);
const handleSocketClose = React.useCallback(() => {
setSocketStatus('close');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketStatus('error');
}, []);
const channel = useSocketChannel({
topic: `transactions:${ router.query.id }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isLoading || isError || data.status !== null,
});
useSocketMessage({
channel,
event: 'collated',
handler: handleStatusUpdateMessage,
});
return {
...queryResult,
socketStatus,
};
}
...@@ -1916,6 +1916,11 @@ ...@@ -1916,6 +1916,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/phoenix@^1.5.4":
version "1.5.4"
resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.5.4.tgz#c08a1da6d7b4e365f6a1fe1ff9aada55f5356d24"
integrity sha512-L5eZmzw89eXBKkiqVBcJfU1QGx9y+wurRIEgt0cuLH0hwNtVUxtx+6cu0R2STwWj468sjXyBYPYDtGclUd1kjQ==
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.5" version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
...@@ -4471,6 +4476,11 @@ path-type@^4.0.0: ...@@ -4471,6 +4476,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
phoenix@^1.6.15:
version "1.6.15"
resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.6.15.tgz#efb2088a310cde333b3762002831b79dedf76002"
integrity sha512-O6AG5jTkZOOkdd/GOSCsM4v3bzBoyRnC5bEi57KhX/Daba6FvnBRzt0nhEeRRiVQGLSxDlyb0dUe9CkYWMZd8g==
picocolors@^1.0.0: picocolors@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment