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:
@ -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();
|
||||||
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user