Use common provider for explorer cached data (#11582)

This commit is contained in:
Justin Starry 2020-08-12 22:41:04 +08:00 committed by GitHub
parent 8ddb116659
commit a992bb5f94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 312 additions and 586 deletions

View File

@ -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 <LoadingCard message="Loading owned tokens" />;

View File

@ -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 <TokenHistoryTable tokens={tokens} />;
@ -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"

View File

@ -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 <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) {
return <LoadingCard message="Loading history" />;
}
@ -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 }) {
</div>
<div className="card-footer">
{history.foundOldest ? (
{history.data.foundOldest ? (
<div className="text-muted text-center">Fetched full history</div>
) : (
<button

View File

@ -1,10 +1,7 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
import {
FetchStatus,
useFetchAccountInfo,
useAccountInfo,
} from "providers/accounts";
import { FetchStatus } from "providers/cache";
import { useFetchAccountInfo, useAccountInfo } from "providers/accounts";
import { StakeAccountSection } from "components/account/StakeAccountSection";
import { TokenAccountSection } from "components/account/TokenAccountSection";
import { ErrorCard } from "components/common/ErrorCard";
@ -62,12 +59,13 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
return <LoadingCard />;
} else if (
info.status === FetchStatus.FetchFailed ||
info.lamports === undefined
info.data?.lamports === undefined
) {
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") {
let stakeAccountType, stakeAccount;
if ("accountType" in data.parsed) {
@ -80,15 +78,15 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
return (
<StakeAccountSection
account={info}
account={account}
stakeAccount={stakeAccount}
stakeAccountType={stakeAccountType}
/>
);
} else if (data && data.name === "spl-token") {
return <TokenAccountSection account={info} tokenAccount={data.parsed} />;
return <TokenAccountSection account={account} tokenAccount={data.parsed} />;
} 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 }) {
const address = pubkey.toBase58();
const info = useAccountInfo(address);
if (!info || info.lamports === undefined) return null;
if (info?.data === undefined) return null;
return (
<>

View File

@ -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 <LoadingCard />;
} else if (status?.fetchStatus === FetchStatus.FetchFailed) {
} else if (status.status === FetchStatus.FetchFailed) {
return (
<ErrorCard retry={() => fetchStatus(signature)} text="Fetch Failed" />
);
} else if (!status.info) {
} else if (!status.data?.info) {
if (firstAvailableBlock !== undefined) {
return (
<ErrorCard
@ -86,7 +86,7 @@ function StatusCard({ signature }: Props) {
return <ErrorCard retry={() => 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 <LoadingCard />;
} else if (details?.fetchStatus === FetchStatus.FetchFailed) {
} else if (details.status === FetchStatus.FetchFailed) {
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" />;
}
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 <ErrorCard retry={refreshDetails} text="No instructions found" />;
}
const result = status.info.result;
const result = status.data.info.result;
const instructionDetails = transaction.message.instructions.map(
(next, index) => {
if ("parsed" in next) {

View File

@ -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<AccountHistory>;
type Dispatch = Cache.Dispatch<HistoryUpdate>;
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<State | undefined>(undefined);
@ -108,11 +59,11 @@ const DispatchContext = React.createContext<Dispatch | undefined>(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 (
<StateContext.Provider value={state}>
@ -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<AccountHistory> | 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 });

View File

@ -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<Account>;
type Dispatch = Cache.Dispatch<Account>;
const StateContext = React.createContext<State | 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 };
export function AccountsProvider({ children }: AccountsProviderProps) {
const { url } = useCluster();
const [state, dispatch] = React.useReducer(reducer, {
url,
accounts: {},
});
const [state, dispatch] = Cache.useReducer<Account>(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 (
<StateContext.Provider value={state}>
@ -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<Account> | 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() {

View File

@ -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<AccountTokens>;
type Dispatch = Cache.Dispatch<AccountTokens>;
const StateContext = React.createContext<State | 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 };
export function TokensProvider({ children }: ProviderProps) {
const { url } = useCluster();
const [state, dispatch] = React.useReducer(reducer, { url, map: {} });
const [state, dispatch] = Cache.useReducer<AccountTokens>(url);
React.useEffect(() => {
dispatch({ url, type: "clear" });
}, [url]);
dispatch({ url, type: ActionType.Clear });
}, [dispatch, url]);
return (
<StateContext.Provider value={state}>
@ -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<AccountTokens> | 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() {

View 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 };
}
}
}

View File

@ -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<Details>;
type Dispatch = Cache.Dispatch<Details>;
export const StateContext = React.createContext<State | undefined>(undefined);
export const DispatchContext = React.createContext<Dispatch | undefined>(
@ -86,11 +24,11 @@ export const DispatchContext = React.createContext<Dispatch | undefined>(
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<Details>(url);
React.useEffect(() => {
dispatch({ type: ActionType.Clear, url });
}, [url]);
}, [dispatch, url]);
return (
<StateContext.Provider value={state}>
@ -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<Details> | undefined {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(
`useTransactionDetails must be used within a TransactionsProvider`
);
}
return context.entries[signature];
}

View File

@ -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<TransactionStatus>;
type Dispatch = Cache.Dispatch<TransactionStatus>;
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(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<TransactionStatus>(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 (
<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(
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<TransactionStatus> | 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];
}