Files
solana/explorer/src/providers/accounts/index.tsx

187 lines
4.9 KiB
TypeScript
Raw Normal View History

2020-03-31 21:58:48 +08:00
import React from "react";
import { StakeAccount as StakeAccountWasm } from "solana-sdk-wasm";
2020-05-14 22:14:28 +08:00
import { PublicKey, Connection, StakeProgram } from "@solana/web3.js";
import { useCluster } from "../cluster";
2020-05-14 22:14:28 +08:00
import { HistoryProvider } from "./history";
import { TokensProvider, TOKEN_PROGRAM_ID } from "./tokens";
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";
2020-05-14 22:14:28 +08:00
export { useAccountHistory } from "./history";
2020-03-31 21:58:48 +08:00
export type StakeProgramData = {
name: "stake";
parsed: StakeAccount | StakeAccountWasm;
};
export type TokenProgramData = {
name: "spl-token";
parsed: TokenAccount;
};
export type ProgramData = StakeProgramData | TokenProgramData;
2020-03-31 21:58:48 +08:00
export interface Details {
executable: boolean;
owner: PublicKey;
space?: number;
data?: ProgramData;
2020-03-31 21:58:48 +08:00
}
export interface Account {
pubkey: PublicKey;
lamports: number;
2020-03-31 21:58:48 +08:00
details?: Details;
}
type State = Cache.State<Account>;
type Dispatch = Cache.Dispatch<Account>;
2020-03-31 21:58:48 +08:00
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type AccountsProviderProps = { children: React.ReactNode };
export function AccountsProvider({ children }: AccountsProviderProps) {
const { url } = useCluster();
const [state, dispatch] = Cache.useReducer<Account>(url);
2020-03-31 21:58:48 +08:00
// Clear accounts cache whenever cluster is changed
2020-03-31 21:58:48 +08:00
React.useEffect(() => {
dispatch({ type: ActionType.Clear, url });
}, [dispatch, url]);
2020-03-31 21:58:48 +08:00
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
<TokensProvider>
<HistoryProvider>{children}</HistoryProvider>
</TokensProvider>
2020-03-31 21:58:48 +08:00
</DispatchContext.Provider>
</StateContext.Provider>
);
}
2020-05-12 18:32:14 +08:00
async function fetchAccountInfo(
2020-03-31 21:58:48 +08:00
dispatch: Dispatch,
2020-05-12 18:32:14 +08:00
pubkey: PublicKey,
url: string
2020-03-31 21:58:48 +08:00
) {
dispatch({
type: ActionType.Update,
key: pubkey.toBase58(),
status: Cache.FetchStatus.Fetching,
url,
2020-03-31 21:58:48 +08:00
});
let data;
2020-05-12 18:32:14 +08:00
let fetchStatus;
2020-03-31 21:58:48 +08:00
try {
const result = (
await new Connection(url, "single").getParsedAccountInfo(pubkey)
).value;
let lamports, details;
2020-04-06 18:54:29 +08:00
if (result === null) {
2020-04-05 16:31:40 +08:00
lamports = 0;
} else {
2020-04-06 18:54:29 +08:00
lamports = result.lamports;
2020-05-14 15:30:33 +08:00
// Only save data in memory if we can decode it
let space;
if (!("parsed" in result.data)) {
space = result.data.length;
}
let data: ProgramData | undefined;
2020-05-14 15:30:33 +08:00
if (result.owner.equals(StakeProgram.programId)) {
2020-05-14 23:20:35 +08:00
try {
let parsed;
if ("parsed" in result.data) {
const info = coerce(result.data.parsed, ParsedInfo);
parsed = coerce(info, StakeAccount);
} else {
const wasm = await import("solana-sdk-wasm");
parsed = wasm.StakeAccount.fromAccountData(result.data);
}
data = {
name: "stake",
parsed,
};
2020-05-14 23:20:35 +08:00
} catch (err) {
console.error("Failed to parse stake account", err);
2020-05-14 23:20:35 +08:00
// TODO store error state in Account info
}
} else if ("parsed" in result.data) {
if (result.owner.equals(TOKEN_PROGRAM_ID)) {
try {
const info = coerce(result.data.parsed, ParsedInfo);
const parsed = coerce(info, TokenAccount);
data = {
name: "spl-token",
parsed,
};
} catch (err) {
// TODO store error state in Account info
}
}
2020-05-14 15:30:33 +08:00
}
2020-04-06 18:54:29 +08:00
details = {
space,
2020-04-06 18:54:29 +08:00
executable: result.executable,
2020-05-14 15:30:33 +08:00
owner: result.owner,
2020-06-24 16:07:47 +08:00
data,
2020-04-06 18:54:29 +08:00
};
2020-04-05 16:31:40 +08:00
}
data = { pubkey, lamports, details };
2020-05-14 22:14:28 +08:00
fetchStatus = FetchStatus.Fetched;
2020-04-06 18:54:29 +08:00
} catch (error) {
console.error("Failed to fetch account info", error);
2020-05-14 22:14:28 +08:00
fetchStatus = FetchStatus.FetchFailed;
2020-03-31 21:58:48 +08:00
}
dispatch({
type: ActionType.Update,
status: fetchStatus,
data,
key: pubkey.toBase58(),
url,
});
2020-04-21 23:30:52 +08:00
}
2020-03-31 21:58:48 +08:00
export function useAccounts() {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useAccounts must be used within a AccountsProvider`);
}
return context.entries;
2020-03-31 21:58:48 +08:00
}
export function useAccountInfo(
address: string
): Cache.CacheEntry<Account> | undefined {
2020-04-21 23:30:52 +08:00
const context = React.useContext(StateContext);
2020-05-12 18:32:14 +08:00
2020-04-21 23:30:52 +08:00
if (!context) {
2020-05-12 18:32:14 +08:00
throw new Error(`useAccountInfo must be used within a AccountsProvider`);
2020-04-21 23:30:52 +08:00
}
return context.entries[address];
2020-04-21 23:30:52 +08:00
}
2020-05-12 18:32:14 +08:00
export function useFetchAccountInfo() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {
throw new Error(
`useFetchAccountInfo must be used within a AccountsProvider`
);
}
const { url } = useCluster();
2020-05-12 18:32:14 +08:00
return (pubkey: PublicKey) => {
fetchAccountInfo(dispatch, pubkey, url);
2020-05-12 18:32:14 +08:00
};
}