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">
<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>
......@@ -4,20 +4,22 @@ import React from 'react';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
export const retry = (failureCount: number, error: unknown) => {
const errorPayload = getErrorObjPayload<{ status: number }>(error);
const status = errorPayload?.status || getErrorObjStatusCode(error);
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
}
return failureCount < 2;
};
export default function useQueryClientConfig() {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
const errorPayload = getErrorObjPayload<{ status: number }>(error);
const status = errorPayload?.status || getErrorObjStatusCode(error);
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
}
return failureCount < 2;
},
retry,
throwOnError: (error) => {
const status = getErrorObjStatusCode(error);
// 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) => {
return {
container: {
[$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: {
[$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 = {
status: 'ok' | 'error' | null;
block: number | null;
timestamp: string | null;
confirmation_duration: Array<number>;
confirmation_duration: Array<number> | null;
from: AddressParam;
value: string;
fee: Fee;
gas_price: string;
gas_price: string | null;
type: number | null;
gas_used: string | null;
gas_limit: string;
......@@ -49,7 +49,7 @@ export type Transaction = {
decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: string;
exchange_rate: string | null;
method: string | null;
tx_types: Array<TransactionType>;
tx_tag: string | null;
......
......@@ -4,10 +4,9 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -15,6 +14,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
import TxDetails from 'ui/tx/TxDetails';
import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded';
import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped';
import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
......@@ -23,37 +23,42 @@ import TxState from 'ui/tx/TxState';
import TxSubHeading from 'ui/tx/TxSubHeading';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
import TxUserOps from 'ui/tx/TxUserOps';
import useTxQuery from 'ui/tx/useTxQuery';
const TransactionPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const hash = getQueryParamString(router.query.hash);
const txQuery = useTxQuery();
const { data, isPlaceholderData, isError, error, errorUpdateCount } = txQuery;
const { data, isPlaceholderData } = useApiQuery('tx', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: TX,
},
});
const tabs: Array<RoutedTab> = [
{
id: 'index',
title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details',
component: <TxDetails/>,
},
config.features.suave.isEnabled && data?.wrapped ?
{ id: 'wrapped', title: 'Regular tx details', component: <TxDetailsWrapped data={ data.wrapped }/> } :
undefined,
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
config.features.userOps.isEnabled ? { id: 'user_ops', title: 'User operations', component: <TxUserOps/> } : undefined,
{ id: 'internal', title: 'Internal txns', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> },
{ id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
].filter(Boolean);
const showDegradedView = (isError || isPlaceholderData) && errorUpdateCount > 0;
const tabs: Array<RoutedTab> = (() => {
const detailsComponent = showDegradedView ?
<TxDetailsDegraded hash={ hash } txQuery={ txQuery }/> :
<TxDetails txQuery={ txQuery }/>;
return [
{
id: 'index',
title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details',
component: detailsComponent,
},
config.features.suave.isEnabled && data?.wrapped ?
{ id: 'wrapped', title: 'Regular tx details', component: <TxDetailsWrapped data={ data.wrapped }/> } :
undefined,
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer txQuery={ txQuery }/> },
config.features.userOps.isEnabled ?
{ id: 'user_ops', title: 'User operations', component: <TxUserOps txQuery={ txQuery }/> } :
undefined,
{ id: 'internal', title: 'Internal txns', component: <TxInternals txQuery={ txQuery }/> },
{ 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);
})();
const tabIndex = useTabIndexFromQuery(tabs);
......@@ -79,6 +84,23 @@ const TransactionPageContent = () => {
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 (
<>
<TextAd mb={ 6 }/>
......@@ -88,12 +110,7 @@ const TransactionPageContent = () => {
contentAfter={ tags }
secondRow={ titleSecondRow }
/>
{ isPlaceholderData ? (
<>
<TabsSkeleton tabs={ tabs } mt={ 6 }/>
{ tabs[tabIndex]?.component }
</>
) : <RoutedTabs tabs={ tabs }/> }
{ content }
</>
);
};
......
......@@ -8,6 +8,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery';
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 { USER_OP } from 'stubs/userOps';
import TextAd from 'ui/shared/ad/TextAd';
......@@ -18,6 +20,7 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
import TxLogs from 'ui/tx/TxLogs';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
import useTxQuery from 'ui/tx/useTxQuery';
import UserOpDetails from 'ui/userOp/UserOpDetails';
import UserOpRaw from 'ui/userOp/UserOpRaw';
......@@ -27,13 +30,15 @@ const UserOp = () => {
const hash = getQueryParamString(router.query.hash);
const userOpQuery = useApiQuery('user_op', {
pathParams: { hash: hash },
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: USER_OP,
},
});
const txQuery = useTxQuery({ hash: userOpQuery.data?.transaction_hash, isEnabled: !userOpQuery.isPlaceholderData });
const filterTokenTransfersByLogIndex = React.useCallback((tt: TokenTransfer) => {
if (!userOpQuery.data) {
return true;
......@@ -61,22 +66,14 @@ const UserOp = () => {
{
id: '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 }/> },
]), [ userOpQuery, filterTokenTransfersByLogIndex, filterLogsByLogIndex ]);
]), [ userOpQuery, txQuery, filterTokenTransfersByLogIndex, filterLogsByLogIndex ]);
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 hasGoBackLink = appProps.referrer && appProps.referrer.includes('/ops');
......@@ -90,6 +87,9 @@ const UserOp = () => {
};
}, [ appProps.referrer ]);
throwOnAbsentParamError(hash);
throwOnResourceLoadError(userOpQuery);
const titleSecondRow = <UserOpEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } fontFamily="heading"/>;
return (
......
......@@ -18,7 +18,7 @@ const DetailsTimestamp = ({ timestamp, isLoading }: Props) => {
<Skeleton isLoaded={ !isLoading } ml={ 2 }>
{ dayjs(timestamp).fromNow() }
</Skeleton>
<TextSeparator/>
<TextSeparator color="gray.500"/>
<Skeleton isLoaded={ !isLoading } whiteSpace="normal">
{ dayjs(timestamp).format('llll') }
</Skeleton>
......
......@@ -2,10 +2,10 @@ import { useColorMode } from '@chakra-ui/react';
import { jsonRpcProvider } from '@wagmi/core/providers/jsonRpc';
import { createWeb3Modal, useWeb3ModalTheme, defaultWagmiConfig } from '@web3modal/wagmi/react';
import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, WagmiConfig } from 'wagmi';
import config from 'configs/app';
import currentChain from 'lib/web3/currentChain';
import colors from 'theme/foundations/colors';
import { BODY_TYPEFACE } from 'theme/foundations/typography';
import zIndices from 'theme/foundations/zIndices';
......@@ -18,31 +18,6 @@ const getConfig = () => {
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(
[ 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';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxInternals from './TxInternals';
import type { TxQuery } from './useTxQuery';
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 hooksConfig = {
router: {
......@@ -18,18 +18,20 @@ const hooksConfig = {
};
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({
status: 200,
body: JSON.stringify(internalTxsMock.baseResponse),
}));
const txQuery = {
data: txMock.base,
isPlaceholderData: false,
isError: false,
} as TxQuery;
const component = await mount(
<TestApp>
<TxInternals/>
<TxInternals txQuery={ txQuery }/>
</TestApp>,
{ hooksConfig },
);
......
......@@ -3,7 +3,6 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import { SECOND } from 'lib/consts';
// import { apos } from 'lib/html-entities';
import { INTERNAL_TX } from 'stubs/internalTx';
import { generateListStub } from 'stubs/utils';
......@@ -19,7 +18,8 @@ import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
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';
import type { TxQuery } from './useTxQuery';
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
value: [ 'value-desc', 'value-asc', undefined ],
......@@ -62,17 +62,20 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
// item.to.hash.toLowerCase().includes(formattedSearchTerm);
// };
const TxInternals = () => {
interface Props {
txQuery: TxQuery;
}
const TxInternals = ({ txQuery }: Props) => {
// filters are not implemented yet in api
// const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
// const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'tx_internal_txs',
pathParams: { hash: txInfo.data?.hash },
pathParams: { hash: txQuery.data?.hash },
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 }),
},
});
......@@ -90,8 +93,8 @@ const TxInternals = () => {
};
}, [ isPlaceholderData ]);
if (!txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data?.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
if (!txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data?.status) {
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
}
const filteredData = data?.items
......@@ -125,7 +128,7 @@ const TxInternals = () => {
return (
<DataListDisplay
isError={ isError || txInfo.isError }
isError={ isError || txQuery.isError }
items={ data?.items }
emptyText="There are no internal transactions for this transaction."
// filterProps={{
......
......@@ -3,7 +3,6 @@ import React from 'react';
import type { Log } from 'types/api/log';
import { SECOND } from 'lib/consts';
import { LOG } from 'stubs/log';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
......@@ -13,29 +12,29 @@ import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
type Props = {
txHash?: string;
import type { TxQuery } from './useTxQuery';
interface Props {
txQuery: TxQuery;
logsFilter?: (log: Log) => boolean;
}
const TxLogs = ({ txHash, logsFilter }: Props) => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND, txHash });
const TxLogs = ({ txQuery, logsFilter }: Props) => {
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'tx_logs',
pathParams: { hash: txInfo.data?.hash },
pathParams: { hash: txQuery.data?.hash },
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 }),
},
});
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
}
if (isError || txInfo.isError) {
if (isError || txQuery.isError) {
return <DataFetchAlert/>;
}
......
......@@ -5,7 +5,6 @@ import type { SocketMessage } from 'lib/socket/types';
import type { RawTracesResponse } from 'types/api/rawTrace';
import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -14,19 +13,23 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
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 [ rawTraces, setRawTraces ] = React.useState<RawTracesResponse>();
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isPlaceholderData, isError } = useApiQuery('tx_raw_trace', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash) && Boolean(txInfo.data?.status) && isQueryEnabled,
enabled: Boolean(hash) && Boolean(txQuery.data?.status) && isQueryEnabled,
placeholderData: TX_RAW_TRACE,
},
});
......@@ -39,7 +42,7 @@ const TxRawTrace = () => {
const channel = useSocketChannel({
topic: `transactions:${ hash }`,
isDisabled: !hash || txInfo.isPlaceholderData || !txInfo.data?.status,
isDisabled: !hash || txQuery.isPlaceholderData || !txQuery.data?.status,
onJoin: enableQuery,
onSocketError: enableQuery,
});
......@@ -49,11 +52,11 @@ const TxRawTrace = () => {
handler: handleRawTraceMessage,
});
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
}
if (isError || txInfo.isError) {
if (isError || txQuery.isError) {
return <DataFetchAlert/>;
}
......
......@@ -7,8 +7,8 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
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 hooksConfig = {
router: {
......@@ -17,18 +17,20 @@ const hooksConfig = {
};
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({
status: 200,
body: JSON.stringify(txStateMock.baseResponse),
}));
const txQuery = {
data: txMock.base,
isPlaceholderData: false,
isError: false,
} as TxQuery;
const component = await mount(
<TestApp>
<TxState/>
<TxState txQuery={ txQuery }/>
</TestApp>,
{ hooksConfig },
);
......
import { Accordion, Hide, Show, Text } from '@chakra-ui/react';
import React from 'react';
import { SECOND } from 'lib/consts';
import { TX_STATE_CHANGES } from 'stubs/txStateChanges';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
......@@ -9,18 +8,21 @@ import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxStateList from 'ui/tx/state/TxStateList';
import TxStateTable from 'ui/tx/state/TxStateTable';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import TxPendingAlert from './TxPendingAlert';
import TxSocketAlert from './TxSocketAlert';
import type { TxQuery } from './useTxQuery';
const TxState = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
interface Props {
txQuery: TxQuery;
}
const TxState = ({ txQuery }: Props) => {
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'tx_state_changes',
pathParams: { hash: txInfo.data?.hash },
pathParams: { hash: txQuery.data?.hash },
options: {
enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.hash) && Boolean(txQuery.data?.status),
placeholderData: {
items: TX_STATE_CHANGES,
next_page_params: {
......@@ -31,8 +33,8 @@ const TxState = () => {
},
});
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
}
const content = data ? (
......@@ -54,12 +56,14 @@ const TxState = () => {
return (
<>
<Text mb={ 6 }>
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.
</Text>
{ !isError && !txQuery.isError && (
<Text mb={ 6 }>
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.
</Text>
) }
<DataListDisplay
isError={ isError }
isError={ isError || txQuery.isError }
items={ data?.items }
emptyText="There are no state changes for this transaction."
content={ content }
......
......@@ -5,7 +5,6 @@ import React from 'react';
import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import { SECOND } from 'lib/consts';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import { apos } from 'lib/html-entities';
import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
......@@ -20,27 +19,26 @@ import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
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);
type Props = {
txHash?: string;
tokenTransferFilter?: (tt: TokenTransfer) => boolean;
interface Props {
txQuery: TxQuery;
tokenTransferFilter?: (data: TokenTransfer) => boolean;
}
const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => {
const txsInfo = useFetchTxInfo({ updateDelay: 5 * SECOND, txHash });
const TxTokenTransfer = ({ txQuery, tokenTransferFilter }: Props) => {
const router = useRouter();
const [ typeFilter, setTypeFilter ] = React.useState<Array<TokenType>>(getTokenFilterValue(router.query.type) || []);
const tokenTransferQuery = useQueryWithPages({
resourceName: 'tx_token_transfers',
pathParams: { hash: txsInfo.data?.hash.toString() },
pathParams: { hash: txQuery.data?.hash.toString() },
options: {
enabled: !txsInfo.isPlaceholderData && Boolean(txsInfo.data?.status && txsInfo.data?.hash),
enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.status && txQuery.data?.hash),
placeholderData: getTokenTransfersStub(),
},
filters: { type: typeFilter },
......@@ -51,11 +49,11 @@ const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => {
setTypeFilter(nextValue);
}, [ tokenTransferQuery ]);
if (!txsInfo.isPending && !txsInfo.isPlaceholderData && !txsInfo.isError && !txsInfo.data.status) {
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>;
if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
}
if (txsInfo.isError || tokenTransferQuery.isError) {
if (txQuery.isError || tokenTransferQuery.isError) {
return <DataFetchAlert/>;
}
......@@ -97,7 +95,7 @@ const TxTokenTransfer = ({ txHash, tokenTransferFilter }: Props) => {
return (
<DataListDisplay
isError={ txsInfo.isError || tokenTransferQuery.isError }
isError={ txQuery.isError || tokenTransferQuery.isError }
items={ items }
emptyText="There are no token transfers."
filterProps={{
......
import React from 'react';
import { SECOND } from 'lib/consts';
import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import UserOpsContent from 'ui/userOps/UserOpsContent';
const TxUserOps = () => {
const txsInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
import type { TxQuery } from './useTxQuery';
interface Props {
txQuery: TxQuery;
}
const TxUserOps = ({ txQuery }: Props) => {
const userOpsQuery = useQueryWithPages({
resourceName: 'user_ops',
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
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) {
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>;
if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) {
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
}
return <UserOpsContent query={ userOpsQuery } showTx={ false }/>;
......
......@@ -8,12 +8,12 @@ import { currencyUnits } from 'lib/units';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
interface Props {
gasPrice: string;
gasPrice: string | null;
isLoading?: boolean;
}
const TxDetailsGasPrice = ({ gasPrice, isLoading }: Props) => {
if (config.UI.views.tx.hiddenFields?.gas_price) {
if (config.UI.views.tx.hiddenFields?.gas_price || !gasPrice) {
return null;
}
......
......@@ -4,12 +4,10 @@ import React from 'react';
import * as txMock from 'mocks/txs/tx';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import TxDetails from './TxDetails';
import TxInfo from './TxInfo';
const API_URL = buildApiUrl('tx', { hash: '1' });
const hooksConfig = {
router: {
query: { hash: 1 },
......@@ -17,14 +15,9 @@ const hooksConfig = {
};
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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.base } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -38,14 +31,9 @@ test('between addresses +@mobile +@dark-mode', 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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.withContractCreation } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -57,14 +45,9 @@ test('creating contact', 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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.withTokenTransfer } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -76,14 +59,9 @@ test('with token transfer +@mobile', 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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.withDecodedRevertReason } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -95,14 +73,9 @@ test('with decoded revert 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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.withRawRevertReason } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -114,14 +87,9 @@ test('with decoded raw reason', 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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.pending } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -135,14 +103,9 @@ test('pending', 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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.withActionsUniswap } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -159,14 +122,9 @@ const l2Test = test.extend({
});
l2Test('l2', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.l2tx),
}));
const component = await mount(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.l2tx } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -185,14 +143,9 @@ const mainnetTest = test.extend({
});
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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.l2tx } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......@@ -209,14 +162,9 @@ const stabilityTest = test.extend({
});
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(
<TestApp>
<TxDetails/>
<TxInfo data={ txMock.stabilityTx } isLoading={ false }/>
</TestApp>,
{ hooksConfig },
);
......
This diff is collapsed.
import { useBoolean } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
......@@ -9,45 +10,63 @@ import type { Transaction } from 'types/api/transaction';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { retry } from 'lib/api/useQueryClientConfig';
import { SECOND } from 'lib/consts';
import delay from 'lib/delay';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX, TX_ZKEVM_L2 } from 'stubs/tx';
interface Params {
onTxStatusUpdate?: () => void;
updateDelay?: number;
txHash?: string;
export type TxQuery = UseQueryResult<Transaction, ResourceError<{ status: number }>> & {
socketStatus: 'close' | 'error' | undefined;
setRefetchOnError: {
on: () => void;
off: () => void;
toggle: () => void;
};
}
type ReturnType = UseQueryResult<Transaction, ResourceError<{ status: number }>> & {
socketStatus: 'close' | 'error' | undefined;
interface Params {
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 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', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
enabled: Boolean(hash) && params?.isEnabled !== false,
refetchOnMount: false,
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() => {
updateDelay && await delay(updateDelay);
await delay(5 * SECOND);
queryClient.invalidateQueries({
queryKey: getResourceKey('tx', { pathParams: { hash } }),
});
onTxStatusUpdate?.();
}, [ onTxStatusUpdate, queryClient, hash, updateDelay ]);
}, [ queryClient, hash ]);
const handleSocketClose = React.useCallback(() => {
setSocketStatus('close');
......@@ -61,7 +80,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay, txHash }
topic: `transactions:${ hash }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isPending || isError || data.status !== null,
isDisabled: isPending || isPlaceholderData || isError || data.status !== null,
});
useSocketMessage({
channel,
......@@ -69,8 +88,9 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay, txHash }
handler: handleStatusUpdateMessage,
});
return {
return React.useMemo(() => ({
...queryResult,
socketStatus,
};
setRefetchOnError: setRefetchEnabled,
}), [ queryResult, socketStatus, setRefetchEnabled ]);
}
......@@ -11,6 +11,7 @@ import type { ResourceError } from 'lib/api/resources';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import { space } from 'lib/html-entities';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
......@@ -116,7 +117,7 @@ const UserOpDetails = ({ query }: Props) => {
>
<CurrencyValue
value={ data.fee }
currency={ config.chain.currency.symbol }
currency={ currencyUnits.ether }
isLoading={ isPlaceholderData }
/>
</DetailsInfoItem>
......@@ -212,17 +213,17 @@ const UserOpDetails = ({ query }: Props) => {
title="Max 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">
{ 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>
</DetailsInfoItem><DetailsInfoItem
title="Max 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">
{ 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>
</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