Add transaction details modal

This commit is contained in:
Justin Starry 2020-04-01 19:40:27 +08:00 committed by Michael Vines
parent ef7be97540
commit 611f2ae957
5 changed files with 375 additions and 40 deletions

View File

@ -1,53 +1,71 @@
import React from "react";
import { ClusterProvider } from "./providers/cluster";
import { TransactionsProvider } from "./providers/transactions";
import {
TransactionsProvider,
useTransactionsDispatch,
useTransactions,
ActionType
} from "./providers/transactions";
import { AccountsProvider } from "./providers/accounts";
import { BlocksProvider } from "./providers/blocks";
import ClusterStatusButton from "./components/ClusterStatusButton";
import AccountsCard from "./components/AccountsCard";
import TransactionsCard from "./components/TransactionsCard";
import ClusterModal from "./components/ClusterModal";
import TransactionModal from "./components/TransactionModal";
import Logo from "./img/logos-solana/light-explorer-logo.svg";
function App() {
const [showModal, setShowModal] = React.useState(false);
const [showClusterModal, setShowClusterModal] = React.useState(false);
return (
<ClusterProvider>
<ClusterModal show={showModal} onClose={() => setShowModal(false)} />
<div className="main-content">
<div className="header">
<div className="container">
<div className="header-body">
<div className="row align-items-end">
<div className="col">
<img src={Logo} width="250" alt="Solana Explorer" />
<TransactionsProvider>
<BlocksProvider>
<ClusterModal
show={showClusterModal}
onClose={() => setShowClusterModal(false)}
/>
<TransactionModal />
<div className="main-content">
<div className="header">
<div className="container">
<div className="header-body">
<div className="row align-items-end">
<div className="col">
<img src={Logo} width="250" alt="Solana Explorer" />
</div>
<div className="col-auto">
<ClusterStatusButton
onClick={() => setShowClusterModal(true)}
/>
</div>
</div>
</div>
<div className="col-auto">
<ClusterStatusButton onClick={() => setShowModal(true)} />
</div>
</div>
<div className="container">
<div className="row">
<div className="col-12">
<TransactionsCard />
</div>
</div>
<div className="row">
<div className="col-12">
<AccountsProvider>
<AccountsCard />
</AccountsProvider>
</div>
</div>
</div>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-12">
<TransactionsProvider>
<TransactionsCard />
</TransactionsProvider>
</div>
</div>
<div className="row">
<div className="col-12">
<AccountsProvider>
<AccountsCard />
</AccountsProvider>
</div>
</div>
</div>
</div>
<Overlay show={showModal} onClick={() => setShowModal(false)} />
<Overlay
show={showClusterModal}
onClick={() => setShowClusterModal(false)}
/>
</BlocksProvider>
</TransactionsProvider>
</ClusterProvider>
);
}
@ -58,8 +76,19 @@ type OverlayProps = {
};
function Overlay({ show, onClick }: OverlayProps) {
if (show)
return <div className="modal-backdrop fade show" onClick={onClick}></div>;
const { selected } = useTransactions();
const dispatch = useTransactionsDispatch();
if (show || !!selected)
return (
<div
className="modal-backdrop fade show"
onClick={() => {
onClick();
dispatch({ type: ActionType.Deselect });
}}
></div>
);
return <div className="fade"></div>;
}

View File

@ -0,0 +1,60 @@
import React from "react";
import {
useTransactions,
useTransactionsDispatch,
ActionType,
Selected
} from "../providers/transactions";
import { useBlocks } from "../providers/blocks";
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-center">
<div className="modal-content">
<div className="modal-body" onClick={e => e.stopPropagation()}>
<span className="close" onClick={onClose}>
&times;
</span>
<h2 className="text-center mb-4 mt-4">Transaction Details</h2>
<TransactionDetails selected={selected} />
</div>
</div>
</div>
);
};
return (
<div
className={`modal fade fixed-right${show ? " show" : ""}`}
onClick={onClose}
>
{renderContent()}
</div>
);
}
function TransactionDetails({ selected }: { selected: Selected }) {
const { blocks } = useBlocks();
const block = blocks[selected.slot];
if (!block) return <span>{"block not found"}</span>;
if (!block.transactions) {
return <span>loading</span>;
}
const tx = block.transactions[selected.signature];
if (!tx) return <span>{"sig not found"}</span>;
return <code>{JSON.stringify(tx)}</code>;
}
export default TransactionModal;

View File

@ -58,6 +58,7 @@ function TransactionsCard() {
<th className="text-muted">Signature</th>
<th className="text-muted">Confirmations</th>
<th className="text-muted">Slot Number</th>
<th className="text-muted">Details</th>
</tr>
</thead>
<tbody className="list">
@ -88,8 +89,11 @@ function TransactionsCard() {
</td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
{transactions.map(transaction => renderTransactionRow(transaction))}
{transactions.map(transaction =>
renderTransactionRow(transaction, dispatch, url)
)}
</tbody>
</table>
</div>
@ -109,7 +113,11 @@ const renderHeader = () => {
);
};
const renderTransactionRow = (transaction: Transaction) => {
const renderTransactionRow = (
transaction: Transaction,
dispatch: any,
url: string
) => {
let statusText;
let statusClass;
switch (transaction.status) {
@ -140,6 +148,32 @@ const renderTransactionRow = (transaction: Transaction) => {
const slotText = `${transaction.slot || "-"}`;
const confirmationsText = `${transaction.confirmations || "-"}`;
const renderDetails = () => {
let onClick, icon;
if (transaction.confirmations === "max") {
icon = "more-horizontal";
onClick = () =>
dispatch({
type: ActionType.Select,
signature: transaction.signature
});
} else {
icon = "refresh-cw";
onClick = () => {
checkTransactionStatus(dispatch, transaction.signature, url);
};
}
return (
<button
className="btn btn-rounded-circle btn-white btn-sm"
onClick={onClick}
>
<span className={`fe fe-${icon}`}></span>
</button>
);
};
return (
<tr key={transaction.signature}>
<td>
@ -155,6 +189,7 @@ const renderTransactionRow = (transaction: Transaction) => {
</td>
<td className="text-uppercase">{confirmationsText}</td>
<td>{slotText}</td>
<td>{renderDetails()}</td>
</tr>
);
};

View File

@ -0,0 +1,176 @@
import React from "react";
import bs58 from "bs58";
import { Connection, Transaction } from "@solana/web3.js";
import { useCluster, ClusterStatus } from "./cluster";
import { useTransactions } from "./transactions";
export enum Status {
Checking,
CheckFailed,
Success
}
type Transactions = { [signature: string]: Transaction };
export interface Block {
status: Status;
transactions?: Transactions;
}
export type Blocks = { [slot: number]: Block };
interface State {
blocks: Blocks;
}
export enum ActionType {
Update,
Add,
Remove
}
interface Update {
type: ActionType.Update;
slot: number;
status: Status;
transactions?: Transactions;
}
interface Add {
type: ActionType.Add;
slots: number[];
}
interface Remove {
type: ActionType.Remove;
slots: number[];
}
type Action = Update | Add | Remove;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Add: {
if (action.slots.length === 0) return state;
const blocks = { ...state.blocks };
action.slots.forEach(slot => {
if (!blocks[slot]) {
blocks[slot] = {
status: Status.Checking
};
}
});
return { ...state, blocks };
}
case ActionType.Remove: {
if (action.slots.length === 0) return state;
const blocks = { ...state.blocks };
action.slots.forEach(slot => {
delete blocks[slot];
});
return { ...state, blocks };
}
case ActionType.Update: {
let block = state.blocks[action.slot];
if (block) {
block = {
...block,
status: action.status,
transactions: action.transactions
};
const blocks = {
...state.blocks,
[action.slot]: block
};
return { ...state, blocks };
}
break;
}
}
return state;
}
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type BlocksProviderProps = { children: React.ReactNode };
export function BlocksProvider({ children }: BlocksProviderProps) {
const [state, dispatch] = React.useReducer(reducer, { blocks: {} });
const { transactions } = useTransactions();
const { status, url } = useCluster();
// Filter blocks for current transaction slots
React.useEffect(() => {
if (status !== ClusterStatus.Connected) return;
const remove: number[] = [];
const txSlots = transactions
.map(tx => tx.slot)
.filter(x => x)
.reduce((set, slot) => set.add(slot), new Set());
Object.keys(state.blocks).forEach(blockKey => {
const slot = parseInt(blockKey);
if (!txSlots.has(slot)) {
remove.push(slot);
}
});
dispatch({ type: ActionType.Remove, slots: remove });
const fetchSlots = new Set<number>();
transactions.forEach(tx => {
if (tx.slot && tx.confirmations === "max" && !state.blocks[tx.slot])
fetchSlots.add(tx.slot);
});
const fetchList: number[] = [];
fetchSlots.forEach(s => fetchList.push(s));
dispatch({ type: ActionType.Add, slots: fetchList });
fetchSlots.forEach(slot => {
fetchBlock(dispatch, slot, url);
});
}, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
async function fetchBlock(dispatch: Dispatch, slot: number, url: string) {
dispatch({
type: ActionType.Update,
status: Status.Checking,
slot
});
let status;
let transactions: Transactions = {};
try {
const block = await new Connection(url).getConfirmedBlock(slot);
block.transactions.forEach(({ transaction }) => {
const signature = transaction.signature;
if (signature) {
const sig = bs58.encode(signature);
transactions[sig] = transaction;
}
});
status = Status.Success;
} catch (error) {
console.error("Failed to fetch confirmed block", error);
status = Status.CheckFailed;
}
dispatch({ type: ActionType.Update, status, slot, transactions });
}
export function useBlocks() {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useBlocks must be used within a BlocksProvider`);
}
return context;
}

View File

@ -27,15 +27,32 @@ export interface Transaction {
signature: TransactionSignature;
}
export interface Selected {
slot: number;
signature: TransactionSignature;
}
type Transactions = { [signature: string]: Transaction };
interface State {
idCounter: number;
selected?: Selected;
transactions: Transactions;
}
export enum ActionType {
UpdateStatus,
InputSignature
InputSignature,
Select,
Deselect
}
interface SelectTransaction {
type: ActionType.Select;
signature: TransactionSignature;
}
interface DeselectTransaction {
type: ActionType.Deselect;
}
interface UpdateStatus {
@ -51,11 +68,27 @@ interface InputSignature {
signature: TransactionSignature;
}
type Action = UpdateStatus | InputSignature;
type Action =
| UpdateStatus
| InputSignature
| SelectTransaction
| DeselectTransaction;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Deselect: {
return { ...state, selected: undefined };
}
case ActionType.Select: {
const tx = state.transactions[action.signature];
if (!tx.slot) return state;
const selected = {
slot: tx.slot,
signature: tx.signature
};
return { ...state, selected };
}
case ActionType.InputSignature: {
if (!!state.transactions[action.signature]) return state;
@ -101,8 +134,9 @@ function urlSignatures(): Array<string> {
.concat(findGetParameter("txns")?.split(",") || [])
.concat(findGetParameter("transaction")?.split(",") || [])
.concat(findGetParameter("transactions")?.split(",") || [])
.concat(findPathSegment("transaction")?.split(",") || [])
.concat(findPathSegment("transactions")?.split(",") || []);
.concat(findPathSegment("tx")?.split(",") || [])
.concat(findPathSegment("txn")?.split(",") || [])
.concat(findPathSegment("transaction")?.split(",") || []);
}
function initState(): State {
@ -227,6 +261,7 @@ export function useTransactions() {
}
return {
idCounter: context.idCounter,
selected: context.selected,
transactions: Object.values(context.transactions).sort((a, b) =>
a.id <= b.id ? 1 : -1
)