Add account details page
This commit is contained in:
parent
59288117b9
commit
29ec98d0a1
|
@ -2,20 +2,19 @@ import React from "react";
|
||||||
import { Link, Switch, Route, Redirect } from "react-router-dom";
|
import { Link, Switch, Route, Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import AccountsCard from "./components/AccountsCard";
|
import AccountsCard from "./components/AccountsCard";
|
||||||
|
import AccountDetails from "./components/AccountDetails";
|
||||||
import TransactionsCard from "./components/TransactionsCard";
|
import TransactionsCard from "./components/TransactionsCard";
|
||||||
import TransactionDetails from "./components/TransactionDetails";
|
import TransactionDetails from "./components/TransactionDetails";
|
||||||
import ClusterModal from "./components/ClusterModal";
|
import ClusterModal from "./components/ClusterModal";
|
||||||
import AccountModal from "./components/AccountModal";
|
|
||||||
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
||||||
import { TX_ALIASES } from "./providers/transactions";
|
import { TX_ALIASES } from "./providers/transactions";
|
||||||
import { ACCOUNT_PATHS } from "./providers/accounts";
|
import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
|
||||||
import TabbedPage from "components/TabbedPage";
|
import TabbedPage from "components/TabbedPage";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClusterModal />
|
<ClusterModal />
|
||||||
<AccountModal />
|
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<nav className="navbar navbar-expand-xl navbar-light">
|
<nav className="navbar navbar-expand-xl navbar-light">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
@ -44,7 +43,16 @@ function App() {
|
||||||
<TransactionsCard />
|
<TransactionsCard />
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={ACCOUNT_PATHS}>
|
<Route
|
||||||
|
exact
|
||||||
|
path={ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map(
|
||||||
|
account => `/${account}/:address`
|
||||||
|
)}
|
||||||
|
render={({ match }) => (
|
||||||
|
<AccountDetails address={match.params.address} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route exact path={ACCOUNT_ALIASES_PLURAL.map(alias => "/" + alias)}>
|
||||||
<TabbedPage tab="Accounts">
|
<TabbedPage tab="Accounts">
|
||||||
<AccountsCard />
|
<AccountsCard />
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useClusterModal } from "providers/cluster";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Status,
|
||||||
|
useFetchAccountInfo,
|
||||||
|
useFetchAccountHistory,
|
||||||
|
useAccountInfo
|
||||||
|
} from "providers/accounts";
|
||||||
|
import { lamportsToSolString } from "utils";
|
||||||
|
import Copyable from "./Copyable";
|
||||||
|
import { displayAddress } from "utils/tx";
|
||||||
|
|
||||||
|
type Props = { address: string };
|
||||||
|
export default function AccountDetails({ address }: Props) {
|
||||||
|
const fetchAccount = useFetchAccountInfo();
|
||||||
|
const [, setShow] = useClusterModal();
|
||||||
|
const [search, setSearch] = React.useState(address);
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
let pubkey: PublicKey | undefined;
|
||||||
|
try {
|
||||||
|
pubkey = new PublicKey(address);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
// TODO handle bad addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAddress = () => {
|
||||||
|
history.push({ ...location, pathname: "/account/" + search });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch account on load
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (pubkey) fetchAccount(pubkey);
|
||||||
|
}, [pubkey?.toBase58()]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const searchInput = (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
onKeyUp={e => e.key === "Enter" && updateAddress()}
|
||||||
|
className="form-control form-control-prepended search text-monospace"
|
||||||
|
placeholder="Search for address"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h6 className="header-pretitle">Details</h6>
|
||||||
|
<h3 className="header-title">Account</h3>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">
|
||||||
|
<ClusterStatusButton onClick={() => setShow(true)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-4 mt-n2 align-items-center">
|
||||||
|
<div className="col d-none d-md-block">
|
||||||
|
<div className="input-group input-group-merge">
|
||||||
|
{searchInput}
|
||||||
|
<div className="input-group-prepend">
|
||||||
|
<div className="input-group-text">
|
||||||
|
<span className="fe fe-search"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col d-block d-md-none">{searchInput}</div>
|
||||||
|
<div className="col-auto ml-n3 d-block d-md-none">
|
||||||
|
<button className="btn btn-white" onClick={updateAddress}>
|
||||||
|
<span className="fe fe-search"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{pubkey && <InfoCard pubkey={pubkey} />}
|
||||||
|
{pubkey && <HistoryCard pubkey={pubkey} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const info = useAccountInfo(address);
|
||||||
|
const refresh = useFetchAccountInfo();
|
||||||
|
|
||||||
|
if (!info || info.status === Status.Checking) {
|
||||||
|
return <LoadingCard />;
|
||||||
|
} else if (
|
||||||
|
info.status === Status.CheckFailed ||
|
||||||
|
info.lamports === undefined
|
||||||
|
) {
|
||||||
|
return <RetryCard retry={() => refresh(pubkey)} text="Fetch Failed" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { details, lamports } = info;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Overview</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-white btn-sm"
|
||||||
|
onClick={() => refresh(pubkey)}
|
||||||
|
>
|
||||||
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableCardBody>
|
||||||
|
<tr>
|
||||||
|
<td>Balance (SOL)</td>
|
||||||
|
<td className="text-right text-uppercase">
|
||||||
|
{lamportsToSolString(lamports)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<tr>
|
||||||
|
<td>Data (Bytes)</td>
|
||||||
|
<td className="text-right">{details.space}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<tr>
|
||||||
|
<td>Owner</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<Copyable text={details.owner.toBase58()}>
|
||||||
|
<code>{displayAddress(details.owner)}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<tr>
|
||||||
|
<td>Executable</td>
|
||||||
|
<td className="text-right">{details.executable ? "Yes" : "No"}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</TableCardBody>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const info = useAccountInfo(address);
|
||||||
|
const refresh = useFetchAccountHistory();
|
||||||
|
|
||||||
|
if (!info || !info.details) {
|
||||||
|
return null;
|
||||||
|
} else if (info.status === Status.FetchingHistory) {
|
||||||
|
return <LoadingCard />;
|
||||||
|
} else if (info.history === undefined) {
|
||||||
|
return (
|
||||||
|
<RetryCard
|
||||||
|
retry={() => refresh(pubkey)}
|
||||||
|
text="Failed to fetch transaction history"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.history.size === 0) {
|
||||||
|
return (
|
||||||
|
<RetryCard
|
||||||
|
retry={() => refresh(pubkey)}
|
||||||
|
text="No transaction history found"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailsList: React.ReactNode[] = [];
|
||||||
|
info.history.forEach((slotTransactions, slot) => {
|
||||||
|
const signatures = Array.from(slotTransactions.entries()).map(
|
||||||
|
([signature, err]) => {
|
||||||
|
return <code className="mb-2 mb-last-0">{signature}</code>;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
detailsList.push(
|
||||||
|
<tr>
|
||||||
|
<td className="vertical-top">Slot {slot}</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<div className="d-inline-flex flex-column align-items-end">
|
||||||
|
{signatures}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Transaction History</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-white btn-sm"
|
||||||
|
onClick={() => refresh(pubkey)}
|
||||||
|
>
|
||||||
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableCardBody>{detailsList}</TableCardBody>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingCard() {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body text-center">
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RetryCard({ retry, text }: { retry: () => void; text: string }) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body text-center">
|
||||||
|
{text}
|
||||||
|
<span className="btn btn-white ml-3 d-none d-md-inline" onClick={retry}>
|
||||||
|
Try Again
|
||||||
|
</span>
|
||||||
|
<div className="d-block d-md-none mt-4">
|
||||||
|
<hr></hr>
|
||||||
|
<span className="btn btn-white" onClick={retry}>
|
||||||
|
Try Again
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCardBody({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<tbody className="list">{children}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,154 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
useSelectedAccount,
|
|
||||||
useAccountsDispatch,
|
|
||||||
ActionType,
|
|
||||||
Account,
|
|
||||||
Status
|
|
||||||
} from "../providers/accounts";
|
|
||||||
import { TransactionError } from "@solana/web3.js";
|
|
||||||
import Copyable from "./Copyable";
|
|
||||||
import Overlay from "./Overlay";
|
|
||||||
|
|
||||||
function AccountModal() {
|
|
||||||
const selected = useSelectedAccount();
|
|
||||||
const dispatch = useAccountsDispatch();
|
|
||||||
const onClose = () => dispatch({ type: ActionType.Select });
|
|
||||||
const show = !!selected;
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
if (!selected) return null;
|
|
||||||
return (
|
|
||||||
<div className="modal-dialog modal-dialog-centered">
|
|
||||||
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
|
||||||
<div className="modal-card card">
|
|
||||||
<div className="card-header">
|
|
||||||
<h4 className="card-header-title">Account Transaction History</h4>
|
|
||||||
<button type="button" className="close" onClick={onClose}>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<AccountDetails account={selected} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={`modal fade${show ? " show" : ""}`} onClick={onClose}>
|
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
<Overlay show={show} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccountDetails({ account }: { account: Account }) {
|
|
||||||
const renderError = (content: React.ReactNode) => {
|
|
||||||
return <span className="text-info">{content}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (account.status === Status.FetchingHistory) {
|
|
||||||
return renderError(
|
|
||||||
<>
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.history === undefined)
|
|
||||||
return renderError("Failed to fetch account transaction history");
|
|
||||||
|
|
||||||
if (account.history.size === 0) return renderError("No transactions found");
|
|
||||||
|
|
||||||
const detailsList: React.ReactNode[] = [];
|
|
||||||
account.history.forEach((slotTransactions, slot) => {
|
|
||||||
detailsList.push(
|
|
||||||
<SlotTransactionDetails
|
|
||||||
slot={slot}
|
|
||||||
statuses={Array.from(slotTransactions.entries())}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{detailsList.map((details, i) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={++i}>
|
|
||||||
{i > 1 ? <hr className="mt-0 mx-n4 mb-4"></hr> : null}
|
|
||||||
{details}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SlotTransactionDetails({
|
|
||||||
slot,
|
|
||||||
statuses
|
|
||||||
}: {
|
|
||||||
slot: number;
|
|
||||||
statuses: [string, TransactionError | null][];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4 className="slot-pill">Slot #{slot}</h4>
|
|
||||||
<div className="list-group list-group-flush">
|
|
||||||
{statuses.map(([signature, err]) => {
|
|
||||||
return (
|
|
||||||
<ListGroupItem
|
|
||||||
key={signature}
|
|
||||||
signature={signature}
|
|
||||||
failed={err !== null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListGroupItem({
|
|
||||||
signature,
|
|
||||||
failed
|
|
||||||
}: {
|
|
||||||
signature: string;
|
|
||||||
failed: boolean;
|
|
||||||
}) {
|
|
||||||
let badgeText, badgeColor;
|
|
||||||
if (failed) {
|
|
||||||
badgeText = "Error";
|
|
||||||
badgeColor = "danger";
|
|
||||||
} else {
|
|
||||||
badgeText = "Success";
|
|
||||||
badgeColor = "primary";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="list-group-item slot-item">
|
|
||||||
<div className="row align-items-center justify-content-between flex-nowrap">
|
|
||||||
<div className="col-auto">
|
|
||||||
<span className={`badge badge-soft-${badgeColor} badge-pill`}>
|
|
||||||
{badgeText}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="col min-width-0">
|
|
||||||
<Copyable text={signature}>
|
|
||||||
<h5 className="mb-0 text-truncate">
|
|
||||||
<code>{signature}</code>
|
|
||||||
</h5>
|
|
||||||
</Copyable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AccountModal;
|
|
|
@ -1,26 +1,22 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
useAccounts,
|
useAccounts,
|
||||||
useAccountsDispatch,
|
|
||||||
Dispatch,
|
|
||||||
fetchAccountInfo,
|
|
||||||
ActionType,
|
|
||||||
Account,
|
Account,
|
||||||
Status
|
Status,
|
||||||
|
useFetchAccountInfo
|
||||||
} from "../providers/accounts";
|
} from "../providers/accounts";
|
||||||
import { assertUnreachable } from "../utils";
|
import { assertUnreachable } from "../utils";
|
||||||
import { displayAddress } from "../utils/tx";
|
import { displayAddress } from "../utils/tx";
|
||||||
import { useCluster } from "../providers/cluster";
|
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import Copyable from "./Copyable";
|
import Copyable from "./Copyable";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
|
|
||||||
function AccountsCard() {
|
function AccountsCard() {
|
||||||
const { accounts, idCounter } = useAccounts();
|
const { accounts, idCounter } = useAccounts();
|
||||||
const dispatch = useAccountsDispatch();
|
const fetchAccountInfo = useFetchAccountInfo();
|
||||||
const addressInput = React.useRef<HTMLInputElement>(null);
|
const addressInput = React.useRef<HTMLInputElement>(null);
|
||||||
const [error, setError] = React.useState("");
|
const [error, setError] = React.useState("");
|
||||||
const { url } = useCluster();
|
|
||||||
|
|
||||||
const onNew = (address: string) => {
|
const onNew = (address: string) => {
|
||||||
if (address.length === 0) return;
|
if (address.length === 0) return;
|
||||||
|
@ -32,9 +28,7 @@ function AccountsCard() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: ActionType.Input, pubkey });
|
fetchAccountInfo(pubkey);
|
||||||
fetchAccountInfo(dispatch, address, url);
|
|
||||||
|
|
||||||
const inputEl = addressInput.current;
|
const inputEl = addressInput.current;
|
||||||
if (inputEl) {
|
if (inputEl) {
|
||||||
inputEl.value = "";
|
inputEl.value = "";
|
||||||
|
@ -91,7 +85,7 @@ function AccountsCard() {
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
{accounts.map(account => renderAccountRow(account, dispatch, url))}
|
{accounts.map(account => renderAccountRow(account))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -111,11 +105,7 @@ const renderHeader = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAccountRow = (
|
const renderAccountRow = (account: Account) => {
|
||||||
account: Account,
|
|
||||||
dispatch: Dispatch,
|
|
||||||
url: string
|
|
||||||
) => {
|
|
||||||
let statusText;
|
let statusText;
|
||||||
let statusClass;
|
let statusClass;
|
||||||
switch (account.status) {
|
switch (account.status) {
|
||||||
|
@ -158,42 +148,6 @@ const renderAccountRow = (
|
||||||
balance = lamportsToSolString(account.lamports);
|
balance = lamportsToSolString(account.lamports);
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
const base58AccountPubkey = account.pubkey.toBase58();
|
||||||
return (
|
return (
|
||||||
<tr key={account.id}>
|
<tr key={account.id}>
|
||||||
|
@ -219,7 +173,17 @@ const renderAccountRow = (
|
||||||
</Copyable>
|
</Copyable>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{renderDetails()}</td>
|
<td>
|
||||||
|
<Link
|
||||||
|
to={location => ({
|
||||||
|
...location,
|
||||||
|
pathname: "/account/" + base58AccountPubkey
|
||||||
|
})}
|
||||||
|
className="btn btn-rounded-circle btn-white btn-sm"
|
||||||
|
>
|
||||||
|
<span className="fe fe-arrow-right"></span>
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
TransactionError,
|
TransactionError,
|
||||||
SignatureStatus
|
SignatureStatus
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { findGetParameter, findPathSegment } from "../utils/url";
|
import { useQuery } from "../utils/url";
|
||||||
import { useCluster, ClusterStatus } from "./cluster";
|
import { useCluster, ClusterStatus } from "./cluster";
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
|
@ -42,18 +42,16 @@ type Accounts = { [address: string]: Account };
|
||||||
interface State {
|
interface State {
|
||||||
idCounter: number;
|
idCounter: number;
|
||||||
accounts: Accounts;
|
accounts: Accounts;
|
||||||
selected?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
Update,
|
Update,
|
||||||
Input,
|
Fetch
|
||||||
Select
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
type: ActionType.Update;
|
type: ActionType.Update;
|
||||||
address: string;
|
pubkey: PublicKey;
|
||||||
data: {
|
data: {
|
||||||
status: Status;
|
status: Status;
|
||||||
lamports?: number;
|
lamports?: number;
|
||||||
|
@ -62,50 +60,53 @@ interface Update {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Input {
|
interface Fetch {
|
||||||
type: ActionType.Input;
|
type: ActionType.Fetch;
|
||||||
pubkey: PublicKey;
|
pubkey: PublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Select {
|
type Action = Update | Fetch;
|
||||||
type: ActionType.Select;
|
|
||||||
address?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Action = Update | Input | Select;
|
|
||||||
export type Dispatch = (action: Action) => void;
|
export type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.Input: {
|
case ActionType.Fetch: {
|
||||||
const address = action.pubkey.toBase58();
|
const address = action.pubkey.toBase58();
|
||||||
if (!!state.accounts[address]) return state;
|
const account = state.accounts[address];
|
||||||
const idCounter = state.idCounter + 1;
|
if (account) {
|
||||||
const accounts = {
|
const accounts = {
|
||||||
...state.accounts,
|
...state.accounts,
|
||||||
[address]: {
|
[address]: {
|
||||||
id: idCounter,
|
id: account.id,
|
||||||
status: Status.Checking,
|
pubkey: account.pubkey,
|
||||||
pubkey: action.pubkey
|
status: Status.Checking
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return { ...state, accounts, idCounter };
|
return { ...state, accounts };
|
||||||
}
|
} else {
|
||||||
|
const idCounter = state.idCounter + 1;
|
||||||
case ActionType.Select: {
|
const accounts = {
|
||||||
return { ...state, selected: action.address };
|
...state.accounts,
|
||||||
|
[address]: {
|
||||||
|
id: idCounter,
|
||||||
|
status: Status.Checking,
|
||||||
|
pubkey: action.pubkey
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { ...state, accounts, idCounter };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case ActionType.Update: {
|
case ActionType.Update: {
|
||||||
let account = state.accounts[action.address];
|
const address = action.pubkey.toBase58();
|
||||||
|
const account = state.accounts[address];
|
||||||
if (account) {
|
if (account) {
|
||||||
account = {
|
|
||||||
...account,
|
|
||||||
...action.data
|
|
||||||
};
|
|
||||||
const accounts = {
|
const accounts = {
|
||||||
...state.accounts,
|
...state.accounts,
|
||||||
[action.address]: account
|
[address]: {
|
||||||
|
...account,
|
||||||
|
...action.data
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return { ...state, accounts };
|
return { ...state, accounts };
|
||||||
}
|
}
|
||||||
|
@ -115,67 +116,49 @@ function reducer(state: State, action: Action): State {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACCOUNT_PATHS = [
|
export const ACCOUNT_ALIASES = ["account", "address"];
|
||||||
"/account",
|
export const ACCOUNT_ALIASES_PLURAL = ["accounts", "addresses"];
|
||||||
"/accounts",
|
|
||||||
"/address",
|
|
||||||
"/addresses"
|
|
||||||
];
|
|
||||||
|
|
||||||
function urlAddresses(): Array<string> {
|
|
||||||
const addresses: Array<string> = [];
|
|
||||||
|
|
||||||
ACCOUNT_PATHS.forEach(path => {
|
|
||||||
const name = path.slice(1);
|
|
||||||
const params = findGetParameter(name)?.split(",") || [];
|
|
||||||
const segments = findPathSegment(name)?.split(",") || [];
|
|
||||||
addresses.push(...params);
|
|
||||||
addresses.push(...segments);
|
|
||||||
});
|
|
||||||
|
|
||||||
return addresses.filter(a => a.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initState(): State {
|
|
||||||
let idCounter = 0;
|
|
||||||
const addresses = urlAddresses();
|
|
||||||
const accounts = addresses.reduce((accounts: Accounts, address) => {
|
|
||||||
if (!!accounts[address]) return accounts;
|
|
||||||
try {
|
|
||||||
const pubkey = new PublicKey(address);
|
|
||||||
const id = ++idCounter;
|
|
||||||
accounts[address] = {
|
|
||||||
id,
|
|
||||||
status: Status.Checking,
|
|
||||||
pubkey
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
// TODO display to user
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
return accounts;
|
|
||||||
}, {});
|
|
||||||
return { idCounter, accounts };
|
|
||||||
}
|
|
||||||
|
|
||||||
const StateContext = React.createContext<State | undefined>(undefined);
|
const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||||
|
|
||||||
type AccountsProviderProps = { children: React.ReactNode };
|
type AccountsProviderProps = { children: React.ReactNode };
|
||||||
export function AccountsProvider({ children }: AccountsProviderProps) {
|
export function AccountsProvider({ children }: AccountsProviderProps) {
|
||||||
const [state, dispatch] = React.useReducer(reducer, undefined, initState);
|
const [state, dispatch] = React.useReducer(reducer, {
|
||||||
|
idCounter: 0,
|
||||||
|
accounts: {}
|
||||||
|
});
|
||||||
|
|
||||||
const { status, url } = useCluster();
|
const { status, url } = useCluster();
|
||||||
|
|
||||||
// Check account statuses on startup and whenever cluster updates
|
// Check account statuses on startup and whenever cluster updates
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status !== ClusterStatus.Connected) return;
|
|
||||||
|
|
||||||
Object.keys(state.accounts).forEach(address => {
|
Object.keys(state.accounts).forEach(address => {
|
||||||
fetchAccountInfo(dispatch, address, url);
|
fetchAccountInfo(dispatch, new PublicKey(address), url, status);
|
||||||
});
|
});
|
||||||
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const query = useQuery();
|
||||||
|
const values = ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map(key =>
|
||||||
|
query.get(key)
|
||||||
|
);
|
||||||
|
React.useEffect(() => {
|
||||||
|
values
|
||||||
|
.filter((value): value is string => value !== null)
|
||||||
|
.flatMap(value => value.split(","))
|
||||||
|
// Remove duplicates
|
||||||
|
.filter((item, pos, self) => self.indexOf(item) === pos)
|
||||||
|
.filter(address => !state.accounts[address])
|
||||||
|
.forEach(address => {
|
||||||
|
try {
|
||||||
|
fetchAccountInfo(dispatch, new PublicKey(address), url, status);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
// TODO handle bad addresses
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
|
@ -185,29 +168,28 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAccountInfo(
|
async function fetchAccountInfo(
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
address: string,
|
pubkey: PublicKey,
|
||||||
url: string
|
url: string,
|
||||||
|
status: ClusterStatus
|
||||||
) {
|
) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.Update,
|
type: ActionType.Fetch,
|
||||||
address,
|
pubkey
|
||||||
data: {
|
|
||||||
status: Status.Checking
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let status;
|
// We will auto-refetch when status is no longer connecting
|
||||||
|
if (status === ClusterStatus.Connecting) return;
|
||||||
|
|
||||||
|
let fetchStatus;
|
||||||
let details;
|
let details;
|
||||||
let lamports;
|
let lamports;
|
||||||
try {
|
try {
|
||||||
const result = await new Connection(url, "recent").getAccountInfo(
|
const result = await new Connection(url, "recent").getAccountInfo(pubkey);
|
||||||
new PublicKey(address)
|
|
||||||
);
|
|
||||||
if (result === null) {
|
if (result === null) {
|
||||||
lamports = 0;
|
lamports = 0;
|
||||||
status = Status.NotFound;
|
fetchStatus = Status.NotFound;
|
||||||
} else {
|
} else {
|
||||||
lamports = result.lamports;
|
lamports = result.lamports;
|
||||||
details = {
|
details = {
|
||||||
|
@ -215,29 +197,35 @@ export async function fetchAccountInfo(
|
||||||
executable: result.executable,
|
executable: result.executable,
|
||||||
owner: result.owner
|
owner: result.owner
|
||||||
};
|
};
|
||||||
status = Status.FetchingHistory;
|
fetchStatus = Status.FetchingHistory;
|
||||||
fetchAccountHistory(dispatch, address, url);
|
fetchAccountHistory(dispatch, pubkey, url);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch account info", error);
|
console.error("Failed to fetch account info", error);
|
||||||
status = Status.CheckFailed;
|
fetchStatus = Status.CheckFailed;
|
||||||
}
|
}
|
||||||
const data = { status, lamports, details };
|
const data = { status: fetchStatus, lamports, details };
|
||||||
dispatch({ type: ActionType.Update, data, address });
|
dispatch({ type: ActionType.Update, data, pubkey });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAccountHistory(
|
async function fetchAccountHistory(
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
address: string,
|
pubkey: PublicKey,
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.Update,
|
||||||
|
data: { status: Status.FetchingHistory },
|
||||||
|
pubkey
|
||||||
|
});
|
||||||
|
|
||||||
let history;
|
let history;
|
||||||
let status;
|
let status;
|
||||||
try {
|
try {
|
||||||
const connection = new Connection(url);
|
const connection = new Connection(url);
|
||||||
const currentSlot = await connection.getSlot();
|
const currentSlot = await connection.getSlot();
|
||||||
const signatures = await connection.getConfirmedSignaturesForAddress(
|
const signatures = await connection.getConfirmedSignaturesForAddress(
|
||||||
new PublicKey(address),
|
pubkey,
|
||||||
Math.max(0, currentSlot - 10000 + 1),
|
Math.max(0, currentSlot - 10000 + 1),
|
||||||
currentSlot
|
currentSlot
|
||||||
);
|
);
|
||||||
|
@ -268,7 +256,7 @@ async function fetchAccountHistory(
|
||||||
status = Status.HistoryFailed;
|
status = Status.HistoryFailed;
|
||||||
}
|
}
|
||||||
const data = { status, history };
|
const data = { status, history };
|
||||||
dispatch({ type: ActionType.Update, data, address });
|
dispatch({ type: ActionType.Update, data, pubkey });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccounts() {
|
export function useAccounts() {
|
||||||
|
@ -284,16 +272,14 @@ export function useAccounts() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectedAccount() {
|
export function useAccountInfo(address: string) {
|
||||||
const context = React.useContext(StateContext);
|
const context = React.useContext(StateContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error(`useAccountInfo must be used within a AccountsProvider`);
|
||||||
`useSelectedAccount must be used within a AccountsProvider`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.selected) return undefined;
|
return context.accounts[address];
|
||||||
return context.accounts[context.selected];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccountsDispatch() {
|
export function useAccountsDispatch() {
|
||||||
|
@ -305,3 +291,31 @@ export function useAccountsDispatch() {
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFetchAccountInfo() {
|
||||||
|
const dispatch = React.useContext(DispatchContext);
|
||||||
|
if (!dispatch) {
|
||||||
|
throw new Error(
|
||||||
|
`useFetchAccountInfo must be used within a AccountsProvider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url, status } = useCluster();
|
||||||
|
return (pubkey: PublicKey) => {
|
||||||
|
fetchAccountInfo(dispatch, pubkey, url, status);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFetchAccountHistory() {
|
||||||
|
const dispatch = React.useContext(DispatchContext);
|
||||||
|
if (!dispatch) {
|
||||||
|
throw new Error(
|
||||||
|
`useFetchAccountHistory must be used within a AccountsProvider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = useCluster();
|
||||||
|
return (pubkey: PublicKey) => {
|
||||||
|
fetchAccountHistory(dispatch, pubkey, url);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ import {
|
||||||
Connection,
|
Connection,
|
||||||
SystemProgram,
|
SystemProgram,
|
||||||
Account,
|
Account,
|
||||||
SignatureResult
|
SignatureResult,
|
||||||
|
PublicKey
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { useQuery } from "../../utils/url";
|
import { useQuery } from "../../utils/url";
|
||||||
import { useCluster, Cluster, ClusterStatus } from "../cluster";
|
import { useCluster, Cluster, ClusterStatus } from "../cluster";
|
||||||
|
@ -14,12 +15,7 @@ import {
|
||||||
DispatchContext as DetailsDispatchContext
|
DispatchContext as DetailsDispatchContext
|
||||||
} from "./details";
|
} from "./details";
|
||||||
import base58 from "bs58";
|
import base58 from "bs58";
|
||||||
import {
|
import { useFetchAccountInfo } from "../accounts";
|
||||||
useAccountsDispatch,
|
|
||||||
fetchAccountInfo,
|
|
||||||
Dispatch as AccountsDispatch,
|
|
||||||
ActionType as AccountsActionType
|
|
||||||
} from "../accounts";
|
|
||||||
|
|
||||||
export enum FetchStatus {
|
export enum FetchStatus {
|
||||||
Fetching,
|
Fetching,
|
||||||
|
@ -72,30 +68,41 @@ type Dispatch = (action: Action) => void;
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.FetchSignature: {
|
case ActionType.FetchSignature: {
|
||||||
if (!!state.transactions[action.signature]) return state;
|
const transaction = state.transactions[action.signature];
|
||||||
|
|
||||||
const nextId = state.idCounter + 1;
|
|
||||||
const transactions = {
|
|
||||||
...state.transactions,
|
|
||||||
[action.signature]: {
|
|
||||||
id: nextId,
|
|
||||||
signature: action.signature,
|
|
||||||
fetchStatus: FetchStatus.Fetching
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return { ...state, transactions, idCounter: nextId };
|
|
||||||
}
|
|
||||||
case ActionType.UpdateStatus: {
|
|
||||||
let transaction = state.transactions[action.signature];
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
transaction = {
|
|
||||||
...transaction,
|
|
||||||
fetchStatus: action.fetchStatus,
|
|
||||||
info: action.info
|
|
||||||
};
|
|
||||||
const transactions = {
|
const transactions = {
|
||||||
...state.transactions,
|
...state.transactions,
|
||||||
[action.signature]: transaction
|
[action.signature]: {
|
||||||
|
...transaction,
|
||||||
|
fetchStatus: FetchStatus.Fetching,
|
||||||
|
info: undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { ...state, transactions };
|
||||||
|
} else {
|
||||||
|
const nextId = state.idCounter + 1;
|
||||||
|
const transactions = {
|
||||||
|
...state.transactions,
|
||||||
|
[action.signature]: {
|
||||||
|
id: nextId,
|
||||||
|
signature: action.signature,
|
||||||
|
fetchStatus: FetchStatus.Fetching
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { ...state, transactions, idCounter: nextId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionType.UpdateStatus: {
|
||||||
|
const transaction = state.transactions[action.signature];
|
||||||
|
if (transaction) {
|
||||||
|
const transactions = {
|
||||||
|
...state.transactions,
|
||||||
|
[action.signature]: {
|
||||||
|
...transaction,
|
||||||
|
fetchStatus: action.fetchStatus,
|
||||||
|
info: action.info
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return { ...state, transactions };
|
return { ...state, transactions };
|
||||||
}
|
}
|
||||||
|
@ -118,23 +125,19 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cluster, status: clusterStatus, url } = useCluster();
|
const { cluster, status: clusterStatus, url } = useCluster();
|
||||||
const accountsDispatch = useAccountsDispatch();
|
const fetchAccount = useFetchAccountInfo();
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const testFlag = query.get("test");
|
const testFlag = query.get("test");
|
||||||
|
|
||||||
// Check transaction statuses whenever cluster updates
|
// Check transaction statuses whenever cluster updates
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
Object.keys(state.transactions).forEach(signature => {
|
Object.keys(state.transactions).forEach(signature => {
|
||||||
dispatch({
|
|
||||||
type: ActionType.FetchSignature,
|
|
||||||
signature
|
|
||||||
});
|
|
||||||
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a test transaction
|
// Create a test transaction
|
||||||
if (cluster === Cluster.Devnet && testFlag !== null) {
|
if (cluster === Cluster.Devnet && testFlag !== null) {
|
||||||
createTestTransaction(dispatch, accountsDispatch, url, clusterStatus);
|
createTestTransaction(dispatch, fetchAccount, url, clusterStatus);
|
||||||
}
|
}
|
||||||
}, [testFlag, cluster, clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [testFlag, cluster, clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
@ -151,10 +154,6 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
||||||
.filter((item, pos, self) => self.indexOf(item) === pos)
|
.filter((item, pos, self) => self.indexOf(item) === pos)
|
||||||
.filter(signature => !state.transactions[signature])
|
.filter(signature => !state.transactions[signature])
|
||||||
.forEach(signature => {
|
.forEach(signature => {
|
||||||
dispatch({
|
|
||||||
type: ActionType.FetchSignature,
|
|
||||||
signature
|
|
||||||
});
|
|
||||||
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
||||||
});
|
});
|
||||||
}, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
@ -170,7 +169,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
||||||
|
|
||||||
async function createTestTransaction(
|
async function createTestTransaction(
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
accountsDispatch: AccountsDispatch,
|
fetchAccount: (pubkey: PublicKey) => void,
|
||||||
url: string,
|
url: string,
|
||||||
clusterStatus: ClusterStatus
|
clusterStatus: ClusterStatus
|
||||||
) {
|
) {
|
||||||
|
@ -187,16 +186,8 @@ async function createTestTransaction(
|
||||||
100000,
|
100000,
|
||||||
"recent"
|
"recent"
|
||||||
);
|
);
|
||||||
dispatch({
|
|
||||||
type: ActionType.FetchSignature,
|
|
||||||
signature
|
|
||||||
});
|
|
||||||
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
||||||
accountsDispatch({
|
fetchAccount(testAccount.publicKey);
|
||||||
type: AccountsActionType.Input,
|
|
||||||
pubkey: testAccount.publicKey
|
|
||||||
});
|
|
||||||
fetchAccountInfo(accountsDispatch, testAccount.publicKey.toBase58(), url);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create test success transaction", error);
|
console.error("Failed to create test success transaction", error);
|
||||||
}
|
}
|
||||||
|
@ -209,10 +200,6 @@ async function createTestTransaction(
|
||||||
lamports: 1
|
lamports: 1
|
||||||
});
|
});
|
||||||
const signature = await connection.sendTransaction(tx, testAccount);
|
const signature = await connection.sendTransaction(tx, testAccount);
|
||||||
dispatch({
|
|
||||||
type: ActionType.FetchSignature,
|
|
||||||
signature
|
|
||||||
});
|
|
||||||
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create test failure transaction", error);
|
console.error("Failed to create test failure transaction", error);
|
||||||
|
@ -226,9 +213,8 @@ export async function fetchTransactionStatus(
|
||||||
status: ClusterStatus
|
status: ClusterStatus
|
||||||
) {
|
) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.UpdateStatus,
|
type: ActionType.FetchSignature,
|
||||||
signature,
|
signature
|
||||||
fetchStatus: FetchStatus.Fetching
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// We will auto-refetch when status is no longer connecting
|
// We will auto-refetch when status is no longer connecting
|
||||||
|
@ -328,10 +314,6 @@ export function useFetchTransactionStatus() {
|
||||||
|
|
||||||
const { url, status } = useCluster();
|
const { url, status } = useCluster();
|
||||||
return (signature: TransactionSignature) => {
|
return (signature: TransactionSignature) => {
|
||||||
dispatch({
|
|
||||||
type: ActionType.FetchSignature,
|
|
||||||
signature
|
|
||||||
});
|
|
||||||
fetchTransactionStatus(dispatch, signature, url, status);
|
fetchTransactionStatus(dispatch, signature, url, status);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,3 +117,13 @@ h4.slot-pill {
|
||||||
.min-width-0 {
|
.min-width-0 {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-last-0 {
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-top {
|
||||||
|
vertical-align: top !important;
|
||||||
|
}
|
Loading…
Reference in New Issue