Add transaction details page

This commit is contained in:
Justin Starry
2020-04-29 20:48:38 +08:00
committed by Michael Vines
parent 9f26cbbbeb
commit 484a4db626
14 changed files with 805 additions and 524 deletions

View File

@@ -6,7 +6,7 @@ import {
Account,
SignatureResult
} from "@solana/web3.js";
import { findGetParameter, findPathSegment } from "../../utils/url";
import { findGetParameter } from "../../utils/url";
import { useCluster, ClusterStatus } from "../cluster";
import {
DetailsProvider,
@@ -20,6 +20,7 @@ import {
Dispatch as AccountsDispatch,
ActionType as AccountsActionType
} from "../accounts";
import { useLocation } from "react-router-dom";
export enum FetchStatus {
Fetching,
@@ -27,28 +28,29 @@ export enum FetchStatus {
Fetched
}
enum Source {
export enum Source {
Url,
Input
Input,
Test
}
export type Confirmations = number | "max";
export interface TransactionStatus {
export interface TransactionStatusInfo {
slot: number;
result: SignatureResult;
confirmations: Confirmations;
}
export interface TransactionState {
export interface TransactionStatus {
id: number;
source: Source;
fetchStatus: FetchStatus;
signature: TransactionSignature;
transactionStatus?: TransactionStatus;
info?: TransactionStatusInfo;
}
type Transactions = { [signature: string]: TransactionState };
type Transactions = { [signature: string]: TransactionStatus };
interface State {
idCounter: number;
selected?: TransactionSignature;
@@ -57,50 +59,29 @@ interface State {
export enum ActionType {
UpdateStatus,
InputSignature,
Select,
Deselect
}
interface SelectTransaction {
type: ActionType.Select;
signature: TransactionSignature;
}
interface DeselectTransaction {
type: ActionType.Deselect;
FetchSignature
}
interface UpdateStatus {
type: ActionType.UpdateStatus;
signature: TransactionSignature;
fetchStatus: FetchStatus;
transactionStatus?: TransactionStatus;
info?: TransactionStatusInfo;
}
interface InputSignature {
type: ActionType.InputSignature;
interface FetchSignature {
type: ActionType.FetchSignature;
signature: TransactionSignature;
source: Source;
}
type Action =
| UpdateStatus
| InputSignature
| SelectTransaction
| DeselectTransaction;
type Action = UpdateStatus | FetchSignature;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Deselect: {
return { ...state, selected: undefined };
}
case ActionType.Select: {
const tx = state.transactions[action.signature];
return { ...state, selected: tx.signature };
}
case ActionType.InputSignature: {
case ActionType.FetchSignature: {
if (!!state.transactions[action.signature]) return state;
const nextId = state.idCounter + 1;
@@ -108,7 +89,7 @@ function reducer(state: State, action: Action): State {
...state.transactions,
[action.signature]: {
id: nextId,
source: Source.Input,
source: action.source,
signature: action.signature,
fetchStatus: FetchStatus.Fetching
}
@@ -120,11 +101,9 @@ function reducer(state: State, action: Action): State {
if (transaction) {
transaction = {
...transaction,
fetchStatus: action.fetchStatus
fetchStatus: action.fetchStatus,
info: action.info
};
if (action.transactionStatus) {
transaction.transactionStatus = action.transactionStatus;
}
const transactions = {
...state.transactions,
[action.signature]: transaction
@@ -137,74 +116,60 @@ function reducer(state: State, action: Action): State {
return state;
}
export const TX_PATHS = [
"/tx",
"/txs",
"/txn",
"/txns",
"/transaction",
"/transactions"
];
function urlSignatures(): Array<string> {
const signatures: Array<string> = [];
TX_PATHS.forEach(path => {
const name = path.slice(1);
const params = findGetParameter(name)?.split(",") || [];
const segments = findPathSegment(name)?.split(",") || [];
signatures.push(...params);
signatures.push(...segments);
});
return signatures.filter(s => s.length > 0);
}
function initState(): State {
let idCounter = 0;
const signatures = urlSignatures();
const transactions = signatures.reduce(
(transactions: Transactions, signature) => {
if (!!transactions[signature]) return transactions;
const nextId = idCounter + 1;
transactions[signature] = {
id: nextId,
source: Source.Url,
signature,
fetchStatus: FetchStatus.Fetching
};
idCounter++;
return transactions;
},
{}
);
return { idCounter, transactions };
}
export const TX_ALIASES = ["tx", "txn", "transaction"];
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 [state, dispatch] = React.useReducer(reducer, {
idCounter: 0,
transactions: {}
});
const { status, url } = useCluster();
const accountsDispatch = useAccountsDispatch();
const search = useLocation().search;
// Check transaction statuses on startup and whenever cluster updates
// Check transaction statuses whenever cluster updates
React.useEffect(() => {
if (status !== ClusterStatus.Connected) return;
Object.keys(state.transactions).forEach(signature => {
dispatch({
type: ActionType.FetchSignature,
signature,
source: Source.Url
});
checkTransactionStatus(dispatch, signature, url);
});
// Create a test transaction
if (findGetParameter("test") !== null) {
createTestTransaction(dispatch, accountsDispatch, url);
}
Object.keys(state.transactions).forEach(signature => {
checkTransactionStatus(dispatch, signature, url);
});
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
// Check for transactions in the url params
React.useEffect(() => {
TX_ALIASES.flatMap(key =>
(findGetParameter(key)?.split(",") || []).concat(
findGetParameter(key + "s")?.split(",") || []
)
)
.flatMap(paramValue => paramValue?.split(",") || [])
.filter(signature => !state.transactions[signature])
.forEach(signature => {
dispatch({
type: ActionType.FetchSignature,
signature,
source: Source.Url
});
checkTransactionStatus(dispatch, signature, url);
});
}, [search]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
@@ -232,7 +197,11 @@ async function createTestTransaction(
100000,
"recent"
);
dispatch({ type: ActionType.InputSignature, signature });
dispatch({
type: ActionType.FetchSignature,
signature,
source: Source.Test
});
checkTransactionStatus(dispatch, signature, url);
accountsDispatch({
type: AccountsActionType.Input,
@@ -251,7 +220,11 @@ async function createTestTransaction(
lamports: 1
});
const signature = await connection.sendTransaction(tx, testAccount);
dispatch({ type: ActionType.InputSignature, signature });
dispatch({
type: ActionType.FetchSignature,
signature,
source: Source.Test
});
checkTransactionStatus(dispatch, signature, url);
} catch (error) {
console.error("Failed to create test failure transaction", error);
@@ -270,7 +243,7 @@ export async function checkTransactionStatus(
});
let fetchStatus;
let transactionStatus: TransactionStatus | undefined;
let info: TransactionStatusInfo | undefined;
try {
const { value } = await new Connection(url).getSignatureStatus(signature, {
searchTransactionHistory: true
@@ -284,7 +257,7 @@ export async function checkTransactionStatus(
confirmations = "max";
}
transactionStatus = {
info = {
slot: value.slot,
confirmations,
result: { err: value.err }
@@ -295,11 +268,12 @@ export async function checkTransactionStatus(
console.error("Failed to fetch transaction status", error);
fetchStatus = FetchStatus.FetchFailed;
}
dispatch({
type: ActionType.UpdateStatus,
signature,
fetchStatus,
transactionStatus
info
});
}
@@ -312,13 +286,36 @@ export function useTransactions() {
}
return {
idCounter: context.idCounter,
selected: context.selected,
transactions: Object.values(context.transactions).sort((a, b) =>
a.id <= b.id ? 1 : -1
)
};
}
export function useTransactionStatus(signature: TransactionSignature) {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(
`useTransactionStatus must be used within a TransactionsProvider`
);
}
return context.transactions[signature];
}
export function useTransactionDetails(signature: TransactionSignature) {
const context = React.useContext(DetailsStateContext);
if (!context) {
throw new Error(
`useTransactionDetails must be used within a TransactionsProvider`
);
}
return context[signature];
}
export function useTransactionsDispatch() {
const context = React.useContext(DispatchContext);
if (!context) {
@@ -338,11 +335,3 @@ export function useDetailsDispatch() {
}
return context;
}
export function useDetails(signature: TransactionSignature) {
const context = React.useContext(DetailsStateContext);
if (!context) {
throw new Error(`useDetails must be used within a TransactionsProvider`);
}
return context[signature];
}