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:
parent
7e5e7673ae
commit
0a8523b349
|
@ -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();
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue