solana/explorer/src/providers/accounts.tsx

308 lines
7.1 KiB
TypeScript
Raw Normal View History

2020-03-31 06:58:48 -07:00
import React from "react";
2020-04-21 08:30:52 -07:00
import {
PublicKey,
Connection,
TransactionSignature,
TransactionError,
SignatureStatus
} from "@solana/web3.js";
import { findGetParameter, findPathSegment } from "../utils/url";
2020-03-26 07:35:02 -07:00
import { useCluster, ClusterStatus } from "./cluster";
2020-03-31 06:58:48 -07:00
export enum Status {
Checking,
CheckFailed,
2020-04-21 08:30:52 -07:00
FetchingHistory,
HistoryFailed,
2020-04-05 01:31:40 -07:00
NotFound,
2020-03-31 06:58:48 -07:00
Success
}
2020-04-21 08:30:52 -07:00
export type History = Map<
number,
Map<TransactionSignature, TransactionError | null>
>;
2020-03-31 06:58:48 -07:00
export interface Details {
executable: boolean;
owner: PublicKey;
space: number;
}
export interface Account {
id: number;
pubkey: PublicKey;
2020-04-21 08:30:52 -07:00
status: Status;
2020-04-05 01:31:40 -07:00
lamports?: number;
2020-03-31 06:58:48 -07:00
details?: Details;
2020-04-21 08:30:52 -07:00
history?: History;
2020-03-31 06:58:48 -07:00
}
type Accounts = { [address: string]: Account };
2020-03-31 06:58:48 -07:00
interface State {
idCounter: number;
accounts: Accounts;
2020-04-21 08:30:52 -07:00
selected?: string;
2020-03-31 06:58:48 -07:00
}
export enum ActionType {
Update,
2020-04-21 08:30:52 -07:00
Input,
Select
2020-03-31 06:58:48 -07:00
}
interface Update {
type: ActionType.Update;
address: string;
2020-04-21 08:30:52 -07:00
data: {
status: Status;
lamports?: number;
details?: Details;
history?: History;
};
2020-03-31 06:58:48 -07:00
}
interface Input {
type: ActionType.Input;
pubkey: PublicKey;
}
2020-04-21 08:30:52 -07:00
interface Select {
type: ActionType.Select;
address?: string;
}
type Action = Update | Input | Select;
export type Dispatch = (action: Action) => void;
2020-03-31 06:58:48 -07:00
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Input: {
const address = action.pubkey.toBase58();
if (!!state.accounts[address]) return state;
2020-03-31 06:58:48 -07:00
const idCounter = state.idCounter + 1;
const accounts = {
...state.accounts,
[address]: {
2020-03-31 06:58:48 -07:00
id: idCounter,
status: Status.Checking,
pubkey: action.pubkey
}
};
return { ...state, accounts, idCounter };
}
2020-04-21 08:30:52 -07:00
case ActionType.Select: {
return { ...state, selected: action.address };
}
2020-03-31 06:58:48 -07:00
case ActionType.Update: {
let account = state.accounts[action.address];
2020-03-31 06:58:48 -07:00
if (account) {
account = {
...account,
2020-04-21 08:30:52 -07:00
...action.data
2020-03-31 06:58:48 -07:00
};
const accounts = {
...state.accounts,
[action.address]: account
2020-03-31 06:58:48 -07:00
};
return { ...state, accounts };
}
break;
}
}
return state;
}
export const ACCOUNT_PATHS = [
"/account",
"/accounts",
"/address",
"/addresses"
];
function urlAddresses(): Array<string> {
const addresses: Array<string> = [];
ACCOUNT_PATHS.forEach(path => {
const name = path.slice(1);
const params = findGetParameter(name)?.split(",") || [];
const segments = findPathSegment(name)?.split(",") || [];
addresses.push(...params);
addresses.push(...segments);
});
return addresses.filter(a => a.length > 0);
2020-03-31 06:58:48 -07:00
}
function initState(): State {
let idCounter = 0;
const addresses = urlAddresses();
const accounts = addresses.reduce((accounts: Accounts, address) => {
if (!!accounts[address]) return accounts;
try {
const pubkey = new PublicKey(address);
const id = ++idCounter;
accounts[address] = {
id,
status: Status.Checking,
pubkey
};
} catch (err) {
// TODO display to user
console.error(err);
}
2020-03-31 06:58:48 -07:00
return accounts;
}, {});
return { idCounter, accounts };
}
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 [state, dispatch] = React.useReducer(reducer, undefined, initState);
const { status, url } = useCluster();
// Check account statuses on startup and whenever cluster updates
React.useEffect(() => {
2020-03-26 07:35:02 -07:00
if (status !== ClusterStatus.Connected) return;
Object.keys(state.accounts).forEach(address => {
fetchAccountInfo(dispatch, address, url);
2020-03-31 06:58:48 -07:00
});
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
export async function fetchAccountInfo(
dispatch: Dispatch,
address: string,
2020-03-31 06:58:48 -07:00
url: string
) {
dispatch({
type: ActionType.Update,
2020-04-21 08:30:52 -07:00
address,
data: {
status: Status.Checking
}
2020-03-31 06:58:48 -07:00
});
let status;
let details;
2020-04-05 01:31:40 -07:00
let lamports;
2020-03-31 06:58:48 -07:00
try {
const result = await new Connection(url, "recent").getAccountInfo(
new PublicKey(address)
);
2020-04-06 03:54:29 -07:00
if (result === null) {
2020-04-05 01:31:40 -07:00
lamports = 0;
status = Status.NotFound;
} else {
2020-04-06 03:54:29 -07:00
lamports = result.lamports;
details = {
space: result.data.length,
executable: result.executable,
owner: result.owner
};
2020-04-21 08:30:52 -07:00
status = Status.FetchingHistory;
fetchAccountHistory(dispatch, address, url);
2020-04-05 01:31:40 -07:00
}
2020-04-06 03:54:29 -07:00
} catch (error) {
console.error("Failed to fetch account info", error);
status = Status.CheckFailed;
2020-03-31 06:58:48 -07:00
}
2020-04-21 08:30:52 -07:00
const data = { status, lamports, details };
dispatch({ type: ActionType.Update, data, address });
}
async function fetchAccountHistory(
dispatch: Dispatch,
address: string,
url: string
) {
let history;
let status;
try {
const connection = new Connection(url);
const currentSlot = await connection.getSlot();
const signatures = await connection.getConfirmedSignaturesForAddress(
new PublicKey(address),
Math.max(0, currentSlot - 10000 + 1),
currentSlot
);
let statuses: (SignatureStatus | null)[] = [];
if (signatures.length > 0) {
statuses = (
await connection.getSignatureStatuses(signatures, {
searchTransactionHistory: true
})
).value;
}
history = new Map();
for (let i = 0; i < statuses.length; i++) {
const status = statuses[i];
if (!status) continue;
let slotSignatures = history.get(status.slot);
if (!slotSignatures) {
slotSignatures = new Map();
history.set(status.slot, slotSignatures);
}
slotSignatures.set(signatures[i], status.err);
}
status = Status.Success;
} catch (error) {
console.error("Failed to fetch account history", error);
status = Status.HistoryFailed;
}
const data = { status, history };
dispatch({ type: ActionType.Update, data, address });
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-04-21 08:30:52 -07:00
export function useSelectedAccount() {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(
`useSelectedAccount must be used within a AccountsProvider`
);
}
if (!context.selected) return undefined;
return context.accounts[context.selected];
}
2020-03-31 06:58:48 -07:00
export function useAccountsDispatch() {
const context = React.useContext(DispatchContext);
if (!context) {
throw new Error(
`useAccountsDispatch must be used within a AccountsProvider`
);
}
return context;
}