Skip to content

Commit

Permalink
Add transaction card component and provider (solana-labs#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
jstarry authored and mvines committed Jun 12, 2020
1 parent 0cae2b1 commit 056cb0e
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 18 deletions.
40 changes: 26 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
}

Expand Down
88 changes: 88 additions & 0 deletions src/components/TransactionsCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
139 changes: 139 additions & 0 deletions src/providers/transactions.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions src/scss/_solana.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit 056cb0e

Please sign in to comment.