Add transaction details page
This commit is contained in:
committed by
Michael Vines
parent
9f26cbbbeb
commit
484a4db626
@ -1,99 +1,61 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link, Switch, Route, Redirect } from "react-router-dom";
|
import { Link, Switch, Route, Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import ClusterStatusButton from "./components/ClusterStatusButton";
|
|
||||||
import AccountsCard from "./components/AccountsCard";
|
import AccountsCard from "./components/AccountsCard";
|
||||||
import TransactionsCard from "./components/TransactionsCard";
|
import TransactionsCard from "./components/TransactionsCard";
|
||||||
|
import TransactionDetails from "./components/TransactionDetails";
|
||||||
import ClusterModal from "./components/ClusterModal";
|
import ClusterModal from "./components/ClusterModal";
|
||||||
import TransactionModal from "./components/TransactionModal";
|
|
||||||
import AccountModal from "./components/AccountModal";
|
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 { useCurrentTab, Tab } from "./providers/tab";
|
import { TX_ALIASES } from "./providers/transactions";
|
||||||
import { TX_PATHS } from "./providers/transactions";
|
|
||||||
import { ACCOUNT_PATHS } from "./providers/accounts";
|
import { ACCOUNT_PATHS } from "./providers/accounts";
|
||||||
|
import TabbedPage from "components/TabbedPage";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [showClusterModal, setShowClusterModal] = React.useState(false);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClusterModal
|
<ClusterModal />
|
||||||
show={showClusterModal}
|
|
||||||
onClose={() => setShowClusterModal(false)}
|
|
||||||
/>
|
|
||||||
<TransactionModal />
|
|
||||||
<AccountModal />
|
<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">
|
||||||
<div className="row align-items-end">
|
<div className="row align-items-end">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<img src={Logo} width="250" alt="Solana Explorer" />
|
<Link to="/">
|
||||||
|
<img src={Logo} width="250" alt="Solana Explorer" />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="header">
|
<Switch>
|
||||||
<div className="container">
|
<Route
|
||||||
<div className="header-body">
|
exact
|
||||||
<div className="row align-items-center d-md-none">
|
path={TX_ALIASES.map(tx => `/${tx}/:signature`)}
|
||||||
<div className="col-12">
|
render={({ match }) => (
|
||||||
<ClusterStatusButton
|
<TransactionDetails signature={match.params.signature} />
|
||||||
expand
|
)}
|
||||||
onClick={() => setShowClusterModal(true)}
|
/>
|
||||||
/>
|
<Route exact path={TX_ALIASES.map(tx => `/${tx}s`)}>
|
||||||
</div>
|
<TabbedPage tab="Transactions">
|
||||||
</div>
|
|
||||||
<div className="row align-items-center">
|
|
||||||
<div className="col">
|
|
||||||
<ul className="nav nav-tabs nav-overflow header-tabs">
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink href="/transactions" tab="Transactions" />
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink href="/accounts" tab="Accounts" />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="col-auto d-none d-md-block">
|
|
||||||
<ClusterStatusButton
|
|
||||||
onClick={() => setShowClusterModal(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container">
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/">
|
|
||||||
<Redirect to="/transactions" />
|
|
||||||
</Route>
|
|
||||||
<Route path={TX_PATHS}>
|
|
||||||
<TransactionsCard />
|
<TransactionsCard />
|
||||||
</Route>
|
</TabbedPage>
|
||||||
<Route path={ACCOUNT_PATHS}>
|
</Route>
|
||||||
|
<Route path={ACCOUNT_PATHS}>
|
||||||
|
<TabbedPage tab="Accounts">
|
||||||
<AccountsCard />
|
<AccountsCard />
|
||||||
</Route>
|
</TabbedPage>
|
||||||
</Switch>
|
</Route>
|
||||||
</div>
|
<Route
|
||||||
|
render={({ location }) => (
|
||||||
|
<Redirect to={{ ...location, pathname: "/transactions" }} />
|
||||||
|
)}
|
||||||
|
></Route>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavLink({ href, tab }: { href: string; tab: Tab }) {
|
|
||||||
let classes = "nav-link";
|
|
||||||
if (tab === useCurrentTab()) {
|
|
||||||
classes += " active";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={href} className={classes}>
|
|
||||||
{tab}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -11,8 +11,9 @@ import {
|
|||||||
import { assertUnreachable } from "../utils";
|
import { assertUnreachable } from "../utils";
|
||||||
import { displayAddress } from "../utils/tx";
|
import { displayAddress } from "../utils/tx";
|
||||||
import { useCluster } from "../providers/cluster";
|
import { useCluster } from "../providers/cluster";
|
||||||
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import Copyable from "./Copyable";
|
import Copyable from "./Copyable";
|
||||||
|
import { lamportsToSolString } from "utils";
|
||||||
|
|
||||||
function AccountsCard() {
|
function AccountsCard() {
|
||||||
const { accounts, idCounter } = useAccounts();
|
const { accounts, idCounter } = useAccounts();
|
||||||
@ -154,7 +155,7 @@ const renderAccountRow = (
|
|||||||
|
|
||||||
let balance = "-";
|
let balance = "-";
|
||||||
if (account.lamports !== undefined) {
|
if (account.lamports !== undefined) {
|
||||||
balance = `◎${(1.0 * account.lamports) / LAMPORTS_PER_SOL}`;
|
balance = lamportsToSolString(account.lamports);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderDetails = () => {
|
const renderDetails = () => {
|
||||||
|
@ -7,17 +7,15 @@ import {
|
|||||||
clusterUrl,
|
clusterUrl,
|
||||||
clusterName,
|
clusterName,
|
||||||
CLUSTERS,
|
CLUSTERS,
|
||||||
Cluster
|
Cluster,
|
||||||
|
useClusterModal
|
||||||
} from "../providers/cluster";
|
} from "../providers/cluster";
|
||||||
import { assertUnreachable } from "../utils";
|
import { assertUnreachable } from "../utils";
|
||||||
import Overlay from "./Overlay";
|
import Overlay from "./Overlay";
|
||||||
|
|
||||||
type Props = {
|
function ClusterModal() {
|
||||||
show: boolean;
|
const [show, setShow] = useClusterModal();
|
||||||
onClose: () => void;
|
const onClose = () => setShow(false);
|
||||||
};
|
|
||||||
|
|
||||||
function ClusterModal({ show, onClose }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
68
explorer/src/components/TabbedPage.tsx
Normal file
68
explorer/src/components/TabbedPage.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { useClusterModal } from "providers/cluster";
|
||||||
|
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||||
|
|
||||||
|
export type Tab = "Transactions" | "Accounts";
|
||||||
|
|
||||||
|
type Props = { children: React.ReactNode; tab: Tab };
|
||||||
|
export default function TabbedPage({ children, tab }: Props) {
|
||||||
|
const [, setShow] = useClusterModal();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body">
|
||||||
|
<div className="row align-items-center d-md-none">
|
||||||
|
<div className="col-12">
|
||||||
|
<ClusterStatusButton expand onClick={() => setShow(true)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<ul className="nav nav-tabs nav-overflow header-tabs">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
href="/transactions"
|
||||||
|
tab="Transactions"
|
||||||
|
current={tab}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink href="/accounts" tab="Accounts" current={tab} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto d-none d-md-block">
|
||||||
|
<ClusterStatusButton onClick={() => setShow(true)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavLink({
|
||||||
|
href,
|
||||||
|
tab,
|
||||||
|
current
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
tab: Tab;
|
||||||
|
current: Tab;
|
||||||
|
}) {
|
||||||
|
const location = useLocation();
|
||||||
|
let classes = "nav-link";
|
||||||
|
if (tab === current) {
|
||||||
|
classes += " active";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={{ ...location, pathname: href }} className={classes}>
|
||||||
|
{tab}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
482
explorer/src/components/TransactionDetails.tsx
Normal file
482
explorer/src/components/TransactionDetails.tsx
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Source,
|
||||||
|
useTransactionStatus,
|
||||||
|
useTransactionDetails,
|
||||||
|
useTransactionsDispatch,
|
||||||
|
useDetailsDispatch,
|
||||||
|
checkTransactionStatus,
|
||||||
|
ActionType,
|
||||||
|
FetchStatus
|
||||||
|
} from "../providers/transactions";
|
||||||
|
import { fetchDetails } from "providers/transactions/details";
|
||||||
|
import { useCluster, useClusterModal } from "providers/cluster";
|
||||||
|
import {
|
||||||
|
TransactionSignature,
|
||||||
|
TransactionInstruction,
|
||||||
|
TransferParams,
|
||||||
|
CreateAccountParams,
|
||||||
|
SystemProgram
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||||
|
import { lamportsToSolString } from "utils";
|
||||||
|
import { displayAddress, decodeCreate, decodeTransfer } from "utils/tx";
|
||||||
|
import Copyable from "./Copyable";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
type Props = { signature: TransactionSignature };
|
||||||
|
export default function TransactionDetails({ signature }: Props) {
|
||||||
|
const dispatch = useTransactionsDispatch();
|
||||||
|
const { url } = useCluster();
|
||||||
|
const [, setShow] = useClusterModal();
|
||||||
|
const [search, setSearch] = React.useState(signature);
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const updateSignature = () => {
|
||||||
|
history.push({ ...location, pathname: "/tx/" + search });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch transaction on load
|
||||||
|
React.useEffect(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.FetchSignature,
|
||||||
|
signature,
|
||||||
|
source: Source.Url
|
||||||
|
});
|
||||||
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
|
}, [signature, dispatch, url]);
|
||||||
|
|
||||||
|
const searchInput = (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
onKeyUp={e => e.key === "Enter" && updateSignature()}
|
||||||
|
className="form-control form-control-prepended search text-monospace"
|
||||||
|
placeholder="Search for signature"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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">Transaction</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={updateSignature}>
|
||||||
|
<span className="fe fe-search"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TransactionStatusCard signature={signature} />
|
||||||
|
<TransactionAccountsCard signature={signature} />
|
||||||
|
<TransactionInstructionsCard signature={signature} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransactionStatusCard({ signature }: Props) {
|
||||||
|
const status = useTransactionStatus(signature);
|
||||||
|
const dispatch = useTransactionsDispatch();
|
||||||
|
const details = useTransactionDetails(signature);
|
||||||
|
const { url } = useCluster();
|
||||||
|
|
||||||
|
const refreshStatus = () => {
|
||||||
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!status || status.fetchStatus === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard />;
|
||||||
|
} else if (status?.fetchStatus === FetchStatus.FetchFailed) {
|
||||||
|
return <RetryCard retry={refreshStatus} text="Fetch Failed" />;
|
||||||
|
} else if (!status.info) {
|
||||||
|
return <RetryCard retry={refreshStatus} text="Not Found" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { info } = status;
|
||||||
|
const renderResult = () => {
|
||||||
|
let statusClass = "success";
|
||||||
|
let statusText = "Success";
|
||||||
|
if (info.result.err) {
|
||||||
|
statusClass = "danger";
|
||||||
|
statusText = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fee = details?.transaction?.meta?.fee;
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<div className="card-header-title">Status</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">
|
||||||
|
<button className="btn btn-white btn-sm" onClick={refreshStatus}>
|
||||||
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="list-group list-group-flush my-n3">
|
||||||
|
<div className="list-group-item">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h5 className="mb-0">Result</h5>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">{renderResult()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="list-group-item">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h5 className="mb-0">Block</h5>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">{info.slot}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="list-group-item">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h5 className="mb-0">Confirmations</h5>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto text-uppercase">
|
||||||
|
{info.confirmations}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{fee && (
|
||||||
|
<div className="list-group-item">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h5 className="mb-0">Fee (SOL)</h5>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">{lamportsToSolString(fee)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransactionAccountsCard({ signature }: Props) {
|
||||||
|
const details = useTransactionDetails(signature);
|
||||||
|
const dispatch = useDetailsDispatch();
|
||||||
|
const { url } = useCluster();
|
||||||
|
|
||||||
|
const refreshDetails = () => fetchDetails(dispatch, signature, url);
|
||||||
|
const transaction = details?.transaction?.transaction;
|
||||||
|
const message = React.useMemo(() => {
|
||||||
|
return transaction?.compileMessage();
|
||||||
|
}, [transaction]);
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return null;
|
||||||
|
} else if (details.fetchStatus === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard />;
|
||||||
|
} else if (details?.fetchStatus === FetchStatus.FetchFailed) {
|
||||||
|
return <RetryCard retry={refreshDetails} text="Fetch Failed" />;
|
||||||
|
} else if (!details.transaction || !message) {
|
||||||
|
return <RetryCard retry={refreshDetails} text="Not Found" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { meta } = details.transaction;
|
||||||
|
if (!meta) {
|
||||||
|
return <RetryCard retry={refreshDetails} text="Metadata Missing" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountRows = message.accountKeys.map((pubkey, index) => {
|
||||||
|
const pre = meta.preBalances[index];
|
||||||
|
const post = meta.postBalances[index];
|
||||||
|
const key = pubkey.toBase58();
|
||||||
|
const renderChange = () => {
|
||||||
|
const change = post - pre;
|
||||||
|
if (change === 0) return "";
|
||||||
|
const sols = lamportsToSolString(change);
|
||||||
|
if (change > 0) {
|
||||||
|
return <span className="badge badge-soft-success">{"+" + sols}</span>;
|
||||||
|
} else {
|
||||||
|
return <span className="badge badge-soft-warning">{"-" + sols}</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>
|
||||||
|
<Copyable text={key}>
|
||||||
|
<code>{displayAddress(pubkey)}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
<td>{renderChange()}</td>
|
||||||
|
<td>{lamportsToSolString(post)}</td>
|
||||||
|
<td>
|
||||||
|
{index === 0 && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Fee Payer</span>
|
||||||
|
)}
|
||||||
|
{!message.isAccountWritable(index) && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Readonly</span>
|
||||||
|
)}
|
||||||
|
{index < message.header.numRequiredSignatures && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Signer</span>
|
||||||
|
)}
|
||||||
|
{message.instructions.find(ix => ix.programIdIndex === index) && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Program</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h4 className="card-header-title">Accounts</h4>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted">Address</th>
|
||||||
|
<th className="text-muted">Change (SOL)</th>
|
||||||
|
<th className="text-muted">Post Balance (SOL)</th>
|
||||||
|
<th className="text-muted">Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">{accountRows}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransactionInstructionsCard({ signature }: Props) {
|
||||||
|
const details = useTransactionDetails(signature);
|
||||||
|
const dispatch = useDetailsDispatch();
|
||||||
|
const { url } = useCluster();
|
||||||
|
const refreshDetails = () => fetchDetails(dispatch, signature, url);
|
||||||
|
|
||||||
|
if (!details || !details.transaction) return null;
|
||||||
|
|
||||||
|
const { transaction } = details.transaction;
|
||||||
|
if (transaction.instructions.length === 0) {
|
||||||
|
return <RetryCard retry={refreshDetails} text="No instructions found" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructionDetails = transaction.instructions.map((ix, index) => {
|
||||||
|
const transfer = decodeTransfer(ix);
|
||||||
|
if (transfer)
|
||||||
|
return <TransferDetails key={index} transfer={transfer} index={index} />;
|
||||||
|
const create = decodeCreate(ix);
|
||||||
|
if (create)
|
||||||
|
return <CreateDetails key={index} create={create} index={index} />;
|
||||||
|
return <InstructionDetails key={index} ix={ix} index={index} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body">Transaction Instruction(s)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{instructionDetails}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransferDetails({
|
||||||
|
transfer,
|
||||||
|
index
|
||||||
|
}: {
|
||||||
|
transfer: TransferParams;
|
||||||
|
index: number;
|
||||||
|
}) {
|
||||||
|
const from = transfer.fromPubkey.toBase58();
|
||||||
|
const to = transfer.toPubkey.toBase58();
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||||
|
<span className="badge badge-soft-dark mr-2">#{index + 1}</span>
|
||||||
|
Transfer
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="list-group list-group-flush my-n3">
|
||||||
|
<ListGroupItem label="Program">
|
||||||
|
<code>{displayAddress(SystemProgram.programId)}</code>
|
||||||
|
</ListGroupItem>
|
||||||
|
<ListGroupItem label="From">
|
||||||
|
<Copyable text={from}>
|
||||||
|
<code>{from}</code>
|
||||||
|
</Copyable>
|
||||||
|
</ListGroupItem>
|
||||||
|
<ListGroupItem label="To">
|
||||||
|
<Copyable text={to}>
|
||||||
|
<code>{to}</code>
|
||||||
|
</Copyable>
|
||||||
|
</ListGroupItem>
|
||||||
|
<ListGroupItem label="Amount (SOL)">
|
||||||
|
{lamportsToSolString(transfer.lamports)}
|
||||||
|
</ListGroupItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateDetails({
|
||||||
|
create,
|
||||||
|
index
|
||||||
|
}: {
|
||||||
|
create: CreateAccountParams;
|
||||||
|
index: number;
|
||||||
|
}) {
|
||||||
|
const from = create.fromPubkey.toBase58();
|
||||||
|
const newKey = create.newAccountPubkey.toBase58();
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||||
|
<span className="badge badge-soft-dark mr-2">#{index + 1}</span>
|
||||||
|
Create Account
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="list-group list-group-flush my-n3">
|
||||||
|
<ListGroupItem label="Program">
|
||||||
|
<code>{displayAddress(SystemProgram.programId)}</code>
|
||||||
|
</ListGroupItem>
|
||||||
|
<ListGroupItem label="From">
|
||||||
|
<Copyable text={from}>
|
||||||
|
<code>{from}</code>
|
||||||
|
</Copyable>
|
||||||
|
</ListGroupItem>
|
||||||
|
<ListGroupItem label="New Account">
|
||||||
|
<Copyable text={newKey}>
|
||||||
|
<code>{newKey}</code>
|
||||||
|
</Copyable>
|
||||||
|
</ListGroupItem>
|
||||||
|
<ListGroupItem label="Amount (SOL)">
|
||||||
|
{lamportsToSolString(create.lamports)}
|
||||||
|
</ListGroupItem>
|
||||||
|
<ListGroupItem label="Data (Bytes)">{create.space}</ListGroupItem>
|
||||||
|
<ListGroupItem label="Owner">
|
||||||
|
<code>{displayAddress(create.programId)}</code>
|
||||||
|
</ListGroupItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstructionDetails({
|
||||||
|
ix,
|
||||||
|
index
|
||||||
|
}: {
|
||||||
|
ix: TransactionInstruction;
|
||||||
|
index: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||||
|
<span className="badge badge-soft-dark mr-2">#{index + 1}</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="list-group list-group-flush my-n3">
|
||||||
|
<ListGroupItem label="Program">
|
||||||
|
<code>{displayAddress(ix.programId)}</code>
|
||||||
|
</ListGroupItem>
|
||||||
|
{ix.keys.map(({ pubkey }, keyIndex) => (
|
||||||
|
<ListGroupItem key={keyIndex} label={`Address #${keyIndex + 1}`}>
|
||||||
|
<Copyable text={pubkey.toBase58()}>
|
||||||
|
<code>{pubkey.toBase58()}</code>
|
||||||
|
</Copyable>
|
||||||
|
</ListGroupItem>
|
||||||
|
))}
|
||||||
|
<ListGroupItem label="Data (Bytes)">{ix.data.length}</ListGroupItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListGroupItem({
|
||||||
|
label,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="list-group-item">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h5 className="mb-0">{label}</h5>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingCard() {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body">
|
||||||
|
<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}
|
||||||
|
<span className="btn btn-white ml-3" onClick={retry}>
|
||||||
|
Try Again
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,223 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
useDetails,
|
|
||||||
useTransactions,
|
|
||||||
useTransactionsDispatch,
|
|
||||||
ActionType
|
|
||||||
} from "../providers/transactions";
|
|
||||||
import { displayAddress, decodeCreate, decodeTransfer } from "../utils/tx";
|
|
||||||
import {
|
|
||||||
LAMPORTS_PER_SOL,
|
|
||||||
TransferParams,
|
|
||||||
CreateAccountParams,
|
|
||||||
TransactionInstruction,
|
|
||||||
TransactionSignature
|
|
||||||
} from "@solana/web3.js";
|
|
||||||
import Copyable from "./Copyable";
|
|
||||||
import Overlay from "./Overlay";
|
|
||||||
|
|
||||||
function TransactionModal() {
|
|
||||||
const { selected } = useTransactions();
|
|
||||||
const dispatch = useTransactionsDispatch();
|
|
||||||
const onClose = () => dispatch({ type: ActionType.Deselect });
|
|
||||||
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">Transaction Details</h4>
|
|
||||||
|
|
||||||
<button type="button" className="close" onClick={onClose}>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TransactionDetails signature={selected} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={`modal fade${show ? " show" : ""}`} onClick={onClose}>
|
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
<Overlay show={show} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TransactionDetails({
|
|
||||||
signature
|
|
||||||
}: {
|
|
||||||
signature: TransactionSignature;
|
|
||||||
}) {
|
|
||||||
const details = useDetails(signature);
|
|
||||||
|
|
||||||
const renderError = (content: React.ReactNode) => {
|
|
||||||
return (
|
|
||||||
<div className="card-body">
|
|
||||||
<span className="text-info">{content}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!details) return renderError("Transaction details not found");
|
|
||||||
|
|
||||||
if (!details.transaction) {
|
|
||||||
return renderError(
|
|
||||||
<>
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { transaction } = details.transaction;
|
|
||||||
if (!transaction) return renderError("Transaction not found");
|
|
||||||
|
|
||||||
if (transaction.instructions.length === 0)
|
|
||||||
return renderError("No instructions found");
|
|
||||||
|
|
||||||
const instructionDetails = transaction.instructions.map((ix, index) => {
|
|
||||||
const transfer = decodeTransfer(ix);
|
|
||||||
if (transfer) return <TransferDetails transfer={transfer} index={index} />;
|
|
||||||
const create = decodeCreate(ix);
|
|
||||||
if (create) return <CreateDetails create={create} index={index} />;
|
|
||||||
return <InstructionDetails ix={ix} index={index} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{instructionDetails.map((details, i) => {
|
|
||||||
return (
|
|
||||||
<div key={++i}>
|
|
||||||
{i > 1 ? <hr className="mt-0 mb-0"></hr> : null}
|
|
||||||
{details}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TransferDetails({
|
|
||||||
transfer,
|
|
||||||
index
|
|
||||||
}: {
|
|
||||||
transfer: TransferParams;
|
|
||||||
index: number;
|
|
||||||
}) {
|
|
||||||
const from = transfer.fromPubkey.toBase58();
|
|
||||||
const to = transfer.toPubkey.toBase58();
|
|
||||||
return (
|
|
||||||
<div className="card-body">
|
|
||||||
<h4 className="ix-pill">{`Instruction #${index + 1} (Transfer)`}</h4>
|
|
||||||
<div className="list-group list-group-flush my-n3">
|
|
||||||
<ListGroupItem label="From">
|
|
||||||
<Copyable text={from}>
|
|
||||||
<code>{from}</code>
|
|
||||||
</Copyable>
|
|
||||||
</ListGroupItem>
|
|
||||||
<ListGroupItem label="To">
|
|
||||||
<Copyable text={to}>
|
|
||||||
<code>{to}</code>
|
|
||||||
</Copyable>
|
|
||||||
</ListGroupItem>
|
|
||||||
<ListGroupItem label="Amount (SOL)">
|
|
||||||
{`◎${(1.0 * transfer.lamports) / LAMPORTS_PER_SOL}`}
|
|
||||||
</ListGroupItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateDetails({
|
|
||||||
create,
|
|
||||||
index
|
|
||||||
}: {
|
|
||||||
create: CreateAccountParams;
|
|
||||||
index: number;
|
|
||||||
}) {
|
|
||||||
const from = create.fromPubkey.toBase58();
|
|
||||||
const newKey = create.newAccountPubkey.toBase58();
|
|
||||||
return (
|
|
||||||
<div className="card-body">
|
|
||||||
<h4 className="ix-pill">{`Instruction #${index +
|
|
||||||
1} (Create Account)`}</h4>
|
|
||||||
<div className="list-group list-group-flush my-n3">
|
|
||||||
<ListGroupItem label="From">
|
|
||||||
<Copyable text={from}>
|
|
||||||
<code>{from}</code>
|
|
||||||
</Copyable>
|
|
||||||
</ListGroupItem>
|
|
||||||
<ListGroupItem label="New Account">
|
|
||||||
<Copyable text={newKey}>
|
|
||||||
<code>{newKey}</code>
|
|
||||||
</Copyable>
|
|
||||||
</ListGroupItem>
|
|
||||||
<ListGroupItem label="Amount (SOL)">
|
|
||||||
{`◎${(1.0 * create.lamports) / LAMPORTS_PER_SOL}`}
|
|
||||||
</ListGroupItem>
|
|
||||||
<ListGroupItem label="Data (Bytes)">{create.space}</ListGroupItem>
|
|
||||||
<ListGroupItem label="Owner">
|
|
||||||
<code>{displayAddress(create.programId)}</code>
|
|
||||||
</ListGroupItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstructionDetails({
|
|
||||||
ix,
|
|
||||||
index
|
|
||||||
}: {
|
|
||||||
ix: TransactionInstruction;
|
|
||||||
index: number;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="card-body">
|
|
||||||
<h4 className="ix-pill">{`Instruction #${index + 1}`}</h4>
|
|
||||||
<div className="list-group list-group-flush my-n3">
|
|
||||||
{ix.keys.map(({ pubkey }, keyIndex) => (
|
|
||||||
<ListGroupItem key={keyIndex} label={`Address #${keyIndex + 1}`}>
|
|
||||||
<Copyable text={pubkey.toBase58()}>
|
|
||||||
<code>{pubkey.toBase58()}</code>
|
|
||||||
</Copyable>
|
|
||||||
</ListGroupItem>
|
|
||||||
))}
|
|
||||||
<ListGroupItem label="Data (Bytes)">{ix.data.length}</ListGroupItem>
|
|
||||||
<ListGroupItem label="Program">
|
|
||||||
<code>{displayAddress(ix.programId)}</code>
|
|
||||||
</ListGroupItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListGroupItem({
|
|
||||||
label,
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="list-group-item ix-item">
|
|
||||||
<div className="row align-items-center">
|
|
||||||
<div className="col">
|
|
||||||
<h5 className="mb-0">{label}</h5>
|
|
||||||
</div>
|
|
||||||
<div className="col-auto">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TransactionModal;
|
|
@ -1,16 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
useTransactions,
|
useTransactions,
|
||||||
useTransactionsDispatch,
|
useTransactionsDispatch,
|
||||||
checkTransactionStatus,
|
checkTransactionStatus,
|
||||||
ActionType,
|
ActionType,
|
||||||
TransactionState,
|
TransactionStatus,
|
||||||
|
Source,
|
||||||
FetchStatus
|
FetchStatus
|
||||||
} from "../providers/transactions";
|
} from "../providers/transactions";
|
||||||
import bs58 from "bs58";
|
import bs58 from "bs58";
|
||||||
import { assertUnreachable } from "../utils";
|
import { assertUnreachable } from "../utils";
|
||||||
import { useCluster } from "../providers/cluster";
|
import { useCluster } from "../providers/cluster";
|
||||||
import Copyable from "./Copyable";
|
import Copyable from "./Copyable";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
function TransactionsCard() {
|
function TransactionsCard() {
|
||||||
const { transactions, idCounter } = useTransactions();
|
const { transactions, idCounter } = useTransactions();
|
||||||
@ -18,6 +21,7 @@ function TransactionsCard() {
|
|||||||
const signatureInput = React.useRef<HTMLInputElement>(null);
|
const signatureInput = React.useRef<HTMLInputElement>(null);
|
||||||
const [error, setError] = React.useState("");
|
const [error, setError] = React.useState("");
|
||||||
const { url } = useCluster();
|
const { url } = useCluster();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const onNew = (signature: string) => {
|
const onNew = (signature: string) => {
|
||||||
if (signature.length === 0) return;
|
if (signature.length === 0) return;
|
||||||
@ -35,7 +39,11 @@ function TransactionsCard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: ActionType.InputSignature, signature });
|
dispatch({
|
||||||
|
type: ActionType.FetchSignature,
|
||||||
|
signature,
|
||||||
|
source: Source.Input
|
||||||
|
});
|
||||||
checkTransactionStatus(dispatch, signature, url);
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
|
|
||||||
const inputEl = signatureInput.current;
|
const inputEl = signatureInput.current;
|
||||||
@ -93,7 +101,7 @@ function TransactionsCard() {
|
|||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
{transactions.map(transaction =>
|
{transactions.map(transaction =>
|
||||||
renderTransactionRow(transaction, dispatch, url)
|
renderTransactionRow(transaction, dispatch, location, url)
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -115,11 +123,12 @@ const renderHeader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderTransactionRow = (
|
const renderTransactionRow = (
|
||||||
transaction: TransactionState,
|
transactionStatus: TransactionStatus,
|
||||||
dispatch: any,
|
dispatch: any,
|
||||||
|
location: any,
|
||||||
url: string
|
url: string
|
||||||
) => {
|
) => {
|
||||||
const { fetchStatus, transactionStatus } = transaction;
|
const { fetchStatus, info, signature, id } = transactionStatus;
|
||||||
|
|
||||||
let statusText;
|
let statusText;
|
||||||
let statusClass;
|
let statusClass;
|
||||||
@ -133,10 +142,10 @@ const renderTransactionRow = (
|
|||||||
statusText = "Fetching";
|
statusText = "Fetching";
|
||||||
break;
|
break;
|
||||||
case FetchStatus.Fetched: {
|
case FetchStatus.Fetched: {
|
||||||
if (!transactionStatus) {
|
if (!info) {
|
||||||
statusClass = "warning";
|
statusClass = "warning";
|
||||||
statusText = "Not Found";
|
statusText = "Not Found";
|
||||||
} else if (transactionStatus.result.err) {
|
} else if (info.result.err) {
|
||||||
statusClass = "danger";
|
statusClass = "danger";
|
||||||
statusText = "Failed";
|
statusText = "Failed";
|
||||||
} else {
|
} else {
|
||||||
@ -151,50 +160,46 @@ const renderTransactionRow = (
|
|||||||
|
|
||||||
let slotText = "-";
|
let slotText = "-";
|
||||||
let confirmationsText = "-";
|
let confirmationsText = "-";
|
||||||
if (transactionStatus) {
|
if (info) {
|
||||||
slotText = `${transactionStatus.slot}`;
|
slotText = `${info.slot}`;
|
||||||
confirmationsText = `${transactionStatus.confirmations}`;
|
confirmationsText = `${info.confirmations}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderDetails = () => {
|
const renderDetails = () => {
|
||||||
let onClick, icon;
|
if (info?.confirmations === "max") {
|
||||||
if (transactionStatus?.confirmations === "max") {
|
return (
|
||||||
icon = "more-horizontal";
|
<Link
|
||||||
onClick = () =>
|
to={{ ...location, pathname: "/tx/" + signature }}
|
||||||
dispatch({
|
className="btn btn-rounded-circle btn-white btn-sm"
|
||||||
type: ActionType.Select,
|
>
|
||||||
signature: transaction.signature
|
<span className="fe fe-arrow-right"></span>
|
||||||
});
|
</Link>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
icon = "refresh-cw";
|
return (
|
||||||
onClick = () => {
|
<button
|
||||||
checkTransactionStatus(dispatch, transaction.signature, url);
|
className="btn btn-rounded-circle btn-white btn-sm"
|
||||||
};
|
onClick={() => {
|
||||||
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="fe fe-refresh-cw"></span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="btn btn-rounded-circle btn-white btn-sm"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<span className={`fe fe-${icon}`}></span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={transaction.signature}>
|
<tr key={signature}>
|
||||||
<td>
|
<td>
|
||||||
<span className="badge badge-soft-dark badge-pill">
|
<span className="badge badge-soft-dark badge-pill">{id}</span>
|
||||||
{transaction.id}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Copyable text={transaction.signature}>
|
<Copyable text={signature}>
|
||||||
<code>{transaction.signature}</code>
|
<code>{signature}</code>
|
||||||
</Copyable>
|
</Copyable>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-uppercase">{confirmationsText}</td>
|
<td className="text-uppercase">{confirmationsText}</td>
|
||||||
|
@ -7,19 +7,16 @@ import * as serviceWorker from "./serviceWorker";
|
|||||||
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 { AccountsProvider } from "./providers/accounts";
|
||||||
import { TabProvider } from "./providers/tab";
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Router>
|
<Router>
|
||||||
<TabProvider>
|
<ClusterProvider>
|
||||||
<ClusterProvider>
|
<AccountsProvider>
|
||||||
<AccountsProvider>
|
<TransactionsProvider>
|
||||||
<TransactionsProvider>
|
<App />
|
||||||
<App />
|
</TransactionsProvider>
|
||||||
</TransactionsProvider>
|
</AccountsProvider>
|
||||||
</AccountsProvider>
|
</ClusterProvider>
|
||||||
</ClusterProvider>
|
|
||||||
</TabProvider>
|
|
||||||
</Router>,
|
</Router>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
@ -125,6 +125,10 @@ function initState(): State {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SetShowModal = React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
const ModalContext = React.createContext<[boolean, SetShowModal] | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
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);
|
||||||
|
|
||||||
@ -135,6 +139,7 @@ export function ClusterProvider({ children }: ClusterProviderProps) {
|
|||||||
undefined,
|
undefined,
|
||||||
initState
|
initState
|
||||||
);
|
);
|
||||||
|
const [showModal, setShowModal] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Connect to cluster immediately
|
// Connect to cluster immediately
|
||||||
@ -144,7 +149,9 @@ export function ClusterProvider({ children }: ClusterProviderProps) {
|
|||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
{children}
|
<ModalContext.Provider value={[showModal, setShowModal]}>
|
||||||
|
{children}
|
||||||
|
</ModalContext.Provider>
|
||||||
</DispatchContext.Provider>
|
</DispatchContext.Provider>
|
||||||
</StateContext.Provider>
|
</StateContext.Provider>
|
||||||
);
|
);
|
||||||
@ -203,3 +210,11 @@ export function useClusterDispatch() {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useClusterModal() {
|
||||||
|
const context = React.useContext(ModalContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`useClusterModal must be used within a ClusterProvider`);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
import { ACCOUNT_PATHS } from "./accounts";
|
|
||||||
|
|
||||||
export type Tab = "Transactions" | "Accounts";
|
|
||||||
const StateContext = React.createContext<Tab | undefined>(undefined);
|
|
||||||
|
|
||||||
type TabProviderProps = { children: React.ReactNode };
|
|
||||||
export function TabProvider({ children }: TabProviderProps) {
|
|
||||||
const location = useLocation();
|
|
||||||
const paths = location.pathname
|
|
||||||
.slice(1)
|
|
||||||
.split("/")
|
|
||||||
.map(name => `/${name}`);
|
|
||||||
let tab: Tab = "Transactions";
|
|
||||||
if (ACCOUNT_PATHS.includes(paths[0].toLowerCase())) {
|
|
||||||
tab = "Accounts";
|
|
||||||
}
|
|
||||||
|
|
||||||
return <StateContext.Provider value={tab}>{children}</StateContext.Provider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCurrentTab() {
|
|
||||||
const context = React.useContext(StateContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(`useCurrentTab must be used within a TabProvider`);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
@ -5,17 +5,10 @@ import {
|
|||||||
ConfirmedTransaction
|
ConfirmedTransaction
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { useCluster, ClusterStatus } from "../cluster";
|
import { useCluster, ClusterStatus } from "../cluster";
|
||||||
import { useTransactions } from "./index";
|
import { useTransactions, FetchStatus } from "./index";
|
||||||
|
|
||||||
export enum Status {
|
|
||||||
Checking,
|
|
||||||
CheckFailed,
|
|
||||||
NotFound,
|
|
||||||
Found
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Details {
|
export interface Details {
|
||||||
status: Status;
|
fetchStatus: FetchStatus;
|
||||||
transaction: ConfirmedTransaction | null;
|
transaction: ConfirmedTransaction | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,13 +16,14 @@ type State = { [signature: string]: Details };
|
|||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
Update,
|
Update,
|
||||||
Add
|
Add,
|
||||||
|
Remove
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
type: ActionType.Update;
|
type: ActionType.Update;
|
||||||
signature: string;
|
signature: string;
|
||||||
status: Status;
|
fetchStatus: FetchStatus;
|
||||||
transaction: ConfirmedTransaction | null;
|
transaction: ConfirmedTransaction | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +32,12 @@ interface Add {
|
|||||||
signatures: TransactionSignature[];
|
signatures: TransactionSignature[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = Update | Add;
|
interface Remove {
|
||||||
|
type: ActionType.Remove;
|
||||||
|
signatures: TransactionSignature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = Update | Add | Remove;
|
||||||
type Dispatch = (action: Action) => void;
|
type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
@ -49,7 +48,7 @@ function reducer(state: State, action: Action): State {
|
|||||||
action.signatures.forEach(signature => {
|
action.signatures.forEach(signature => {
|
||||||
if (!details[signature]) {
|
if (!details[signature]) {
|
||||||
details[signature] = {
|
details[signature] = {
|
||||||
status: Status.Checking,
|
fetchStatus: FetchStatus.Fetching,
|
||||||
transaction: null
|
transaction: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -57,21 +56,26 @@ function reducer(state: State, action: Action): State {
|
|||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ActionType.Remove: {
|
||||||
|
if (action.signatures.length === 0) return state;
|
||||||
|
const details = { ...state };
|
||||||
|
action.signatures.forEach(signature => {
|
||||||
|
delete details[signature];
|
||||||
|
});
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
case ActionType.Update: {
|
case ActionType.Update: {
|
||||||
let details = state[action.signature];
|
let details = state[action.signature];
|
||||||
if (details) {
|
if (details) {
|
||||||
details = {
|
details = {
|
||||||
...details,
|
...details,
|
||||||
status: action.status
|
fetchStatus: action.fetchStatus,
|
||||||
|
transaction: action.transaction
|
||||||
};
|
};
|
||||||
if (action.transaction !== null) {
|
|
||||||
details.transaction = action.transaction;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...{
|
[action.signature]: details
|
||||||
[action.signature]: details
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -94,22 +98,28 @@ export function DetailsProvider({ children }: DetailsProviderProps) {
|
|||||||
|
|
||||||
// Filter blocks for current transaction slots
|
// Filter blocks for current transaction slots
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status !== ClusterStatus.Connected) return;
|
const removeSignatures = new Set<string>();
|
||||||
|
|
||||||
const fetchSignatures = new Set<string>();
|
const fetchSignatures = new Set<string>();
|
||||||
transactions.forEach(({ signature, transactionStatus }) => {
|
transactions.forEach(({ signature, info }) => {
|
||||||
if (transactionStatus?.confirmations === "max" && !state[signature])
|
if (info?.confirmations === "max" && !state[signature])
|
||||||
fetchSignatures.add(signature);
|
fetchSignatures.add(signature);
|
||||||
|
else if (info?.confirmations !== "max" && state[signature])
|
||||||
|
removeSignatures.add(signature);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const removeList: string[] = [];
|
||||||
|
removeSignatures.forEach(s => removeList.push(s));
|
||||||
|
dispatch({ type: ActionType.Remove, signatures: removeList });
|
||||||
|
|
||||||
|
if (status !== ClusterStatus.Connected) return;
|
||||||
|
|
||||||
const fetchList: string[] = [];
|
const fetchList: string[] = [];
|
||||||
fetchSignatures.forEach(s => fetchList.push(s));
|
fetchSignatures.forEach(s => fetchList.push(s));
|
||||||
dispatch({ type: ActionType.Add, signatures: fetchList });
|
dispatch({ type: ActionType.Add, signatures: fetchList });
|
||||||
|
|
||||||
fetchSignatures.forEach(signature => {
|
fetchSignatures.forEach(signature => {
|
||||||
fetchDetails(dispatch, signature, url);
|
fetchDetails(dispatch, signature, url);
|
||||||
});
|
});
|
||||||
}, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [status, transactions]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
@ -120,30 +130,26 @@ export function DetailsProvider({ children }: DetailsProviderProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDetails(
|
export async function fetchDetails(
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
signature: TransactionSignature,
|
signature: TransactionSignature,
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.Update,
|
type: ActionType.Update,
|
||||||
status: Status.Checking,
|
fetchStatus: FetchStatus.Fetching,
|
||||||
transaction: null,
|
transaction: null,
|
||||||
signature
|
signature
|
||||||
});
|
});
|
||||||
|
|
||||||
let status;
|
let fetchStatus;
|
||||||
let transaction = null;
|
let transaction = null;
|
||||||
try {
|
try {
|
||||||
transaction = await new Connection(url).getConfirmedTransaction(signature);
|
transaction = await new Connection(url).getConfirmedTransaction(signature);
|
||||||
if (transaction) {
|
fetchStatus = FetchStatus.Fetched;
|
||||||
status = Status.Found;
|
|
||||||
} else {
|
|
||||||
status = Status.NotFound;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch confirmed transaction", error);
|
console.error("Failed to fetch confirmed transaction", error);
|
||||||
status = Status.CheckFailed;
|
fetchStatus = FetchStatus.FetchFailed;
|
||||||
}
|
}
|
||||||
dispatch({ type: ActionType.Update, status, signature, transaction });
|
dispatch({ type: ActionType.Update, fetchStatus, signature, transaction });
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
Account,
|
Account,
|
||||||
SignatureResult
|
SignatureResult
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { findGetParameter, findPathSegment } from "../../utils/url";
|
import { findGetParameter } from "../../utils/url";
|
||||||
import { useCluster, ClusterStatus } from "../cluster";
|
import { useCluster, ClusterStatus } from "../cluster";
|
||||||
import {
|
import {
|
||||||
DetailsProvider,
|
DetailsProvider,
|
||||||
@ -20,6 +20,7 @@ import {
|
|||||||
Dispatch as AccountsDispatch,
|
Dispatch as AccountsDispatch,
|
||||||
ActionType as AccountsActionType
|
ActionType as AccountsActionType
|
||||||
} from "../accounts";
|
} from "../accounts";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export enum FetchStatus {
|
export enum FetchStatus {
|
||||||
Fetching,
|
Fetching,
|
||||||
@ -27,28 +28,29 @@ export enum FetchStatus {
|
|||||||
Fetched
|
Fetched
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Source {
|
export enum Source {
|
||||||
Url,
|
Url,
|
||||||
Input
|
Input,
|
||||||
|
Test
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Confirmations = number | "max";
|
export type Confirmations = number | "max";
|
||||||
|
|
||||||
export interface TransactionStatus {
|
export interface TransactionStatusInfo {
|
||||||
slot: number;
|
slot: number;
|
||||||
result: SignatureResult;
|
result: SignatureResult;
|
||||||
confirmations: Confirmations;
|
confirmations: Confirmations;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionState {
|
export interface TransactionStatus {
|
||||||
id: number;
|
id: number;
|
||||||
source: Source;
|
source: Source;
|
||||||
fetchStatus: FetchStatus;
|
fetchStatus: FetchStatus;
|
||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
transactionStatus?: TransactionStatus;
|
info?: TransactionStatusInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Transactions = { [signature: string]: TransactionState };
|
type Transactions = { [signature: string]: TransactionStatus };
|
||||||
interface State {
|
interface State {
|
||||||
idCounter: number;
|
idCounter: number;
|
||||||
selected?: TransactionSignature;
|
selected?: TransactionSignature;
|
||||||
@ -57,50 +59,29 @@ interface State {
|
|||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
UpdateStatus,
|
UpdateStatus,
|
||||||
InputSignature,
|
FetchSignature
|
||||||
Select,
|
|
||||||
Deselect
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectTransaction {
|
|
||||||
type: ActionType.Select;
|
|
||||||
signature: TransactionSignature;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeselectTransaction {
|
|
||||||
type: ActionType.Deselect;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateStatus {
|
interface UpdateStatus {
|
||||||
type: ActionType.UpdateStatus;
|
type: ActionType.UpdateStatus;
|
||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
fetchStatus: FetchStatus;
|
fetchStatus: FetchStatus;
|
||||||
transactionStatus?: TransactionStatus;
|
info?: TransactionStatusInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputSignature {
|
interface FetchSignature {
|
||||||
type: ActionType.InputSignature;
|
type: ActionType.FetchSignature;
|
||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
|
source: Source;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action =
|
type Action = UpdateStatus | FetchSignature;
|
||||||
| UpdateStatus
|
|
||||||
| InputSignature
|
|
||||||
| SelectTransaction
|
|
||||||
| DeselectTransaction;
|
|
||||||
|
|
||||||
type Dispatch = (action: Action) => void;
|
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.Deselect: {
|
case ActionType.FetchSignature: {
|
||||||
return { ...state, selected: undefined };
|
|
||||||
}
|
|
||||||
case ActionType.Select: {
|
|
||||||
const tx = state.transactions[action.signature];
|
|
||||||
return { ...state, selected: tx.signature };
|
|
||||||
}
|
|
||||||
case ActionType.InputSignature: {
|
|
||||||
if (!!state.transactions[action.signature]) return state;
|
if (!!state.transactions[action.signature]) return state;
|
||||||
|
|
||||||
const nextId = state.idCounter + 1;
|
const nextId = state.idCounter + 1;
|
||||||
@ -108,7 +89,7 @@ function reducer(state: State, action: Action): State {
|
|||||||
...state.transactions,
|
...state.transactions,
|
||||||
[action.signature]: {
|
[action.signature]: {
|
||||||
id: nextId,
|
id: nextId,
|
||||||
source: Source.Input,
|
source: action.source,
|
||||||
signature: action.signature,
|
signature: action.signature,
|
||||||
fetchStatus: FetchStatus.Fetching
|
fetchStatus: FetchStatus.Fetching
|
||||||
}
|
}
|
||||||
@ -120,11 +101,9 @@ function reducer(state: State, action: Action): State {
|
|||||||
if (transaction) {
|
if (transaction) {
|
||||||
transaction = {
|
transaction = {
|
||||||
...transaction,
|
...transaction,
|
||||||
fetchStatus: action.fetchStatus
|
fetchStatus: action.fetchStatus,
|
||||||
|
info: action.info
|
||||||
};
|
};
|
||||||
if (action.transactionStatus) {
|
|
||||||
transaction.transactionStatus = action.transactionStatus;
|
|
||||||
}
|
|
||||||
const transactions = {
|
const transactions = {
|
||||||
...state.transactions,
|
...state.transactions,
|
||||||
[action.signature]: transaction
|
[action.signature]: transaction
|
||||||
@ -137,74 +116,60 @@ function reducer(state: State, action: Action): State {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TX_PATHS = [
|
export const TX_ALIASES = ["tx", "txn", "transaction"];
|
||||||
"/tx",
|
|
||||||
"/txs",
|
|
||||||
"/txn",
|
|
||||||
"/txns",
|
|
||||||
"/transaction",
|
|
||||||
"/transactions"
|
|
||||||
];
|
|
||||||
|
|
||||||
function urlSignatures(): Array<string> {
|
|
||||||
const signatures: Array<string> = [];
|
|
||||||
|
|
||||||
TX_PATHS.forEach(path => {
|
|
||||||
const name = path.slice(1);
|
|
||||||
const params = findGetParameter(name)?.split(",") || [];
|
|
||||||
const segments = findPathSegment(name)?.split(",") || [];
|
|
||||||
signatures.push(...params);
|
|
||||||
signatures.push(...segments);
|
|
||||||
});
|
|
||||||
|
|
||||||
return signatures.filter(s => s.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initState(): State {
|
|
||||||
let idCounter = 0;
|
|
||||||
const signatures = urlSignatures();
|
|
||||||
const transactions = signatures.reduce(
|
|
||||||
(transactions: Transactions, signature) => {
|
|
||||||
if (!!transactions[signature]) return transactions;
|
|
||||||
const nextId = idCounter + 1;
|
|
||||||
transactions[signature] = {
|
|
||||||
id: nextId,
|
|
||||||
source: Source.Url,
|
|
||||||
signature,
|
|
||||||
fetchStatus: FetchStatus.Fetching
|
|
||||||
};
|
|
||||||
idCounter++;
|
|
||||||
return transactions;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
return { idCounter, transactions };
|
|
||||||
}
|
|
||||||
|
|
||||||
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 TransactionsProviderProps = { children: React.ReactNode };
|
type TransactionsProviderProps = { children: React.ReactNode };
|
||||||
export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
||||||
const [state, dispatch] = React.useReducer(reducer, undefined, initState);
|
const [state, dispatch] = React.useReducer(reducer, {
|
||||||
|
idCounter: 0,
|
||||||
|
transactions: {}
|
||||||
|
});
|
||||||
|
|
||||||
const { status, url } = useCluster();
|
const { status, url } = useCluster();
|
||||||
const accountsDispatch = useAccountsDispatch();
|
const accountsDispatch = useAccountsDispatch();
|
||||||
|
const search = useLocation().search;
|
||||||
|
|
||||||
// Check transaction statuses on startup and whenever cluster updates
|
// Check transaction statuses whenever cluster updates
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status !== ClusterStatus.Connected) return;
|
if (status !== ClusterStatus.Connected) return;
|
||||||
|
|
||||||
|
Object.keys(state.transactions).forEach(signature => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.FetchSignature,
|
||||||
|
signature,
|
||||||
|
source: Source.Url
|
||||||
|
});
|
||||||
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
|
});
|
||||||
|
|
||||||
// Create a test transaction
|
// Create a test transaction
|
||||||
if (findGetParameter("test") !== null) {
|
if (findGetParameter("test") !== null) {
|
||||||
createTestTransaction(dispatch, accountsDispatch, url);
|
createTestTransaction(dispatch, accountsDispatch, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(state.transactions).forEach(signature => {
|
|
||||||
checkTransactionStatus(dispatch, signature, url);
|
|
||||||
});
|
|
||||||
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Check for transactions in the url params
|
||||||
|
React.useEffect(() => {
|
||||||
|
TX_ALIASES.flatMap(key =>
|
||||||
|
(findGetParameter(key)?.split(",") || []).concat(
|
||||||
|
findGetParameter(key + "s")?.split(",") || []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flatMap(paramValue => paramValue?.split(",") || [])
|
||||||
|
.filter(signature => !state.transactions[signature])
|
||||||
|
.forEach(signature => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.FetchSignature,
|
||||||
|
signature,
|
||||||
|
source: Source.Url
|
||||||
|
});
|
||||||
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
|
});
|
||||||
|
}, [search]); // 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}>
|
||||||
@ -232,7 +197,11 @@ async function createTestTransaction(
|
|||||||
100000,
|
100000,
|
||||||
"recent"
|
"recent"
|
||||||
);
|
);
|
||||||
dispatch({ type: ActionType.InputSignature, signature });
|
dispatch({
|
||||||
|
type: ActionType.FetchSignature,
|
||||||
|
signature,
|
||||||
|
source: Source.Test
|
||||||
|
});
|
||||||
checkTransactionStatus(dispatch, signature, url);
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
accountsDispatch({
|
accountsDispatch({
|
||||||
type: AccountsActionType.Input,
|
type: AccountsActionType.Input,
|
||||||
@ -251,7 +220,11 @@ async function createTestTransaction(
|
|||||||
lamports: 1
|
lamports: 1
|
||||||
});
|
});
|
||||||
const signature = await connection.sendTransaction(tx, testAccount);
|
const signature = await connection.sendTransaction(tx, testAccount);
|
||||||
dispatch({ type: ActionType.InputSignature, signature });
|
dispatch({
|
||||||
|
type: ActionType.FetchSignature,
|
||||||
|
signature,
|
||||||
|
source: Source.Test
|
||||||
|
});
|
||||||
checkTransactionStatus(dispatch, signature, url);
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create test failure transaction", error);
|
console.error("Failed to create test failure transaction", error);
|
||||||
@ -270,7 +243,7 @@ export async function checkTransactionStatus(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let fetchStatus;
|
let fetchStatus;
|
||||||
let transactionStatus: TransactionStatus | undefined;
|
let info: TransactionStatusInfo | undefined;
|
||||||
try {
|
try {
|
||||||
const { value } = await new Connection(url).getSignatureStatus(signature, {
|
const { value } = await new Connection(url).getSignatureStatus(signature, {
|
||||||
searchTransactionHistory: true
|
searchTransactionHistory: true
|
||||||
@ -284,7 +257,7 @@ export async function checkTransactionStatus(
|
|||||||
confirmations = "max";
|
confirmations = "max";
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionStatus = {
|
info = {
|
||||||
slot: value.slot,
|
slot: value.slot,
|
||||||
confirmations,
|
confirmations,
|
||||||
result: { err: value.err }
|
result: { err: value.err }
|
||||||
@ -295,11 +268,12 @@ export async function checkTransactionStatus(
|
|||||||
console.error("Failed to fetch transaction status", error);
|
console.error("Failed to fetch transaction status", error);
|
||||||
fetchStatus = FetchStatus.FetchFailed;
|
fetchStatus = FetchStatus.FetchFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.UpdateStatus,
|
type: ActionType.UpdateStatus,
|
||||||
signature,
|
signature,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
transactionStatus
|
info
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,13 +286,36 @@ export function useTransactions() {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
idCounter: context.idCounter,
|
idCounter: context.idCounter,
|
||||||
selected: context.selected,
|
|
||||||
transactions: Object.values(context.transactions).sort((a, b) =>
|
transactions: Object.values(context.transactions).sort((a, b) =>
|
||||||
a.id <= b.id ? 1 : -1
|
a.id <= b.id ? 1 : -1
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTransactionStatus(signature: TransactionSignature) {
|
||||||
|
const context = React.useContext(StateContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
`useTransactionStatus must be used within a TransactionsProvider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.transactions[signature];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTransactionDetails(signature: TransactionSignature) {
|
||||||
|
const context = React.useContext(DetailsStateContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
`useTransactionDetails must be used within a TransactionsProvider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context[signature];
|
||||||
|
}
|
||||||
|
|
||||||
export function useTransactionsDispatch() {
|
export function useTransactionsDispatch() {
|
||||||
const context = React.useContext(DispatchContext);
|
const context = React.useContext(DispatchContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@ -338,11 +335,3 @@ export function useDetailsDispatch() {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDetails(signature: TransactionSignature) {
|
|
||||||
const context = React.useContext(DetailsStateContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(`useDetails must be used within a TransactionsProvider`);
|
|
||||||
}
|
|
||||||
return context[signature];
|
|
||||||
}
|
|
||||||
|
@ -12,6 +12,7 @@ code {
|
|||||||
|
|
||||||
.copyable {
|
.copyable {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
& > div:hover {
|
& > div:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -20,7 +21,6 @@ code {
|
|||||||
.popover.bs-popover-top {
|
.popover.bs-popover-top {
|
||||||
background-color: $dark;
|
background-color: $dark;
|
||||||
top: -4rem;
|
top: -4rem;
|
||||||
left: 40%;
|
|
||||||
|
|
||||||
.popover-body {
|
.popover-body {
|
||||||
color: white;
|
color: white;
|
||||||
@ -60,12 +60,14 @@ code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-signature, .text-address {
|
.text-signature,
|
||||||
|
.text-address {
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.text-signature, input.text-address {
|
input.text-signature,
|
||||||
padding: 0 0.75rem
|
input.text-address {
|
||||||
|
padding: 0 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4.ix-pill {
|
h4.ix-pill {
|
||||||
@ -87,19 +89,22 @@ h4.slot-pill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:last-child {
|
.list-group-item:last-child {
|
||||||
&.ix-item, &.slot-item {
|
&.ix-item,
|
||||||
|
&.slot-item {
|
||||||
border-bottom-width: 0px;
|
border-bottom-width: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group:last-child .list-group-item:last-child {
|
.list-group:last-child .list-group-item:last-child {
|
||||||
&.ix-item, &.slot-item {
|
&.ix-item,
|
||||||
|
&.slot-item {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:first-child {
|
.list-group-item:first-child {
|
||||||
&.ix-item, &.slot-item {
|
&.ix-item,
|
||||||
|
&.slot-item {
|
||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
|
||||||
|
|
||||||
export function assertUnreachable(x: never): never {
|
export function assertUnreachable(x: never): never {
|
||||||
throw new Error("Unreachable!");
|
throw new Error("Unreachable!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lamportsToSolString(lamports: number): string {
|
||||||
|
return `◎${(1.0 * Math.abs(lamports)) / LAMPORTS_PER_SOL}`;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user