Fix false negative not found response

This commit is contained in:
Justin Starry 2020-05-07 16:41:42 +08:00 committed by Michael Vines
parent e31b30c595
commit b68dc5cf85
4 changed files with 93 additions and 106 deletions

View File

@ -2,12 +2,10 @@ import React from "react";
import bs58 from "bs58"; import bs58 from "bs58";
import { import {
Source, Source,
useFetchTransactionStatus,
useTransactionStatus, useTransactionStatus,
useTransactionDetails, useTransactionDetails,
useTransactionsDispatch,
useDetailsDispatch, useDetailsDispatch,
checkTransactionStatus,
ActionType,
FetchStatus FetchStatus
} from "../providers/transactions"; } from "../providers/transactions";
import { fetchDetails } from "providers/transactions/details"; import { fetchDetails } from "providers/transactions/details";
@ -28,8 +26,7 @@ import { useHistory, useLocation } from "react-router-dom";
type Props = { signature: TransactionSignature }; type Props = { signature: TransactionSignature };
export default function TransactionDetails({ signature }: Props) { export default function TransactionDetails({ signature }: Props) {
const dispatch = useTransactionsDispatch(); const fetchTransaction = useFetchTransactionStatus();
const { url } = useCluster();
const [, setShow] = useClusterModal(); const [, setShow] = useClusterModal();
const [search, setSearch] = React.useState(signature); const [search, setSearch] = React.useState(signature);
const history = useHistory(); const history = useHistory();
@ -41,13 +38,8 @@ export default function TransactionDetails({ signature }: Props) {
// Fetch transaction on load // Fetch transaction on load
React.useEffect(() => { React.useEffect(() => {
dispatch({ fetchTransaction(signature, Source.Url);
type: ActionType.FetchSignature, }, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
signature,
source: Source.Url
});
checkTransactionStatus(dispatch, signature, url);
}, [signature, dispatch, url]);
const searchInput = ( const searchInput = (
<input <input
@ -104,20 +96,15 @@ export default function TransactionDetails({ signature }: Props) {
function StatusCard({ signature }: Props) { function StatusCard({ signature }: Props) {
const status = useTransactionStatus(signature); const status = useTransactionStatus(signature);
const dispatch = useTransactionsDispatch(); const refresh = useFetchTransactionStatus();
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
const { url } = useCluster();
const refreshStatus = () => {
checkTransactionStatus(dispatch, signature, url);
};
if (!status || status.fetchStatus === FetchStatus.Fetching) { if (!status || status.fetchStatus === FetchStatus.Fetching) {
return <LoadingCard />; return <LoadingCard />;
} else if (status?.fetchStatus === FetchStatus.FetchFailed) { } else if (status?.fetchStatus === FetchStatus.FetchFailed) {
return <RetryCard retry={refreshStatus} text="Fetch Failed" />; return <RetryCard retry={() => refresh(signature)} text="Fetch Failed" />;
} else if (!status.info) { } else if (!status.info) {
return <RetryCard retry={refreshStatus} text="Not Found" />; return <RetryCard retry={() => refresh(signature)} text="Not Found" />;
} }
const { info } = status; const { info } = status;
@ -141,7 +128,10 @@ 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">Status</h3> <h3 className="card-header-title">Status</h3>
<button className="btn btn-white btn-sm" onClick={refreshStatus}> <button
className="btn btn-white btn-sm"
onClick={() => refresh(signature)}
>
<span className="fe fe-refresh-cw mr-2"></span> <span className="fe fe-refresh-cw mr-2"></span>
Refresh Refresh
</button> </button>
@ -179,14 +169,25 @@ function AccountsCard({ signature }: Props) {
const dispatch = useDetailsDispatch(); const dispatch = useDetailsDispatch();
const { url } = useCluster(); const { url } = useCluster();
const fetchStatus = useFetchTransactionStatus();
const refreshStatus = () => fetchStatus(signature);
const refreshDetails = () => fetchDetails(dispatch, signature, url); const refreshDetails = () => fetchDetails(dispatch, signature, url);
const transaction = details?.transaction?.transaction; const transaction = details?.transaction?.transaction;
const message = React.useMemo(() => { const message = React.useMemo(() => {
return transaction?.compileMessage(); return transaction?.compileMessage();
}, [transaction]); }, [transaction]);
if (!details) { const status = useTransactionStatus(signature);
if (!status || !status.info) {
return null; return null;
} else if (!details) {
return (
<RetryCard
retry={refreshStatus}
text="Details are not available until the transaction reaches MAX confirmations"
/>
);
} else if (details.fetchStatus === FetchStatus.Fetching) { } else if (details.fetchStatus === FetchStatus.Fetching) {
return <LoadingCard />; return <LoadingCard />;
} else if (details?.fetchStatus === FetchStatus.FetchFailed) { } else if (details?.fetchStatus === FetchStatus.FetchFailed) {
@ -571,11 +572,17 @@ function LoadingCard() {
function RetryCard({ retry, text }: { retry: () => void; text: string }) { function RetryCard({ retry, text }: { retry: () => void; text: string }) {
return ( return (
<div className="card"> <div className="card">
<div className="card-body"> <div className="card-body text-center">
{text} {text}
<span className="btn btn-white ml-3" onClick={retry}> <span className="btn btn-white ml-3 d-none d-md-inline" onClick={retry}>
Try Again Try Again
</span> </span>
<div className="d-block d-md-none mt-4">
<hr></hr>
<span className="btn btn-white" onClick={retry}>
Try Again
</span>
</div>
</div> </div>
</div> </div>
); );

View File

@ -2,24 +2,20 @@ import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { import {
useTransactions, useTransactions,
useTransactionsDispatch,
checkTransactionStatus,
ActionType,
TransactionStatus, TransactionStatus,
Source, Source,
FetchStatus FetchStatus,
useFetchTransactionStatus
} from "../providers/transactions"; } from "../providers/transactions";
import bs58 from "bs58"; import bs58 from "bs58";
import { assertUnreachable } from "../utils"; import { assertUnreachable } from "../utils";
import { useCluster } from "../providers/cluster";
import Copyable from "./Copyable"; import Copyable from "./Copyable";
function TransactionsCard() { function TransactionsCard() {
const { transactions, idCounter } = useTransactions(); const { transactions, idCounter } = useTransactions();
const dispatch = useTransactionsDispatch(); const fetchTransaction = useFetchTransactionStatus();
const signatureInput = React.useRef<HTMLInputElement>(null); const signatureInput = React.useRef<HTMLInputElement>(null);
const [error, setError] = React.useState(""); const [error, setError] = React.useState("");
const { url } = useCluster();
const onNew = (signature: string) => { const onNew = (signature: string) => {
if (signature.length === 0) return; if (signature.length === 0) return;
@ -37,13 +33,7 @@ function TransactionsCard() {
return; return;
} }
dispatch({ fetchTransaction(signature, Source.Input);
type: ActionType.FetchSignature,
signature,
source: Source.Input
});
checkTransactionStatus(dispatch, signature, url);
const inputEl = signatureInput.current; const inputEl = signatureInput.current;
if (inputEl) { if (inputEl) {
inputEl.value = ""; inputEl.value = "";
@ -98,9 +88,7 @@ function TransactionsCard() {
<td>-</td> <td>-</td>
<td></td> <td></td>
</tr> </tr>
{transactions.map(transaction => {transactions.map(transaction => renderTransactionRow(transaction))}
renderTransactionRow(transaction, dispatch, url)
)}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -120,11 +108,7 @@ const renderHeader = () => {
); );
}; };
const renderTransactionRow = ( const renderTransactionRow = (transactionStatus: TransactionStatus) => {
transactionStatus: TransactionStatus,
dispatch: any,
url: string
) => {
const { fetchStatus, info, signature, id } = transactionStatus; const { fetchStatus, info, signature, id } = transactionStatus;
let statusText; let statusText;
@ -162,30 +146,6 @@ const renderTransactionRow = (
confirmationsText = `${info.confirmations}`; confirmationsText = `${info.confirmations}`;
} }
const renderDetails = () => {
if (info?.confirmations === "max") {
return (
<Link
to={location => ({ ...location, pathname: "/tx/" + signature })}
className="btn btn-rounded-circle btn-white btn-sm"
>
<span className="fe fe-arrow-right"></span>
</Link>
);
} else {
return (
<button
className="btn btn-rounded-circle btn-white btn-sm"
onClick={() => {
checkTransactionStatus(dispatch, signature, url);
}}
>
<span className="fe fe-refresh-cw"></span>
</button>
);
}
};
return ( return (
<tr key={signature}> <tr key={signature}>
<td> <td>
@ -201,7 +161,14 @@ const renderTransactionRow = (
</td> </td>
<td className="text-uppercase">{confirmationsText}</td> <td className="text-uppercase">{confirmationsText}</td>
<td>{slotText}</td> <td>{slotText}</td>
<td>{renderDetails()}</td> <td>
<Link
to={location => ({ ...location, pathname: "/tx/" + signature })}
className="btn btn-rounded-circle btn-white btn-sm"
>
<span className="fe fe-arrow-right"></span>
</Link>
</td>
</tr> </tr>
); );
}; };

View File

@ -61,28 +61,24 @@ interface State {
status: ClusterStatus; status: ClusterStatus;
} }
interface Connecting { interface Action {
status: ClusterStatus.Connecting; status: ClusterStatus;
cluster: Cluster; cluster: Cluster;
customUrl: string; customUrl: string;
} }
interface Connected {
status: ClusterStatus.Connected;
}
interface Failure {
status: ClusterStatus.Failure;
}
type Action = Connected | Connecting | Failure;
type Dispatch = (action: Action) => void; type Dispatch = (action: Action) => void;
function clusterReducer(state: State, action: Action): State { function clusterReducer(state: State, action: Action): State {
switch (action.status) { switch (action.status) {
case ClusterStatus.Connected: case ClusterStatus.Connected:
case ClusterStatus.Failure: { case ClusterStatus.Failure: {
return Object.assign({}, state, { status: action.status }); if (
state.cluster !== action.cluster ||
state.customUrl !== action.customUrl
)
return state;
return action;
} }
case ClusterStatus.Connecting: { case ClusterStatus.Connecting: {
return action; return action;
@ -197,10 +193,10 @@ async function updateCluster(
try { try {
const connection = new Connection(clusterUrl(cluster, customUrl)); const connection = new Connection(clusterUrl(cluster, customUrl));
await connection.getRecentBlockhash(); await connection.getRecentBlockhash();
dispatch({ status: ClusterStatus.Connected }); dispatch({ status: ClusterStatus.Connected, cluster, customUrl });
} catch (error) { } catch (error) {
console.error("Failed to update cluster", error); console.error("Failed to update cluster", error);
dispatch({ status: ClusterStatus.Failure }); dispatch({ status: ClusterStatus.Failure, cluster, customUrl });
} }
} }

View File

@ -7,7 +7,7 @@ import {
SignatureResult SignatureResult
} from "@solana/web3.js"; } from "@solana/web3.js";
import { useQuery } from "../../utils/url"; import { useQuery } from "../../utils/url";
import { useCluster, Cluster } from "../cluster"; import { useCluster, Cluster, ClusterStatus } from "../cluster";
import { import {
DetailsProvider, DetailsProvider,
StateContext as DetailsStateContext, StateContext as DetailsStateContext,
@ -127,7 +127,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
transactions: {} transactions: {}
}); });
const { cluster, url } = useCluster(); const { cluster, status: clusterStatus, url } = useCluster();
const accountsDispatch = useAccountsDispatch(); const accountsDispatch = useAccountsDispatch();
const query = useQuery(); const query = useQuery();
const testFlag = query.get("test"); const testFlag = query.get("test");
@ -140,14 +140,14 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
signature, signature,
source: Source.Url source: Source.Url
}); });
checkTransactionStatus(dispatch, signature, url); fetchTransactionStatus(dispatch, signature, url, clusterStatus);
}); });
// Create a test transaction // Create a test transaction
if (cluster === Cluster.Devnet && testFlag !== null) { if (cluster === Cluster.Devnet && testFlag !== null) {
createTestTransaction(dispatch, accountsDispatch, url); createTestTransaction(dispatch, accountsDispatch, url, clusterStatus);
} }
}, [testFlag, cluster, url]); // eslint-disable-line react-hooks/exhaustive-deps }, [testFlag, cluster, clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
// Check for transactions in the url params // Check for transactions in the url params
const values = TX_ALIASES.flatMap(key => [ const values = TX_ALIASES.flatMap(key => [
@ -167,7 +167,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
signature, signature,
source: Source.Url source: Source.Url
}); });
checkTransactionStatus(dispatch, signature, url); fetchTransactionStatus(dispatch, signature, url, clusterStatus);
}); });
}, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps }, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps
@ -183,7 +183,8 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
async function createTestTransaction( async function createTestTransaction(
dispatch: Dispatch, dispatch: Dispatch,
accountsDispatch: AccountsDispatch, accountsDispatch: AccountsDispatch,
url: string url: string,
clusterStatus: ClusterStatus
) { ) {
const testKey = process.env.REACT_APP_TEST_KEY; const testKey = process.env.REACT_APP_TEST_KEY;
let testAccount = new Account(); let testAccount = new Account();
@ -203,7 +204,7 @@ async function createTestTransaction(
signature, signature,
source: Source.Test source: Source.Test
}); });
checkTransactionStatus(dispatch, signature, url); fetchTransactionStatus(dispatch, signature, url, clusterStatus);
accountsDispatch({ accountsDispatch({
type: AccountsActionType.Input, type: AccountsActionType.Input,
pubkey: testAccount.publicKey pubkey: testAccount.publicKey
@ -226,16 +227,17 @@ async function createTestTransaction(
signature, signature,
source: Source.Test source: Source.Test
}); });
checkTransactionStatus(dispatch, signature, url); fetchTransactionStatus(dispatch, signature, url, clusterStatus);
} catch (error) { } catch (error) {
console.error("Failed to create test failure transaction", error); console.error("Failed to create test failure transaction", error);
} }
} }
export async function checkTransactionStatus( export async function fetchTransactionStatus(
dispatch: Dispatch, dispatch: Dispatch,
signature: TransactionSignature, signature: TransactionSignature,
url: string url: string,
status: ClusterStatus
) { ) {
dispatch({ dispatch({
type: ActionType.UpdateStatus, type: ActionType.UpdateStatus,
@ -243,6 +245,9 @@ export async function checkTransactionStatus(
fetchStatus: FetchStatus.Fetching fetchStatus: FetchStatus.Fetching
}); });
// We will auto-refetch when status is no longer connecting
if (status === ClusterStatus.Connecting) return;
let fetchStatus; let fetchStatus;
let info: TransactionStatusInfo | undefined; let info: TransactionStatusInfo | undefined;
try { try {
@ -317,16 +322,6 @@ export function useTransactionDetails(signature: TransactionSignature) {
return context[signature]; return context[signature];
} }
export function useTransactionsDispatch() {
const context = React.useContext(DispatchContext);
if (!context) {
throw new Error(
`useTransactionsDispatch must be used within a TransactionsProvider`
);
}
return context;
}
export function useDetailsDispatch() { export function useDetailsDispatch() {
const context = React.useContext(DetailsDispatchContext); const context = React.useContext(DetailsDispatchContext);
if (!context) { if (!context) {
@ -336,3 +331,25 @@ export function useDetailsDispatch() {
} }
return context; return context;
} }
export function useFetchTransactionStatus() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {
throw new Error(
`useFetchTransactionStatus must be used within a TransactionsProvider`
);
}
const { url, status } = useCluster();
return (signature: TransactionSignature, source?: Source) => {
if (source !== undefined) {
dispatch({
type: ActionType.FetchSignature,
signature,
source
});
}
fetchTransactionStatus(dispatch, signature, url, status);
};
}