Skip to content

Commit

Permalink
Add signature input to tx table (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
jstarry authored and mvines committed Jun 12, 2020
1 parent bc2e257 commit e82ad07
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 28 deletions.
8 changes: 8 additions & 0 deletions explorer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/bs58": "^4.0.1",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"bootstrap": "^4.4.1",
"bs58": "^4.0.1",
"node-sass": "^4.13.1",
"prettier": "^1.19.1",
"react": "^16.13.0",
Expand Down
3 changes: 3 additions & 0 deletions explorer/src/components/NetworkModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
NETWORKS,
Network
} from "../providers/network";
import { assertUnreachable } from "../utils";

type Props = {
show: boolean;
Expand Down Expand Up @@ -103,6 +104,8 @@ function NetworkToggle() {
case NetworkStatus.Failure:
activeSuffix = "danger";
break;
default:
assertUnreachable(status);
}

return (
Expand Down
88 changes: 80 additions & 8 deletions explorer/src/components/TransactionsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import React from "react";
import {
useTransactions,
useTransactionsDispatch,
checkTransactionStatus,
ActionType,
Transaction,
Status
} from "../providers/transactions";
import bs58 from "bs58";
import { assertUnreachable } from "../utils";
import { useNetwork } from "../providers/network";

function TransactionsCard() {
const { transactions } = useTransactions();
const { transactions, idCounter } = useTransactions();
const dispatch = useTransactionsDispatch();
const signatureInput = React.useRef<HTMLInputElement>(null);
const [error, setError] = React.useState("");
const { url } = useNetwork();

const onNew = (signature: string) => {
if (signature.length === 0) return;
try {
const length = bs58.decode(signature).length;
if (length > 64) {
setError("Signature is too short");
return;
} else if (length < 64) {
setError("Signature is too short");
return;
}
} catch (err) {
setError(`${err}`);
return;
}

dispatch({ type: ActionType.InputSignature, signature });
checkTransactionStatus(dispatch, idCounter + 1, signature, url);

const inputEl = signatureInput.current;
if (inputEl) {
inputEl.value = "";
}
};

return (
<div className="card">
Expand All @@ -16,16 +51,46 @@ function TransactionsCard() {
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted text-center">
<span className="fe fe-hash"></span>
</th>
<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)
)}
<tr>
<td>
<span className="badge badge-soft-dark badge-pill">
{idCounter + 1}
</span>
</td>

<td>
<span className={`badge badge-soft-primary`}>New</span>
</td>
<td>
<input
type="text"
onInput={() => setError("")}
onKeyDown={e =>
e.keyCode === 13 && onNew(e.currentTarget.value)
}
onSubmit={e => onNew(e.currentTarget.value)}
ref={signatureInput}
className={`form-control text-signature text-monospace ${
error ? "is-invalid" : ""
}`}
placeholder="abcd..."
/>
{error ? <div className="invalid-feedback">{error}</div> : null}
</td>
<td>-</td>
<td>-</td>
</tr>
{transactions.map(transaction => renderTransactionRow(transaction))}
</tbody>
</table>
</div>
Expand Down Expand Up @@ -65,22 +130,29 @@ const renderTransactionRow = (transaction: Transaction) => {
statusClass = "danger";
statusText = "Failed";
break;
case Status.Pending:
case Status.Missing:
statusClass = "warning";
statusText = "Pending";
statusText = "Not Found";
break;
default:
return assertUnreachable(transaction.status);
}

return (
<tr key={transaction.signature}>
<td>
<span className="badge badge-soft-dark badge-pill">
{transaction.id}
</span>
</td>
<td>
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
</td>
<td>
<code>{transaction.signature}</code>
</td>
<td>TODO</td>
<td>TODO</td>
<td>-</td>
<td>-</td>
</tr>
);
};
Expand Down
2 changes: 1 addition & 1 deletion explorer/src/providers/network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function NetworkProvider({ children }: NetworkProviderProps) {
);
}

export function networkUrl(network: Network, customUrl: string) {
export function networkUrl(network: Network, customUrl: string): string {
switch (network) {
case Network.Devnet:
return DEVNET_URL;
Expand Down
80 changes: 61 additions & 19 deletions explorer/src/providers/transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ export enum Status {
CheckFailed,
Success,
Failure,
Pending
Missing
}

enum Source {
Url,
Input
}

export interface Transaction {
id: number;
status: Status;
recent: boolean;
source: Source;
signature: TransactionSignature;
}

Expand All @@ -24,20 +29,52 @@ interface State {
transactions: Transactions;
}

export enum ActionType {
UpdateStatus,
InputSignature
}

interface UpdateStatus {
type: ActionType.UpdateStatus;
id: number;
status: Status;
}

type Action = UpdateStatus;
interface InputSignature {
type: ActionType.InputSignature;
signature: TransactionSignature;
}

type Action = UpdateStatus | InputSignature;
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 };
switch (action.type) {
case ActionType.InputSignature: {
const idCounter = state.idCounter + 1;
const transactions = {
...state.transactions,
[idCounter]: {
id: idCounter,
status: Status.Checking,
source: Source.Input,
signature: action.signature
}
};
return { ...state, transactions, idCounter };
}
case ActionType.UpdateStatus: {
let transaction = state.transactions[action.id];
if (transaction) {
transaction = { ...transaction, status: action.status };
const transactions = {
...state.transactions,
[action.id]: transaction
};
return { ...state, transactions };
}
break;
}
}
return state;
}
Expand All @@ -51,7 +88,7 @@ function initState(): State {
transactions[id] = {
id,
status: Status.Checking,
recent: true,
source: Source.Url,
signature
};
return transactions;
Expand All @@ -72,9 +109,8 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {

// 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);
checkTransactionStatus(dispatch, tx.id, tx.signature, url);
});
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps

Expand All @@ -89,23 +125,24 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {

export async function checkTransactionStatus(
dispatch: Dispatch,
transaction: Transaction,
connection: Connection
id: number,
signature: TransactionSignature,
url: string
) {
const id = transaction.id;
dispatch({
type: ActionType.UpdateStatus,
status: Status.Checking,
id
});

let status;
try {
const signatureStatus = await connection.getSignatureStatus(
transaction.signature
const signatureStatus = await new Connection(url).getSignatureStatus(
signature
);

if (signatureStatus === null) {
status = Status.Pending;
status = Status.Missing;
} else if ("Ok" in signatureStatus) {
status = Status.Success;
} else {
Expand All @@ -115,7 +152,7 @@ export async function checkTransactionStatus(
console.error("Failed to check transaction status", error);
status = Status.CheckFailed;
}
dispatch({ status, id });
dispatch({ type: ActionType.UpdateStatus, status, id });
}

export function useTransactions() {
Expand All @@ -125,7 +162,12 @@ export function useTransactions() {
`useTransactions must be used within a TransactionsProvider`
);
}
return context;
return {
idCounter: context.idCounter,
transactions: Object.values(context.transactions).sort((a, b) =>
a.id <= b.id ? 1 : -1
)
};
}

export function useTransactionsDispatch() {
Expand Down
8 changes: 8 additions & 0 deletions explorer/src/scss/_solana.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,12 @@ code {
cursor: text;
}
}
}

.text-signature {
font-size: 85%;
}

input.text-signature {
padding: 0 0.75rem
}
4 changes: 4 additions & 0 deletions explorer/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export function findGetParameter(parameterName: string): string | null {
});
return result;
}

export function assertUnreachable(x: never): never {
throw new Error("Unreachable!");
}

0 comments on commit e82ad07

Please sign in to comment.