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