2020-03-31 06:58:48 -07:00
|
|
|
import React from "react";
|
2020-05-14 08:20:35 -07:00
|
|
|
import { StakeAccount } from "solana-sdk-wasm";
|
2020-05-14 07:14:28 -07:00
|
|
|
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";
|
2020-03-31 06:58:48 -07:00
|
|
|
|
2020-05-14 07:14:28 -07:00
|
|
|
export enum FetchStatus {
|
|
|
|
Fetching,
|
|
|
|
FetchFailed,
|
2020-06-24 01:07:47 -07:00
|
|
|
Fetched,
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface Details {
|
|
|
|
executable: boolean;
|
|
|
|
owner: PublicKey;
|
|
|
|
space: number;
|
2020-05-14 08:20:35 -07:00
|
|
|
data?: StakeAccount;
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface Account {
|
|
|
|
id: number;
|
|
|
|
pubkey: PublicKey;
|
2020-05-14 07:14:28 -07:00
|
|
|
status: FetchStatus;
|
2020-04-05 01:31:40 -07:00
|
|
|
lamports?: number;
|
2020-03-31 06:58:48 -07:00
|
|
|
details?: Details;
|
|
|
|
}
|
|
|
|
|
2020-04-01 01:35:12 -07:00
|
|
|
type Accounts = { [address: string]: Account };
|
2020-03-31 06:58:48 -07:00
|
|
|
interface State {
|
|
|
|
idCounter: number;
|
|
|
|
accounts: Accounts;
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum ActionType {
|
|
|
|
Update,
|
2020-06-24 01:07:47 -07:00
|
|
|
Fetch,
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Update {
|
|
|
|
type: ActionType.Update;
|
2020-05-12 03:32:14 -07:00
|
|
|
pubkey: PublicKey;
|
2020-04-21 08:30:52 -07:00
|
|
|
data: {
|
2020-05-14 07:14:28 -07:00
|
|
|
status: FetchStatus;
|
2020-04-21 08:30:52 -07:00
|
|
|
lamports?: number;
|
|
|
|
details?: Details;
|
|
|
|
};
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
interface Fetch {
|
|
|
|
type: ActionType.Fetch;
|
2020-03-31 06:58:48 -07:00
|
|
|
pubkey: PublicKey;
|
|
|
|
}
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
type Action = Update | Fetch;
|
2020-05-14 07:14:28 -07:00
|
|
|
type Dispatch = (action: Action) => void;
|
2020-03-31 06:58:48 -07:00
|
|
|
|
|
|
|
function reducer(state: State, action: Action): State {
|
|
|
|
switch (action.type) {
|
2020-05-12 03:32:14 -07:00
|
|
|
case ActionType.Fetch: {
|
2020-04-01 01:35:12 -07:00
|
|
|
const address = action.pubkey.toBase58();
|
2020-05-12 03:32:14 -07:00
|
|
|
const account = state.accounts[address];
|
|
|
|
if (account) {
|
|
|
|
const accounts = {
|
|
|
|
...state.accounts,
|
|
|
|
[address]: {
|
|
|
|
id: account.id,
|
|
|
|
pubkey: account.pubkey,
|
2020-06-24 01:07:47 -07:00
|
|
|
status: FetchStatus.Fetching,
|
|
|
|
},
|
2020-05-12 03:32:14 -07:00
|
|
|
};
|
|
|
|
return { ...state, accounts };
|
|
|
|
} else {
|
|
|
|
const idCounter = state.idCounter + 1;
|
|
|
|
const accounts = {
|
|
|
|
...state.accounts,
|
|
|
|
[address]: {
|
|
|
|
id: idCounter,
|
2020-05-14 07:14:28 -07:00
|
|
|
status: FetchStatus.Fetching,
|
2020-06-24 01:07:47 -07:00
|
|
|
pubkey: action.pubkey,
|
|
|
|
},
|
2020-05-12 03:32:14 -07:00
|
|
|
};
|
|
|
|
return { ...state, accounts, idCounter };
|
|
|
|
}
|
2020-04-21 08:30:52 -07:00
|
|
|
}
|
|
|
|
|
2020-03-31 06:58:48 -07:00
|
|
|
case ActionType.Update: {
|
2020-05-12 03:32:14 -07:00
|
|
|
const address = action.pubkey.toBase58();
|
|
|
|
const account = state.accounts[address];
|
2020-03-31 06:58:48 -07:00
|
|
|
if (account) {
|
|
|
|
const accounts = {
|
|
|
|
...state.accounts,
|
2020-05-12 03:32:14 -07:00
|
|
|
[address]: {
|
|
|
|
...account,
|
2020-06-24 01:07:47 -07:00
|
|
|
...action.data,
|
|
|
|
},
|
2020-03-31 06:58:48 -07:00
|
|
|
};
|
|
|
|
return { ...state, accounts };
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
export const ACCOUNT_ALIASES = ["account", "address"];
|
|
|
|
export const ACCOUNT_ALIASES_PLURAL = ["accounts", "addresses"];
|
2020-03-31 06:58:48 -07: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) {
|
2020-05-12 03:32:14 -07:00
|
|
|
const [state, dispatch] = React.useReducer(reducer, {
|
|
|
|
idCounter: 0,
|
2020-06-24 01:07:47 -07:00
|
|
|
accounts: {},
|
2020-05-12 03:32:14 -07:00
|
|
|
});
|
2020-03-31 06:58:48 -07:00
|
|
|
|
|
|
|
const { status, url } = useCluster();
|
|
|
|
|
|
|
|
// Check account statuses on startup and whenever cluster updates
|
|
|
|
React.useEffect(() => {
|
2020-06-24 01:07:47 -07:00
|
|
|
Object.keys(state.accounts).forEach((address) => {
|
2020-05-12 03:32:14 -07:00
|
|
|
fetchAccountInfo(dispatch, new PublicKey(address), url, status);
|
2020-03-31 06:58:48 -07:00
|
|
|
});
|
|
|
|
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
const query = useQuery();
|
2020-06-24 01:07:47 -07:00
|
|
|
const values = ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map((key) =>
|
2020-05-12 03:32:14 -07:00
|
|
|
query.get(key)
|
|
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
|
|
values
|
|
|
|
.filter((value): value is string => value !== null)
|
2020-06-24 01:07:47 -07:00
|
|
|
.flatMap((value) => value.split(","))
|
2020-05-12 03:32:14 -07:00
|
|
|
// Remove duplicates
|
|
|
|
.filter((item, pos, self) => self.indexOf(item) === pos)
|
2020-06-24 01:07:47 -07:00
|
|
|
.filter((address) => !state.accounts[address])
|
|
|
|
.forEach((address) => {
|
2020-05-12 03:32:14 -07:00
|
|
|
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
|
|
|
|
|
2020-03-31 06:58:48 -07:00
|
|
|
return (
|
|
|
|
<StateContext.Provider value={state}>
|
|
|
|
<DispatchContext.Provider value={dispatch}>
|
2020-05-14 07:14:28 -07:00
|
|
|
<HistoryProvider>{children}</HistoryProvider>
|
2020-03-31 06:58:48 -07:00
|
|
|
</DispatchContext.Provider>
|
|
|
|
</StateContext.Provider>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
async function fetchAccountInfo(
|
2020-03-31 06:58:48 -07:00
|
|
|
dispatch: Dispatch,
|
2020-05-12 03:32:14 -07:00
|
|
|
pubkey: PublicKey,
|
|
|
|
url: string,
|
|
|
|
status: ClusterStatus
|
2020-03-31 06:58:48 -07:00
|
|
|
) {
|
|
|
|
dispatch({
|
2020-05-12 03:32:14 -07:00
|
|
|
type: ActionType.Fetch,
|
2020-06-24 01:07:47 -07:00
|
|
|
pubkey,
|
2020-03-31 06:58:48 -07:00
|
|
|
});
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
// We will auto-refetch when status is no longer connecting
|
|
|
|
if (status === ClusterStatus.Connecting) return;
|
|
|
|
|
|
|
|
let fetchStatus;
|
2020-03-31 06:58:48 -07:00
|
|
|
let details;
|
2020-04-05 01:31:40 -07:00
|
|
|
let lamports;
|
2020-03-31 06:58:48 -07:00
|
|
|
try {
|
2020-05-12 03:32:14 -07:00
|
|
|
const result = await new Connection(url, "recent").getAccountInfo(pubkey);
|
2020-04-06 03:54:29 -07:00
|
|
|
if (result === null) {
|
2020-04-05 01:31:40 -07:00
|
|
|
lamports = 0;
|
|
|
|
} else {
|
2020-04-06 03:54:29 -07:00
|
|
|
lamports = result.lamports;
|
2020-05-14 00:30:33 -07:00
|
|
|
let data = undefined;
|
|
|
|
|
|
|
|
// Only save data in memory if we can decode it
|
|
|
|
if (result.owner.equals(StakeProgram.programId)) {
|
2020-05-14 08:20:35 -07:00
|
|
|
try {
|
|
|
|
const wasm = await import("solana-sdk-wasm");
|
|
|
|
data = wasm.StakeAccount.fromAccountData(result.data);
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Unexpected error loading wasm", err);
|
|
|
|
// TODO store error state in Account info
|
|
|
|
}
|
2020-05-14 00:30:33 -07:00
|
|
|
}
|
|
|
|
|
2020-04-06 03:54:29 -07:00
|
|
|
details = {
|
|
|
|
space: result.data.length,
|
|
|
|
executable: result.executable,
|
2020-05-14 00:30:33 -07:00
|
|
|
owner: result.owner,
|
2020-06-24 01:07:47 -07:00
|
|
|
data,
|
2020-04-06 03:54:29 -07:00
|
|
|
};
|
2020-04-05 01:31:40 -07:00
|
|
|
}
|
2020-05-14 07:14:28 -07:00
|
|
|
fetchStatus = FetchStatus.Fetched;
|
2020-04-06 03:54:29 -07:00
|
|
|
} catch (error) {
|
|
|
|
console.error("Failed to fetch account info", error);
|
2020-05-14 07:14:28 -07:00
|
|
|
fetchStatus = FetchStatus.FetchFailed;
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
2020-05-12 03:32:14 -07:00
|
|
|
const data = { status: fetchStatus, lamports, details };
|
|
|
|
dispatch({ type: ActionType.Update, data, pubkey });
|
2020-04-21 08:30:52 -07:00
|
|
|
}
|
|
|
|
|
2020-03-31 06:58:48 -07:00
|
|
|
export function useAccounts() {
|
|
|
|
const context = React.useContext(StateContext);
|
|
|
|
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
|
2020-06-24 01:07:47 -07:00
|
|
|
),
|
2020-03-31 06:58:48 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
export function useAccountInfo(address: string) {
|
2020-04-21 08:30:52 -07:00
|
|
|
const context = React.useContext(StateContext);
|
2020-05-12 03:32:14 -07:00
|
|
|
|
2020-04-21 08:30:52 -07:00
|
|
|
if (!context) {
|
2020-05-12 03:32:14 -07:00
|
|
|
throw new Error(`useAccountInfo must be used within a AccountsProvider`);
|
2020-04-21 08:30:52 -07:00
|
|
|
}
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
return context.accounts[address];
|
2020-04-21 08:30:52 -07:00
|
|
|
}
|
|
|
|
|
2020-05-12 03:32:14 -07:00
|
|
|
export function useFetchAccountInfo() {
|
|
|
|
const dispatch = React.useContext(DispatchContext);
|
|
|
|
if (!dispatch) {
|
|
|
|
throw new Error(
|
|
|
|
`useFetchAccountInfo must be used within a AccountsProvider`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { url, status } = useCluster();
|
|
|
|
return (pubkey: PublicKey) => {
|
|
|
|
fetchAccountInfo(dispatch, pubkey, url, status);
|
|
|
|
};
|
|
|
|
}
|