diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx
index 1f22f20bf1..26fb6dac4f 100644
--- a/explorer/src/App.tsx
+++ b/explorer/src/App.tsx
@@ -2,7 +2,9 @@ import React from "react";
import { ClusterProvider } from "./providers/cluster";
import { TransactionsProvider } from "./providers/transactions";
+import { AccountsProvider } from "./providers/accounts";
import ClusterStatusButton from "./components/ClusterStatusButton";
+import AccountsCard from "./components/AccountsCard";
import TransactionsCard from "./components/TransactionsCard";
import ClusterModal from "./components/ClusterModal";
import Logo from "./img/logos-solana/light-explorer-logo.svg";
@@ -36,6 +38,13 @@ function App() {
+
setShowModal(false)} />
diff --git a/explorer/src/components/AccountsCard.tsx b/explorer/src/components/AccountsCard.tsx
new file mode 100644
index 0000000000..f540f5ca7a
--- /dev/null
+++ b/explorer/src/components/AccountsCard.tsx
@@ -0,0 +1,184 @@
+import React from "react";
+import {
+ useAccounts,
+ useAccountsDispatch,
+ fetchAccountInfo,
+ ActionType,
+ Account,
+ Status
+} from "../providers/accounts";
+import { assertUnreachable } from "../utils";
+import { useCluster } from "../providers/cluster";
+import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
+
+function AccountsCard() {
+ const { accounts, idCounter } = useAccounts();
+ const dispatch = useAccountsDispatch();
+ const addressInput = React.useRef(null);
+ const [error, setError] = React.useState("");
+ const [showSOL, setShowSOL] = React.useState(true);
+ const { url } = useCluster();
+
+ const onNew = (address: string) => {
+ if (address.length === 0) return;
+ let pubkey;
+ try {
+ pubkey = new PublicKey(address);
+ } catch (err) {
+ setError(`${err}`);
+ return;
+ }
+
+ dispatch({ type: ActionType.Input, pubkey });
+ fetchAccountInfo(dispatch, idCounter + 1, pubkey, url);
+
+ const inputEl = addressInput.current;
+ if (inputEl) {
+ inputEl.value = "";
+ }
+ };
+
+ return (
+
+ {renderHeader(showSOL, setShowSOL)}
+
+
+
+ );
+}
+
+const renderHeader = (
+ showSOL: boolean,
+ setShowSOL: (show: boolean) => void
+) => {
+ return (
+
+
+
+
Look Up Account(s)
+
+
+
Display SOL
+
+
+
+
+
+
+
+ );
+};
+
+const renderAccountRow = (account: Account, showSOL: boolean) => {
+ let statusText;
+ let statusClass;
+ switch (account.status) {
+ case Status.CheckFailed:
+ statusClass = "danger";
+ statusText = "Error";
+ break;
+ case Status.Checking:
+ statusClass = "info";
+ statusText = "Fetching";
+ break;
+ case Status.Success:
+ if (account.details?.executable) {
+ statusClass = "dark";
+ statusText = "Executable";
+ } else {
+ statusClass = "success";
+ statusText = "Found";
+ }
+ break;
+ default:
+ return assertUnreachable(account.status);
+ }
+
+ let data = "-";
+ let balance = "-";
+ let owner = "-";
+ if (account.details) {
+ data = `${account.details.space}`;
+ if (showSOL) {
+ balance = `${(1.0 * account.details.lamports) / LAMPORTS_PER_SOL}`;
+ } else {
+ balance = `${account.details.lamports}`;
+ }
+ owner = `${account.details.owner.toBase58()}`;
+ }
+
+ return (
+
+
+ {account.id}
+ |
+
+ {statusText}
+ |
+
+ {account.pubkey.toBase58()}
+ |
+ {balance} |
+ {data} |
+ {owner === "-" ? owner : {owner} } |
+
+ );
+};
+
+export default AccountsCard;
diff --git a/explorer/src/components/TransactionsCard.tsx b/explorer/src/components/TransactionsCard.tsx
index 66e22ea8d2..9c10ff2b51 100644
--- a/explorer/src/components/TransactionsCard.tsx
+++ b/explorer/src/components/TransactionsCard.tsx
@@ -23,7 +23,7 @@ function TransactionsCard() {
try {
const length = bs58.decode(signature).length;
if (length > 64) {
- setError("Signature is too short");
+ setError("Signature is too long");
return;
} else if (length < 64) {
setError("Signature is too short");
diff --git a/explorer/src/providers/accounts.tsx b/explorer/src/providers/accounts.tsx
new file mode 100644
index 0000000000..d580c4c02a
--- /dev/null
+++ b/explorer/src/providers/accounts.tsx
@@ -0,0 +1,199 @@
+import React from "react";
+import { PublicKey, Connection } from "@solana/web3.js";
+import { findGetParameter, findPathSegment } from "../utils";
+import { useCluster } from "./cluster";
+
+export enum Status {
+ Checking,
+ CheckFailed,
+ Success
+}
+
+enum Source {
+ Url,
+ Input
+}
+
+export interface Details {
+ executable: boolean;
+ owner: PublicKey;
+ lamports: number;
+ space: number;
+}
+
+export interface Account {
+ id: number;
+ status: Status;
+ source: Source;
+ pubkey: PublicKey;
+ details?: Details;
+}
+
+type Accounts = { [id: number]: Account };
+interface State {
+ idCounter: number;
+ accounts: Accounts;
+}
+
+export enum ActionType {
+ Update,
+ Input
+}
+
+interface Update {
+ type: ActionType.Update;
+ id: number;
+ status: Status;
+ details?: Details;
+}
+
+interface Input {
+ type: ActionType.Input;
+ pubkey: PublicKey;
+}
+
+type Action = Update | Input;
+type Dispatch = (action: Action) => void;
+
+function reducer(state: State, action: Action): State {
+ switch (action.type) {
+ case ActionType.Input: {
+ const idCounter = state.idCounter + 1;
+ const accounts = {
+ ...state.accounts,
+ [idCounter]: {
+ id: idCounter,
+ status: Status.Checking,
+ source: Source.Input,
+ pubkey: action.pubkey
+ }
+ };
+ return { ...state, accounts, idCounter };
+ }
+ case ActionType.Update: {
+ let account = state.accounts[action.id];
+ if (account) {
+ account = {
+ ...account,
+ status: action.status,
+ details: action.details
+ };
+ const accounts = {
+ ...state.accounts,
+ [action.id]: account
+ };
+ return { ...state, accounts };
+ }
+ break;
+ }
+ }
+ return state;
+}
+
+function urlPublicKeys(): Array {
+ const keys: Array = [];
+ return keys
+ .concat(findGetParameter("account")?.split(",") || [])
+ .concat(findGetParameter("accounts")?.split(",") || [])
+ .concat(findPathSegment("account")?.split(",") || [])
+ .concat(findPathSegment("accounts")?.split(",") || [])
+ .concat(findGetParameter("address")?.split(",") || [])
+ .concat(findGetParameter("addresses")?.split(",") || [])
+ .concat(findPathSegment("address")?.split(",") || [])
+ .concat(findPathSegment("addresses")?.split(",") || [])
+ .map(key => new PublicKey(key));
+}
+
+function initState(): State {
+ let idCounter = 0;
+ const pubkeys = urlPublicKeys();
+ const accounts = pubkeys.reduce((accounts: Accounts, pubkey) => {
+ const id = ++idCounter;
+ accounts[id] = {
+ id,
+ status: Status.Checking,
+ source: Source.Url,
+ pubkey
+ };
+ return accounts;
+ }, {});
+ return { idCounter, accounts };
+}
+
+const StateContext = React.createContext(undefined);
+const DispatchContext = React.createContext(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(() => {
+ Object.values(state.accounts).forEach(account => {
+ fetchAccountInfo(dispatch, account.id, account.pubkey, url);
+ });
+ }, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export async function fetchAccountInfo(
+ dispatch: Dispatch,
+ id: number,
+ pubkey: PublicKey,
+ url: string
+) {
+ dispatch({
+ type: ActionType.Update,
+ status: Status.Checking,
+ id
+ });
+
+ let status;
+ let details;
+ try {
+ const result = await new Connection(url).getAccountInfo(pubkey);
+ details = {
+ space: result.data.length,
+ executable: result.executable,
+ lamports: result.lamports,
+ owner: result.owner
+ };
+ status = Status.Success;
+ } catch (error) {
+ console.error("Failed to fetch account info", error);
+ status = Status.CheckFailed;
+ }
+ dispatch({ type: ActionType.Update, status, details, id });
+}
+
+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
+ )
+ };
+}
+
+export function useAccountsDispatch() {
+ const context = React.useContext(DispatchContext);
+ if (!context) {
+ throw new Error(
+ `useAccountsDispatch must be used within a AccountsProvider`
+ );
+ }
+ return context;
+}
diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss
index 995c85ab17..53c3cd441d 100644
--- a/explorer/src/scss/_solana.scss
+++ b/explorer/src/scss/_solana.scss
@@ -34,10 +34,10 @@ code {
}
}
-.text-signature {
+.text-signature, .text-address {
font-size: 85%;
}
-input.text-signature {
+input.text-signature, input.text-address {
padding: 0 0.75rem
}
\ No newline at end of file