diff --git a/explorer/src/components/TransactionModal.tsx b/explorer/src/components/TransactionModal.tsx index ecd47a384..fedef9f3d 100644 --- a/explorer/src/components/TransactionModal.tsx +++ b/explorer/src/components/TransactionModal.tsx @@ -1,12 +1,12 @@ import React from "react"; import { + useDetails, useTransactions, useTransactionsDispatch, ActionType, Selected } from "../providers/transactions"; import { displayAddress, decodeCreate, decodeTransfer } from "../utils/tx"; -import { useBlocks } from "../providers/blocks"; import { LAMPORTS_PER_SOL, TransferParams, @@ -54,8 +54,7 @@ function TransactionModal() { } function TransactionDetails({ selected }: { selected: Selected }) { - const { blocks } = useBlocks(); - const block = blocks[selected.slot]; + const details = useDetails(selected.signature); const renderError = (content: React.ReactNode) => { return ( @@ -65,9 +64,9 @@ function TransactionDetails({ selected }: { selected: Selected }) { ); }; - if (!block) return renderError("Transaction block not found"); + if (!details) return renderError("Transaction details not found"); - if (!block.transactions) { + if (!details.transaction) { return renderError( <> @@ -76,7 +75,7 @@ function TransactionDetails({ selected }: { selected: Selected }) { ); } - const transaction = block.transactions[selected.signature]; + const { transaction } = details.transaction; if (!transaction) return renderError("Transaction not found"); if (transaction.instructions.length === 0) diff --git a/explorer/src/index.tsx b/explorer/src/index.tsx index 71f7dcfe4..96ec23df8 100644 --- a/explorer/src/index.tsx +++ b/explorer/src/index.tsx @@ -5,7 +5,6 @@ import "./scss/theme.scss"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import { ClusterProvider } from "./providers/cluster"; -import { BlocksProvider } from "./providers/blocks"; import { TransactionsProvider } from "./providers/transactions"; import { AccountsProvider } from "./providers/accounts"; import { TabProvider } from "./providers/tab"; @@ -16,9 +15,7 @@ ReactDOM.render( - - - + diff --git a/explorer/src/providers/blocks.tsx b/explorer/src/providers/blocks.tsx deleted file mode 100644 index 3462909b7..000000000 --- a/explorer/src/providers/blocks.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React from "react"; -import bs58 from "bs58"; -import { Connection, Transaction } from "@solana/web3.js"; -import { useCluster, ClusterStatus } from "./cluster"; -import { useTransactions } from "./transactions"; - -export enum Status { - Checking, - CheckFailed, - Success -} - -type Transactions = { [signature: string]: Transaction }; -export interface Block { - status: Status; - transactions?: Transactions; -} - -export type Blocks = { [slot: number]: Block }; -interface State { - blocks: Blocks; -} - -export enum ActionType { - Update, - Add, - Remove -} - -interface Update { - type: ActionType.Update; - slot: number; - status: Status; - transactions?: Transactions; -} - -interface Add { - type: ActionType.Add; - slots: number[]; -} - -interface Remove { - type: ActionType.Remove; - slots: number[]; -} - -type Action = Update | Add | Remove; -type Dispatch = (action: Action) => void; - -function reducer(state: State, action: Action): State { - switch (action.type) { - case ActionType.Add: { - if (action.slots.length === 0) return state; - const blocks = { ...state.blocks }; - action.slots.forEach(slot => { - if (!blocks[slot]) { - blocks[slot] = { - status: Status.Checking - }; - } - }); - return { ...state, blocks }; - } - case ActionType.Remove: { - if (action.slots.length === 0) return state; - const blocks = { ...state.blocks }; - action.slots.forEach(slot => { - delete blocks[slot]; - }); - return { ...state, blocks }; - } - case ActionType.Update: { - let block = state.blocks[action.slot]; - if (block) { - block = { - ...block, - status: action.status, - transactions: action.transactions - }; - const blocks = { - ...state.blocks, - [action.slot]: block - }; - return { ...state, blocks }; - } - break; - } - } - return state; -} - -const StateContext = React.createContext(undefined); -const DispatchContext = React.createContext(undefined); - -type BlocksProviderProps = { children: React.ReactNode }; -export function BlocksProvider({ children }: BlocksProviderProps) { - const [state, dispatch] = React.useReducer(reducer, { blocks: {} }); - - const { transactions } = useTransactions(); - const { status, url } = useCluster(); - - // Filter blocks for current transaction slots - React.useEffect(() => { - if (status !== ClusterStatus.Connected) return; - - const remove: number[] = []; - const txSlots = transactions - .map(tx => tx.slot) - .filter(x => x) - .reduce((set, slot) => set.add(slot), new Set()); - Object.keys(state.blocks).forEach(blockKey => { - const slot = parseInt(blockKey); - if (!txSlots.has(slot)) { - remove.push(slot); - } - }); - - dispatch({ type: ActionType.Remove, slots: remove }); - - const fetchSlots = new Set(); - transactions.forEach(tx => { - if (tx.slot && tx.confirmations === "max" && !state.blocks[tx.slot]) - fetchSlots.add(tx.slot); - }); - - const fetchList: number[] = []; - fetchSlots.forEach(s => fetchList.push(s)); - dispatch({ type: ActionType.Add, slots: fetchList }); - - fetchSlots.forEach(slot => { - fetchBlock(dispatch, slot, url); - }); - }, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps - - return ( - - - {children} - - - ); -} - -async function fetchBlock(dispatch: Dispatch, slot: number, url: string) { - dispatch({ - type: ActionType.Update, - status: Status.Checking, - slot - }); - - let status; - let transactions: Transactions = {}; - try { - const block = await new Connection(url).getConfirmedBlock(slot); - block.transactions.forEach(({ transaction }) => { - const signature = transaction.signature; - if (signature) { - const sig = bs58.encode(signature); - transactions[sig] = transaction; - } - }); - status = Status.Success; - } catch (error) { - console.error("Failed to fetch confirmed block", error); - status = Status.CheckFailed; - } - dispatch({ type: ActionType.Update, status, slot, transactions }); -} - -export function useBlocks() { - const context = React.useContext(StateContext); - if (!context) { - throw new Error(`useBlocks must be used within a BlocksProvider`); - } - return context; -} diff --git a/explorer/src/providers/transactions/details.tsx b/explorer/src/providers/transactions/details.tsx new file mode 100644 index 000000000..574c0b05e --- /dev/null +++ b/explorer/src/providers/transactions/details.tsx @@ -0,0 +1,149 @@ +import React from "react"; +import { + Connection, + TransactionSignature, + ConfirmedTransaction +} from "@solana/web3.js"; +import { useCluster, ClusterStatus } from "../cluster"; +import { useTransactions } from "./index"; + +export enum Status { + Checking, + CheckFailed, + NotFound, + Found +} + +export interface Details { + status: Status; + transaction: ConfirmedTransaction | null; +} + +type State = { [signature: string]: Details }; + +export enum ActionType { + Update, + Add +} + +interface Update { + type: ActionType.Update; + signature: string; + status: Status; + transaction: ConfirmedTransaction | null; +} + +interface Add { + type: ActionType.Add; + signatures: TransactionSignature[]; +} + +type Action = Update | Add; +type Dispatch = (action: Action) => void; + +function reducer(state: State, action: Action): State { + switch (action.type) { + case ActionType.Add: { + if (action.signatures.length === 0) return state; + const details = { ...state }; + action.signatures.forEach(signature => { + if (!details[signature]) { + details[signature] = { + status: Status.Checking, + transaction: null + }; + } + }); + return details; + } + + case ActionType.Update: { + let details = state[action.signature]; + if (details) { + details = { + ...details, + status: action.status + }; + if (action.transaction !== null) { + details.transaction = action.transaction; + } + return { + ...state, + ...{ + [action.signature]: details + } + }; + } + break; + } + } + return state; +} + +export const StateContext = React.createContext(undefined); +export const DispatchContext = React.createContext( + undefined +); + +type DetailsProviderProps = { children: React.ReactNode }; +export function DetailsProvider({ children }: DetailsProviderProps) { + const [state, dispatch] = React.useReducer(reducer, {}); + + const { transactions } = useTransactions(); + const { status, url } = useCluster(); + + // Filter blocks for current transaction slots + React.useEffect(() => { + if (status !== ClusterStatus.Connected) return; + + const fetchSignatures = new Set(); + transactions.forEach(tx => { + if (tx.slot && tx.confirmations === "max" && !state[tx.signature]) + fetchSignatures.add(tx.signature); + }); + + const fetchList: string[] = []; + fetchSignatures.forEach(s => fetchList.push(s)); + dispatch({ type: ActionType.Add, signatures: fetchList }); + + fetchSignatures.forEach(signature => { + fetchDetails(dispatch, signature, url); + }); + }, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + + {children} + + + ); +} + +async function fetchDetails( + dispatch: Dispatch, + signature: TransactionSignature, + url: string +) { + dispatch({ + type: ActionType.Update, + status: Status.Checking, + transaction: null, + signature + }); + + let status; + let transaction = null; + try { + transaction = await new Connection(url).getConfirmedTransaction(signature); + if (transaction) { + status = Status.Found; + } else { + status = Status.NotFound; + } + } catch (error) { + console.error("Failed to fetch confirmed transaction", error); + status = Status.CheckFailed; + } + dispatch({ type: ActionType.Update, status, signature, transaction }); +} diff --git a/explorer/src/providers/transactions.tsx b/explorer/src/providers/transactions/index.tsx similarity index 90% rename from explorer/src/providers/transactions.tsx rename to explorer/src/providers/transactions/index.tsx index 69271b18b..71d2294f4 100644 --- a/explorer/src/providers/transactions.tsx +++ b/explorer/src/providers/transactions/index.tsx @@ -5,15 +5,20 @@ import { SystemProgram, Account } from "@solana/web3.js"; -import { findGetParameter, findPathSegment } from "../utils/url"; -import { useCluster, ClusterStatus } from "../providers/cluster"; +import { findGetParameter, findPathSegment } from "../../utils/url"; +import { useCluster, ClusterStatus } from "../cluster"; +import { + DetailsProvider, + StateContext as DetailsStateContext, + DispatchContext as DetailsDispatchContext +} from "./details"; import base58 from "bs58"; import { useAccountsDispatch, fetchAccountInfo, Dispatch as AccountsDispatch, ActionType as AccountsActionType -} from "./accounts"; +} from "../accounts"; export enum Status { Checking, @@ -207,7 +212,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) { return ( - {children} + {children} ); @@ -330,3 +335,21 @@ export function useTransactionsDispatch() { } return context; } + +export function useDetailsDispatch() { + const context = React.useContext(DetailsDispatchContext); + if (!context) { + throw new Error( + `useDetailsDispatch must be used within a TransactionsProvider` + ); + } + 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]; +} diff --git a/explorer/tsconfig.json b/explorer/tsconfig.json index af10394b4..fa6b7e23e 100644 --- a/explorer/tsconfig.json +++ b/explorer/tsconfig.json @@ -13,7 +13,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "baseUrl": "src" }, "include": ["src"] }