Clean up explorer data providers (#11334)

This commit is contained in:
Justin Starry 2020-08-02 20:18:28 +08:00 committed by GitHub
parent 0d8f3139ae
commit b6ea9f1861
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 134 additions and 207 deletions

View File

@ -19,8 +19,6 @@ import { useFetchAccountHistory } from "providers/accounts/history";
type Props = { address: string };
export default function AccountDetails({ address }: Props) {
const fetchAccount = useFetchAccountInfo();
let pubkey: PublicKey | undefined;
try {
pubkey = new PublicKey(address);
@ -29,11 +27,6 @@ export default function AccountDetails({ address }: Props) {
// TODO handle bad addresses
}
// Fetch account on load
React.useEffect(() => {
if (pubkey) fetchAccount(pubkey);
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="container mt-n3">
<div className="header">
@ -49,10 +42,16 @@ export default function AccountDetails({ address }: Props) {
}
function AccountCards({ pubkey }: { pubkey: PublicKey }) {
const fetchAccount = useFetchAccountInfo();
const address = pubkey.toBase58();
const info = useAccountInfo(address);
const refresh = useFetchAccountInfo();
// Fetch account on load
React.useEffect(() => {
if (pubkey && !info) fetchAccount(pubkey);
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
if (!info || info.status === FetchStatus.Fetching) {
return <LoadingCard />;
} else if (

View File

@ -28,13 +28,6 @@ import { isCached } from "providers/transactions/cached";
type Props = { signature: TransactionSignature };
export default function TransactionDetails({ signature }: Props) {
const fetchTransaction = useFetchTransactionStatus();
// Fetch transaction on load
React.useEffect(() => {
fetchTransaction(signature);
}, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="container mt-n3">
<div className="header">
@ -52,11 +45,17 @@ export default function TransactionDetails({ signature }: Props) {
}
function StatusCard({ signature }: Props) {
const fetchStatus = useFetchTransactionStatus();
const status = useTransactionStatus(signature);
const refresh = useFetchTransactionStatus();
const details = useTransactionDetails(signature);
const { firstAvailableBlock } = useCluster();
// Fetch transaction on load
React.useEffect(() => {
if (!status) fetchStatus(signature);
}, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
if (!status || status.fetchStatus === FetchStatus.Fetching) {
return <LoadingCard />;
} else if (status?.fetchStatus === FetchStatus.FetchFailed) {

View File

@ -19,7 +19,7 @@ type State = { [address: string]: AccountHistory };
export enum ActionType {
Update,
Add,
Remove,
Clear,
}
interface Update {
@ -32,38 +32,26 @@ interface Update {
interface Add {
type: ActionType.Add;
addresses: string[];
address: string;
}
interface Remove {
type: ActionType.Remove;
addresses: string[];
interface Clear {
type: ActionType.Clear;
}
type Action = Update | Add | Remove;
type Action = Update | Add | Clear;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Add: {
if (action.addresses.length === 0) return state;
const details = { ...state };
action.addresses.forEach((address) => {
if (!details[address]) {
details[address] = {
status: FetchStatus.Fetching,
};
}
});
return details;
}
case ActionType.Remove: {
if (action.addresses.length === 0) return state;
const details = { ...state };
action.addresses.forEach((address) => {
delete details[address];
});
const address = action.address;
if (!details[address]) {
details[address] = {
status: FetchStatus.Fetching,
};
}
return details;
}
@ -87,6 +75,10 @@ function reducer(state: State, action: Action): State {
}
break;
}
case ActionType.Clear: {
return {};
}
}
return state;
}
@ -100,45 +92,33 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type HistoryProviderProps = { children: React.ReactNode };
export function HistoryProvider({ children }: HistoryProviderProps) {
const [state, dispatch] = React.useReducer(reducer, {});
const { accounts } = useAccounts();
const { accounts, lastFetchedAddress } = useAccounts();
const { url } = useCluster();
const manager = React.useRef(new HistoryManager(url));
React.useEffect(() => {
manager.current = new HistoryManager(url);
dispatch({ type: ActionType.Clear });
}, [url]);
// Fetch history for new accounts
React.useEffect(() => {
const removeAddresses = new Set<string>();
const fetchAddresses = new Set<string>();
accounts.forEach(({ pubkey, lamports }) => {
const address = pubkey.toBase58();
if (lamports !== undefined && !state[address])
fetchAddresses.add(address);
else if (lamports === undefined && state[address])
removeAddresses.add(address);
});
const removeList: string[] = [];
removeAddresses.forEach((address) => {
manager.current.removeAccountHistory(address);
removeList.push(address);
});
dispatch({ type: ActionType.Remove, addresses: removeList });
const fetchList: string[] = [];
fetchAddresses.forEach((s) => fetchList.push(s));
dispatch({ type: ActionType.Add, addresses: fetchList });
fetchAddresses.forEach((address) => {
fetchAccountHistory(
dispatch,
new PublicKey(address),
manager.current,
true
);
});
}, [accounts]); // eslint-disable-line react-hooks/exhaustive-deps
if (lastFetchedAddress) {
const infoFetched =
accounts[lastFetchedAddress] &&
accounts[lastFetchedAddress].lamports !== undefined;
const noHistory = !state[lastFetchedAddress];
if (infoFetched && noHistory) {
dispatch({ type: ActionType.Add, address: lastFetchedAddress });
fetchAccountHistory(
dispatch,
new PublicKey(lastFetchedAddress),
manager.current,
true
);
}
}
}, [accounts, lastFetchedAddress]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<ManagerContext.Provider value={manager.current}>

View File

@ -1,7 +1,6 @@
import React from "react";
import { StakeAccount } from "solana-sdk-wasm";
import { PublicKey, Connection, StakeProgram } from "@solana/web3.js";
import { useQuery } from "../../utils/url";
import { useCluster, ClusterStatus } from "../cluster";
import { HistoryProvider } from "./history";
export { useAccountHistory } from "./history";
@ -20,7 +19,6 @@ export interface Details {
}
export interface Account {
id: number;
pubkey: PublicKey;
status: FetchStatus;
lamports?: number;
@ -29,13 +27,14 @@ export interface Account {
type Accounts = { [address: string]: Account };
interface State {
idCounter: number;
accounts: Accounts;
lastFetchedAddress: string | undefined;
}
export enum ActionType {
Update,
Fetch,
Clear,
}
interface Update {
@ -53,7 +52,11 @@ interface Fetch {
pubkey: PublicKey;
}
type Action = Update | Fetch;
interface Clear {
type: ActionType.Clear;
}
type Action = Update | Fetch | Clear;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
@ -65,23 +68,20 @@ function reducer(state: State, action: Action): State {
const accounts = {
...state.accounts,
[address]: {
id: account.id,
pubkey: account.pubkey,
status: FetchStatus.Fetching,
},
};
return { ...state, accounts };
return { ...state, accounts, lastFetchedAddress: address };
} else {
const idCounter = state.idCounter + 1;
const accounts = {
...state.accounts,
[address]: {
id: idCounter,
status: FetchStatus.Fetching,
pubkey: action.pubkey,
},
};
return { ...state, accounts, idCounter };
return { ...state, accounts, lastFetchedAddress: address };
}
}
@ -100,6 +100,13 @@ function reducer(state: State, action: Action): State {
}
break;
}
case ActionType.Clear: {
return {
...state,
accounts: {},
};
}
}
return state;
}
@ -113,40 +120,20 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type AccountsProviderProps = { children: React.ReactNode };
export function AccountsProvider({ children }: AccountsProviderProps) {
const [state, dispatch] = React.useReducer(reducer, {
idCounter: 0,
accounts: {},
lastFetchedAddress: undefined,
});
const { status, url } = useCluster();
// Check account statuses on startup and whenever cluster updates
const { status, url } = useCluster();
React.useEffect(() => {
Object.keys(state.accounts).forEach((address) => {
fetchAccountInfo(dispatch, new PublicKey(address), url, status);
});
if (status === ClusterStatus.Connecting) {
dispatch({ type: ActionType.Clear });
} else if (status === ClusterStatus.Connected && state.lastFetchedAddress) {
fetchAccountInfo(dispatch, new PublicKey(state.lastFetchedAddress), url);
}
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
const query = useQuery();
const values = ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map((key) =>
query.get(key)
);
React.useEffect(() => {
values
.filter((value): value is string => value !== null)
.flatMap((value) => value.split(","))
// Remove duplicates
.filter((item, pos, self) => self.indexOf(item) === pos)
.filter((address) => !state.accounts[address])
.forEach((address) => {
try {
fetchAccountInfo(dispatch, new PublicKey(address), url, status);
} catch (err) {
console.error(err);
// TODO handle bad addresses
}
});
}, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
@ -159,17 +146,13 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
async function fetchAccountInfo(
dispatch: Dispatch,
pubkey: PublicKey,
url: string,
status: ClusterStatus
url: string
) {
dispatch({
type: ActionType.Fetch,
pubkey,
});
// We will auto-refetch when status is no longer connecting
if (status === ClusterStatus.Connecting) return;
let fetchStatus;
let details;
let lamports;
@ -213,12 +196,7 @@ export function useAccounts() {
if (!context) {
throw new Error(`useAccounts must be used within a AccountsProvider`);
}
return {
idCounter: context.idCounter,
accounts: Object.values(context.accounts).sort((a, b) =>
a.id <= b.id ? 1 : -1
),
};
return context;
}
export function useAccountInfo(address: string) {
@ -239,8 +217,8 @@ export function useFetchAccountInfo() {
);
}
const { url, status } = useCluster();
const { url } = useCluster();
return (pubkey: PublicKey) => {
fetchAccountInfo(dispatch, pubkey, url, status);
fetchAccountInfo(dispatch, pubkey, url);
};
}

View File

@ -18,7 +18,7 @@ type State = { [signature: string]: Details };
export enum ActionType {
Update,
Add,
Remove,
Clear,
}
interface Update {
@ -30,39 +30,27 @@ interface Update {
interface Add {
type: ActionType.Add;
signatures: TransactionSignature[];
signature: TransactionSignature;
}
interface Remove {
type: ActionType.Remove;
signatures: TransactionSignature[];
interface Clear {
type: ActionType.Clear;
}
type Action = Update | Add | Remove;
type Action = Update | Add | Clear;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Add: {
if (action.signatures.length === 0) return state;
const details = { ...state };
action.signatures.forEach((signature) => {
if (!details[signature]) {
details[signature] = {
fetchStatus: FetchStatus.Fetching,
transaction: null,
};
}
});
return details;
}
case ActionType.Remove: {
if (action.signatures.length === 0) return state;
const details = { ...state };
action.signatures.forEach((signature) => {
delete details[signature];
});
const signature = action.signature;
if (!details[signature]) {
details[signature] = {
fetchStatus: FetchStatus.Fetching,
transaction: null,
};
}
return details;
}
@ -81,6 +69,10 @@ function reducer(state: State, action: Action): State {
}
break;
}
case ActionType.Clear: {
return {};
}
}
return state;
}
@ -93,32 +85,26 @@ export const DispatchContext = React.createContext<Dispatch | undefined>(
type DetailsProviderProps = { children: React.ReactNode };
export function DetailsProvider({ children }: DetailsProviderProps) {
const [state, dispatch] = React.useReducer(reducer, {});
const { transactions } = useTransactions();
const { transactions, lastFetched } = useTransactions();
const { url } = useCluster();
React.useEffect(() => {
dispatch({ type: ActionType.Clear });
}, [url]);
// Filter blocks for current transaction slots
React.useEffect(() => {
const removeSignatures = new Set<string>();
const fetchSignatures = new Set<string>();
transactions.forEach(({ signature, info }) => {
if (info?.confirmations === "max" && !state[signature])
fetchSignatures.add(signature);
else if (info?.confirmations !== "max" && state[signature])
removeSignatures.add(signature);
});
const removeList: string[] = [];
removeSignatures.forEach((s) => removeList.push(s));
dispatch({ type: ActionType.Remove, signatures: removeList });
const fetchList: string[] = [];
fetchSignatures.forEach((s) => fetchList.push(s));
dispatch({ type: ActionType.Add, signatures: fetchList });
fetchSignatures.forEach((signature) => {
fetchDetails(dispatch, signature, url);
});
}, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps
if (lastFetched) {
const confirmed =
transactions[lastFetched] &&
transactions[lastFetched].info?.confirmations === "max";
const noDetails = !state[lastFetched];
if (confirmed && noDetails) {
dispatch({ type: ActionType.Add, signature: lastFetched });
fetchDetails(dispatch, lastFetched, url);
}
}
}, [transactions, lastFetched]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>

View File

@ -8,7 +8,7 @@ import {
PublicKey,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import { useQuery } from "../../utils/url";
import { useQuery } from "utils/url";
import { useCluster, Cluster, ClusterStatus } from "../cluster";
import {
DetailsProvider,
@ -36,7 +36,6 @@ export interface TransactionStatusInfo {
}
export interface TransactionStatus {
id: number;
fetchStatus: FetchStatus;
signature: TransactionSignature;
info?: TransactionStatusInfo;
@ -44,13 +43,14 @@ export interface TransactionStatus {
type Transactions = { [signature: string]: TransactionStatus };
interface State {
idCounter: number;
transactions: Transactions;
lastFetched: TransactionSignature | undefined;
}
export enum ActionType {
UpdateStatus,
FetchSignature,
Clear,
}
interface UpdateStatus {
@ -65,14 +65,18 @@ interface FetchSignature {
signature: TransactionSignature;
}
type Action = UpdateStatus | FetchSignature;
interface Clear {
type: ActionType.Clear;
}
type Action = UpdateStatus | FetchSignature | Clear;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.FetchSignature: {
const transaction = state.transactions[action.signature];
const signature = action.signature;
const transaction = state.transactions[signature];
if (transaction) {
const transactions = {
...state.transactions,
@ -82,18 +86,16 @@ function reducer(state: State, action: Action): State {
info: undefined,
},
};
return { ...state, transactions };
return { ...state, transactions, lastFetched: signature };
} else {
const nextId = state.idCounter + 1;
const transactions = {
...state.transactions,
[action.signature]: {
id: nextId,
signature: action.signature,
fetchStatus: FetchStatus.Fetching,
},
};
return { ...state, transactions, idCounter: nextId };
return { ...state, transactions, lastFetched: signature };
}
}
@ -112,6 +114,13 @@ function reducer(state: State, action: Action): State {
}
break;
}
case ActionType.Clear: {
return {
...state,
transactions: {},
};
}
}
return state;
}
@ -124,8 +133,8 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type TransactionsProviderProps = { children: React.ReactNode };
export function TransactionsProvider({ children }: TransactionsProviderProps) {
const [state, dispatch] = React.useReducer(reducer, {
idCounter: 0,
transactions: {},
lastFetched: undefined,
});
const { cluster, status: clusterStatus, url } = useCluster();
@ -135,9 +144,11 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
// Check transaction statuses whenever cluster updates
React.useEffect(() => {
Object.keys(state.transactions).forEach((signature) => {
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
});
if (clusterStatus === ClusterStatus.Connecting) {
dispatch({ type: ActionType.Clear });
} else if (clusterStatus === ClusterStatus.Connected && state.lastFetched) {
fetchTransactionStatus(dispatch, state.lastFetched, url);
}
// Create a test transaction
if (cluster === Cluster.Devnet && testFlag !== null) {
@ -145,23 +156,6 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
}
}, [testFlag, cluster, clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
// Check for transactions in the url params
const values = TX_ALIASES.flatMap((key) => [
query.get(key),
query.get(key + "s"),
]);
React.useEffect(() => {
values
.filter((value): value is string => value !== null)
.flatMap((value) => value.split(","))
// Remove duplicates
.filter((item, pos, self) => self.indexOf(item) === pos)
.filter((signature) => !state.transactions[signature])
.forEach((signature) => {
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
});
}, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
@ -189,7 +183,7 @@ async function createTestTransaction(
testAccount.publicKey,
100000
);
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
fetchTransactionStatus(dispatch, signature, url);
fetchAccount(testAccount.publicKey);
} catch (error) {
console.error("Failed to create test success transaction", error);
@ -208,7 +202,7 @@ async function createTestTransaction(
[testAccount],
{ confirmations: 1, skipPreflight: false }
);
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
fetchTransactionStatus(dispatch, signature, url);
} catch (error) {
console.error("Failed to create test failure transaction", error);
}
@ -217,17 +211,13 @@ async function createTestTransaction(
export async function fetchTransactionStatus(
dispatch: Dispatch,
signature: TransactionSignature,
url: string,
status: ClusterStatus
url: string
) {
dispatch({
type: ActionType.FetchSignature,
signature,
});
// We will auto-refetch when status is no longer connecting
if (status === ClusterStatus.Connecting) return;
let fetchStatus;
let info: TransactionStatusInfo | undefined;
if (isCached(url, signature)) {
@ -281,12 +271,7 @@ export function useTransactions() {
`useTransactions must be used within a TransactionsProvider`
);
}
return {
idCounter: context.idCounter,
transactions: Object.values(context.transactions).sort((a, b) =>
a.id <= b.id ? 1 : -1
),
};
return context;
}
export function useTransactionStatus(signature: TransactionSignature) {
@ -321,8 +306,8 @@ export function useFetchTransactionStatus() {
);
}
const { url, status } = useCluster();
const { url } = useCluster();
return (signature: TransactionSignature) => {
fetchTransactionStatus(dispatch, signature, url, status);
fetchTransactionStatus(dispatch, signature, url);
};
}