Commit 5d908544 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Graceful service degradation: tx details tab (#1525)

* display main data

* display rest of tx fields

* placeholder data

* always show tabs

* manage error and retries on details tab

* don't retry if degraded view is active

* fix tests

* fix tx receipt

* change styles and wording for warning

* tweaks

* gray alert style change and update screenshots

* add retries to RPC call

* fixes after merge
parent f6ccec07
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
<path fill="currentColor" d="M22.576 11.994a1.212 1.212 0 0 0 0-2.424H9.14L10.703 8a1.212 1.212 0 0 0-1.709-1.709L5.358 9.928a1.212 1.212 0 0 0-.267 1.32 1.212 1.212 0 0 0 1.121.746h16.364Z"/> <path fill="currentColor" d="M22.576 11.994a1.212 1.212 0 0 0 0-2.424H9.14L10.703 8a1.212 1.212 0 0 0-1.709-1.709L5.358 9.928a1.212 1.212 0 0 0-.267 1.32 1.212 1.212 0 0 0 1.121.746h16.364Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M15.859 18.661c.091-.363.14-.754.14-1.161 0-.445-.059-.871-.167-1.263h7.955a1.213 1.213 0 0 1 1.121.745 1.212 1.212 0 0 1-.267 1.321l-3.636 3.637a1.214 1.214 0 0 1-2.049-.347 1.212 1.212 0 0 1 .34-1.362l1.564-1.57h-5.001ZM12.226 20.523a3.636 3.636 0 0 0 1.275-1.83 3.615 3.615 0 0 0-.035-2.225 3.638 3.638 0 0 0-1.334-1.787 3.675 3.675 0 0 0-4.264 0 3.638 3.638 0 0 0-1.333 1.787 3.615 3.615 0 0 0-.036 2.226 3.636 3.636 0 0 0 1.275 1.829 5.482 5.482 0 0 0-2.714 2.606.588.588 0 0 0 .038.583.593.593 0 0 0 .51.288h1.413C7.2 22.76 8.465 21.8 10 21.8c1.535 0 2.8.96 2.979 2.2h1.412a.599.599 0 0 0 .511-.288.588.588 0 0 0 .038-.583 5.482 5.482 0 0 0-2.714-2.606ZM11.7 17.5a1.7 1.7 0 1 1-3.4 0 1.7 1.7 0 0 1 3.4 0Z" clip-rule="evenodd"/> <path fill="currentColor" fill-rule="evenodd" d="M15.859 18.661c.091-.363.14-.754.14-1.161 0-.445-.059-.871-.167-1.263h7.955a1.213 1.213 0 0 1 1.121.745 1.212 1.212 0 0 1-.267 1.321l-3.636 3.637a1.214 1.214 0 0 1-2.049-.347 1.212 1.212 0 0 1 .34-1.362l1.564-1.57h-5.001Zm-3.633 1.862a3.636 3.636 0 0 0 1.275-1.83 3.615 3.615 0 0 0-.035-2.225 3.638 3.638 0 0 0-1.334-1.787 3.675 3.675 0 0 0-4.264 0 3.638 3.638 0 0 0-1.333 1.787 3.615 3.615 0 0 0-.036 2.226 3.636 3.636 0 0 0 1.275 1.829 5.482 5.482 0 0 0-2.714 2.606.588.588 0 0 0 .038.583.593.593 0 0 0 .51.288h1.413C7.2 22.76 8.465 21.8 10 21.8c1.535 0 2.8.96 2.979 2.2h1.412a.599.599 0 0 0 .511-.288.588.588 0 0 0 .038-.583 5.482 5.482 0 0 0-2.714-2.606ZM11.7 17.5a1.7 1.7 0 1 1-3.4 0 1.7 1.7 0 0 1 3.4 0Z" clip-rule="evenodd"/>
</svg> </svg>
...@@ -4,12 +4,7 @@ import React from 'react'; ...@@ -4,12 +4,7 @@ import React from 'react';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
export default function useQueryClientConfig() { export const retry = (failureCount: number, error: unknown) => {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
const errorPayload = getErrorObjPayload<{ status: number }>(error); const errorPayload = getErrorObjPayload<{ status: number }>(error);
const status = errorPayload?.status || getErrorObjStatusCode(error); const status = errorPayload?.status || getErrorObjStatusCode(error);
if (status && status >= 400 && status < 500) { if (status && status >= 400 && status < 500) {
...@@ -17,7 +12,14 @@ export default function useQueryClientConfig() { ...@@ -17,7 +12,14 @@ export default function useQueryClientConfig() {
return false; return false;
} }
return failureCount < 2; return failureCount < 2;
}, };
export default function useQueryClientConfig() {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry,
throwOnError: (error) => { throwOnError: (error) => {
const status = getErrorObjStatusCode(error); const status = getErrorObjStatusCode(error);
// don't catch error for "Too many requests" response // don't catch error for "Too many requests" response
......
export default function hetToDecimal(hex: string) {
const strippedHex = hex.startsWith('0x') ? hex.slice(2) : hex;
return parseInt(strippedHex, 16);
}
import { createPublicClient, http } from 'viem';
import currentChain from './currentChain';
export const publicClient = createPublicClient({
chain: currentChain,
transport: http(),
batch: {
multicall: true,
},
});
import type { Chain } from 'wagmi';
import config from 'configs/app';
const currentChain: Chain = {
id: Number(config.chain.id),
name: config.chain.name ?? '',
network: config.chain.name ?? '',
nativeCurrency: {
decimals: config.chain.currency.decimals,
name: config.chain.currency.name ?? '',
symbol: config.chain.currency.symbol ?? '',
},
rpcUrls: {
'public': {
http: [ config.chain.rpcUrl ?? '' ],
},
'default': {
http: [ config.chain.rpcUrl ?? '' ],
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: config.app.baseUrl,
},
},
};
export default currentChain;
import type { Chain, GetBlockReturnType, GetTransactionReturnType, TransactionReceipt } from 'viem';
import { ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx';
export const GET_TRANSACTION: GetTransactionReturnType<Chain, 'latest'> = {
blockHash: BLOCK_HASH,
blockNumber: BigInt(10361367),
from: ADDRESS_HASH,
gas: BigInt(800000),
maxPriorityFeePerGas: BigInt(2),
maxFeePerGas: BigInt(14),
hash: TX_HASH,
input: '0x7898e0',
nonce: 117694,
to: ADDRESS_HASH,
transactionIndex: 60,
value: BigInt(42),
type: 'eip1559',
accessList: [],
chainId: 5,
v: BigInt(0),
r: '0x2c5022ff7f78a22f1a99afbd568f75cb52812189ed8c264c8310e0b8dba2c8a8',
s: '0x50938f87c92b9eeb9777507ca8f7397840232d00d1dbac3edac6c115b4656763',
yParity: 1,
typeHex: '0x2',
};
export const GET_TRANSACTION_RECEIPT: TransactionReceipt = {
blockHash: BLOCK_HASH,
blockNumber: BigInt(10361367),
contractAddress: null,
cumulativeGasUsed: BigInt(39109),
effectiveGasPrice: BigInt(13),
from: ADDRESS_HASH,
gasUsed: BigInt(39109),
logs: [],
logsBloom: '0x0',
status: 'success',
to: ADDRESS_HASH,
transactionHash: TX_HASH,
transactionIndex: 60,
type: '0x2',
};
export const GET_TRANSACTION_CONFIRMATIONS = BigInt(420);
export const GET_BLOCK: GetBlockReturnType<Chain, false, 'latest'> = {
baseFeePerGas: BigInt(11),
difficulty: BigInt(111),
extraData: '0xd8830',
gasLimit: BigInt(800000),
gasUsed: BigInt(42000),
hash: BLOCK_HASH,
logsBloom: '0x008000',
miner: ADDRESS_HASH,
mixHash: BLOCK_HASH,
nonce: '0x0000000000000000',
number: BigInt(10361367),
parentHash: BLOCK_HASH,
receiptsRoot: BLOCK_HASH,
sha3Uncles: BLOCK_HASH,
size: BigInt(88),
stateRoot: BLOCK_HASH,
timestamp: BigInt(1628580000),
totalDifficulty: BigInt(10361367),
transactions: [
TX_HASH,
],
transactionsRoot: TX_HASH,
uncles: [],
withdrawals: [ ],
withdrawalsRoot: TX_HASH,
sealFields: [ '0x00' ],
};
...@@ -56,10 +56,10 @@ const variantSubtle = definePartsStyle((props) => { ...@@ -56,10 +56,10 @@ const variantSubtle = definePartsStyle((props) => {
return { return {
container: { container: {
[$fg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.800' : `colors.${ colorScheme }.500`, [$fg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.800' : `colors.${ colorScheme }.500`,
[$bg.variable]: colorScheme === 'gray' ? 'colors.gray.100' : bg.light, [$bg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.100' : bg.light,
_dark: { _dark: {
[$fg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.800' : `colors.${ colorScheme }.200`, [$fg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.800' : `colors.${ colorScheme }.200`,
[$bg.variable]: colorScheme === 'gray' ? 'colors.gray.800' : bg.dark, [$bg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.200' : bg.dark,
}, },
}, },
}; };
......
...@@ -29,11 +29,11 @@ export type Transaction = { ...@@ -29,11 +29,11 @@ export type Transaction = {
status: 'ok' | 'error' | null; status: 'ok' | 'error' | null;
block: number | null; block: number | null;
timestamp: string | null; timestamp: string | null;
confirmation_duration: Array<number>; confirmation_duration: Array<number> | null;
from: AddressParam; from: AddressParam;
value: string; value: string;
fee: Fee; fee: Fee;
gas_price: string; gas_price: string | null;
type: number | null; type: number | null;
gas_used: string | null; gas_used: string | null;
gas_limit: string; gas_limit: string;
...@@ -49,7 +49,7 @@ export type Transaction = { ...@@ -49,7 +49,7 @@ export type Transaction = {
decoded_input: DecodedInput | null; decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null; token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean; token_transfers_overflow: boolean;
exchange_rate: string; exchange_rate: string | null;
method: string | null; method: string | null;
tx_types: Array<TransactionType>; tx_types: Array<TransactionType>;
tx_tag: string | null; tx_tag: string | null;
......
...@@ -4,10 +4,9 @@ import React from 'react'; ...@@ -4,10 +4,9 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags'; import EntityTags from 'ui/shared/EntityTags';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -15,6 +14,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; ...@@ -15,6 +14,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded';
import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped'; import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
...@@ -23,37 +23,42 @@ import TxState from 'ui/tx/TxState'; ...@@ -23,37 +23,42 @@ import TxState from 'ui/tx/TxState';
import TxSubHeading from 'ui/tx/TxSubHeading'; import TxSubHeading from 'ui/tx/TxSubHeading';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
import TxUserOps from 'ui/tx/TxUserOps'; import TxUserOps from 'ui/tx/TxUserOps';
import useTxQuery from 'ui/tx/useTxQuery';
const TransactionPageContent = () => { const TransactionPageContent = () => {
const router = useRouter(); const router = useRouter();
const appProps = useAppContext(); const appProps = useAppContext();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const txQuery = useTxQuery();
const { data, isPlaceholderData, isError, error, errorUpdateCount } = txQuery;
const { data, isPlaceholderData } = useApiQuery('tx', { const showDegradedView = (isError || isPlaceholderData) && errorUpdateCount > 0;
pathParams: { hash },
queryOptions: { const tabs: Array<RoutedTab> = (() => {
enabled: Boolean(hash), const detailsComponent = showDegradedView ?
placeholderData: TX, <TxDetailsDegraded hash={ hash } txQuery={ txQuery }/> :
}, <TxDetails txQuery={ txQuery }/>;
});
const tabs: Array<RoutedTab> = [ return [
{ {
id: 'index', id: 'index',
title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details',
component: <TxDetails/>, component: detailsComponent,
}, },
config.features.suave.isEnabled && data?.wrapped ? config.features.suave.isEnabled && data?.wrapped ?
{ id: 'wrapped', title: 'Regular tx details', component: <TxDetailsWrapped data={ data.wrapped }/> } : { id: 'wrapped', title: 'Regular tx details', component: <TxDetailsWrapped data={ data.wrapped }/> } :
undefined, undefined,
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> }, { id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer txQuery={ txQuery }/> },
config.features.userOps.isEnabled ? { id: 'user_ops', title: 'User operations', component: <TxUserOps/> } : undefined, config.features.userOps.isEnabled ?
{ id: 'internal', title: 'Internal txns', component: <TxInternals/> }, { id: 'user_ops', title: 'User operations', component: <TxUserOps txQuery={ txQuery }/> } :
{ id: 'logs', title: 'Logs', component: <TxLogs/> }, undefined,
{ id: 'state', title: 'State', component: <TxState/> }, { id: 'internal', title: 'Internal txns', component: <TxInternals txQuery={ txQuery }/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> }, { id: 'logs', title: 'Logs', component: <TxLogs txQuery={ txQuery }/> },
{ id: 'state', title: 'State', component: <TxState txQuery={ txQuery }/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace txQuery={ txQuery }/> },
].filter(Boolean); ].filter(Boolean);
})();
const tabIndex = useTabIndexFromQuery(tabs); const tabIndex = useTabIndexFromQuery(tabs);
...@@ -79,6 +84,23 @@ const TransactionPageContent = () => { ...@@ -79,6 +84,23 @@ const TransactionPageContent = () => {
const titleSecondRow = <TxSubHeading hash={ hash } hasTag={ Boolean(data?.tx_tag) }/>; const titleSecondRow = <TxSubHeading hash={ hash } hasTag={ Boolean(data?.tx_tag) }/>;
const content = (() => {
if (isPlaceholderData && !showDegradedView) {
return (
<>
<TabsSkeleton tabs={ tabs } mt={ 6 }/>
{ tabs[tabIndex]?.component }
</>
);
}
return <RoutedTabs tabs={ tabs }/>;
})();
if (error?.status === 422) {
throwOnResourceLoadError({ resource: 'tx', error, isError: true });
}
return ( return (
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
...@@ -88,12 +110,7 @@ const TransactionPageContent = () => { ...@@ -88,12 +110,7 @@ const TransactionPageContent = () => {
contentAfter={ tags } contentAfter={ tags }
secondRow={ titleSecondRow } secondRow={ titleSecondRow }
/> />
{ isPlaceholderData ? ( { content }
<>
<TabsSkeleton tabs={ tabs } mt={ 6 }/>
{ tabs[tabIndex]?.component }
</>
) : <RoutedTabs tabs={ tabs }/> }
</> </>
); );
}; };
......
...@@ -8,6 +8,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; ...@@ -8,6 +8,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { USER_OP } from 'stubs/userOps'; import { USER_OP } from 'stubs/userOps';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
...@@ -18,6 +20,7 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; ...@@ -18,6 +20,7 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
import useTxQuery from 'ui/tx/useTxQuery';
import UserOpDetails from 'ui/userOp/UserOpDetails'; import UserOpDetails from 'ui/userOp/UserOpDetails';
import UserOpRaw from 'ui/userOp/UserOpRaw'; import UserOpRaw from 'ui/userOp/UserOpRaw';
...@@ -27,13 +30,15 @@ const UserOp = () => { ...@@ -27,13 +30,15 @@ const UserOp = () => {
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const userOpQuery = useApiQuery('user_op', { const userOpQuery = useApiQuery('user_op', {
pathParams: { hash: hash }, pathParams: { hash },
queryOptions: { queryOptions: {
enabled: Boolean(hash), enabled: Boolean(hash),
placeholderData: USER_OP, placeholderData: USER_OP,
}, },
}); });
const txQuery = useTxQuery({ hash: userOpQuery.data?.transaction_hash, isEnabled: !userOpQuery.isPlaceholderData });
const filterTokenTransfersByLogIndex = React.useCallback((tt: TokenTransfer) => { const filterTokenTransfersByLogIndex = React.useCallback((tt: TokenTransfer) => {
if (!userOpQuery.data) { if (!userOpQuery.data) {
return true; return true;
...@@ -61,22 +66,14 @@ const UserOp = () => { ...@@ -61,22 +66,14 @@ const UserOp = () => {
{ {
id: 'token_transfers', id: 'token_transfers',
title: 'Token transfers', title: 'Token transfers',
component: <TxTokenTransfer txHash={ userOpQuery.data?.transaction_hash } tokenTransferFilter={ filterTokenTransfersByLogIndex }/>, component: <TxTokenTransfer txQuery={ txQuery } tokenTransferFilter={ filterTokenTransfersByLogIndex }/>,
}, },
{ id: 'logs', title: 'Logs', component: <TxLogs txHash={ userOpQuery.data?.transaction_hash } logsFilter={ filterLogsByLogIndex }/> }, { id: 'logs', title: 'Logs', component: <TxLogs txQuery={ txQuery } logsFilter={ filterLogsByLogIndex }/> },
{ id: 'raw', title: 'Raw', component: <UserOpRaw rawData={ userOpQuery.data?.raw } isLoading={ userOpQuery.isPlaceholderData }/> }, { id: 'raw', title: 'Raw', component: <UserOpRaw rawData={ userOpQuery.data?.raw } isLoading={ userOpQuery.isPlaceholderData }/> },
]), [ userOpQuery, filterTokenTransfersByLogIndex, filterLogsByLogIndex ]); ]), [ userOpQuery, txQuery, filterTokenTransfersByLogIndex, filterLogsByLogIndex ]);
const tabIndex = useTabIndexFromQuery(tabs); const tabIndex = useTabIndexFromQuery(tabs);
if (!hash) {
throw new Error('User operation not found', { cause: { status: 404 } });
}
if (userOpQuery.isError) {
throw new Error(undefined, { cause: userOpQuery.error });
}
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/ops'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/ops');
...@@ -90,6 +87,9 @@ const UserOp = () => { ...@@ -90,6 +87,9 @@ const UserOp = () => {
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
throwOnAbsentParamError(hash);
throwOnResourceLoadError(userOpQuery);
const titleSecondRow = <UserOpEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } fontFamily="heading"/>; const titleSecondRow = <UserOpEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } fontFamily="heading"/>;
return ( return (
......
...@@ -18,7 +18,7 @@ const DetailsTimestamp = ({ timestamp, isLoading }: Props) => { ...@@ -18,7 +18,7 @@ const DetailsTimestamp = ({ timestamp, isLoading }: Props) => {
<Skeleton isLoaded={ !isLoading } ml={ 2 }> <Skeleton isLoaded={ !isLoading } ml={ 2 }>
{ dayjs(timestamp).fromNow() } { dayjs(timestamp).fromNow() }
</Skeleton> </Skeleton>
<TextSeparator/> <TextSeparator color="gray.500"/>
<Skeleton isLoaded={ !isLoading } whiteSpace="normal"> <Skeleton isLoaded={ !isLoading } whiteSpace="normal">
{ dayjs(timestamp).format('llll') } { dayjs(timestamp).format('llll') }
</Skeleton> </Skeleton>
......
...@@ -2,10 +2,10 @@ import { useColorMode } from '@chakra-ui/react'; ...@@ -2,10 +2,10 @@ import { useColorMode } from '@chakra-ui/react';
import { jsonRpcProvider } from '@wagmi/core/providers/jsonRpc'; import { jsonRpcProvider } from '@wagmi/core/providers/jsonRpc';
import { createWeb3Modal, useWeb3ModalTheme, defaultWagmiConfig } from '@web3modal/wagmi/react'; import { createWeb3Modal, useWeb3ModalTheme, defaultWagmiConfig } from '@web3modal/wagmi/react';
import React from 'react'; import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, WagmiConfig } from 'wagmi'; import { configureChains, WagmiConfig } from 'wagmi';
import config from 'configs/app'; import config from 'configs/app';
import currentChain from 'lib/web3/currentChain';
import colors from 'theme/foundations/colors'; import colors from 'theme/foundations/colors';
import { BODY_TYPEFACE } from 'theme/foundations/typography'; import { BODY_TYPEFACE } from 'theme/foundations/typography';
import zIndices from 'theme/foundations/zIndices'; import zIndices from 'theme/foundations/zIndices';
...@@ -18,31 +18,6 @@ const getConfig = () => { ...@@ -18,31 +18,6 @@ const getConfig = () => {
throw new Error(); throw new Error();
} }
const currentChain: Chain = {
id: Number(config.chain.id),
name: config.chain.name || '',
network: config.chain.name || '',
nativeCurrency: {
decimals: config.chain.currency.decimals,
name: config.chain.currency.name || '',
symbol: config.chain.currency.symbol || '',
},
rpcUrls: {
'public': {
http: [ config.chain.rpcUrl || '' ],
},
'default': {
http: [ config.chain.rpcUrl || '' ],
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: config.app.baseUrl,
},
},
};
const { chains } = configureChains( const { chains } = configureChains(
[ currentChain ], [ currentChain ],
[ [
......
import { Alert, Skeleton, Spinner, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
isLoading?: boolean;
className?: string;
}
const ServiceDegradationWarning = ({ isLoading, className }: Props) => {
return (
<Skeleton className={ className } isLoaded={ !isLoading }>
<Alert status="warning" colorScheme="gray" alignItems={{ base: 'flex-start', lg: 'center' }}>
<Spinner size="sm" mr={ 2 } my={{ base: '3px', lg: 0 }} flexShrink={ 0 }/>
Data sync in progress... page will refresh automatically once transaction data is available
</Alert>
</Skeleton>
);
};
export default React.memo(chakra(ServiceDegradationWarning));
import { Alert, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
interface Props {
isLoading?: boolean;
className?: string;
}
const TestnetWarning = ({ isLoading, className }: Props) => {
if (!config.chain.isTestnet) {
return null;
}
return (
<Skeleton className={ className } isLoaded={ !isLoading }>
<Alert status="warning">This is a testnet transaction only</Alert>
</Skeleton>
);
};
export default React.memo(chakra(TestnetWarning));
This diff is collapsed.
import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Chain, GetBlockReturnType, GetTransactionReturnType, TransactionReceipt } from 'viem';
import type { Transaction } from 'types/api/transaction';
import { SECOND } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import hexToDecimal from 'lib/hexToDecimal';
import { publicClient } from 'lib/web3/client';
import { GET_BLOCK, GET_TRANSACTION, GET_TRANSACTION_RECEIPT, GET_TRANSACTION_CONFIRMATIONS } from 'stubs/RPC';
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
import TestnetWarning from 'ui/shared/alerts/TestnetWarning';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxInfo from './details/TxInfo';
import type { TxQuery } from './useTxQuery';
type RpcResponseType = [
GetTransactionReturnType<Chain, 'latest'>,
TransactionReceipt | null,
bigint | null,
GetBlockReturnType<Chain, false, 'latest'> | null,
];
interface Props {
hash: string;
txQuery: TxQuery;
}
const TxDetailsDegraded = ({ hash, txQuery }: Props) => {
const [ originalError ] = React.useState(txQuery.error);
const query = useQuery<RpcResponseType, unknown, Transaction | null>({
queryKey: [ 'RPC', 'tx', { hash } ],
queryFn: async() => {
const tx = await publicClient.getTransaction({ hash: hash as `0x${ string }` });
if (!tx) {
throw new Error('Not found');
}
const txReceipt = await publicClient.getTransactionReceipt({ hash: hash as `0x${ string }` }).catch(() => null);
const block = await publicClient.getBlock({ blockHash: tx.blockHash }).catch(() => null);
const latestBlock = await publicClient.getBlock().catch(() => null);
const confirmations = latestBlock && block ? latestBlock.number - block.number + BigInt(1) : null;
return [
tx,
txReceipt,
confirmations,
block,
];
},
select: (response) => {
const [ tx, txReceipt, txConfirmations, block ] = response;
const status = (() => {
if (!txReceipt) {
return null;
}
return txReceipt.status === 'success' ? 'ok' : 'error';
})();
const gasPrice = txReceipt?.effectiveGasPrice ?? tx.gasPrice;
const unknownAddress = {
is_contract: false,
is_verified: false,
implementation_name: '',
name: '',
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
};
return {
from: { ...unknownAddress, hash: tx.from as string },
to: tx.to ? { ...unknownAddress, hash: tx.to as string } : null,
hash: tx.hash as string,
timestamp: block?.timestamp ? dayjs.unix(Number(block.timestamp)).format() : null,
confirmation_duration: null,
status,
block: tx.blockNumber ? Number(tx.blockNumber) : null,
value: tx.value.toString(),
gas_price: txReceipt?.effectiveGasPrice.toString() ?? tx.gasPrice?.toString() ?? null,
base_fee_per_gas: block?.baseFeePerGas?.toString() ?? null,
max_fee_per_gas: tx.maxFeePerGas?.toString() ?? null,
max_priority_fee_per_gas: tx.maxPriorityFeePerGas?.toString() ?? null,
nonce: tx.nonce,
position: tx.transactionIndex,
type: tx.typeHex ? hexToDecimal(tx.typeHex) : null,
raw_input: tx.input,
gas_used: txReceipt?.gasUsed?.toString() ?? null,
gas_limit: tx.gas.toString(),
confirmations: txConfirmations && txConfirmations > 0 ? Number(txConfirmations) : 0,
fee: {
value: txReceipt && gasPrice ? (txReceipt.gasUsed * gasPrice).toString() : null,
type: 'actual',
},
created_contract: txReceipt?.contractAddress ?
{ ...unknownAddress, hash: txReceipt.contractAddress, is_contract: true } :
null,
result: '',
priority_fee: null,
tx_burnt_fee: null,
revert_reason: null,
decoded_input: null,
has_error_in_internal_txs: null,
token_transfers: null,
token_transfers_overflow: false,
exchange_rate: null,
method: null,
tx_types: [],
tx_tag: null,
actions: [],
};
},
placeholderData: [
GET_TRANSACTION,
GET_TRANSACTION_RECEIPT,
GET_TRANSACTION_CONFIRMATIONS,
GET_BLOCK,
],
refetchOnMount: false,
enabled: !txQuery.isPlaceholderData,
retry: 2,
retryDelay: 5 * SECOND,
});
const hasData = Boolean(query.data);
React.useEffect(() => {
if (!query.isPlaceholderData && hasData) {
txQuery.setRefetchOnError.on();
}
}, [ hasData, query.isPlaceholderData, txQuery ]);
React.useEffect(() => {
return () => {
txQuery.setRefetchOnError.off();
};
}, [ txQuery.setRefetchOnError ]);
if (!query.data) {
if (originalError?.status === 404) {
throw Error('Not found', { cause: { status: 404 } as unknown as Error });
}
return <DataFetchAlert/>;
}
return (
<>
<Flex rowGap={ 2 } mb={ 6 } flexDir="column">
<TestnetWarning isLoading={ query.isPlaceholderData }/>
{ originalError?.status !== 404 && <ServiceDegradationWarning isLoading={ query.isPlaceholderData }/> }
</Flex>
<TxInfo data={ query.data } isLoading={ query.isPlaceholderData }/>
</>
);
};
export default React.memo(TxDetailsDegraded);
...@@ -7,9 +7,9 @@ import TestApp from 'playwright/TestApp'; ...@@ -7,9 +7,9 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxInternals from './TxInternals'; import TxInternals from './TxInternals';
import type { TxQuery } from './useTxQuery';
const TX_HASH = txMock.base.hash; const TX_HASH = txMock.base.hash;
const API_URL_TX = buildApiUrl('tx', { hash: TX_HASH });
const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { hash: TX_HASH }); const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { hash: TX_HASH });
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -18,18 +18,20 @@ const hooksConfig = { ...@@ -18,18 +18,20 @@ const hooksConfig = {
}; };
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL_TX, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({ await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(internalTxsMock.baseResponse), body: JSON.stringify(internalTxsMock.baseResponse),
})); }));
const txQuery = {
data: txMock.base,
isPlaceholderData: false,
isError: false,
} as TxQuery;
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxInternals/> <TxInternals txQuery={ txQuery }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -3,7 +3,6 @@ import React from 'react'; ...@@ -3,7 +3,6 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
import { SECOND } from 'lib/consts';
// import { apos } from 'lib/html-entities'; // import { apos } from 'lib/html-entities';
import { INTERNAL_TX } from 'stubs/internalTx'; import { INTERNAL_TX } from 'stubs/internalTx';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
...@@ -19,7 +18,8 @@ import TxInternalsTable from 'ui/tx/internals/TxInternalsTable'; ...@@ -19,7 +18,8 @@ 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 TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import type { TxQuery } from './useTxQuery';
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 ],
...@@ -62,17 +62,20 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT ...@@ -62,17 +62,20 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
// item.to.hash.toLowerCase().includes(formattedSearchTerm); // item.to.hash.toLowerCase().includes(formattedSearchTerm);
// }; // };
const TxInternals = () => { interface Props {
txQuery: TxQuery;
}
const TxInternals = ({ txQuery }: Props) => {
// filters are not implemented yet in api // filters are not implemented yet in api
// 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, isPlaceholderData, isError, pagination } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'tx_internal_txs', resourceName: 'tx_internal_txs',
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txQuery.data?.hash },
options: { options: {
enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.hash) && Boolean(txQuery.data?.status),
placeholderData: generateListStub<'tx_internal_txs'>(INTERNAL_TX, 3, { next_page_params: null }), placeholderData: generateListStub<'tx_internal_txs'>(INTERNAL_TX, 3, { next_page_params: null }),
}, },
}); });
...@@ -90,8 +93,8 @@ const TxInternals = () => { ...@@ -90,8 +93,8 @@ const TxInternals = () => {
}; };
}, [ isPlaceholderData ]); }, [ isPlaceholderData ]);
if (!txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data?.status) { if (!txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data?.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
} }
const filteredData = data?.items const filteredData = data?.items
...@@ -125,7 +128,7 @@ const TxInternals = () => { ...@@ -125,7 +128,7 @@ const TxInternals = () => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError || txInfo.isError } isError={ isError || txQuery.isError }
items={ data?.items } items={ data?.items }
emptyText="There are no internal transactions for this transaction." emptyText="There are no internal transactions for this transaction."
// filterProps={{ // filterProps={{
......
...@@ -3,7 +3,6 @@ import React from 'react'; ...@@ -3,7 +3,6 @@ import React from 'react';
import type { Log } from 'types/api/log'; import type { Log } from 'types/api/log';
import { SECOND } from 'lib/consts';
import { LOG } from 'stubs/log'; import { LOG } from 'stubs/log';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
...@@ -13,29 +12,29 @@ import Pagination from 'ui/shared/pagination/Pagination'; ...@@ -13,29 +12,29 @@ import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
type Props = { import type { TxQuery } from './useTxQuery';
txHash?: string;
interface Props {
txQuery: TxQuery;
logsFilter?: (log: Log) => boolean; logsFilter?: (log: Log) => boolean;
} }
const TxLogs = ({ txHash, logsFilter }: Props) => { const TxLogs = ({ txQuery, logsFilter }: Props) => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND, txHash });
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'tx_logs', resourceName: 'tx_logs',
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txQuery.data?.hash },
options: { options: {
enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.hash) && Boolean(txQuery.data?.status),
placeholderData: generateListStub<'tx_logs'>(LOG, 3, { next_page_params: null }), placeholderData: generateListStub<'tx_logs'>(LOG, 3, { next_page_params: null }),
}, },
}); });
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) { if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
} }
if (isError || txInfo.isError) { if (isError || txQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -5,7 +5,6 @@ import type { SocketMessage } from 'lib/socket/types'; ...@@ -5,7 +5,6 @@ import type { SocketMessage } from 'lib/socket/types';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -14,19 +13,23 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; ...@@ -14,19 +13,23 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => { import type { TxQuery } from './useTxQuery';
interface Props {
txQuery: TxQuery;
}
const TxRawTrace = ({ txQuery }: Props) => {
const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
const [ rawTraces, setRawTraces ] = React.useState<RawTracesResponse>(); const [ rawTraces, setRawTraces ] = React.useState<RawTracesResponse>();
const router = useRouter(); const router = useRouter();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isPlaceholderData, isError } = useApiQuery('tx_raw_trace', { const { data, isPlaceholderData, isError } = useApiQuery('tx_raw_trace', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { queryOptions: {
enabled: Boolean(hash) && Boolean(txInfo.data?.status) && isQueryEnabled, enabled: Boolean(hash) && Boolean(txQuery.data?.status) && isQueryEnabled,
placeholderData: TX_RAW_TRACE, placeholderData: TX_RAW_TRACE,
}, },
}); });
...@@ -39,7 +42,7 @@ const TxRawTrace = () => { ...@@ -39,7 +42,7 @@ const TxRawTrace = () => {
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `transactions:${ hash }`, topic: `transactions:${ hash }`,
isDisabled: !hash || txInfo.isPlaceholderData || !txInfo.data?.status, isDisabled: !hash || txQuery.isPlaceholderData || !txQuery.data?.status,
onJoin: enableQuery, onJoin: enableQuery,
onSocketError: enableQuery, onSocketError: enableQuery,
}); });
...@@ -49,11 +52,11 @@ const TxRawTrace = () => { ...@@ -49,11 +52,11 @@ const TxRawTrace = () => {
handler: handleRawTraceMessage, handler: handleRawTraceMessage,
}); });
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) { if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
} }
if (isError || txInfo.isError) { if (isError || txQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -7,8 +7,8 @@ import TestApp from 'playwright/TestApp'; ...@@ -7,8 +7,8 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxState from './TxState'; import TxState from './TxState';
import type { TxQuery } from './useTxQuery';
const TX_INFO_API_URL = buildApiUrl('tx', { hash: txMock.base.hash });
const TX_STATE_API_URL = buildApiUrl('tx_state_changes', { hash: txMock.base.hash }); const TX_STATE_API_URL = buildApiUrl('tx_state_changes', { hash: txMock.base.hash });
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -17,18 +17,20 @@ const hooksConfig = { ...@@ -17,18 +17,20 @@ const hooksConfig = {
}; };
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ mount, page }) => {
await page.route(TX_INFO_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
await page.route(TX_STATE_API_URL, (route) => route.fulfill({ await page.route(TX_STATE_API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(txStateMock.baseResponse), body: JSON.stringify(txStateMock.baseResponse),
})); }));
const txQuery = {
data: txMock.base,
isPlaceholderData: false,
isError: false,
} as TxQuery;
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxState/> <TxState txQuery={ txQuery }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
import { Accordion, Hide, Show, Text } from '@chakra-ui/react'; import { Accordion, Hide, Show, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { SECOND } from 'lib/consts';
import { TX_STATE_CHANGES } from 'stubs/txStateChanges'; import { TX_STATE_CHANGES } from 'stubs/txStateChanges';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
...@@ -9,18 +8,21 @@ import Pagination from 'ui/shared/pagination/Pagination'; ...@@ -9,18 +8,21 @@ import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxStateList from 'ui/tx/state/TxStateList'; import TxStateList from 'ui/tx/state/TxStateList';
import TxStateTable from 'ui/tx/state/TxStateTable'; import TxStateTable from 'ui/tx/state/TxStateTable';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import TxPendingAlert from './TxPendingAlert'; import TxPendingAlert from './TxPendingAlert';
import TxSocketAlert from './TxSocketAlert'; import TxSocketAlert from './TxSocketAlert';
import type { TxQuery } from './useTxQuery';
const TxState = () => { interface Props {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); txQuery: TxQuery;
}
const TxState = ({ txQuery }: Props) => {
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'tx_state_changes', resourceName: 'tx_state_changes',
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txQuery.data?.hash },
options: { options: {
enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.hash) && Boolean(txQuery.data?.status),
placeholderData: { placeholderData: {
items: TX_STATE_CHANGES, items: TX_STATE_CHANGES,
next_page_params: { next_page_params: {
...@@ -31,8 +33,8 @@ const TxState = () => { ...@@ -31,8 +33,8 @@ const TxState = () => {
}, },
}); });
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) { if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
} }
const content = data ? ( const content = data ? (
...@@ -54,12 +56,14 @@ const TxState = () => { ...@@ -54,12 +56,14 @@ const TxState = () => {
return ( return (
<> <>
{ !isError && !txQuery.isError && (
<Text mb={ 6 }> <Text mb={ 6 }>
A set of information that represents the current state is updated when a transaction takes place on the network. A set of information that represents the current state is updated when a transaction takes place on the network.
The below is a summary of those changes. The below is a summary of those changes.
</Text> </Text>
) }
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError || txQuery.isError }
items={ data?.items } items={ data?.items }
emptyText="There are no state changes for this transaction." emptyText="There are no state changes for this transaction."
content={ content } content={ content }
......
...@@ -5,7 +5,6 @@ import React from 'react'; ...@@ -5,7 +5,6 @@ import React from 'react';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import { SECOND } from 'lib/consts';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
...@@ -20,27 +19,26 @@ import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; ...@@ -20,27 +19,26 @@ import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import type { TxQuery } from './useTxQuery';
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPE_IDS); const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPE_IDS);
type Props = { interface Props {
txHash?: string; txQuery: TxQuery;
tokenTransferFilter?: (tt: TokenTransfer) => boolean; tokenTransferFilter?: (data: TokenTransfer) => boolean;
} }
const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => { const TxTokenTransfer = ({ txQuery, tokenTransferFilter }: Props) => {
const txsInfo = useFetchTxInfo({ updateDelay: 5 * SECOND, txHash });
const router = useRouter(); const router = useRouter();
const [ typeFilter, setTypeFilter ] = React.useState<Array<TokenType>>(getTokenFilterValue(router.query.type) || []); const [ typeFilter, setTypeFilter ] = React.useState<Array<TokenType>>(getTokenFilterValue(router.query.type) || []);
const tokenTransferQuery = useQueryWithPages({ const tokenTransferQuery = useQueryWithPages({
resourceName: 'tx_token_transfers', resourceName: 'tx_token_transfers',
pathParams: { hash: txsInfo.data?.hash.toString() }, pathParams: { hash: txQuery.data?.hash.toString() },
options: { options: {
enabled: !txsInfo.isPlaceholderData && Boolean(txsInfo.data?.status && txsInfo.data?.hash), enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.status && txQuery.data?.hash),
placeholderData: getTokenTransfersStub(), placeholderData: getTokenTransfersStub(),
}, },
filters: { type: typeFilter }, filters: { type: typeFilter },
...@@ -51,11 +49,11 @@ const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => { ...@@ -51,11 +49,11 @@ const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => {
setTypeFilter(nextValue); setTypeFilter(nextValue);
}, [ tokenTransferQuery ]); }, [ tokenTransferQuery ]);
if (!txsInfo.isPending && !txsInfo.isPlaceholderData && !txsInfo.isError && !txsInfo.data.status) { if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>; return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
} }
if (txsInfo.isError || tokenTransferQuery.isError) { if (txQuery.isError || tokenTransferQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -97,7 +95,7 @@ const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => { ...@@ -97,7 +95,7 @@ const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ txsInfo.isError || tokenTransferQuery.isError } isError={ txQuery.isError || tokenTransferQuery.isError }
items={ items } items={ items }
emptyText="There are no token transfers." emptyText="There are no token transfers."
filterProps={{ filterProps={{
......
import React from 'react'; import React from 'react';
import { SECOND } from 'lib/consts';
import { USER_OPS_ITEM } from 'stubs/userOps'; import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import UserOpsContent from 'ui/userOps/UserOpsContent'; import UserOpsContent from 'ui/userOps/UserOpsContent';
const TxUserOps = () => { import type { TxQuery } from './useTxQuery';
const txsInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
interface Props {
txQuery: TxQuery;
}
const TxUserOps = ({ txQuery }: Props) => {
const userOpsQuery = useQueryWithPages({ const userOpsQuery = useQueryWithPages({
resourceName: 'user_ops', resourceName: 'user_ops',
options: { options: {
enabled: !txsInfo.isPlaceholderData && Boolean(txsInfo.data?.status && txsInfo.data?.hash), enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.status && txQuery.data?.hash),
// most often there is only one user op in one tx // most often there is only one user op in one tx
placeholderData: generateListStub<'user_ops'>(USER_OPS_ITEM, 1, { next_page_params: null }), placeholderData: generateListStub<'user_ops'>(USER_OPS_ITEM, 1, { next_page_params: null }),
}, },
filters: { transaction_hash: txsInfo.data?.hash }, filters: { transaction_hash: txQuery.data?.hash },
}); });
if (!txsInfo.isPending && !txsInfo.isPlaceholderData && !txsInfo.isError && !txsInfo.data.status) { if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>; return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
} }
return <UserOpsContent query={ userOpsQuery } showTx={ false }/>; return <UserOpsContent query={ userOpsQuery } showTx={ false }/>;
......
...@@ -8,12 +8,12 @@ import { currencyUnits } from 'lib/units'; ...@@ -8,12 +8,12 @@ import { currencyUnits } from 'lib/units';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
interface Props { interface Props {
gasPrice: string; gasPrice: string | null;
isLoading?: boolean; isLoading?: boolean;
} }
const TxDetailsGasPrice = ({ gasPrice, isLoading }: Props) => { const TxDetailsGasPrice = ({ gasPrice, isLoading }: Props) => {
if (config.UI.views.tx.hiddenFields?.gas_price) { if (config.UI.views.tx.hiddenFields?.gas_price || !gasPrice) {
return null; return null;
} }
......
...@@ -4,12 +4,10 @@ import React from 'react'; ...@@ -4,12 +4,10 @@ import React from 'react';
import * as txMock from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
import TxDetails from './TxDetails'; import TxInfo from './TxInfo';
const API_URL = buildApiUrl('tx', { hash: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: 1 }, query: { hash: 1 },
...@@ -17,14 +15,9 @@ const hooksConfig = { ...@@ -17,14 +15,9 @@ const hooksConfig = {
}; };
test('between addresses +@mobile +@dark-mode', async({ mount, page }) => { test('between addresses +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.base } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -38,14 +31,9 @@ test('between addresses +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -38,14 +31,9 @@ test('between addresses +@mobile +@dark-mode', async({ mount, page }) => {
}); });
test('creating contact', async({ mount, page }) => { test('creating contact', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withContractCreation),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.withContractCreation } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -57,14 +45,9 @@ test('creating contact', async({ mount, page }) => { ...@@ -57,14 +45,9 @@ test('creating contact', async({ mount, page }) => {
}); });
test('with token transfer +@mobile', async({ mount, page }) => { test('with token transfer +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withTokenTransfer),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.withTokenTransfer } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -76,14 +59,9 @@ test('with token transfer +@mobile', async({ mount, page }) => { ...@@ -76,14 +59,9 @@ test('with token transfer +@mobile', async({ mount, page }) => {
}); });
test('with decoded revert reason', async({ mount, page }) => { test('with decoded revert reason', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withDecodedRevertReason),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.withDecodedRevertReason } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -95,14 +73,9 @@ test('with decoded revert reason', async({ mount, page }) => { ...@@ -95,14 +73,9 @@ test('with decoded revert reason', async({ mount, page }) => {
}); });
test('with decoded raw reason', async({ mount, page }) => { test('with decoded raw reason', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withRawRevertReason),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.withRawRevertReason } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -114,14 +87,9 @@ test('with decoded raw reason', async({ mount, page }) => { ...@@ -114,14 +87,9 @@ test('with decoded raw reason', async({ mount, page }) => {
}); });
test('pending', async({ mount, page }) => { test('pending', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.pending),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.pending } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -135,14 +103,9 @@ test('pending', async({ mount, page }) => { ...@@ -135,14 +103,9 @@ test('pending', async({ mount, page }) => {
}); });
test('with actions uniswap +@mobile +@dark-mode', async({ mount, page }) => { test('with actions uniswap +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withActionsUniswap),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.withActionsUniswap } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -159,14 +122,9 @@ const l2Test = test.extend({ ...@@ -159,14 +122,9 @@ const l2Test = test.extend({
}); });
l2Test('l2', async({ mount, page }) => { l2Test('l2', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.l2tx),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.l2tx } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -185,14 +143,9 @@ const mainnetTest = test.extend({ ...@@ -185,14 +143,9 @@ const mainnetTest = test.extend({
}); });
mainnetTest('without testnet warning', async({ mount, page }) => { mainnetTest('without testnet warning', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.l2tx),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.l2tx } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -209,14 +162,9 @@ const stabilityTest = test.extend({ ...@@ -209,14 +162,9 @@ const stabilityTest = test.extend({
}); });
stabilityTest('stability customization', async({ mount, page }) => { stabilityTest('stability customization', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.stabilityTx),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDetails/> <TxInfo data={ txMock.stabilityTx } isLoading={ false }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
This diff is collapsed.
import { useBoolean } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -9,45 +10,63 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -9,45 +10,63 @@ import type { Transaction } from 'types/api/transaction';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { retry } from 'lib/api/useQueryClientConfig';
import { SECOND } from 'lib/consts';
import delay from 'lib/delay'; import delay from 'lib/delay';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX, TX_ZKEVM_L2 } from 'stubs/tx'; import { TX, TX_ZKEVM_L2 } from 'stubs/tx';
interface Params { export type TxQuery = UseQueryResult<Transaction, ResourceError<{ status: number }>> & {
onTxStatusUpdate?: () => void; socketStatus: 'close' | 'error' | undefined;
updateDelay?: number; setRefetchOnError: {
txHash?: string; on: () => void;
off: () => void;
toggle: () => void;
};
} }
type ReturnType = UseQueryResult<Transaction, ResourceError<{ status: number }>> & { interface Params {
socketStatus: 'close' | 'error' | undefined; hash?: string;
isEnabled?: boolean;
} }
export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay, txHash }: Params | undefined = {}): ReturnType { export default function useTxQuery(params?: Params): TxQuery {
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const [ isRefetchEnabled, setRefetchEnabled ] = useBoolean(false);
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const hash = txHash || getQueryParamString(router.query.hash); const hash = params?.hash ?? getQueryParamString(router.query.hash);
const queryResult = useApiQuery<'tx', { status: number }>('tx', { const queryResult = useApiQuery<'tx', { status: number }>('tx', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { queryOptions: {
enabled: Boolean(hash), enabled: Boolean(hash) && params?.isEnabled !== false,
refetchOnMount: false, refetchOnMount: false,
placeholderData: config.features.zkEvmRollup.isEnabled ? TX_ZKEVM_L2 : TX, placeholderData: config.features.zkEvmRollup.isEnabled ? TX_ZKEVM_L2 : TX,
retry: (failureCount, error) => {
if (isRefetchEnabled) {
return false;
}
return retry(failureCount, error);
},
refetchInterval: (): number | false => {
return isRefetchEnabled ? 15 * SECOND : false;
},
}, },
}); });
const { data, isError, isPending } = queryResult; const { data, isError, isPlaceholderData, isPending } = queryResult;
const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => { const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => {
updateDelay && await delay(updateDelay); await delay(5 * SECOND);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: getResourceKey('tx', { pathParams: { hash } }), queryKey: getResourceKey('tx', { pathParams: { hash } }),
}); });
onTxStatusUpdate?.(); }, [ queryClient, hash ]);
}, [ onTxStatusUpdate, queryClient, hash, updateDelay ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketStatus('close'); setSocketStatus('close');
...@@ -61,7 +80,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay, txHash } ...@@ -61,7 +80,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay, txHash }
topic: `transactions:${ hash }`, topic: `transactions:${ hash }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: isPending || isError || data.status !== null, isDisabled: isPending || isPlaceholderData || isError || data.status !== null,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -69,8 +88,9 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay, txHash } ...@@ -69,8 +88,9 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay, txHash }
handler: handleStatusUpdateMessage, handler: handleStatusUpdateMessage,
}); });
return { return React.useMemo(() => ({
...queryResult, ...queryResult,
socketStatus, socketStatus,
}; setRefetchOnError: setRefetchEnabled,
}), [ queryResult, socketStatus, setRefetchEnabled ]);
} }
...@@ -11,6 +11,7 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -11,6 +11,7 @@ import type { ResourceError } from 'lib/api/resources';
import { WEI, WEI_IN_GWEI } from 'lib/consts'; import { WEI, WEI_IN_GWEI } from 'lib/consts';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
...@@ -116,7 +117,7 @@ const UserOpDetails = ({ query }: Props) => { ...@@ -116,7 +117,7 @@ const UserOpDetails = ({ query }: Props) => {
> >
<CurrencyValue <CurrencyValue
value={ data.fee } value={ data.fee }
currency={ config.chain.currency.symbol } currency={ currencyUnits.ether }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
</DetailsInfoItem> </DetailsInfoItem>
...@@ -212,17 +213,17 @@ const UserOpDetails = ({ query }: Props) => { ...@@ -212,17 +213,17 @@ const UserOpDetails = ({ query }: Props) => {
title="Max fee per gas" title="Max fee per gas"
hint="Maximum fee per gas " hint="Maximum fee per gas "
> >
<Text>{ BigNumber(data.max_fee_per_gas).dividedBy(WEI).toFixed() } { config.chain.currency.symbol } </Text> <Text>{ BigNumber(data.max_fee_per_gas).dividedBy(WEI).toFixed() } { currencyUnits.ether } </Text>
<Text variant="secondary" whiteSpace="pre"> <Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.max_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei) { space }({ BigNumber(data.max_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })
</Text> </Text>
</DetailsInfoItem><DetailsInfoItem </DetailsInfoItem><DetailsInfoItem
title="Max priority fee per gas" title="Max priority fee per gas"
hint="Maximum priority fee per gas" hint="Maximum priority fee per gas"
> >
<Text>{ BigNumber(data.max_priority_fee_per_gas).dividedBy(WEI).toFixed() } { config.chain.currency.symbol } </Text> <Text>{ BigNumber(data.max_priority_fee_per_gas).dividedBy(WEI).toFixed() } { currencyUnits.ether } </Text>
<Text variant="secondary" whiteSpace="pre"> <Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.max_priority_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei) { space }({ BigNumber(data.max_priority_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })
</Text> </Text>
</DetailsInfoItem> </DetailsInfoItem>
</> </>
......
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