219 lines
5.7 KiB
TypeScript
219 lines
5.7 KiB
TypeScript
import React from "react";
|
|
import {
|
|
useAccounts,
|
|
useAccountsDispatch,
|
|
Dispatch,
|
|
fetchAccountInfo,
|
|
ActionType,
|
|
Account,
|
|
Status
|
|
} from "../providers/accounts";
|
|
import { assertUnreachable } from "../utils";
|
|
import { displayAddress } from "../utils/tx";
|
|
import { useCluster } from "../providers/cluster";
|
|
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
|
import Copyable from "./Copyable";
|
|
|
|
function AccountsCard() {
|
|
const { accounts, idCounter } = useAccounts();
|
|
const dispatch = useAccountsDispatch();
|
|
const addressInput = React.useRef<HTMLInputElement>(null);
|
|
const [error, setError] = React.useState("");
|
|
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, address, url);
|
|
|
|
const inputEl = addressInput.current;
|
|
if (inputEl) {
|
|
inputEl.value = "";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="card">
|
|
{renderHeader()}
|
|
|
|
<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 (SOL)</th>
|
|
<th className="text-muted">Data (bytes)</th>
|
|
<th className="text-muted">Owner</th>
|
|
<th className="text-muted">Details</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>
|
|
<td></td>
|
|
</tr>
|
|
{accounts.map(account => renderAccountRow(account, dispatch, url))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const renderHeader = () => {
|
|
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>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderAccountRow = (
|
|
account: Account,
|
|
dispatch: Dispatch,
|
|
url: string
|
|
) => {
|
|
let statusText;
|
|
let statusClass;
|
|
switch (account.status) {
|
|
case Status.NotFound:
|
|
statusClass = "danger";
|
|
statusText = "Not Found";
|
|
break;
|
|
case Status.CheckFailed:
|
|
case Status.HistoryFailed:
|
|
statusClass = "danger";
|
|
statusText = "Error";
|
|
break;
|
|
case Status.Checking:
|
|
case Status.FetchingHistory:
|
|
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 owner = "-";
|
|
if (account.details) {
|
|
data = `${account.details.space}`;
|
|
owner = displayAddress(account.details.owner);
|
|
}
|
|
|
|
let balance = "-";
|
|
if (account.lamports !== undefined) {
|
|
balance = `◎${(1.0 * account.lamports) / LAMPORTS_PER_SOL}`;
|
|
}
|
|
|
|
const renderDetails = () => {
|
|
let onClick, icon;
|
|
switch (account.status) {
|
|
case Status.Success:
|
|
icon = "more-horizontal";
|
|
onClick = () =>
|
|
dispatch({
|
|
type: ActionType.Select,
|
|
address: account.pubkey.toBase58()
|
|
});
|
|
break;
|
|
|
|
case Status.CheckFailed:
|
|
case Status.HistoryFailed: {
|
|
icon = "refresh-cw";
|
|
onClick = () => {
|
|
fetchAccountInfo(dispatch, account.pubkey.toBase58(), url);
|
|
};
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<button
|
|
className="btn btn-rounded-circle btn-white btn-sm"
|
|
onClick={onClick}
|
|
>
|
|
<span className={`fe fe-${icon}`}></span>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
const base58AccountPubkey = account.pubkey.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>
|
|
<Copyable text={base58AccountPubkey}>
|
|
<code>{base58AccountPubkey}</code>
|
|
</Copyable>
|
|
</td>
|
|
<td>{balance}</td>
|
|
<td>{data}</td>
|
|
<td>{owner === "-" ? owner : <code>{owner}</code>}</td>
|
|
<td>{renderDetails()}</td>
|
|
</tr>
|
|
);
|
|
};
|
|
|
|
export default AccountsCard;
|