Add account address lookup table

This commit is contained in:
Justin Starry 2020-03-31 21:58:48 +08:00 committed by Michael Vines
parent 1d7dbe859a
commit 5bdeeda569
5 changed files with 395 additions and 3 deletions

View File

@ -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() {
</TransactionsProvider>
</div>
</div>
<div className="row">
<div className="col-12">
<AccountsProvider>
<AccountsCard />
</AccountsProvider>
</div>
</div>
</div>
</div>
<Overlay show={showModal} onClick={() => setShowModal(false)} />

View File

@ -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<HTMLInputElement>(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 (
<div className="card">
{renderHeader(showSOL, setShowSOL)}
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">
<span className="fe fe-hash"></span>
</th>
<th className="text-muted">Status</th>
<th className="text-muted">Address</th>
<th className="text-muted">
Balance ({showSOL ? "SOL" : "lamports"})
</th>
<th className="text-muted">Data (bytes)</th>
<th className="text-muted">Owner</th>
</tr>
</thead>
<tbody className="list">
<tr>
<td>
<span className="badge badge-soft-dark badge-pill">
{idCounter + 1}
</span>
</td>
<td>
<span className={`badge badge-soft-dark`}>New</span>
</td>
<td>
<input
type="text"
onInput={() => setError("")}
onKeyDown={e =>
e.keyCode === 13 && onNew(e.currentTarget.value)
}
onSubmit={e => onNew(e.currentTarget.value)}
ref={addressInput}
className={`form-control text-address text-monospace ${
error ? "is-invalid" : ""
}`}
placeholder="input account address"
/>
{error ? <div className="invalid-feedback">{error}</div> : null}
</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
{accounts.map(account => renderAccountRow(account, showSOL))}
</tbody>
</table>
</div>
</div>
);
}
const renderHeader = (
showSOL: boolean,
setShowSOL: (show: boolean) => void
) => {
return (
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Look Up Account(s)</h4>
</div>
<span className="text-muted mr-3">Display SOL</span>
<div className="custom-control custom-switch">
<input
type="checkbox"
className="custom-control-input"
checked={showSOL}
/>
<label
className="custom-control-label"
onClick={() => setShowSOL(!showSOL)}
></label>
</div>
</div>
</div>
);
};
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 (
<tr key={account.id}>
<td>
<span className="badge badge-soft-dark badge-pill">{account.id}</span>
</td>
<td>
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
</td>
<td>
<code>{account.pubkey.toBase58()}</code>
</td>
<td>{balance}</td>
<td>{data}</td>
<td>{owner === "-" ? owner : <code>{owner}</code>}</td>
</tr>
);
};
export default AccountsCard;

View File

@ -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");

View File

@ -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<PublicKey> {
const keys: Array<string> = [];
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<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(() => {
Object.values(state.accounts).forEach(account => {
fetchAccountInfo(dispatch, account.id, account.pubkey, url);
});
}, [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,
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;
}

View File

@ -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
}