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" "@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": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "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/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
"@types/bs58": "^4.0.1",
"@types/jest": "^24.0.0", "@types/jest": "^24.0.0",
"@types/node": "^12.0.0", "@types/node": "^12.0.0",
"@types/react": "^16.9.0", "@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0", "@types/react-dom": "^16.9.0",
"bootstrap": "^4.4.1", "bootstrap": "^4.4.1",
"bs58": "^4.0.1",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"react": "^16.13.0", "react": "^16.13.0",

View File

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

View File

@ -1,12 +1,47 @@
import React from "react"; import React from "react";
import { import {
useTransactions, useTransactions,
useTransactionsDispatch,
checkTransactionStatus,
ActionType,
Transaction, Transaction,
Status Status
} from "../providers/transactions"; } from "../providers/transactions";
import bs58 from "bs58";
import { assertUnreachable } from "../utils";
import { useNetwork } from "../providers/network";
function TransactionsCard() { 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 ( return (
<div className="card"> <div className="card">
@ -16,6 +51,9 @@ function TransactionsCard() {
<table className="table table-sm table-nowrap card-table"> <table className="table table-sm table-nowrap card-table">
<thead> <thead>
<tr> <tr>
<th className="text-muted text-center">
<span className="fe fe-hash"></span>
</th>
<th className="text-muted">Status</th> <th className="text-muted">Status</th>
<th className="text-muted">Signature</th> <th className="text-muted">Signature</th>
<th className="text-muted">Confirmations</th> <th className="text-muted">Confirmations</th>
@ -23,9 +61,36 @@ function TransactionsCard() {
</tr> </tr>
</thead> </thead>
<tbody className="list"> <tbody className="list">
{Object.values(transactions).map(transaction => <tr>
renderTransactionRow(transaction) <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> </tbody>
</table> </table>
</div> </div>
@ -65,22 +130,29 @@ const renderTransactionRow = (transaction: Transaction) => {
statusClass = "danger"; statusClass = "danger";
statusText = "Failed"; statusText = "Failed";
break; break;
case Status.Pending: case Status.Missing:
statusClass = "warning"; statusClass = "warning";
statusText = "Pending"; statusText = "Not Found";
break; break;
default:
return assertUnreachable(transaction.status);
} }
return ( return (
<tr key={transaction.signature}> <tr key={transaction.signature}>
<td>
<span className="badge badge-soft-dark badge-pill">
{transaction.id}
</span>
</td>
<td> <td>
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span> <span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
</td> </td>
<td> <td>
<code>{transaction.signature}</code> <code>{transaction.signature}</code>
</td> </td>
<td>TODO</td> <td>-</td>
<td>TODO</td> <td>-</td>
</tr> </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) { switch (network) {
case Network.Devnet: case Network.Devnet:
return DEVNET_URL; return DEVNET_URL;

View File

@ -8,13 +8,18 @@ export enum Status {
CheckFailed, CheckFailed,
Success, Success,
Failure, Failure,
Pending Missing
}
enum Source {
Url,
Input
} }
export interface Transaction { export interface Transaction {
id: number; id: number;
status: Status; status: Status;
recent: boolean; source: Source;
signature: TransactionSignature; signature: TransactionSignature;
} }
@ -24,20 +29,52 @@ interface State {
transactions: Transactions; transactions: Transactions;
} }
export enum ActionType {
UpdateStatus,
InputSignature
}
interface UpdateStatus { interface UpdateStatus {
type: ActionType.UpdateStatus;
id: number; id: number;
status: Status; status: Status;
} }
type Action = UpdateStatus; interface InputSignature {
type: ActionType.InputSignature;
signature: TransactionSignature;
}
type Action = UpdateStatus | InputSignature;
type Dispatch = (action: Action) => void; type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State { function reducer(state: State, action: Action): State {
let transaction = state.transactions[action.id]; switch (action.type) {
if (transaction) { case ActionType.InputSignature: {
transaction = { ...transaction, status: action.status }; const idCounter = state.idCounter + 1;
const transactions = { ...state.transactions, [action.id]: transaction }; const transactions = {
return { ...state, 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; return state;
} }
@ -51,7 +88,7 @@ function initState(): State {
transactions[id] = { transactions[id] = {
id, id,
status: Status.Checking, status: Status.Checking,
recent: true, source: Source.Url,
signature signature
}; };
return transactions; return transactions;
@ -72,9 +109,8 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
// Check transaction statuses on startup and whenever network updates // Check transaction statuses on startup and whenever network updates
React.useEffect(() => { React.useEffect(() => {
const connection = new Connection(url);
Object.values(state.transactions).forEach(tx => { 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 }, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
@ -89,23 +125,24 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
export async function checkTransactionStatus( export async function checkTransactionStatus(
dispatch: Dispatch, dispatch: Dispatch,
transaction: Transaction, id: number,
connection: Connection signature: TransactionSignature,
url: string
) { ) {
const id = transaction.id;
dispatch({ dispatch({
type: ActionType.UpdateStatus,
status: Status.Checking, status: Status.Checking,
id id
}); });
let status; let status;
try { try {
const signatureStatus = await connection.getSignatureStatus( const signatureStatus = await new Connection(url).getSignatureStatus(
transaction.signature signature
); );
if (signatureStatus === null) { if (signatureStatus === null) {
status = Status.Pending; status = Status.Missing;
} else if ("Ok" in signatureStatus) { } else if ("Ok" in signatureStatus) {
status = Status.Success; status = Status.Success;
} else { } else {
@ -115,7 +152,7 @@ export async function checkTransactionStatus(
console.error("Failed to check transaction status", error); console.error("Failed to check transaction status", error);
status = Status.CheckFailed; status = Status.CheckFailed;
} }
dispatch({ status, id }); dispatch({ type: ActionType.UpdateStatus, status, id });
} }
export function useTransactions() { export function useTransactions() {
@ -125,7 +162,12 @@ export function useTransactions() {
`useTransactions must be used within a TransactionsProvider` `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() { export function useTransactionsDispatch() {

View File

@ -32,4 +32,12 @@ code {
cursor: text; 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; return result;
} }
export function assertUnreachable(x: never): never {
throw new Error("Unreachable!");
}