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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 { 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 (
<div className="container mt-n3">
<div className="header">
@ -52,8 +100,8 @@ export function TransactionDetailsPage({ signature: raw }: Props) {
<ErrorCard text={`Signature "${raw}" is not valid`} />
) : (
<>
<StatusCard signature={signature} />
<AccountsCard signature={signature} />
<StatusCard signature={signature} autoRefresh={autoRefresh} />
<AccountsCard signature={signature} autoRefresh={autoRefresh} />
<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 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 <LoadingCard />;
} 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) {
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Overview</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(signature)}
>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
{autoRefresh === AutoRefresh.Active ? (
<span className="spinner-grow spinner-grow-sm"></span>
) : (
<button
className="btn btn-white btn-sm"
onClick={() => fetchStatus(signature)}
>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
)}
</div>
<TableCardBody>
@ -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 (
<ErrorCard
retry={refreshStatus}
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 />;
} else if (details.status === FetchStatus.FetchFailed) {
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 details = useTransactionDetails(signature);
const fetchDetails = useFetchTransactionDetails();

View File

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