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 { 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)} />
|
||||
|
|
|
@ -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 {
|
||||
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");
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
||||
input.text-signature {
|
||||
input.text-signature, input.text-address {
|
||||
padding: 0 0.75rem
|
||||
}
|
Loading…
Reference in New Issue