Add transaction card component and provider (#5)
This commit is contained in:
parent
de1df895a0
commit
6006c30ace
|
@ -1,28 +1,40 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { NetworkProvider } from "./providers/network";
|
import { NetworkProvider } from "./providers/network";
|
||||||
import NetworkStatusButton from "./components/networkStatusButton";
|
import { TransactionsProvider } from "./providers/transactions";
|
||||||
|
import NetworkStatusButton from "./components/NetworkStatusButton";
|
||||||
|
import TransactionsCard from "./components/TransactionsCard";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<NetworkProvider>
|
||||||
<div className="header">
|
<div className="main-content">
|
||||||
<div className="container-fluid">
|
<div className="header">
|
||||||
<div className="header-body">
|
<div className="container">
|
||||||
<div className="row align-items-end">
|
<div className="header-body">
|
||||||
<div className="col">
|
<div className="row align-items-end">
|
||||||
<h6 className="header-pretitle">Beta</h6>
|
<div className="col">
|
||||||
<h1 className="header-title">Solana Explorer</h1>
|
<h6 className="header-pretitle">Beta</h6>
|
||||||
</div>
|
<h1 className="header-title">Solana Explorer</h1>
|
||||||
<div className="col-auto">
|
</div>
|
||||||
<NetworkProvider>
|
<div className="col-auto">
|
||||||
<NetworkStatusButton />
|
<NetworkStatusButton />
|
||||||
</NetworkProvider>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12">
|
||||||
|
<TransactionsProvider>
|
||||||
|
<TransactionsCard />
|
||||||
|
</TransactionsProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NetworkProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,22 +6,22 @@ function NetworkStatusButton() {
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NetworkStatus.Connected:
|
case NetworkStatus.Connected:
|
||||||
return <a className="btn btn-primary lift">{url}</a>;
|
return <span className="btn btn-white lift">{url}</span>;
|
||||||
|
|
||||||
case NetworkStatus.Connecting:
|
case NetworkStatus.Connecting:
|
||||||
return (
|
return (
|
||||||
<a className="btn btn-warning lift">
|
<span className="btn btn-warning lift">
|
||||||
{"Connecting "}
|
{"Connecting "}
|
||||||
<span
|
<span
|
||||||
className="spinner-grow spinner-grow-sm text-dark"
|
className="spinner-grow spinner-grow-sm text-dark"
|
||||||
role="status"
|
role="status"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></span>
|
></span>
|
||||||
</a>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
case NetworkStatus.Failure:
|
case NetworkStatus.Failure:
|
||||||
return <a className="btn btn-danger lift">Disconnected</a>;
|
return <span className="btn btn-danger lift">Disconnected</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
useTransactions,
|
||||||
|
Transaction,
|
||||||
|
Status
|
||||||
|
} from "../providers/transactions";
|
||||||
|
|
||||||
|
function TransactionsCard() {
|
||||||
|
const { transactions } = useTransactions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
{renderHeader()}
|
||||||
|
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted">Status</th>
|
||||||
|
<th className="text-muted">Signature</th>
|
||||||
|
<th className="text-muted">Confirmations</th>
|
||||||
|
<th className="text-muted">Slot Number</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">
|
||||||
|
{Object.values(transactions).map(transaction =>
|
||||||
|
renderTransactionRow(transaction)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
return (
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h4 className="card-header-title">Transactions</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTransactionRow = (transaction: Transaction) => {
|
||||||
|
let statusText;
|
||||||
|
let statusClass;
|
||||||
|
switch (transaction.status) {
|
||||||
|
case Status.CheckFailed:
|
||||||
|
statusClass = "dark";
|
||||||
|
statusText = "Network Error";
|
||||||
|
break;
|
||||||
|
case Status.Checking:
|
||||||
|
statusClass = "info";
|
||||||
|
statusText = "Checking";
|
||||||
|
break;
|
||||||
|
case Status.Success:
|
||||||
|
statusClass = "success";
|
||||||
|
statusText = "Success";
|
||||||
|
break;
|
||||||
|
case Status.Failure:
|
||||||
|
statusClass = "danger";
|
||||||
|
statusText = "Failed";
|
||||||
|
break;
|
||||||
|
case Status.Pending:
|
||||||
|
statusClass = "warning";
|
||||||
|
statusText = "Pending";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={transaction.signature}>
|
||||||
|
<td>
|
||||||
|
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>{transaction.signature}</code>
|
||||||
|
</td>
|
||||||
|
<td>TODO</td>
|
||||||
|
<td>TODO</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransactionsCard;
|
|
@ -0,0 +1,139 @@
|
||||||
|
import React from "react";
|
||||||
|
import { TransactionSignature, Connection } from "@solana/web3.js";
|
||||||
|
import { findGetParameter } from "../utils";
|
||||||
|
import { useNetwork } from "../providers/network";
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
Checking,
|
||||||
|
CheckFailed,
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
Pending
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id: number;
|
||||||
|
status: Status;
|
||||||
|
recent: boolean;
|
||||||
|
signature: TransactionSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transactions = { [id: number]: Transaction };
|
||||||
|
interface State {
|
||||||
|
idCounter: number;
|
||||||
|
transactions: Transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateStatus {
|
||||||
|
id: number;
|
||||||
|
status: Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = UpdateStatus;
|
||||||
|
type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
|
function reducer(state: State, action: Action): State {
|
||||||
|
let transaction = state.transactions[action.id];
|
||||||
|
if (transaction) {
|
||||||
|
transaction = { ...transaction, status: action.status };
|
||||||
|
const transactions = { ...state.transactions, [action.id]: transaction };
|
||||||
|
return { ...state, transactions };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initState(): State {
|
||||||
|
let idCounter = 0;
|
||||||
|
const signatures = findGetParameter("txs")?.split(",") || [];
|
||||||
|
const transactions = signatures.reduce(
|
||||||
|
(transactions: Transactions, signature) => {
|
||||||
|
const id = ++idCounter;
|
||||||
|
transactions[id] = {
|
||||||
|
id,
|
||||||
|
status: Status.Checking,
|
||||||
|
recent: true,
|
||||||
|
signature
|
||||||
|
};
|
||||||
|
return transactions;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return { idCounter, transactions };
|
||||||
|
}
|
||||||
|
|
||||||
|
const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
|
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||||
|
|
||||||
|
type TransactionsProviderProps = { children: React.ReactNode };
|
||||||
|
export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
||||||
|
const [state, dispatch] = React.useReducer(reducer, undefined, initState);
|
||||||
|
|
||||||
|
const { status, url } = useNetwork();
|
||||||
|
|
||||||
|
// Check transaction statuses on startup and whenever network updates
|
||||||
|
React.useEffect(() => {
|
||||||
|
const connection = new Connection(url);
|
||||||
|
Object.values(state.transactions).forEach(tx => {
|
||||||
|
checkTransactionStatus(dispatch, tx, connection);
|
||||||
|
});
|
||||||
|
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateContext.Provider value={state}>
|
||||||
|
<DispatchContext.Provider value={dispatch}>
|
||||||
|
{children}
|
||||||
|
</DispatchContext.Provider>
|
||||||
|
</StateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkTransactionStatus(
|
||||||
|
dispatch: Dispatch,
|
||||||
|
transaction: Transaction,
|
||||||
|
connection: Connection
|
||||||
|
) {
|
||||||
|
const id = transaction.id;
|
||||||
|
dispatch({
|
||||||
|
status: Status.Checking,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
|
||||||
|
let status;
|
||||||
|
try {
|
||||||
|
const signatureStatus = await connection.getSignatureStatus(
|
||||||
|
transaction.signature
|
||||||
|
);
|
||||||
|
|
||||||
|
if (signatureStatus === null) {
|
||||||
|
status = Status.Pending;
|
||||||
|
} else if ("Ok" in signatureStatus) {
|
||||||
|
status = Status.Success;
|
||||||
|
} else {
|
||||||
|
status = Status.Failure;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check transaction status", error);
|
||||||
|
status = Status.CheckFailed;
|
||||||
|
}
|
||||||
|
dispatch({ status, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTransactions() {
|
||||||
|
const context = React.useContext(StateContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
`useTransactions must be used within a TransactionsProvider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTransactionsDispatch() {
|
||||||
|
const context = React.useContext(DispatchContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
`useTransactionsDispatch must be used within a TransactionsProvider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
|
@ -2,3 +2,10 @@
|
||||||
// solana.scss
|
// solana.scss
|
||||||
// Use this to write your custom SCSS
|
// Use this to write your custom SCSS
|
||||||
//
|
//
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0.33rem;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
background-color: $gray-200;
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue