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"]
}