diff --git a/explorer/package-lock.json b/explorer/package-lock.json
index 251a8dc950..6250274365 100644
--- a/explorer/package-lock.json
+++ b/explorer/package-lock.json
@@ -1263,6 +1263,11 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
+ "@react-hook/debounce": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-2.0.5.tgz",
+ "integrity": "sha512-WrwQ1e4vx5lxxxEpGPPliMcs6oUJ8cyEb/GL8OEUPhBW4WL8YRSDW5oPnsOqIPqhHDyQMHgMipXWgj7QVmRMKA=="
+ },
"@sheerun/mutationobserver-shim": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
diff --git a/explorer/package.json b/explorer/package.json
index 452ff7974a..b094f57e60 100644
--- a/explorer/package.json
+++ b/explorer/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@react-hook/debounce": "^2.0.5",
"@solana/web3.js": "^0.56.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
diff --git a/explorer/src/components/ClusterModal.tsx b/explorer/src/components/ClusterModal.tsx
index ccfcf23635..af693d0579 100644
--- a/explorer/src/components/ClusterModal.tsx
+++ b/explorer/src/components/ClusterModal.tsx
@@ -1,5 +1,6 @@
import React from "react";
-import { Link, useLocation, useHistory } from "react-router-dom";
+import { Link, useHistory, useLocation } from "react-router-dom";
+import { useDebounceCallback } from "@react-hook/debounce";
import { Location } from "history";
import {
useCluster,
@@ -9,10 +10,12 @@ import {
clusterSlug,
CLUSTERS,
Cluster,
- useClusterModal
+ useClusterModal,
+ useUpdateCustomUrl
} from "../providers/cluster";
import { assertUnreachable } from "../utils";
import Overlay from "./Overlay";
+import { useQuery } from "utils/url";
function ClusterModal() {
const [show, setShow] = useClusterModal();
@@ -46,34 +49,35 @@ function ClusterModal() {
type InputProps = { activeSuffix: string; active: boolean };
function CustomClusterInput({ activeSuffix, active }: InputProps) {
const { customUrl } = useCluster();
+ const updateCustomUrl = useUpdateCustomUrl();
const [editing, setEditing] = React.useState(false);
+ const query = useQuery();
const history = useHistory();
const location = useLocation();
const customClass = (prefix: string) =>
active ? `${prefix}-${activeSuffix}` : "";
- const clusterLocation = (location: Location, url: string) => {
- const params = new URLSearchParams(location.search);
- params.set("clusterUrl", url);
- params.delete("cluster");
+ const clusterLocation = (location: Location) => {
+ if (customUrl.length > 0) query.set("cluster", "custom");
return {
...location,
- search: params.toString()
+ search: query.toString()
};
};
- const updateCustomUrl = React.useCallback(
- (url: string) => {
- history.push(clusterLocation(location, url));
- },
- [history, location]
- );
+ const onUrlInput = useDebounceCallback((url: string) => {
+ updateCustomUrl(url);
+ if (url.length > 0) {
+ query.set("cluster", "custom");
+ history.push({ ...location, search: query.toString() });
+ }
+ }, 500);
const inputTextClass = editing ? "" : "text-muted";
return (
clusterLocation(location, customUrl)}
+ to={location => clusterLocation(location)}
className="btn input-group input-group-merge p-0"
>
setEditing(true)}
onBlur={() => setEditing(false)}
- onInput={e => updateCustomUrl(e.currentTarget.value)}
+ onInput={e => onUrlInput(e.currentTarget.value)}
/>
@@ -133,14 +137,10 @@ function ClusterToggle() {
const clusterLocation = (location: Location) => {
const params = new URLSearchParams(location.search);
const slug = clusterSlug(net);
- if (slug && slug !== "mainnet-beta") {
+ if (slug !== "mainnet-beta") {
params.set("cluster", slug);
- params.delete("clusterUrl");
} else {
params.delete("cluster");
- if (slug === "mainnet-beta") {
- params.delete("clusterUrl");
- }
}
return {
...location,
diff --git a/explorer/src/components/TransactionDetails.tsx b/explorer/src/components/TransactionDetails.tsx
index 3daf01679a..380dea50aa 100644
--- a/explorer/src/components/TransactionDetails.tsx
+++ b/explorer/src/components/TransactionDetails.tsx
@@ -3,10 +3,9 @@ import {
useFetchTransactionStatus,
useTransactionStatus,
useTransactionDetails,
- useDetailsDispatch,
FetchStatus
} from "../providers/transactions";
-import { fetchDetails } from "providers/transactions/details";
+import { useFetchTransactionDetails } from "providers/transactions/details";
import { useCluster, useClusterModal } from "providers/cluster";
import {
TransactionSignature,
@@ -206,12 +205,11 @@ function StatusCard({ signature }: Props) {
function AccountsCard({ signature }: Props) {
const details = useTransactionDetails(signature);
- const dispatch = useDetailsDispatch();
- const { url } = useCluster();
const fetchStatus = useFetchTransactionStatus();
+ const fetchDetails = useFetchTransactionDetails();
const refreshStatus = () => fetchStatus(signature);
- const refreshDetails = () => fetchDetails(dispatch, signature, url);
+ const refreshDetails = () => fetchDetails(signature);
const transaction = details?.transaction?.transaction;
const message = React.useMemo(() => {
return transaction?.compileMessage();
@@ -308,9 +306,8 @@ function AccountsCard({ signature }: Props) {
function InstructionsSection({ signature }: Props) {
const status = useTransactionStatus(signature);
const details = useTransactionDetails(signature);
- const dispatch = useDetailsDispatch();
- const { url } = useCluster();
- const refreshDetails = () => fetchDetails(dispatch, signature, url);
+ const fetchDetails = useFetchTransactionDetails();
+ const refreshDetails = () => fetchDetails(signature);
if (!status || !status.info || !details || !details.transaction) return null;
diff --git a/explorer/src/providers/cluster.tsx b/explorer/src/providers/cluster.tsx
index 3ea32f0e2b..392af5a89c 100644
--- a/explorer/src/providers/cluster.tsx
+++ b/explorer/src/providers/cluster.tsx
@@ -1,6 +1,7 @@
import React from "react";
import { clusterApiUrl, Connection } from "@solana/web3.js";
import { useQuery } from "../utils/url";
+import { useHistory, useLocation } from "react-router-dom";
export enum ClusterStatus {
Connected,
@@ -22,7 +23,7 @@ export const CLUSTERS = [
Cluster.Custom
];
-export function clusterSlug(cluster: Cluster): string | undefined {
+export function clusterSlug(cluster: Cluster): string {
switch (cluster) {
case Cluster.MainnetBeta:
return "mainnet-beta";
@@ -31,7 +32,7 @@ export function clusterSlug(cluster: Cluster): string | undefined {
case Cluster.Devnet:
return "devnet";
case Cluster.Custom:
- return undefined;
+ return "custom";
}
}
@@ -52,8 +53,20 @@ export const MAINNET_BETA_URL = clusterApiUrl("mainnet-beta");
export const TESTNET_URL = clusterApiUrl("testnet");
export const DEVNET_URL = clusterApiUrl("devnet");
+export function clusterUrl(cluster: Cluster, customUrl: string): string {
+ switch (cluster) {
+ case Cluster.Devnet:
+ return DEVNET_URL;
+ case Cluster.MainnetBeta:
+ return MAINNET_BETA_URL;
+ case Cluster.Testnet:
+ return TESTNET_URL;
+ case Cluster.Custom:
+ return customUrl;
+ }
+}
+
export const DEFAULT_CLUSTER = Cluster.MainnetBeta;
-export const DEFAULT_CUSTOM_URL = "http://localhost:8899";
interface State {
cluster: Cluster;
@@ -88,51 +101,19 @@ function clusterReducer(state: State, action: Action): State {
}
}
-function parseQuery(
- query: URLSearchParams
-): { cluster: Cluster; customUrl: string } {
+function parseQuery(query: URLSearchParams): Cluster {
const clusterParam = query.get("cluster");
- const clusterUrlParam = query.get("clusterUrl");
-
- let cluster;
- let customUrl = DEFAULT_CUSTOM_URL;
- switch (clusterUrlParam) {
- case MAINNET_BETA_URL:
- cluster = Cluster.MainnetBeta;
- break;
- case DEVNET_URL:
- cluster = Cluster.Devnet;
- break;
- case TESTNET_URL:
- cluster = Cluster.Testnet;
- break;
- }
-
switch (clusterParam) {
- case "mainnet-beta":
- cluster = Cluster.MainnetBeta;
- break;
+ case "custom":
+ return Cluster.Custom;
case "devnet":
- cluster = Cluster.Devnet;
- break;
+ return Cluster.Devnet;
case "testnet":
- cluster = Cluster.Testnet;
- break;
+ return Cluster.Testnet;
+ case "mainnet-beta":
+ default:
+ return Cluster.MainnetBeta;
}
-
- if (!cluster) {
- if (!clusterUrlParam) {
- cluster = DEFAULT_CLUSTER;
- } else {
- cluster = Cluster.Custom;
- customUrl = clusterUrlParam;
- }
- }
-
- return {
- cluster,
- customUrl
- };
}
type SetShowModal = React.Dispatch>;
@@ -146,16 +127,28 @@ type ClusterProviderProps = { children: React.ReactNode };
export function ClusterProvider({ children }: ClusterProviderProps) {
const [state, dispatch] = React.useReducer(clusterReducer, {
cluster: DEFAULT_CLUSTER,
- customUrl: DEFAULT_CUSTOM_URL,
+ customUrl: "",
status: ClusterStatus.Connecting
});
const [showModal, setShowModal] = React.useState(false);
- const { cluster, customUrl } = parseQuery(useQuery());
+ const query = useQuery();
+ const cluster = parseQuery(query);
+ const history = useHistory();
+ const location = useLocation();
- // Reconnect to cluster when it changes
+ // Reconnect to cluster when params change
React.useEffect(() => {
- updateCluster(dispatch, cluster, customUrl);
- }, [cluster, customUrl]);
+ if (cluster === Cluster.Custom) {
+ // Remove cluster param if custom url has not been set
+ if (state.customUrl.length === 0) {
+ query.delete("cluster");
+ history.push({ ...location, search: query.toString() });
+ return;
+ }
+ }
+
+ updateCluster(dispatch, cluster, state.customUrl);
+ }, [cluster, state.customUrl]); // eslint-disable-line react-hooks/exhaustive-deps
return (
@@ -168,19 +161,6 @@ export function ClusterProvider({ children }: ClusterProviderProps) {
);
}
-export function clusterUrl(cluster: Cluster, customUrl: string): string {
- switch (cluster) {
- case Cluster.Devnet:
- return DEVNET_URL;
- case Cluster.MainnetBeta:
- return MAINNET_BETA_URL;
- case Cluster.Testnet:
- return TESTNET_URL;
- case Cluster.Custom:
- return customUrl;
- }
-}
-
async function updateCluster(
dispatch: Dispatch,
cluster: Cluster,
@@ -207,6 +187,17 @@ async function updateCluster(
}
}
+export function useUpdateCustomUrl() {
+ const dispatch = React.useContext(DispatchContext);
+ if (!dispatch) {
+ throw new Error(`useUpdateCustomUrl must be used within a ClusterProvider`);
+ }
+
+ return (customUrl: string) => {
+ updateCluster(dispatch, Cluster.Custom, customUrl);
+ };
+}
+
export function useCluster() {
const context = React.useContext(StateContext);
if (!context) {
diff --git a/explorer/src/providers/transactions/details.tsx b/explorer/src/providers/transactions/details.tsx
index fa218ca319..106c1467fe 100644
--- a/explorer/src/providers/transactions/details.tsx
+++ b/explorer/src/providers/transactions/details.tsx
@@ -128,7 +128,7 @@ export function DetailsProvider({ children }: DetailsProviderProps) {
);
}
-export async function fetchDetails(
+async function fetchDetails(
dispatch: Dispatch,
signature: TransactionSignature,
url: string
@@ -151,3 +151,17 @@ export async function fetchDetails(
}
dispatch({ type: ActionType.Update, fetchStatus, signature, transaction });
}
+
+export function useFetchTransactionDetails() {
+ const dispatch = React.useContext(DispatchContext);
+ if (!dispatch) {
+ throw new Error(
+ `useFetchTransactionDetails must be used within a TransactionsProvider`
+ );
+ }
+
+ const { url } = useCluster();
+ return (signature: TransactionSignature) => {
+ url && fetchDetails(dispatch, signature, url);
+ };
+}
diff --git a/explorer/src/providers/transactions/index.tsx b/explorer/src/providers/transactions/index.tsx
index a982a65db9..ad7808ec0e 100644
--- a/explorer/src/providers/transactions/index.tsx
+++ b/explorer/src/providers/transactions/index.tsx
@@ -12,8 +12,7 @@ import { useQuery } from "../../utils/url";
import { useCluster, Cluster, ClusterStatus } from "../cluster";
import {
DetailsProvider,
- StateContext as DetailsStateContext,
- DispatchContext as DetailsDispatchContext
+ StateContext as DetailsStateContext
} from "./details";
import base58 from "bs58";
import { useFetchAccountInfo } from "../accounts";
@@ -308,16 +307,6 @@ export function useTransactionDetails(signature: TransactionSignature) {
return context[signature];
}
-export function useDetailsDispatch() {
- const context = React.useContext(DetailsDispatchContext);
- if (!context) {
- throw new Error(
- `useDetailsDispatch must be used within a TransactionsProvider`
- );
- }
- return context;
-}
-
export function useFetchTransactionStatus() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {