diff --git a/explorer/src/components/AccountDetails.tsx b/explorer/src/components/AccountDetails.tsx index 90e99070bf..3eeb857d45 100644 --- a/explorer/src/components/AccountDetails.tsx +++ b/explorer/src/components/AccountDetails.tsx @@ -19,8 +19,6 @@ import { useFetchAccountHistory } from "providers/accounts/history"; type Props = { address: string }; export default function AccountDetails({ address }: Props) { - const fetchAccount = useFetchAccountInfo(); - let pubkey: PublicKey | undefined; try { pubkey = new PublicKey(address); @@ -29,11 +27,6 @@ export default function AccountDetails({ address }: Props) { // TODO handle bad addresses } - // Fetch account on load - React.useEffect(() => { - if (pubkey) fetchAccount(pubkey); - }, [address]); // eslint-disable-line react-hooks/exhaustive-deps - return (
@@ -49,10 +42,16 @@ export default function AccountDetails({ address }: Props) { } function AccountCards({ pubkey }: { pubkey: PublicKey }) { + const fetchAccount = useFetchAccountInfo(); const address = pubkey.toBase58(); const info = useAccountInfo(address); const refresh = useFetchAccountInfo(); + // Fetch account on load + React.useEffect(() => { + if (pubkey && !info) fetchAccount(pubkey); + }, [address]); // eslint-disable-line react-hooks/exhaustive-deps + if (!info || info.status === FetchStatus.Fetching) { return ; } else if ( diff --git a/explorer/src/components/TransactionDetails.tsx b/explorer/src/components/TransactionDetails.tsx index a528ba83b9..cd84cce015 100644 --- a/explorer/src/components/TransactionDetails.tsx +++ b/explorer/src/components/TransactionDetails.tsx @@ -28,13 +28,6 @@ import { isCached } from "providers/transactions/cached"; type Props = { signature: TransactionSignature }; export default function TransactionDetails({ signature }: Props) { - const fetchTransaction = useFetchTransactionStatus(); - - // Fetch transaction on load - React.useEffect(() => { - fetchTransaction(signature); - }, [signature]); // eslint-disable-line react-hooks/exhaustive-deps - return (
@@ -52,11 +45,17 @@ export default function TransactionDetails({ signature }: Props) { } function StatusCard({ signature }: Props) { + const fetchStatus = useFetchTransactionStatus(); const status = useTransactionStatus(signature); const refresh = useFetchTransactionStatus(); const details = useTransactionDetails(signature); const { firstAvailableBlock } = useCluster(); + // Fetch transaction on load + React.useEffect(() => { + if (!status) fetchStatus(signature); + }, [signature]); // eslint-disable-line react-hooks/exhaustive-deps + if (!status || status.fetchStatus === FetchStatus.Fetching) { return ; } else if (status?.fetchStatus === FetchStatus.FetchFailed) { diff --git a/explorer/src/providers/accounts/history.tsx b/explorer/src/providers/accounts/history.tsx index 13b90e6c23..1bc699dfff 100644 --- a/explorer/src/providers/accounts/history.tsx +++ b/explorer/src/providers/accounts/history.tsx @@ -19,7 +19,7 @@ type State = { [address: string]: AccountHistory }; export enum ActionType { Update, Add, - Remove, + Clear, } interface Update { @@ -32,38 +32,26 @@ interface Update { interface Add { type: ActionType.Add; - addresses: string[]; + address: string; } -interface Remove { - type: ActionType.Remove; - addresses: string[]; +interface Clear { + type: ActionType.Clear; } -type Action = Update | Add | Remove; +type Action = Update | Add | Clear; type Dispatch = (action: Action) => void; function reducer(state: State, action: Action): State { switch (action.type) { case ActionType.Add: { - if (action.addresses.length === 0) return state; const details = { ...state }; - action.addresses.forEach((address) => { - if (!details[address]) { - details[address] = { - status: FetchStatus.Fetching, - }; - } - }); - return details; - } - - case ActionType.Remove: { - if (action.addresses.length === 0) return state; - const details = { ...state }; - action.addresses.forEach((address) => { - delete details[address]; - }); + const address = action.address; + if (!details[address]) { + details[address] = { + status: FetchStatus.Fetching, + }; + } return details; } @@ -87,6 +75,10 @@ function reducer(state: State, action: Action): State { } break; } + + case ActionType.Clear: { + return {}; + } } return state; } @@ -100,45 +92,33 @@ const DispatchContext = React.createContext(undefined); type HistoryProviderProps = { children: React.ReactNode }; export function HistoryProvider({ children }: HistoryProviderProps) { const [state, dispatch] = React.useReducer(reducer, {}); - const { accounts } = useAccounts(); + const { accounts, lastFetchedAddress } = useAccounts(); const { url } = useCluster(); const manager = React.useRef(new HistoryManager(url)); React.useEffect(() => { manager.current = new HistoryManager(url); + dispatch({ type: ActionType.Clear }); }, [url]); // Fetch history for new accounts React.useEffect(() => { - const removeAddresses = new Set(); - const fetchAddresses = new Set(); - accounts.forEach(({ pubkey, lamports }) => { - const address = pubkey.toBase58(); - if (lamports !== undefined && !state[address]) - fetchAddresses.add(address); - else if (lamports === undefined && state[address]) - removeAddresses.add(address); - }); - - const removeList: string[] = []; - removeAddresses.forEach((address) => { - manager.current.removeAccountHistory(address); - removeList.push(address); - }); - dispatch({ type: ActionType.Remove, addresses: removeList }); - - const fetchList: string[] = []; - fetchAddresses.forEach((s) => fetchList.push(s)); - dispatch({ type: ActionType.Add, addresses: fetchList }); - fetchAddresses.forEach((address) => { - fetchAccountHistory( - dispatch, - new PublicKey(address), - manager.current, - true - ); - }); - }, [accounts]); // eslint-disable-line react-hooks/exhaustive-deps + if (lastFetchedAddress) { + const infoFetched = + accounts[lastFetchedAddress] && + accounts[lastFetchedAddress].lamports !== undefined; + const noHistory = !state[lastFetchedAddress]; + if (infoFetched && noHistory) { + dispatch({ type: ActionType.Add, address: lastFetchedAddress }); + fetchAccountHistory( + dispatch, + new PublicKey(lastFetchedAddress), + manager.current, + true + ); + } + } + }, [accounts, lastFetchedAddress]); // eslint-disable-line react-hooks/exhaustive-deps return ( diff --git a/explorer/src/providers/accounts/index.tsx b/explorer/src/providers/accounts/index.tsx index 6a56ac4c40..fc3defc2a1 100644 --- a/explorer/src/providers/accounts/index.tsx +++ b/explorer/src/providers/accounts/index.tsx @@ -1,7 +1,6 @@ import React from "react"; import { StakeAccount } from "solana-sdk-wasm"; import { PublicKey, Connection, StakeProgram } from "@solana/web3.js"; -import { useQuery } from "../../utils/url"; import { useCluster, ClusterStatus } from "../cluster"; import { HistoryProvider } from "./history"; export { useAccountHistory } from "./history"; @@ -20,7 +19,6 @@ export interface Details { } export interface Account { - id: number; pubkey: PublicKey; status: FetchStatus; lamports?: number; @@ -29,13 +27,14 @@ export interface Account { type Accounts = { [address: string]: Account }; interface State { - idCounter: number; accounts: Accounts; + lastFetchedAddress: string | undefined; } export enum ActionType { Update, Fetch, + Clear, } interface Update { @@ -53,7 +52,11 @@ interface Fetch { pubkey: PublicKey; } -type Action = Update | Fetch; +interface Clear { + type: ActionType.Clear; +} + +type Action = Update | Fetch | Clear; type Dispatch = (action: Action) => void; function reducer(state: State, action: Action): State { @@ -65,23 +68,20 @@ function reducer(state: State, action: Action): State { const accounts = { ...state.accounts, [address]: { - id: account.id, pubkey: account.pubkey, status: FetchStatus.Fetching, }, }; - return { ...state, accounts }; + return { ...state, accounts, lastFetchedAddress: address }; } else { - const idCounter = state.idCounter + 1; const accounts = { ...state.accounts, [address]: { - id: idCounter, status: FetchStatus.Fetching, pubkey: action.pubkey, }, }; - return { ...state, accounts, idCounter }; + return { ...state, accounts, lastFetchedAddress: address }; } } @@ -100,6 +100,13 @@ function reducer(state: State, action: Action): State { } break; } + + case ActionType.Clear: { + return { + ...state, + accounts: {}, + }; + } } return state; } @@ -113,40 +120,20 @@ const DispatchContext = React.createContext(undefined); type AccountsProviderProps = { children: React.ReactNode }; export function AccountsProvider({ children }: AccountsProviderProps) { const [state, dispatch] = React.useReducer(reducer, { - idCounter: 0, accounts: {}, + lastFetchedAddress: undefined, }); - const { status, url } = useCluster(); - // Check account statuses on startup and whenever cluster updates + const { status, url } = useCluster(); React.useEffect(() => { - Object.keys(state.accounts).forEach((address) => { - fetchAccountInfo(dispatch, new PublicKey(address), url, status); - }); + if (status === ClusterStatus.Connecting) { + dispatch({ type: ActionType.Clear }); + } else if (status === ClusterStatus.Connected && state.lastFetchedAddress) { + fetchAccountInfo(dispatch, new PublicKey(state.lastFetchedAddress), url); + } }, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps - const query = useQuery(); - const values = ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map((key) => - query.get(key) - ); - React.useEffect(() => { - values - .filter((value): value is string => value !== null) - .flatMap((value) => value.split(",")) - // Remove duplicates - .filter((item, pos, self) => self.indexOf(item) === pos) - .filter((address) => !state.accounts[address]) - .forEach((address) => { - try { - fetchAccountInfo(dispatch, new PublicKey(address), url, status); - } catch (err) { - console.error(err); - // TODO handle bad addresses - } - }); - }, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps - return ( @@ -159,17 +146,13 @@ export function AccountsProvider({ children }: AccountsProviderProps) { async function fetchAccountInfo( dispatch: Dispatch, pubkey: PublicKey, - url: string, - status: ClusterStatus + url: string ) { dispatch({ type: ActionType.Fetch, pubkey, }); - // We will auto-refetch when status is no longer connecting - if (status === ClusterStatus.Connecting) return; - let fetchStatus; let details; let lamports; @@ -213,12 +196,7 @@ export function useAccounts() { if (!context) { throw new Error(`useAccounts must be used within a AccountsProvider`); } - return { - idCounter: context.idCounter, - accounts: Object.values(context.accounts).sort((a, b) => - a.id <= b.id ? 1 : -1 - ), - }; + return context; } export function useAccountInfo(address: string) { @@ -239,8 +217,8 @@ export function useFetchAccountInfo() { ); } - const { url, status } = useCluster(); + const { url } = useCluster(); return (pubkey: PublicKey) => { - fetchAccountInfo(dispatch, pubkey, url, status); + fetchAccountInfo(dispatch, pubkey, url); }; } diff --git a/explorer/src/providers/transactions/details.tsx b/explorer/src/providers/transactions/details.tsx index 030a7f1d16..7abce55053 100644 --- a/explorer/src/providers/transactions/details.tsx +++ b/explorer/src/providers/transactions/details.tsx @@ -18,7 +18,7 @@ type State = { [signature: string]: Details }; export enum ActionType { Update, Add, - Remove, + Clear, } interface Update { @@ -30,39 +30,27 @@ interface Update { interface Add { type: ActionType.Add; - signatures: TransactionSignature[]; + signature: TransactionSignature; } -interface Remove { - type: ActionType.Remove; - signatures: TransactionSignature[]; +interface Clear { + type: ActionType.Clear; } -type Action = Update | Add | Remove; +type Action = Update | Add | Clear; type Dispatch = (action: Action) => void; function reducer(state: State, action: Action): State { switch (action.type) { case ActionType.Add: { - if (action.signatures.length === 0) return state; const details = { ...state }; - action.signatures.forEach((signature) => { - if (!details[signature]) { - details[signature] = { - fetchStatus: FetchStatus.Fetching, - transaction: null, - }; - } - }); - return details; - } - - case ActionType.Remove: { - if (action.signatures.length === 0) return state; - const details = { ...state }; - action.signatures.forEach((signature) => { - delete details[signature]; - }); + const signature = action.signature; + if (!details[signature]) { + details[signature] = { + fetchStatus: FetchStatus.Fetching, + transaction: null, + }; + } return details; } @@ -81,6 +69,10 @@ function reducer(state: State, action: Action): State { } break; } + + case ActionType.Clear: { + return {}; + } } return state; } @@ -93,32 +85,26 @@ export const DispatchContext = React.createContext( type DetailsProviderProps = { children: React.ReactNode }; export function DetailsProvider({ children }: DetailsProviderProps) { const [state, dispatch] = React.useReducer(reducer, {}); - - const { transactions } = useTransactions(); + const { transactions, lastFetched } = useTransactions(); const { url } = useCluster(); + React.useEffect(() => { + dispatch({ type: ActionType.Clear }); + }, [url]); + // Filter blocks for current transaction slots React.useEffect(() => { - const removeSignatures = new Set(); - const fetchSignatures = new Set(); - transactions.forEach(({ signature, info }) => { - if (info?.confirmations === "max" && !state[signature]) - fetchSignatures.add(signature); - else if (info?.confirmations !== "max" && state[signature]) - removeSignatures.add(signature); - }); - - const removeList: string[] = []; - removeSignatures.forEach((s) => removeList.push(s)); - dispatch({ type: ActionType.Remove, signatures: removeList }); - - const fetchList: string[] = []; - fetchSignatures.forEach((s) => fetchList.push(s)); - dispatch({ type: ActionType.Add, signatures: fetchList }); - fetchSignatures.forEach((signature) => { - fetchDetails(dispatch, signature, url); - }); - }, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps + if (lastFetched) { + const confirmed = + transactions[lastFetched] && + transactions[lastFetched].info?.confirmations === "max"; + const noDetails = !state[lastFetched]; + if (confirmed && noDetails) { + dispatch({ type: ActionType.Add, signature: lastFetched }); + fetchDetails(dispatch, lastFetched, url); + } + } + }, [transactions, lastFetched]); // eslint-disable-line react-hooks/exhaustive-deps return ( diff --git a/explorer/src/providers/transactions/index.tsx b/explorer/src/providers/transactions/index.tsx index 9e07e1f8f8..77aa77cae4 100644 --- a/explorer/src/providers/transactions/index.tsx +++ b/explorer/src/providers/transactions/index.tsx @@ -8,7 +8,7 @@ import { PublicKey, sendAndConfirmTransaction, } from "@solana/web3.js"; -import { useQuery } from "../../utils/url"; +import { useQuery } from "utils/url"; import { useCluster, Cluster, ClusterStatus } from "../cluster"; import { DetailsProvider, @@ -36,7 +36,6 @@ export interface TransactionStatusInfo { } export interface TransactionStatus { - id: number; fetchStatus: FetchStatus; signature: TransactionSignature; info?: TransactionStatusInfo; @@ -44,13 +43,14 @@ export interface TransactionStatus { type Transactions = { [signature: string]: TransactionStatus }; interface State { - idCounter: number; transactions: Transactions; + lastFetched: TransactionSignature | undefined; } export enum ActionType { UpdateStatus, FetchSignature, + Clear, } interface UpdateStatus { @@ -65,14 +65,18 @@ interface FetchSignature { signature: TransactionSignature; } -type Action = UpdateStatus | FetchSignature; +interface Clear { + type: ActionType.Clear; +} +type Action = UpdateStatus | FetchSignature | Clear; type Dispatch = (action: Action) => void; function reducer(state: State, action: Action): State { switch (action.type) { case ActionType.FetchSignature: { - const transaction = state.transactions[action.signature]; + const signature = action.signature; + const transaction = state.transactions[signature]; if (transaction) { const transactions = { ...state.transactions, @@ -82,18 +86,16 @@ function reducer(state: State, action: Action): State { info: undefined, }, }; - return { ...state, transactions }; + return { ...state, transactions, lastFetched: signature }; } else { - const nextId = state.idCounter + 1; const transactions = { ...state.transactions, [action.signature]: { - id: nextId, signature: action.signature, fetchStatus: FetchStatus.Fetching, }, }; - return { ...state, transactions, idCounter: nextId }; + return { ...state, transactions, lastFetched: signature }; } } @@ -112,6 +114,13 @@ function reducer(state: State, action: Action): State { } break; } + + case ActionType.Clear: { + return { + ...state, + transactions: {}, + }; + } } return state; } @@ -124,8 +133,8 @@ const DispatchContext = React.createContext(undefined); type TransactionsProviderProps = { children: React.ReactNode }; export function TransactionsProvider({ children }: TransactionsProviderProps) { const [state, dispatch] = React.useReducer(reducer, { - idCounter: 0, transactions: {}, + lastFetched: undefined, }); const { cluster, status: clusterStatus, url } = useCluster(); @@ -135,9 +144,11 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) { // Check transaction statuses whenever cluster updates React.useEffect(() => { - Object.keys(state.transactions).forEach((signature) => { - fetchTransactionStatus(dispatch, signature, url, clusterStatus); - }); + if (clusterStatus === ClusterStatus.Connecting) { + dispatch({ type: ActionType.Clear }); + } else if (clusterStatus === ClusterStatus.Connected && state.lastFetched) { + fetchTransactionStatus(dispatch, state.lastFetched, url); + } // Create a test transaction if (cluster === Cluster.Devnet && testFlag !== null) { @@ -145,23 +156,6 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) { } }, [testFlag, cluster, clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps - // Check for transactions in the url params - const values = TX_ALIASES.flatMap((key) => [ - query.get(key), - query.get(key + "s"), - ]); - React.useEffect(() => { - values - .filter((value): value is string => value !== null) - .flatMap((value) => value.split(",")) - // Remove duplicates - .filter((item, pos, self) => self.indexOf(item) === pos) - .filter((signature) => !state.transactions[signature]) - .forEach((signature) => { - fetchTransactionStatus(dispatch, signature, url, clusterStatus); - }); - }, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps - return ( @@ -189,7 +183,7 @@ async function createTestTransaction( testAccount.publicKey, 100000 ); - fetchTransactionStatus(dispatch, signature, url, clusterStatus); + fetchTransactionStatus(dispatch, signature, url); fetchAccount(testAccount.publicKey); } catch (error) { console.error("Failed to create test success transaction", error); @@ -208,7 +202,7 @@ async function createTestTransaction( [testAccount], { confirmations: 1, skipPreflight: false } ); - fetchTransactionStatus(dispatch, signature, url, clusterStatus); + fetchTransactionStatus(dispatch, signature, url); } catch (error) { console.error("Failed to create test failure transaction", error); } @@ -217,17 +211,13 @@ async function createTestTransaction( export async function fetchTransactionStatus( dispatch: Dispatch, signature: TransactionSignature, - url: string, - status: ClusterStatus + url: string ) { dispatch({ type: ActionType.FetchSignature, signature, }); - // We will auto-refetch when status is no longer connecting - if (status === ClusterStatus.Connecting) return; - let fetchStatus; let info: TransactionStatusInfo | undefined; if (isCached(url, signature)) { @@ -281,12 +271,7 @@ export function useTransactions() { `useTransactions must be used within a TransactionsProvider` ); } - return { - idCounter: context.idCounter, - transactions: Object.values(context.transactions).sort((a, b) => - a.id <= b.id ? 1 : -1 - ), - }; + return context; } export function useTransactionStatus(signature: TransactionSignature) { @@ -321,8 +306,8 @@ export function useFetchTransactionStatus() { ); } - const { url, status } = useCluster(); + const { url } = useCluster(); return (signature: TransactionSignature) => { - fetchTransactionStatus(dispatch, signature, url, status); + fetchTransactionStatus(dispatch, signature, url); }; }