Add transaction card component and provider (#5)
This commit is contained in:
parent
de1df895a0
commit
6006c30ace
|
@ -1,12 +1,15 @@
|
|||
import React from "react";
|
||||
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() {
|
||||
return (
|
||||
<NetworkProvider>
|
||||
<div className="main-content">
|
||||
<div className="header">
|
||||
<div className="container-fluid">
|
||||
<div className="container">
|
||||
<div className="header-body">
|
||||
<div className="row align-items-end">
|
||||
<div className="col">
|
||||
|
@ -14,15 +17,24 @@ function App() {
|
|||
<h1 className="header-title">Solana Explorer</h1>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<NetworkProvider>
|
||||
<NetworkStatusButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<TransactionsProvider>
|
||||
<TransactionsCard />
|
||||
</TransactionsProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NetworkProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,22 +6,22 @@ function NetworkStatusButton() {
|
|||
|
||||
switch (status) {
|
||||
case NetworkStatus.Connected:
|
||||
return <a className="btn btn-primary lift">{url}</a>;
|
||||
return <span className="btn btn-white lift">{url}</span>;
|
||||
|
||||
case NetworkStatus.Connecting:
|
||||
return (
|
||||
<a className="btn btn-warning lift">
|
||||
<span className="btn btn-warning lift">
|
||||
{"Connecting "}
|
||||
<span
|
||||
className="spinner-grow spinner-grow-sm text-dark"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
|
||||
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
|
||||
// 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