Add signature input to tx table (#9)

This commit is contained in:
Justin Starry 2020-03-19 22:31:05 +08:00 committed by Michael Vines
parent 82543886fb
commit 237b3ae025
8 changed files with 167 additions and 28 deletions

View File

@ -1595,6 +1595,14 @@
"@babel/types": "^7.3.0"
}
},
"@types/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==",
"requires": {
"base-x": "^3.0.6"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",

View File

@ -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",

View File

@ -9,6 +9,7 @@ import {
NETWORKS,
Network
} from "../providers/network";
import { assertUnreachable } from "../utils";
type Props = {
show: boolean;
@ -103,6 +104,8 @@ function NetworkToggle() {
case NetworkStatus.Failure:
activeSuffix = "danger";
break;
default:
assertUnreachable(status);
}
return (

View File

@ -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">
@ -16,6 +51,9 @@ 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>
@ -23,9 +61,36 @@ function TransactionsCard() {
</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>
@ -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>
);
};

View File

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

View File

@ -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;
}
@ -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;
}
@ -51,7 +88,7 @@ function initState(): State {
transactions[id] = {
id,
status: Status.Checking,
recent: true,
source: Source.Url,
signature
};
return transactions;
@ -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
@ -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 {
@ -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() {
@ -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() {

View File

@ -32,4 +32,12 @@ code {
cursor: text;
}
}
}
.text-signature {
font-size: 85%;
}
input.text-signature {
padding: 0 0.75rem
}

View File

@ -10,3 +10,7 @@ export function findGetParameter(parameterName: string): string | null {
});
return result;
}
export function assertUnreachable(x: never): never {
throw new Error("Unreachable!");
}