diff --git a/explorer/package-lock.json b/explorer/package-lock.json index 1223a9eb1b30f7..e2c2a87523f924 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -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", diff --git a/explorer/package.json b/explorer/package.json index 53abcc90e39f1a..12c79374911eed 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -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", diff --git a/explorer/src/components/NetworkModal.tsx b/explorer/src/components/NetworkModal.tsx index b86df65f07c9e9..cb0664478c9c22 100644 --- a/explorer/src/components/NetworkModal.tsx +++ b/explorer/src/components/NetworkModal.tsx @@ -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 ( diff --git a/explorer/src/components/TransactionsCard.tsx b/explorer/src/components/TransactionsCard.tsx index 7223d653f1f6b4..30188fdfce4bde 100644 --- a/explorer/src/components/TransactionsCard.tsx +++ b/explorer/src/components/TransactionsCard.tsx @@ -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(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 (
@@ -16,6 +51,9 @@ function TransactionsCard() { + @@ -23,9 +61,36 @@ function TransactionsCard() { - {Object.values(transactions).map(transaction => - renderTransactionRow(transaction) - )} + + + + + + + + + {transactions.map(transaction => renderTransactionRow(transaction))}
+ + Status Signature Confirmations
+ + {idCounter + 1} + + + New + + 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 ?
{error}
: null} +
--
@@ -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 ( + + + {transaction.id} + + {statusText} {transaction.signature} - TODO - TODO + - + - ); }; diff --git a/explorer/src/providers/network.tsx b/explorer/src/providers/network.tsx index 498da862e57935..e4f5e0ca7a892a 100644 --- a/explorer/src/providers/network.tsx +++ b/explorer/src/providers/network.tsx @@ -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; diff --git a/explorer/src/providers/transactions.tsx b/explorer/src/providers/transactions.tsx index 4aca742259128a..ada85231e95a18 100644 --- a/explorer/src/providers/transactions.tsx +++ b/explorer/src/providers/transactions.tsx @@ -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() { diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss index 57553e3d752089..995c85ab17ff44 100644 --- a/explorer/src/scss/_solana.scss +++ b/explorer/src/scss/_solana.scss @@ -32,4 +32,12 @@ code { cursor: text; } } +} + +.text-signature { + font-size: 85%; +} + +input.text-signature { + padding: 0 0.75rem } \ No newline at end of file diff --git a/explorer/src/utils.ts b/explorer/src/utils.ts index fc96ca2a2b6d0c..bb9fb4d9739244 100644 --- a/explorer/src/utils.ts +++ b/explorer/src/utils.ts @@ -10,3 +10,7 @@ export function findGetParameter(parameterName: string): string | null { }); return result; } + +export function assertUnreachable(x: never): never { + throw new Error("Unreachable!"); +}