Add transaction card component and provider (#5)

This commit is contained in:
Justin Starry 2020-03-17 12:04:04 +08:00 committed by Michael Vines
parent de1df895a0
commit 6006c30ace
5 changed files with 264 additions and 18 deletions

View File

@ -1,28 +1,40 @@
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 (
<div className="main-content">
<div className="header">
<div className="container-fluid">
<div className="header-body">
<div className="row align-items-end">
<div className="col">
<h6 className="header-pretitle">Beta</h6>
<h1 className="header-title">Solana Explorer</h1>
</div>
<div className="col-auto">
<NetworkProvider>
<NetworkProvider>
<div className="main-content">
<div className="header">
<div className="container">
<div className="header-body">
<div className="row align-items-end">
<div className="col">
<h6 className="header-pretitle">Beta</h6>
<h1 className="header-title">Solana Explorer</h1>
</div>
<div className="col-auto">
<NetworkStatusButton />
</NetworkProvider>
</div>
</div>
</div>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-12">
<TransactionsProvider>
<TransactionsCard />
</TransactionsProvider>
</div>
</div>
</div>
</div>
</div>
</NetworkProvider>
);
}

View File

@ -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>;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}