Add account address lookup table
This commit is contained in:
parent
1d7dbe859a
commit
5bdeeda569
|
@ -2,7 +2,9 @@ import React from "react";
|
||||||
|
|
||||||
import { ClusterProvider } from "./providers/cluster";
|
import { ClusterProvider } from "./providers/cluster";
|
||||||
import { TransactionsProvider } from "./providers/transactions";
|
import { TransactionsProvider } from "./providers/transactions";
|
||||||
|
import { AccountsProvider } from "./providers/accounts";
|
||||||
import ClusterStatusButton from "./components/ClusterStatusButton";
|
import ClusterStatusButton from "./components/ClusterStatusButton";
|
||||||
|
import AccountsCard from "./components/AccountsCard";
|
||||||
import TransactionsCard from "./components/TransactionsCard";
|
import TransactionsCard from "./components/TransactionsCard";
|
||||||
import ClusterModal from "./components/ClusterModal";
|
import ClusterModal from "./components/ClusterModal";
|
||||||
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
||||||
|
@ -36,6 +38,13 @@ function App() {
|
||||||
</TransactionsProvider>
|
</TransactionsProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12">
|
||||||
|
<AccountsProvider>
|
||||||
|
<AccountsCard />
|
||||||
|
</AccountsProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Overlay show={showModal} onClick={() => setShowModal(false)} />
|
<Overlay show={showModal} onClick={() => setShowModal(false)} />
|
||||||
|
|
|
@ -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;
|
|
@ -23,7 +23,7 @@ function TransactionsCard() {
|
||||||
try {
|
try {
|
||||||
const length = bs58.decode(signature).length;
|
const length = bs58.decode(signature).length;
|
||||||
if (length > 64) {
|
if (length > 64) {
|
||||||
setError("Signature is too short");
|
setError("Signature is too long");
|
||||||
return;
|
return;
|
||||||
} else if (length < 64) {
|
} else if (length < 64) {
|
||||||
setError("Signature is too short");
|
setError("Signature is too short");
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -34,10 +34,10 @@ code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-signature {
|
.text-signature, .text-address {
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.text-signature {
|
input.text-signature, input.text-address {
|
||||||
padding: 0 0.75rem
|
padding: 0 0.75rem
|
||||||
}
|
}
|
Loading…
Reference in New Issue