From 0a8523b349b1d9895c8b13a7329a2f1ad17c5b3b Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 28 Aug 2020 14:17:12 -0700 Subject: [PATCH] explorer: Auto-update transactions until they reach max confirmation (#11841) * explorer: Auto-update transactions until they reach max confirmation * convert to side effect * proper cleanup * minor cleanup * pull isAutoRefresh from context, refactor, and add loading indicator / dhide refresh * split effects into two, manage interval in one effect only * simplify interval * move autoRefresh up a level, use computed value * flip conditional for readability * accidentally factored out not found case * add attempts bailout * run prettier * bailout after 5 polls of 0 confirmations * move bailout into state, change autoRefresh prop to enum to support bailout state * run prettier to clean up formatting * reintroduce details not available until max confirmations message * add error card with refresh if zero confirmation bailout * allow retry on bailouts --- explorer/src/pages/TransactionDetailsPage.tsx | 129 ++++++++++++++---- explorer/src/providers/transactions/index.tsx | 6 +- 2 files changed, 106 insertions(+), 29 deletions(-) diff --git a/explorer/src/pages/TransactionDetailsPage.tsx b/explorer/src/pages/TransactionDetailsPage.tsx index 8079c13f13..feaf39a84b 100644 --- a/explorer/src/pages/TransactionDetailsPage.tsx +++ b/explorer/src/pages/TransactionDetailsPage.tsx @@ -29,8 +29,24 @@ import { intoTransactionInstruction } from "utils/tx"; import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard"; import { FetchStatus } from "providers/cache"; -type Props = { signature: TransactionSignature }; -export function TransactionDetailsPage({ signature: raw }: Props) { +const AUTO_REFRESH_INTERVAL = 2000; +const ZERO_CONFIRMATION_BAILOUT = 5; + +type SignatureProps = { + signature: TransactionSignature; +}; + +enum AutoRefresh { + Active, + Inactive, + BailedOut, +} + +type AutoRefreshProps = { + autoRefresh: AutoRefresh; +}; + +export function TransactionDetailsPage({ signature: raw }: SignatureProps) { let signature: TransactionSignature | undefined; try { @@ -40,6 +56,38 @@ export function TransactionDetailsPage({ signature: raw }: Props) { } } catch (err) {} + const status = useTransactionStatus(signature); + const [zeroConfirmationRetries, setZeroConfirmationRetries] = React.useState( + 0 + ); + + let autoRefresh = AutoRefresh.Inactive; + + if (zeroConfirmationRetries >= ZERO_CONFIRMATION_BAILOUT) { + autoRefresh = AutoRefresh.BailedOut; + } else if (status?.data?.info && status.data.info.confirmations !== "max") { + autoRefresh = AutoRefresh.Active; + } + + React.useEffect(() => { + if ( + status?.status === FetchStatus.Fetched && + status.data?.info && + status.data.info.confirmations === 0 + ) { + setZeroConfirmationRetries((retries) => retries + 1); + } + }, [status]); + + React.useEffect(() => { + if ( + status?.status === FetchStatus.Fetching && + autoRefresh === AutoRefresh.BailedOut + ) { + setZeroConfirmationRetries(0); + } + }, [status, autoRefresh, setZeroConfirmationRetries]); + return (
@@ -52,8 +100,8 @@ export function TransactionDetailsPage({ signature: raw }: Props) { ) : ( <> - - + + )} @@ -61,19 +109,14 @@ export function TransactionDetailsPage({ signature: raw }: Props) { ); } -function StatusCard({ signature }: Props) { +function StatusCard({ + signature, + autoRefresh, +}: SignatureProps & AutoRefreshProps) { const fetchStatus = useFetchTransactionStatus(); const status = useTransactionStatus(signature); - const fetchDetails = useFetchTransactionDetails(); const details = useTransactionDetails(signature); const { firstAvailableBlock, status: clusterStatus } = useCluster(); - const refresh = React.useCallback( - (signature: string) => { - fetchStatus(signature); - fetchDetails(signature); - }, - [fetchStatus, fetchDetails] - ); // Fetch transaction on load React.useEffect(() => { @@ -82,7 +125,25 @@ function StatusCard({ signature }: Props) { } }, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps - if (!status || status.status === FetchStatus.Fetching) { + // Effect to set and clear interval for auto-refresh + React.useEffect(() => { + if (autoRefresh === AutoRefresh.Active) { + let intervalHandle: NodeJS.Timeout = setInterval( + () => fetchStatus(signature), + AUTO_REFRESH_INTERVAL + ); + + return () => { + clearInterval(intervalHandle); + }; + } + }, [autoRefresh, fetchStatus, signature]); + + if ( + !status || + (status.status === FetchStatus.Fetching && + autoRefresh === AutoRefresh.Inactive) + ) { return ; } else if (status.status === FetchStatus.FetchFailed) { return ( @@ -102,6 +163,7 @@ function StatusCard({ signature }: Props) { } const { info } = status.data; + const renderResult = () => { let statusClass = "success"; let statusText = "Success"; @@ -134,13 +196,17 @@ function StatusCard({ signature }: Props) {

Overview

- + {autoRefresh === AutoRefresh.Active ? ( + + ) : ( + + )}
@@ -211,13 +277,16 @@ function StatusCard({ signature }: Props) { ); } -function AccountsCard({ signature }: Props) { +function AccountsCard({ + signature, + autoRefresh, +}: SignatureProps & AutoRefreshProps) { const { url } = useCluster(); const details = useTransactionDetails(signature); - const fetchStatus = useFetchTransactionStatus(); const fetchDetails = useFetchTransactionDetails(); - const refreshStatus = () => fetchStatus(signature); + const fetchStatus = useFetchTransactionStatus(); const refreshDetails = () => fetchDetails(signature); + const refreshStatus = () => fetchStatus(signature); const transaction = details?.data?.transaction?.transaction; const message = transaction?.message; const status = useTransactionStatus(signature); @@ -231,14 +300,18 @@ function AccountsCard({ signature }: Props) { if (!status?.data?.info) { return null; - } else if (!details) { + } else if (autoRefresh === AutoRefresh.BailedOut) { return ( ); - } else if (details.status === FetchStatus.Fetching) { + } else if (autoRefresh === AutoRefresh.Active) { + return ( + + ); + } else if (!details || details.status === FetchStatus.Fetching) { return ; } else if (details.status === FetchStatus.FetchFailed) { return ; @@ -317,7 +390,7 @@ function AccountsCard({ signature }: Props) { ); } -function InstructionsSection({ signature }: Props) { +function InstructionsSection({ signature }: SignatureProps) { const status = useTransactionStatus(signature); const details = useTransactionDetails(signature); const fetchDetails = useFetchTransactionDetails(); diff --git a/explorer/src/providers/transactions/index.tsx b/explorer/src/providers/transactions/index.tsx index 7782e2772b..d4d66f3e94 100644 --- a/explorer/src/providers/transactions/index.tsx +++ b/explorer/src/providers/transactions/index.tsx @@ -131,7 +131,7 @@ export function useTransactions() { } export function useTransactionStatus( - signature: TransactionSignature + signature: TransactionSignature | undefined ): Cache.CacheEntry | undefined { const context = React.useContext(StateContext); @@ -141,6 +141,10 @@ export function useTransactionStatus( ); } + if (signature === undefined) { + return undefined; + } + return context.entries[signature]; }