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
This commit is contained in:
Josh
2020-08-28 14:17:12 -07:00
committed by GitHub
parent 7e5e7673ae
commit 0a8523b349
2 changed files with 106 additions and 29 deletions

View File

@ -29,8 +29,24 @@ import { intoTransactionInstruction } from "utils/tx";
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard"; import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
import { FetchStatus } from "providers/cache"; import { FetchStatus } from "providers/cache";
type Props = { signature: TransactionSignature }; const AUTO_REFRESH_INTERVAL = 2000;
export function TransactionDetailsPage({ signature: raw }: Props) { 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; let signature: TransactionSignature | undefined;
try { try {
@ -40,6 +56,38 @@ export function TransactionDetailsPage({ signature: raw }: Props) {
} }
} catch (err) {} } 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 ( return (
<div className="container mt-n3"> <div className="container mt-n3">
<div className="header"> <div className="header">
@ -52,8 +100,8 @@ export function TransactionDetailsPage({ signature: raw }: Props) {
<ErrorCard text={`Signature "${raw}" is not valid`} /> <ErrorCard text={`Signature "${raw}" is not valid`} />
) : ( ) : (
<> <>
<StatusCard signature={signature} /> <StatusCard signature={signature} autoRefresh={autoRefresh} />
<AccountsCard signature={signature} /> <AccountsCard signature={signature} autoRefresh={autoRefresh} />
<InstructionsSection signature={signature} /> <InstructionsSection signature={signature} />
</> </>
)} )}
@ -61,19 +109,14 @@ export function TransactionDetailsPage({ signature: raw }: Props) {
); );
} }
function StatusCard({ signature }: Props) { function StatusCard({
signature,
autoRefresh,
}: SignatureProps & AutoRefreshProps) {
const fetchStatus = useFetchTransactionStatus(); const fetchStatus = useFetchTransactionStatus();
const status = useTransactionStatus(signature); const status = useTransactionStatus(signature);
const fetchDetails = useFetchTransactionDetails();
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
const { firstAvailableBlock, status: clusterStatus } = useCluster(); const { firstAvailableBlock, status: clusterStatus } = useCluster();
const refresh = React.useCallback(
(signature: string) => {
fetchStatus(signature);
fetchDetails(signature);
},
[fetchStatus, fetchDetails]
);
// Fetch transaction on load // Fetch transaction on load
React.useEffect(() => { React.useEffect(() => {
@ -82,7 +125,25 @@ function StatusCard({ signature }: Props) {
} }
}, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps }, [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 <LoadingCard />; return <LoadingCard />;
} else if (status.status === FetchStatus.FetchFailed) { } else if (status.status === FetchStatus.FetchFailed) {
return ( return (
@ -102,6 +163,7 @@ function StatusCard({ signature }: Props) {
} }
const { info } = status.data; const { info } = status.data;
const renderResult = () => { const renderResult = () => {
let statusClass = "success"; let statusClass = "success";
let statusText = "Success"; let statusText = "Success";
@ -134,13 +196,17 @@ function StatusCard({ signature }: Props) {
<div className="card"> <div className="card">
<div className="card-header align-items-center"> <div className="card-header align-items-center">
<h3 className="card-header-title">Overview</h3> <h3 className="card-header-title">Overview</h3>
{autoRefresh === AutoRefresh.Active ? (
<span className="spinner-grow spinner-grow-sm"></span>
) : (
<button <button
className="btn btn-white btn-sm" className="btn btn-white btn-sm"
onClick={() => refresh(signature)} onClick={() => fetchStatus(signature)}
> >
<span className="fe fe-refresh-cw mr-2"></span> <span className="fe fe-refresh-cw mr-2"></span>
Refresh Refresh
</button> </button>
)}
</div> </div>
<TableCardBody> <TableCardBody>
@ -211,13 +277,16 @@ function StatusCard({ signature }: Props) {
); );
} }
function AccountsCard({ signature }: Props) { function AccountsCard({
signature,
autoRefresh,
}: SignatureProps & AutoRefreshProps) {
const { url } = useCluster(); const { url } = useCluster();
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
const fetchStatus = useFetchTransactionStatus();
const fetchDetails = useFetchTransactionDetails(); const fetchDetails = useFetchTransactionDetails();
const refreshStatus = () => fetchStatus(signature); const fetchStatus = useFetchTransactionStatus();
const refreshDetails = () => fetchDetails(signature); const refreshDetails = () => fetchDetails(signature);
const refreshStatus = () => fetchStatus(signature);
const transaction = details?.data?.transaction?.transaction; const transaction = details?.data?.transaction?.transaction;
const message = transaction?.message; const message = transaction?.message;
const status = useTransactionStatus(signature); const status = useTransactionStatus(signature);
@ -231,14 +300,18 @@ function AccountsCard({ signature }: Props) {
if (!status?.data?.info) { if (!status?.data?.info) {
return null; return null;
} else if (!details) { } else if (autoRefresh === AutoRefresh.BailedOut) {
return ( return (
<ErrorCard <ErrorCard
retry={refreshStatus}
text="Details are not available until the transaction reaches MAX confirmations" text="Details are not available until the transaction reaches MAX confirmations"
retry={refreshStatus}
/> />
); );
} else if (details.status === FetchStatus.Fetching) { } else if (autoRefresh === AutoRefresh.Active) {
return (
<ErrorCard text="Details are not available until the transaction reaches MAX confirmations" />
);
} else if (!details || details.status === FetchStatus.Fetching) {
return <LoadingCard />; return <LoadingCard />;
} else if (details.status === FetchStatus.FetchFailed) { } else if (details.status === FetchStatus.FetchFailed) {
return <ErrorCard retry={refreshDetails} text="Fetch Failed" />; return <ErrorCard retry={refreshDetails} text="Fetch Failed" />;
@ -317,7 +390,7 @@ function AccountsCard({ signature }: Props) {
); );
} }
function InstructionsSection({ signature }: Props) { function InstructionsSection({ signature }: SignatureProps) {
const status = useTransactionStatus(signature); const status = useTransactionStatus(signature);
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
const fetchDetails = useFetchTransactionDetails(); const fetchDetails = useFetchTransactionDetails();

View File

@ -131,7 +131,7 @@ export function useTransactions() {
} }
export function useTransactionStatus( export function useTransactionStatus(
signature: TransactionSignature signature: TransactionSignature | undefined
): Cache.CacheEntry<TransactionStatus> | undefined { ): Cache.CacheEntry<TransactionStatus> | undefined {
const context = React.useContext(StateContext); const context = React.useContext(StateContext);
@ -141,6 +141,10 @@ export function useTransactionStatus(
); );
} }
if (signature === undefined) {
return undefined;
}
return context.entries[signature]; return context.entries[signature];
} }