diff --git a/explorer/src/components/account/OwnedTokensCard.tsx b/explorer/src/components/account/OwnedTokensCard.tsx
index 2845de2c6b..428f8a11f9 100644
--- a/explorer/src/components/account/OwnedTokensCard.tsx
+++ b/explorer/src/components/account/OwnedTokensCard.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
-import { FetchStatus } from "providers/accounts";
+import { FetchStatus } from "providers/cache";
import {
useFetchAccountOwnedTokens,
useAccountOwnedTokens,
@@ -24,7 +24,8 @@ export function OwnedTokensCard({ pubkey }: { pubkey: PublicKey }) {
return null;
}
- const { status, tokens } = ownedTokens;
+ const { status } = ownedTokens;
+ const tokens = ownedTokens.data?.tokens;
const fetching = status === FetchStatus.Fetching;
if (fetching && (tokens === undefined || tokens.length === 0)) {
return ;
diff --git a/explorer/src/components/account/TokenHistoryCard.tsx b/explorer/src/components/account/TokenHistoryCard.tsx
index dcd1fdff1d..434e70a9b9 100644
--- a/explorer/src/components/account/TokenHistoryCard.tsx
+++ b/explorer/src/components/account/TokenHistoryCard.tsx
@@ -4,7 +4,7 @@ import {
ConfirmedSignatureInfo,
ParsedInstruction,
} from "@solana/web3.js";
-import { FetchStatus } from "providers/accounts";
+import { FetchStatus } from "providers/cache";
import {
useAccountHistories,
useFetchAccountHistory,
@@ -34,7 +34,7 @@ export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
return null;
}
- const { tokens } = ownedTokens;
+ const tokens = ownedTokens.data?.tokens;
if (tokens === undefined || tokens.length === 0) return null;
return ;
@@ -62,17 +62,17 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
const fetchedFullHistory = tokens.every((token) => {
const history = accountHistories[token.pubkey.toBase58()];
- return history && history.foundOldest === true;
+ return history?.data?.foundOldest === true;
});
const fetching = tokens.some((token) => {
const history = accountHistories[token.pubkey.toBase58()];
- return history && history.status === FetchStatus.Fetching;
+ return history?.status === FetchStatus.Fetching;
});
const failed = tokens.some((token) => {
const history = accountHistories[token.pubkey.toBase58()];
- return history && history.status === FetchStatus.FetchFailed;
+ return history?.status === FetchStatus.FetchFailed;
});
const mintAndTxs = tokens
@@ -81,12 +81,13 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
history: accountHistories[token.pubkey.toBase58()],
}))
.filter(({ history }) => {
- return (
- history !== undefined && history.fetched && history.fetched.length > 0
- );
+ return history?.data?.fetched && history.data.fetched.length > 0;
})
.flatMap(({ mint, history }) =>
- (history.fetched as ConfirmedSignatureInfo[]).map((tx) => ({ mint, tx }))
+ (history?.data?.fetched as ConfirmedSignatureInfo[]).map((tx) => ({
+ mint,
+ tx,
+ }))
);
if (mintAndTxs.length === 0) {
@@ -196,7 +197,8 @@ function TokenTransactionRow({
if (!details) fetchDetails(tx.signature);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
- const instructions = details?.transaction?.transaction.message.instructions;
+ const instructions =
+ details?.data?.transaction?.transaction.message.instructions;
if (instructions) {
const tokenInstructions = instructions.filter(
(ix) => "parsed" in ix && ix.program === "spl-token"
diff --git a/explorer/src/components/account/TransactionHistoryCard.tsx b/explorer/src/components/account/TransactionHistoryCard.tsx
index 81c2ba8a87..0a584d45e6 100644
--- a/explorer/src/components/account/TransactionHistoryCard.tsx
+++ b/explorer/src/components/account/TransactionHistoryCard.tsx
@@ -1,10 +1,7 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
-import {
- FetchStatus,
- useAccountInfo,
- useAccountHistory,
-} from "providers/accounts";
+import { FetchStatus } from "providers/cache";
+import { useAccountInfo, useAccountHistory } from "providers/accounts";
import { useFetchAccountHistory } from "providers/accounts/history";
import { Signature } from "components/common/Signature";
import { ErrorCard } from "components/common/ErrorCard";
@@ -22,9 +19,11 @@ export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
if (!history) refresh();
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
- if (!info || !history || info.lamports === undefined) {
+ if (!history || info?.data === undefined) {
return null;
- } else if (history.fetched === undefined) {
+ }
+
+ if (history?.data === undefined) {
if (history.status === FetchStatus.Fetching) {
return ;
}
@@ -34,7 +33,8 @@ export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
);
}
- if (history.fetched.length === 0) {
+ const transactions = history.data.fetched;
+ if (transactions.length === 0) {
if (history.status === FetchStatus.Fetching) {
return ;
}
@@ -48,8 +48,6 @@ export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
}
const detailsList: React.ReactNode[] = [];
- const transactions = history.fetched;
-
for (var i = 0; i < transactions.length; i++) {
const slot = transactions[i].slot;
const slotTransactions = [transactions[i]];
@@ -126,7 +124,7 @@ export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
- {history.foundOldest ? (
+ {history.data.foundOldest ? (
Fetched full history
) : (
;
} else if (
info.status === FetchStatus.FetchFailed ||
- info.lamports === undefined
+ info.data?.lamports === undefined
) {
return
refresh(pubkey)} text="Fetch Failed" />;
}
- const data = info.details?.data;
+ const account = info.data;
+ const data = account?.details?.data;
if (data && data.name === "stake") {
let stakeAccountType, stakeAccount;
if ("accountType" in data.parsed) {
@@ -80,15 +78,15 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
return (
);
} else if (data && data.name === "spl-token") {
- return ;
+ return ;
} else {
- return ;
+ return ;
}
}
@@ -96,7 +94,7 @@ type MoreTabs = "history" | "tokens";
function MoreSection({ pubkey, tab }: { pubkey: PublicKey; tab: MoreTabs }) {
const address = pubkey.toBase58();
const info = useAccountInfo(address);
- if (!info || info.lamports === undefined) return null;
+ if (info?.data === undefined) return null;
return (
<>
diff --git a/explorer/src/pages/TransactionDetailsPage.tsx b/explorer/src/pages/TransactionDetailsPage.tsx
index eb3696a78d..7dbb584896 100644
--- a/explorer/src/pages/TransactionDetailsPage.tsx
+++ b/explorer/src/pages/TransactionDetailsPage.tsx
@@ -3,7 +3,6 @@ import {
useFetchTransactionStatus,
useTransactionStatus,
useTransactionDetails,
- FetchStatus,
} from "providers/transactions";
import { useFetchTransactionDetails } from "providers/transactions/details";
import { useCluster, ClusterStatus } from "providers/cluster";
@@ -27,6 +26,7 @@ import { Address } from "components/common/Address";
import { Signature } from "components/common/Signature";
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 }: Props) {
@@ -67,13 +67,13 @@ function StatusCard({ signature }: Props) {
}
}, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps
- if (!status || status.fetchStatus === FetchStatus.Fetching) {
+ if (!status || status.status === FetchStatus.Fetching) {
return ;
- } else if (status?.fetchStatus === FetchStatus.FetchFailed) {
+ } else if (status.status === FetchStatus.FetchFailed) {
return (
fetchStatus(signature)} text="Fetch Failed" />
);
- } else if (!status.info) {
+ } else if (!status.data?.info) {
if (firstAvailableBlock !== undefined) {
return (
fetchStatus(signature)} text="Not Found" />;
}
- const { info } = status;
+ const { info } = status.data;
const renderResult = () => {
let statusClass = "success";
let statusText = "Success";
@@ -102,8 +102,8 @@ function StatusCard({ signature }: Props) {
);
};
- const fee = details?.transaction?.meta?.fee;
- const transaction = details?.transaction?.transaction;
+ const fee = details?.data?.transaction?.meta?.fee;
+ const transaction = details?.data?.transaction?.transaction;
const blockhash = transaction?.message.recentBlockhash;
const isNonce = (() => {
if (!transaction) return false;
@@ -203,18 +203,18 @@ function AccountsCard({ signature }: Props) {
const fetchDetails = useFetchTransactionDetails();
const refreshStatus = () => fetchStatus(signature);
const refreshDetails = () => fetchDetails(signature);
- const transaction = details?.transaction?.transaction;
+ const transaction = details?.data?.transaction?.transaction;
const message = transaction?.message;
const status = useTransactionStatus(signature);
// Fetch details on load
React.useEffect(() => {
- if (status?.info?.confirmations === "max" && !details) {
+ if (status?.data?.info?.confirmations === "max" && !details) {
fetchDetails(signature);
}
}, [signature, details, status, fetchDetails]);
- if (!status || !status.info) {
+ if (!status?.data?.info) {
return null;
} else if (!details) {
return (
@@ -223,15 +223,15 @@ function AccountsCard({ signature }: Props) {
text="Details are not available until the transaction reaches MAX confirmations"
/>
);
- } else if (details.fetchStatus === FetchStatus.Fetching) {
+ } else if (details.status === FetchStatus.Fetching) {
return ;
- } else if (details?.fetchStatus === FetchStatus.FetchFailed) {
+ } else if (details.status === FetchStatus.FetchFailed) {
return ;
- } else if (!details.transaction || !message) {
+ } else if (!details.data?.transaction || !message) {
return ;
}
- const { meta } = details.transaction;
+ const { meta } = details.data.transaction;
if (!meta) {
if (isCached(url, signature)) {
return null;
@@ -308,14 +308,14 @@ function InstructionsSection({ signature }: Props) {
const fetchDetails = useFetchTransactionDetails();
const refreshDetails = () => fetchDetails(signature);
- if (!status || !status.info || !details || !details.transaction) return null;
+ if (!status?.data?.info || !details?.data?.transaction) return null;
- const { transaction } = details.transaction;
+ const { transaction } = details.data.transaction;
if (transaction.message.instructions.length === 0) {
return ;
}
- const result = status.info.result;
+ const result = status.data.info.result;
const instructionDetails = transaction.message.instructions.map(
(next, index) => {
if ("parsed" in next) {
diff --git a/explorer/src/providers/accounts/history.tsx b/explorer/src/providers/accounts/history.tsx
index be46a9cce6..306b0557f0 100644
--- a/explorer/src/providers/accounts/history.tsx
+++ b/explorer/src/providers/accounts/history.tsx
@@ -5,51 +5,29 @@ import {
TransactionSignature,
Connection,
} from "@solana/web3.js";
-import { FetchStatus } from "./index";
import { useCluster } from "../cluster";
+import * as Cache from "providers/cache";
+import { ActionType, FetchStatus } from "providers/cache";
-interface AccountHistory {
- status: FetchStatus;
- fetched?: ConfirmedSignatureInfo[];
+type AccountHistory = {
+ fetched: ConfirmedSignatureInfo[];
foundOldest: boolean;
-}
-
-type State = {
- url: string;
- map: { [address: string]: AccountHistory };
};
-export enum ActionType {
- Update,
- Clear,
-}
-
-interface Update {
- type: ActionType.Update;
- url: string;
- pubkey: PublicKey;
- status: FetchStatus;
- fetched?: ConfirmedSignatureInfo[];
+type HistoryUpdate = {
+ history?: AccountHistory;
before?: TransactionSignature;
- foundOldest?: boolean;
-}
+};
-interface Clear {
- type: ActionType.Clear;
- url: string;
-}
-
-type Action = Update | Clear;
-type Dispatch = (action: Action) => void;
+type State = Cache.State;
+type Dispatch = Cache.Dispatch;
function combineFetched(
- fetched: ConfirmedSignatureInfo[] | undefined,
+ fetched: ConfirmedSignatureInfo[],
current: ConfirmedSignatureInfo[] | undefined,
before: TransactionSignature | undefined
) {
- if (fetched === undefined) {
- return current;
- } else if (current === undefined) {
+ if (current === undefined) {
return fetched;
}
@@ -60,46 +38,19 @@ function combineFetched(
}
}
-function reducer(state: State, action: Action): State {
- switch (action.type) {
- case ActionType.Update: {
- if (action.url !== state.url) return state;
- const address = action.pubkey.toBase58();
- if (state.map[address]) {
- return {
- ...state,
- map: {
- ...state.map,
- [address]: {
- status: action.status,
- fetched: combineFetched(
- action.fetched,
- state.map[address].fetched,
- action.before
- ),
- foundOldest: action.foundOldest || state.map[address].foundOldest,
- },
- },
- };
- } else {
- return {
- ...state,
- map: {
- ...state.map,
- [address]: {
- status: action.status,
- fetched: action.fetched,
- foundOldest: action.foundOldest || false,
- },
- },
- };
- }
- }
-
- case ActionType.Clear: {
- return { url: action.url, map: {} };
- }
- }
+function reconcile(
+ history: AccountHistory | undefined,
+ update: HistoryUpdate | undefined
+) {
+ if (update?.history === undefined) return;
+ return {
+ fetched: combineFetched(
+ update.history.fetched,
+ history?.fetched,
+ update?.before
+ ),
+ foundOldest: update?.history?.foundOldest || history?.foundOldest || false,
+ };
}
const StateContext = React.createContext(undefined);
@@ -108,11 +59,11 @@ const DispatchContext = React.createContext(undefined);
type HistoryProviderProps = { children: React.ReactNode };
export function HistoryProvider({ children }: HistoryProviderProps) {
const { url } = useCluster();
- const [state, dispatch] = React.useReducer(reducer, { url, map: {} });
+ const [state, dispatch] = Cache.useCustomReducer(url, reconcile);
React.useEffect(() => {
dispatch({ type: ActionType.Clear, url });
- }, [url]);
+ }, [dispatch, url]);
return (
@@ -132,20 +83,22 @@ async function fetchAccountHistory(
dispatch({
type: ActionType.Update,
status: FetchStatus.Fetching,
- pubkey,
+ key: pubkey.toBase58(),
url,
});
let status;
- let fetched;
- let foundOldest;
+ let history;
try {
const connection = new Connection(url);
- fetched = await connection.getConfirmedSignaturesForAddress2(
+ const fetched = await connection.getConfirmedSignaturesForAddress2(
pubkey,
options
);
- foundOldest = fetched.length < options.limit;
+ history = {
+ fetched,
+ foundOldest: fetched.length < options.limit,
+ };
status = FetchStatus.Fetched;
} catch (error) {
console.error("Failed to fetch account history", error);
@@ -154,11 +107,12 @@ async function fetchAccountHistory(
dispatch({
type: ActionType.Update,
url,
+ key: pubkey.toBase58(),
status,
- fetched,
- before: options?.before,
- pubkey,
- foundOldest,
+ data: {
+ history,
+ before: options?.before,
+ },
});
}
@@ -171,17 +125,19 @@ export function useAccountHistories() {
);
}
- return context.map;
+ return context.entries;
}
-export function useAccountHistory(address: string) {
+export function useAccountHistory(
+ address: string
+): Cache.CacheEntry | undefined {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useAccountHistory must be used within a AccountsProvider`);
}
- return context.map[address];
+ return context.entries[address];
}
export function useFetchAccountHistory() {
@@ -195,10 +151,11 @@ export function useFetchAccountHistory() {
}
return (pubkey: PublicKey, refresh?: boolean) => {
- const before = state.map[pubkey.toBase58()];
- if (!refresh && before && before.fetched && before.fetched.length > 0) {
- if (before.foundOldest) return;
- const oldest = before.fetched[before.fetched.length - 1].signature;
+ const before = state.entries[pubkey.toBase58()];
+ if (!refresh && before?.data?.fetched && before.data.fetched.length > 0) {
+ if (before.data.foundOldest) return;
+ const oldest =
+ before.data.fetched[before.data.fetched.length - 1].signature;
fetchAccountHistory(dispatch, pubkey, url, { before: oldest, limit: 25 });
} else {
fetchAccountHistory(dispatch, pubkey, url, { limit: 25 });
diff --git a/explorer/src/providers/accounts/index.tsx b/explorer/src/providers/accounts/index.tsx
index 82f3f95199..1118988f22 100644
--- a/explorer/src/providers/accounts/index.tsx
+++ b/explorer/src/providers/accounts/index.tsx
@@ -8,14 +8,10 @@ import { coerce } from "superstruct";
import { ParsedInfo } from "validators";
import { StakeAccount } from "validators/accounts/stake";
import { TokenAccount } from "validators/accounts/token";
+import * as Cache from "providers/cache";
+import { ActionType, FetchStatus } from "providers/cache";
export { useAccountHistory } from "./history";
-export enum FetchStatus {
- Fetching,
- FetchFailed,
- Fetched,
-}
-
export type StakeProgramData = {
name: "stake";
parsed: StakeAccount | StakeAccountWasm;
@@ -37,98 +33,12 @@ export interface Details {
export interface Account {
pubkey: PublicKey;
- status: FetchStatus;
- lamports?: number;
+ lamports: number;
details?: Details;
}
-type Accounts = { [address: string]: Account };
-interface State {
- accounts: Accounts;
- url: string;
-}
-
-export enum ActionType {
- Update,
- Fetch,
- Clear,
-}
-
-interface Update {
- type: ActionType.Update;
- url: string;
- pubkey: PublicKey;
- data: {
- status: FetchStatus;
- lamports?: number;
- details?: Details;
- };
-}
-
-interface Fetch {
- type: ActionType.Fetch;
- url: string;
- pubkey: PublicKey;
-}
-
-interface Clear {
- type: ActionType.Clear;
- url: string;
-}
-
-type Action = Update | Fetch | Clear;
-type Dispatch = (action: Action) => void;
-
-function reducer(state: State, action: Action): State {
- if (action.type === ActionType.Clear) {
- return { url: action.url, accounts: {} };
- } else if (action.url !== state.url) {
- return state;
- }
-
- switch (action.type) {
- case ActionType.Fetch: {
- const address = action.pubkey.toBase58();
- const account = state.accounts[address];
- if (account) {
- const accounts = {
- ...state.accounts,
- [address]: {
- pubkey: account.pubkey,
- status: FetchStatus.Fetching,
- },
- };
- return { ...state, accounts };
- } else {
- const accounts = {
- ...state.accounts,
- [address]: {
- status: FetchStatus.Fetching,
- pubkey: action.pubkey,
- },
- };
- return { ...state, accounts };
- }
- }
-
- case ActionType.Update: {
- const address = action.pubkey.toBase58();
- const account = state.accounts[address];
- if (account) {
- const accounts = {
- ...state.accounts,
- [address]: {
- ...account,
- ...action.data,
- },
- };
- return { ...state, accounts };
- }
- break;
- }
- }
- return state;
-}
+type State = Cache.State;
+type Dispatch = Cache.Dispatch;
const StateContext = React.createContext(undefined);
const DispatchContext = React.createContext(undefined);
@@ -136,15 +46,12 @@ const DispatchContext = React.createContext(undefined);
type AccountsProviderProps = { children: React.ReactNode };
export function AccountsProvider({ children }: AccountsProviderProps) {
const { url } = useCluster();
- const [state, dispatch] = React.useReducer(reducer, {
- url,
- accounts: {},
- });
+ const [state, dispatch] = Cache.useReducer(url);
- // Clear account statuses whenever cluster is changed
+ // Clear accounts cache whenever cluster is changed
React.useEffect(() => {
dispatch({ type: ActionType.Clear, url });
- }, [url]);
+ }, [dispatch, url]);
return (
@@ -163,18 +70,20 @@ async function fetchAccountInfo(
url: string
) {
dispatch({
- type: ActionType.Fetch,
- pubkey,
+ type: ActionType.Update,
+ key: pubkey.toBase58(),
+ status: Cache.FetchStatus.Fetching,
url,
});
+ let data;
let fetchStatus;
- let details;
- let lamports;
try {
const result = (
await new Connection(url, "single").getParsedAccountInfo(pubkey)
).value;
+
+ let lamports, details;
if (result === null) {
lamports = 0;
} else {
@@ -227,13 +136,19 @@ async function fetchAccountInfo(
data,
};
}
+ data = { pubkey, lamports, details };
fetchStatus = FetchStatus.Fetched;
} catch (error) {
console.error("Failed to fetch account info", error);
fetchStatus = FetchStatus.FetchFailed;
}
- const data = { status: fetchStatus, lamports, details };
- dispatch({ type: ActionType.Update, data, pubkey, url });
+ dispatch({
+ type: ActionType.Update,
+ status: fetchStatus,
+ data,
+ key: pubkey.toBase58(),
+ url,
+ });
}
export function useAccounts() {
@@ -241,17 +156,19 @@ export function useAccounts() {
if (!context) {
throw new Error(`useAccounts must be used within a AccountsProvider`);
}
- return context.accounts;
+ return context.entries;
}
-export function useAccountInfo(address: string) {
+export function useAccountInfo(
+ address: string
+): Cache.CacheEntry | undefined {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useAccountInfo must be used within a AccountsProvider`);
}
- return context.accounts[address];
+ return context.entries[address];
}
export function useFetchAccountInfo() {
diff --git a/explorer/src/providers/accounts/tokens.tsx b/explorer/src/providers/accounts/tokens.tsx
index 4008ec6db7..3f2ee369ae 100644
--- a/explorer/src/providers/accounts/tokens.tsx
+++ b/explorer/src/providers/accounts/tokens.tsx
@@ -1,6 +1,7 @@
import React from "react";
import { Connection, PublicKey } from "@solana/web3.js";
-import { FetchStatus } from "./index";
+import * as Cache from "providers/cache";
+import { ActionType, FetchStatus } from "providers/cache";
import { TokenAccountInfo } from "validators/accounts/token";
import { useCluster } from "../cluster";
import { coerce } from "superstruct";
@@ -11,63 +12,11 @@ export type TokenInfoWithPubkey = {
};
interface AccountTokens {
- status: FetchStatus;
tokens?: TokenInfoWithPubkey[];
}
-interface Update {
- type: "update";
- url: string;
- pubkey: PublicKey;
- status: FetchStatus;
- tokens?: TokenInfoWithPubkey[];
-}
-
-interface Clear {
- type: "clear";
- url: string;
-}
-
-type Action = Update | Clear;
-type State = {
- url: string;
- map: { [address: string]: AccountTokens };
-};
-
-type Dispatch = (action: Action) => void;
-
-function reducer(state: State, action: Action): State {
- if (action.type === "clear") {
- return {
- url: action.url,
- map: {},
- };
- } else if (action.url !== state.url) {
- return state;
- }
-
- const address = action.pubkey.toBase58();
- let addressEntry = state.map[address];
- if (addressEntry && action.status === FetchStatus.Fetching) {
- addressEntry = {
- ...addressEntry,
- status: FetchStatus.Fetching,
- };
- } else {
- addressEntry = {
- tokens: action.tokens,
- status: action.status,
- };
- }
-
- return {
- ...state,
- map: {
- ...state.map,
- [address]: addressEntry,
- },
- };
-}
+type State = Cache.State;
+type Dispatch = Cache.Dispatch;
const StateContext = React.createContext(undefined);
const DispatchContext = React.createContext(undefined);
@@ -75,11 +24,11 @@ const DispatchContext = React.createContext(undefined);
type ProviderProps = { children: React.ReactNode };
export function TokensProvider({ children }: ProviderProps) {
const { url } = useCluster();
- const [state, dispatch] = React.useReducer(reducer, { url, map: {} });
+ const [state, dispatch] = Cache.useReducer(url);
React.useEffect(() => {
- dispatch({ url, type: "clear" });
- }, [url]);
+ dispatch({ url, type: ActionType.Clear });
+ }, [dispatch, url]);
return (
@@ -99,33 +48,38 @@ async function fetchAccountTokens(
pubkey: PublicKey,
url: string
) {
+ const key = pubkey.toBase58();
dispatch({
- type: "update",
+ type: ActionType.Update,
+ key,
status: FetchStatus.Fetching,
- pubkey,
url,
});
let status;
- let tokens;
+ let data;
try {
const { value } = await new Connection(
url,
"recent"
).getParsedTokenAccountsByOwner(pubkey, { programId: TOKEN_PROGRAM_ID });
- tokens = value.map((accountInfo) => {
- const parsedInfo = accountInfo.account.data.parsed.info;
- const info = coerce(parsedInfo, TokenAccountInfo);
- return { info, pubkey: accountInfo.pubkey };
- });
+ data = {
+ tokens: value.map((accountInfo) => {
+ const parsedInfo = accountInfo.account.data.parsed.info;
+ const info = coerce(parsedInfo, TokenAccountInfo);
+ return { info, pubkey: accountInfo.pubkey };
+ }),
+ };
status = FetchStatus.Fetched;
} catch (error) {
status = FetchStatus.FetchFailed;
}
- dispatch({ type: "update", url, status, tokens, pubkey });
+ dispatch({ type: ActionType.Update, url, status, data, key });
}
-export function useAccountOwnedTokens(address: string) {
+export function useAccountOwnedTokens(
+ address: string
+): Cache.CacheEntry | undefined {
const context = React.useContext(StateContext);
if (!context) {
@@ -134,7 +88,7 @@ export function useAccountOwnedTokens(address: string) {
);
}
- return context.map[address];
+ return context.entries[address];
}
export function useFetchAccountOwnedTokens() {
diff --git a/explorer/src/providers/cache.tsx b/explorer/src/providers/cache.tsx
new file mode 100644
index 0000000000..27484166ce
--- /dev/null
+++ b/explorer/src/providers/cache.tsx
@@ -0,0 +1,108 @@
+import React from "react";
+
+export enum FetchStatus {
+ Fetching,
+ FetchFailed,
+ Fetched,
+}
+
+export type CacheEntry = {
+ status: FetchStatus;
+ data?: T;
+};
+
+export type State = {
+ entries: {
+ [key: string]: CacheEntry;
+ };
+ url: string;
+};
+
+export enum ActionType {
+ Update,
+ Clear,
+}
+
+export type Update = {
+ type: ActionType.Update;
+ url: string;
+ key: string;
+ status: FetchStatus;
+ data?: T;
+};
+
+export type Clear = {
+ type: ActionType.Clear;
+ url: string;
+};
+
+export type Action = Update | Clear;
+export type Dispatch = (action: Action) => void;
+type Reducer = (state: State, action: Action) => State;
+type Reconciler = (
+ entry: T | undefined,
+ update: U | undefined
+) => T | undefined;
+
+function defaultReconciler(entry: T | undefined, update: T | undefined) {
+ if (entry) {
+ if (update) {
+ return {
+ ...entry,
+ ...update,
+ };
+ } else {
+ return entry;
+ }
+ } else {
+ return update;
+ }
+}
+
+function defaultReducer(state: State, action: Action) {
+ return reducer(state, action, defaultReconciler);
+}
+
+export function useReducer(url: string) {
+ return React.useReducer>(defaultReducer, { url, entries: {} });
+}
+
+export function useCustomReducer(
+ url: string,
+ reconciler: Reconciler
+) {
+ const customReducer = React.useMemo(() => {
+ return (state: State, action: Action) => {
+ return reducer(state, action, reconciler);
+ };
+ }, [reconciler]);
+ return React.useReducer>(customReducer, { url, entries: {} });
+}
+
+export function reducer(
+ state: State,
+ action: Action,
+ reconciler: Reconciler
+): State {
+ if (action.type === ActionType.Clear) {
+ return { url: action.url, entries: {} };
+ } else if (action.url !== state.url) {
+ return state;
+ }
+
+ switch (action.type) {
+ case ActionType.Update: {
+ const key = action.key;
+ const entry = state.entries[key];
+ const entries = {
+ ...state.entries,
+ [key]: {
+ ...entry,
+ status: action.status,
+ data: reconciler(entry?.data, action.data),
+ },
+ };
+ return { ...state, entries };
+ }
+ }
+}
diff --git a/explorer/src/providers/transactions/details.tsx b/explorer/src/providers/transactions/details.tsx
index d20775bd15..77e1fbbf4c 100644
--- a/explorer/src/providers/transactions/details.tsx
+++ b/explorer/src/providers/transactions/details.tsx
@@ -5,78 +5,16 @@ import {
ParsedConfirmedTransaction,
} from "@solana/web3.js";
import { useCluster } from "../cluster";
-import { FetchStatus } from "./index";
import { CACHED_DETAILS, isCached } from "./cached";
+import * as Cache from "providers/cache";
+import { ActionType, FetchStatus } from "providers/cache";
export interface Details {
- fetchStatus: FetchStatus;
- transaction: ParsedConfirmedTransaction | null;
+ transaction?: ParsedConfirmedTransaction | null;
}
-type State = {
- entries: { [signature: string]: Details };
- url: string;
-};
-
-export enum ActionType {
- Update,
- Clear,
-}
-
-interface Update {
- type: ActionType.Update;
- url: string;
- signature: string;
- fetchStatus: FetchStatus;
- transaction: ParsedConfirmedTransaction | null;
-}
-
-interface Clear {
- type: ActionType.Clear;
- url: string;
-}
-
-type Action = Update | Clear;
-type Dispatch = (action: Action) => void;
-
-function reducer(state: State, action: Action): State {
- if (action.type === ActionType.Clear) {
- return { url: action.url, entries: {} };
- } else if (action.url !== state.url) {
- return state;
- }
-
- switch (action.type) {
- case ActionType.Update: {
- const signature = action.signature;
- const details = state.entries[signature];
- if (details) {
- return {
- ...state,
- entries: {
- ...state.entries,
- [signature]: {
- ...details,
- fetchStatus: action.fetchStatus,
- transaction: action.transaction,
- },
- },
- };
- } else {
- return {
- ...state,
- entries: {
- ...state.entries,
- [signature]: {
- fetchStatus: FetchStatus.Fetching,
- transaction: null,
- },
- },
- };
- }
- }
- }
-}
+type State = Cache.State;
+type Dispatch = Cache.Dispatch;
export const StateContext = React.createContext(undefined);
export const DispatchContext = React.createContext(
@@ -86,11 +24,11 @@ export const DispatchContext = React.createContext(
type DetailsProviderProps = { children: React.ReactNode };
export function DetailsProvider({ children }: DetailsProviderProps) {
const { url } = useCluster();
- const [state, dispatch] = React.useReducer(reducer, { url, entries: {} });
+ const [state, dispatch] = Cache.useReducer(url);
React.useEffect(() => {
dispatch({ type: ActionType.Clear, url });
- }, [url]);
+ }, [dispatch, url]);
return (
@@ -108,14 +46,13 @@ async function fetchDetails(
) {
dispatch({
type: ActionType.Update,
- fetchStatus: FetchStatus.Fetching,
- transaction: null,
- signature,
+ status: FetchStatus.Fetching,
+ key: signature,
url,
});
let fetchStatus;
- let transaction = null;
+ let transaction;
if (isCached(url, signature)) {
transaction = CACHED_DETAILS[signature];
fetchStatus = FetchStatus.Fetched;
@@ -132,9 +69,9 @@ async function fetchDetails(
}
dispatch({
type: ActionType.Update,
- fetchStatus,
- signature,
- transaction,
+ status: fetchStatus,
+ key: signature,
+ data: { transaction },
url,
});
}
@@ -152,3 +89,17 @@ export function useFetchTransactionDetails() {
url && fetchDetails(dispatch, signature, url);
};
}
+
+export function useTransactionDetails(
+ signature: TransactionSignature
+): Cache.CacheEntry | undefined {
+ const context = React.useContext(StateContext);
+
+ if (!context) {
+ throw new Error(
+ `useTransactionDetails must be used within a TransactionsProvider`
+ );
+ }
+
+ return context.entries[signature];
+}
diff --git a/explorer/src/providers/transactions/index.tsx b/explorer/src/providers/transactions/index.tsx
index 89957cccf4..764b4e0040 100644
--- a/explorer/src/providers/transactions/index.tsx
+++ b/explorer/src/providers/transactions/index.tsx
@@ -2,27 +2,14 @@ import React from "react";
import {
TransactionSignature,
Connection,
- SystemProgram,
- Account,
SignatureResult,
- PublicKey,
- sendAndConfirmTransaction,
} from "@solana/web3.js";
-import { useQuery } from "utils/url";
-import { useCluster, Cluster, ClusterStatus } from "../cluster";
-import {
- DetailsProvider,
- StateContext as DetailsStateContext,
-} from "./details";
-import base58 from "bs58";
-import { useFetchAccountInfo } from "../accounts";
+import { useCluster } from "../cluster";
+import { DetailsProvider } from "./details";
+import * as Cache from "providers/cache";
+import { ActionType, FetchStatus } from "providers/cache";
import { CACHED_STATUSES, isCached } from "./cached";
-
-export enum FetchStatus {
- Fetching,
- FetchFailed,
- Fetched,
-}
+export { useTransactionDetails } from "./details";
export type Confirmations = number | "max";
@@ -36,125 +23,27 @@ export interface TransactionStatusInfo {
}
export interface TransactionStatus {
- fetchStatus: FetchStatus;
signature: TransactionSignature;
- info?: TransactionStatusInfo;
-}
-
-type Transactions = { [signature: string]: TransactionStatus };
-interface State {
- transactions: Transactions;
- url: string;
-}
-
-export enum ActionType {
- UpdateStatus,
- FetchSignature,
- Clear,
-}
-
-interface UpdateStatus {
- type: ActionType.UpdateStatus;
- url: string;
- signature: TransactionSignature;
- fetchStatus: FetchStatus;
- info?: TransactionStatusInfo;
-}
-
-interface FetchSignature {
- type: ActionType.FetchSignature;
- url: string;
- signature: TransactionSignature;
-}
-
-interface Clear {
- type: ActionType.Clear;
- url: string;
-}
-
-type Action = UpdateStatus | FetchSignature | Clear;
-type Dispatch = (action: Action) => void;
-
-function reducer(state: State, action: Action): State {
- if (action.type === ActionType.Clear) {
- return { url: action.url, transactions: {} };
- } else if (action.url !== state.url) {
- return state;
- }
-
- switch (action.type) {
- case ActionType.FetchSignature: {
- const signature = action.signature;
- const transaction = state.transactions[signature];
- if (transaction) {
- const transactions = {
- ...state.transactions,
- [action.signature]: {
- ...transaction,
- fetchStatus: FetchStatus.Fetching,
- info: undefined,
- },
- };
- return { ...state, transactions };
- } else {
- const transactions = {
- ...state.transactions,
- [action.signature]: {
- signature: action.signature,
- fetchStatus: FetchStatus.Fetching,
- },
- };
- return { ...state, transactions };
- }
- }
-
- case ActionType.UpdateStatus: {
- const transaction = state.transactions[action.signature];
- if (transaction) {
- const transactions = {
- ...state.transactions,
- [action.signature]: {
- ...transaction,
- fetchStatus: action.fetchStatus,
- info: action.info,
- },
- };
- return { ...state, transactions };
- }
- break;
- }
- }
- return state;
+ info: TransactionStatusInfo | null;
}
export const TX_ALIASES = ["tx", "txn", "transaction"];
+type State = Cache.State;
+type Dispatch = Cache.Dispatch;
+
const StateContext = React.createContext(undefined);
const DispatchContext = React.createContext(undefined);
type TransactionsProviderProps = { children: React.ReactNode };
export function TransactionsProvider({ children }: TransactionsProviderProps) {
- const { cluster, status: clusterStatus, url } = useCluster();
- const [state, dispatch] = React.useReducer(reducer, {
- transactions: {},
- url,
- });
+ const { url } = useCluster();
+ const [state, dispatch] = Cache.useReducer(url);
- const fetchAccount = useFetchAccountInfo();
- const query = useQuery();
- const testFlag = query.get("test");
-
- // Check transaction statuses whenever cluster updates
+ // Clear accounts cache whenever cluster is changed
React.useEffect(() => {
- if (clusterStatus === ClusterStatus.Connecting) {
- dispatch({ type: ActionType.Clear, url });
- }
-
- // Create a test transaction
- if (cluster === Cluster.Devnet && testFlag !== null) {
- createTestTransaction(dispatch, fetchAccount, url, clusterStatus);
- }
- }, [testFlag, cluster, clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
+ dispatch({ type: ActionType.Clear, url });
+ }, [dispatch, url]);
return (
@@ -165,64 +54,23 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
);
}
-async function createTestTransaction(
- dispatch: Dispatch,
- fetchAccount: (pubkey: PublicKey) => void,
- url: string,
- clusterStatus: ClusterStatus
-) {
- const testKey = process.env.REACT_APP_TEST_KEY;
- let testAccount = new Account();
- if (testKey) {
- testAccount = new Account(base58.decode(testKey));
- }
-
- try {
- const connection = new Connection(url, "recent");
- const signature = await connection.requestAirdrop(
- testAccount.publicKey,
- 100000
- );
- fetchTransactionStatus(dispatch, signature, url);
- fetchAccount(testAccount.publicKey);
- } catch (error) {
- console.error("Failed to create test success transaction", error);
- }
-
- try {
- const connection = new Connection(url, "recent");
- const tx = SystemProgram.transfer({
- fromPubkey: testAccount.publicKey,
- toPubkey: testAccount.publicKey,
- lamports: 1,
- });
- const signature = await sendAndConfirmTransaction(
- connection,
- tx,
- [testAccount],
- { confirmations: 1, skipPreflight: false }
- );
- fetchTransactionStatus(dispatch, signature, url);
- } catch (error) {
- console.error("Failed to create test failure transaction", error);
- }
-}
-
export async function fetchTransactionStatus(
dispatch: Dispatch,
signature: TransactionSignature,
url: string
) {
dispatch({
- type: ActionType.FetchSignature,
- signature,
+ type: ActionType.Update,
+ key: signature,
+ status: FetchStatus.Fetching,
url,
});
let fetchStatus;
- let info: TransactionStatusInfo | undefined;
+ let data;
if (isCached(url, signature)) {
- info = CACHED_STATUSES[signature];
+ const info = CACHED_STATUSES[signature];
+ data = { signature, info };
fetchStatus = FetchStatus.Fetched;
} else {
try {
@@ -231,6 +79,7 @@ export async function fetchTransactionStatus(
searchTransactionHistory: true,
});
+ let info = null;
if (value !== null) {
let blockTime = null;
try {
@@ -260,6 +109,7 @@ export async function fetchTransactionStatus(
result: { err: value.err },
};
}
+ data = { signature, info };
fetchStatus = FetchStatus.Fetched;
} catch (error) {
console.error("Failed to fetch transaction status", error);
@@ -268,10 +118,10 @@ export async function fetchTransactionStatus(
}
dispatch({
- type: ActionType.UpdateStatus,
- signature,
- fetchStatus,
- info,
+ type: ActionType.Update,
+ key: signature,
+ status: fetchStatus,
+ data,
url,
});
}
@@ -286,7 +136,9 @@ export function useTransactions() {
return context;
}
-export function useTransactionStatus(signature: TransactionSignature) {
+export function useTransactionStatus(
+ signature: TransactionSignature
+): Cache.CacheEntry | undefined {
const context = React.useContext(StateContext);
if (!context) {
@@ -295,18 +147,6 @@ export function useTransactionStatus(signature: TransactionSignature) {
);
}
- return context.transactions[signature];
-}
-
-export function useTransactionDetails(signature: TransactionSignature) {
- const context = React.useContext(DetailsStateContext);
-
- if (!context) {
- throw new Error(
- `useTransactionDetails must be used within a TransactionsProvider`
- );
- }
-
return context.entries[signature];
}