UX improvements
This commit is contained in:
committed by
Michael Vines
parent
484a4db626
commit
42225446c4
30
explorer/package-lock.json
generated
30
explorer/package-lock.json
generated
@ -9657,6 +9657,22 @@
|
|||||||
"prepend-http": "^1.0.0",
|
"prepend-http": "^1.0.0",
|
||||||
"query-string": "^4.1.0",
|
"query-string": "^4.1.0",
|
||||||
"sort-keys": "^1.0.0"
|
"sort-keys": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"query-string": {
|
||||||
|
"version": "4.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
|
||||||
|
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
|
||||||
|
"requires": {
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
|
"strict-uri-encode": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strict-uri-encode": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm-run-all": {
|
"npm-run-all": {
|
||||||
@ -11446,15 +11462,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||||
},
|
},
|
||||||
"query-string": {
|
|
||||||
"version": "4.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
|
|
||||||
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
|
|
||||||
"requires": {
|
|
||||||
"object-assign": "^4.1.0",
|
|
||||||
"strict-uri-encode": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
@ -13175,11 +13182,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
|
||||||
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
|
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
|
||||||
},
|
},
|
||||||
"strict-uri-encode": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
|
||||||
},
|
|
||||||
"string-length": {
|
"string-length": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
||||||
|
@ -21,7 +21,7 @@ function App() {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="row align-items-end">
|
<div className="row align-items-end">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Link to="/">
|
<Link to={location => ({ ...location, pathname: "/" })}>
|
||||||
<img src={Logo} width="250" alt="Solana Explorer" />
|
<img src={Logo} width="250" alt="Solana Explorer" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,8 +125,8 @@ const renderAccountRow = (
|
|||||||
break;
|
break;
|
||||||
case Status.CheckFailed:
|
case Status.CheckFailed:
|
||||||
case Status.HistoryFailed:
|
case Status.HistoryFailed:
|
||||||
statusClass = "danger";
|
statusClass = "dark";
|
||||||
statusText = "Error";
|
statusText = "Cluster Error";
|
||||||
break;
|
break;
|
||||||
case Status.Checking:
|
case Status.Checking:
|
||||||
case Status.FetchingHistory:
|
case Status.FetchingHistory:
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link, useLocation, useHistory } from "react-router-dom";
|
||||||
|
import { Location } from "history";
|
||||||
import {
|
import {
|
||||||
useCluster,
|
useCluster,
|
||||||
useClusterDispatch,
|
|
||||||
updateCluster,
|
|
||||||
ClusterStatus,
|
ClusterStatus,
|
||||||
clusterUrl,
|
clusterUrl,
|
||||||
clusterName,
|
clusterName,
|
||||||
|
clusterSlug,
|
||||||
CLUSTERS,
|
CLUSTERS,
|
||||||
Cluster,
|
Cluster,
|
||||||
useClusterModal
|
useClusterModal
|
||||||
@ -45,19 +46,35 @@ function ClusterModal() {
|
|||||||
type InputProps = { activeSuffix: string; active: boolean };
|
type InputProps = { activeSuffix: string; active: boolean };
|
||||||
function CustomClusterInput({ activeSuffix, active }: InputProps) {
|
function CustomClusterInput({ activeSuffix, active }: InputProps) {
|
||||||
const { customUrl } = useCluster();
|
const { customUrl } = useCluster();
|
||||||
const dispatch = useClusterDispatch();
|
|
||||||
const [editing, setEditing] = React.useState(false);
|
const [editing, setEditing] = React.useState(false);
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const customClass = (prefix: string) =>
|
const customClass = (prefix: string) =>
|
||||||
active ? `${prefix}-${activeSuffix}` : "";
|
active ? `${prefix}-${activeSuffix}` : "";
|
||||||
|
|
||||||
|
const clusterLocation = (location: Location, url: string) => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
params.set("clusterUrl", url);
|
||||||
|
params.delete("cluster");
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
search: params.toString()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCustomUrl = React.useCallback(
|
||||||
|
(url: string) => {
|
||||||
|
history.push(clusterLocation(location, url));
|
||||||
|
},
|
||||||
|
[history, location]
|
||||||
|
);
|
||||||
|
|
||||||
const inputTextClass = editing ? "" : "text-muted";
|
const inputTextClass = editing ? "" : "text-muted";
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
|
to={location => clusterLocation(location, customUrl)}
|
||||||
className="btn input-group input-group-merge p-0"
|
className="btn input-group input-group-merge p-0"
|
||||||
onClick={() =>
|
|
||||||
!active && updateCluster(dispatch, Cluster.Custom, customUrl)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -67,22 +84,19 @@ function CustomClusterInput({ activeSuffix, active }: InputProps) {
|
|||||||
)}`}
|
)}`}
|
||||||
onFocus={() => setEditing(true)}
|
onFocus={() => setEditing(true)}
|
||||||
onBlur={() => setEditing(false)}
|
onBlur={() => setEditing(false)}
|
||||||
onInput={e =>
|
onInput={e => updateCustomUrl(e.currentTarget.value)}
|
||||||
updateCluster(dispatch, Cluster.Custom, e.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="input-group-prepend">
|
<div className="input-group-prepend">
|
||||||
<div className={`input-group-text pr-0 ${customClass("border")}`}>
|
<div className={`input-group-text pr-0 ${customClass("border")}`}>
|
||||||
<span className={customClass("text") || "text-dark"}>Custom:</span>
|
<span className={customClass("text") || "text-dark"}>Custom:</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClusterToggle() {
|
function ClusterToggle() {
|
||||||
const { status, cluster, customUrl } = useCluster();
|
const { status, cluster, customUrl } = useCluster();
|
||||||
const dispatch = useClusterDispatch();
|
|
||||||
|
|
||||||
let activeSuffix = "";
|
let activeSuffix = "";
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -116,21 +130,35 @@ function ClusterToggle() {
|
|||||||
? `border-${activeSuffix} text-${activeSuffix}`
|
? `border-${activeSuffix} text-${activeSuffix}`
|
||||||
: "btn-white text-dark";
|
: "btn-white text-dark";
|
||||||
|
|
||||||
|
const clusterLocation = (location: Location) => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const slug = clusterSlug(net);
|
||||||
|
if (slug && slug !== "mainnet-beta") {
|
||||||
|
params.set("cluster", slug);
|
||||||
|
params.delete("clusterUrl");
|
||||||
|
} else {
|
||||||
|
params.delete("cluster");
|
||||||
|
if (slug === "mainnet-beta") {
|
||||||
|
params.delete("clusterUrl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
search: params.toString()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<Link
|
||||||
key={index}
|
key={index}
|
||||||
className={`btn text-left col-12 mb-3 ${btnClass}`}
|
className={`btn text-left col-12 mb-3 ${btnClass}`}
|
||||||
|
to={clusterLocation}
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
checked={active}
|
|
||||||
onChange={() => updateCluster(dispatch, net, customUrl)}
|
|
||||||
/>
|
|
||||||
{`${clusterName(net)}: `}
|
{`${clusterName(net)}: `}
|
||||||
<span className="text-muted d-inline-block">
|
<span className="text-muted d-inline-block">
|
||||||
{clusterUrl(net, customUrl)}
|
{clusterUrl(net, customUrl)}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,22 +3,23 @@ import React, { useState, ReactNode } from "react";
|
|||||||
type CopyableProps = {
|
type CopyableProps = {
|
||||||
text: string;
|
text: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
bottom?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = "hide" | "copy" | "copied";
|
type State = "hide" | "copy" | "copied";
|
||||||
|
|
||||||
function Popover({ state }: { state: State }) {
|
function Popover({ state, bottom }: { state: State; bottom?: boolean }) {
|
||||||
if (state === "hide") return null;
|
if (state === "hide") return null;
|
||||||
const text = state === "copy" ? "Copy" : "Copied!";
|
const text = state === "copy" ? "Copy" : "Copied!";
|
||||||
return (
|
return (
|
||||||
<div className="popover bs-popover-top show">
|
<div className={`popover bs-popover-${bottom ? "bottom" : "top"} show`}>
|
||||||
<div className="arrow" />
|
<div className="arrow" />
|
||||||
<div className="popover-body">{text}</div>
|
<div className="popover-body">{text}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Copyable({ text, children }: CopyableProps) {
|
function Copyable({ bottom, text, children }: CopyableProps) {
|
||||||
const [state, setState] = useState<State>("hide");
|
const [state, setState] = useState<State>("hide");
|
||||||
|
|
||||||
const copyToClipboard = () => navigator.clipboard.writeText(text);
|
const copyToClipboard = () => navigator.clipboard.writeText(text);
|
||||||
@ -29,15 +30,14 @@ function Copyable({ text, children }: CopyableProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="copyable">
|
<div
|
||||||
<div
|
className="copyable"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseOver={() => setState("copy")}
|
onMouseOver={() => setState("copy")}
|
||||||
onMouseOut={() => state === "copy" && setState("hide")}
|
onMouseOut={() => state === "copy" && setState("hide")}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
<Popover bottom={bottom} state={state} />
|
||||||
<Popover state={state} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useClusterModal } from "providers/cluster";
|
import { useClusterModal } from "providers/cluster";
|
||||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||||
|
|
||||||
@ -54,14 +54,16 @@ function NavLink({
|
|||||||
tab: Tab;
|
tab: Tab;
|
||||||
current: Tab;
|
current: Tab;
|
||||||
}) {
|
}) {
|
||||||
const location = useLocation();
|
|
||||||
let classes = "nav-link";
|
let classes = "nav-link";
|
||||||
if (tab === current) {
|
if (tab === current) {
|
||||||
classes += " active";
|
classes += " active";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={{ ...location, pathname: href }} className={classes}>
|
<Link
|
||||||
|
to={location => ({ ...location, pathname: href })}
|
||||||
|
className={classes}
|
||||||
|
>
|
||||||
{tab}
|
{tab}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import bs58 from "bs58";
|
||||||
import {
|
import {
|
||||||
Source,
|
Source,
|
||||||
useTransactionStatus,
|
useTransactionStatus,
|
||||||
@ -16,7 +17,8 @@ import {
|
|||||||
TransactionInstruction,
|
TransactionInstruction,
|
||||||
TransferParams,
|
TransferParams,
|
||||||
CreateAccountParams,
|
CreateAccountParams,
|
||||||
SystemProgram
|
SystemProgram,
|
||||||
|
SignatureResult
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
@ -123,73 +125,51 @@ function TransactionStatusCard({ signature }: Props) {
|
|||||||
let statusClass = "success";
|
let statusClass = "success";
|
||||||
let statusText = "Success";
|
let statusText = "Success";
|
||||||
if (info.result.err) {
|
if (info.result.err) {
|
||||||
statusClass = "danger";
|
statusClass = "warning";
|
||||||
statusText = "Error";
|
statusText = "Error";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
<h3 className="mb-0">
|
||||||
|
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
||||||
|
</h3>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fee = details?.transaction?.meta?.fee;
|
const fee = details?.transaction?.meta?.fee;
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header align-items-center">
|
||||||
<div className="row align-items-center">
|
<h3 className="card-header-title">Transaction Status</h3>
|
||||||
<div className="col">
|
<button className="btn btn-white btn-sm" onClick={refreshStatus}>
|
||||||
<div className="card-header-title">Status</div>
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
</div>
|
Refresh
|
||||||
<div className="col-auto">
|
</button>
|
||||||
<button className="btn btn-white btn-sm" onClick={refreshStatus}>
|
|
||||||
<span className="fe fe-refresh-cw mr-2"></span>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
|
||||||
<div className="list-group list-group-flush my-n3">
|
|
||||||
<div className="list-group-item">
|
|
||||||
<div className="row align-items-center">
|
|
||||||
<div className="col">
|
|
||||||
<h5 className="mb-0">Result</h5>
|
|
||||||
</div>
|
|
||||||
<div className="col-auto">{renderResult()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="list-group-item">
|
<TableCardBody>
|
||||||
<div className="row align-items-center">
|
<tr>
|
||||||
<div className="col">
|
<td>Result</td>
|
||||||
<h5 className="mb-0">Block</h5>
|
<td className="text-right">{renderResult()}</td>
|
||||||
</div>
|
</tr>
|
||||||
<div className="col-auto">{info.slot}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="list-group-item">
|
<tr>
|
||||||
<div className="row align-items-center">
|
<td>Block</td>
|
||||||
<div className="col">
|
<td className="text-right">{info.slot}</td>
|
||||||
<h5 className="mb-0">Confirmations</h5>
|
</tr>
|
||||||
</div>
|
|
||||||
<div className="col-auto text-uppercase">
|
<tr>
|
||||||
{info.confirmations}
|
<td>Confirmations</td>
|
||||||
</div>
|
<td className="text-right text-uppercase">{info.confirmations}</td>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
|
||||||
{fee && (
|
{fee && (
|
||||||
<div className="list-group-item">
|
<tr>
|
||||||
<div className="row align-items-center">
|
<td>Fee (SOL)</td>
|
||||||
<div className="col">
|
<td className="text-right">{lamportsToSolString(fee)}</td>
|
||||||
<h5 className="mb-0">Fee (SOL)</h5>
|
</tr>
|
||||||
</div>
|
)}
|
||||||
<div className="col-auto">{lamportsToSolString(fee)}</div>
|
</TableCardBody>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -265,7 +245,7 @@ function TransactionAccountsCard({ signature }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h4 className="card-header-title">Accounts</h4>
|
<h3 className="card-header-title">Transaction Accounts</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-responsive mb-0">
|
<div className="table-responsive mb-0">
|
||||||
<table className="table table-sm table-nowrap card-table">
|
<table className="table table-sm table-nowrap card-table">
|
||||||
@ -284,34 +264,108 @@ function TransactionAccountsCard({ signature }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ixResult(result: SignatureResult, index: number) {
|
||||||
|
if (result.err) {
|
||||||
|
const err = result.err as any;
|
||||||
|
const ixError = err["InstructionError"];
|
||||||
|
if (ixError && Array.isArray(ixError)) {
|
||||||
|
const [errorIndex, error] = ixError;
|
||||||
|
if (Number.isInteger(errorIndex) && errorIndex === index) {
|
||||||
|
return ["warning", `"${JSON.stringify(error)}"`];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ["dark"];
|
||||||
|
}
|
||||||
|
return ["success"];
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstructionProps = {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
result: SignatureResult;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function InstructionCard({ title, children, result, index }: InstructionProps) {
|
||||||
|
const [resultClass, errorString] = ixResult(result, index);
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||||
|
<span className={`badge badge-soft-${resultClass} mr-2`}>
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
{title} Instruction
|
||||||
|
</h3>
|
||||||
|
<h3 className="mb-0">
|
||||||
|
<span className="badge badge-soft-warning text-monospace">
|
||||||
|
{errorString}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<TableCardBody>{children}</TableCardBody>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TransactionInstructionsCard({ signature }: Props) {
|
function TransactionInstructionsCard({ signature }: Props) {
|
||||||
|
const status = useTransactionStatus(signature);
|
||||||
const details = useTransactionDetails(signature);
|
const details = useTransactionDetails(signature);
|
||||||
const dispatch = useDetailsDispatch();
|
const dispatch = useDetailsDispatch();
|
||||||
const { url } = useCluster();
|
const { url } = useCluster();
|
||||||
const refreshDetails = () => fetchDetails(dispatch, signature, url);
|
const refreshDetails = () => fetchDetails(dispatch, signature, url);
|
||||||
|
|
||||||
if (!details || !details.transaction) return null;
|
if (!status || !status.info || !details || !details.transaction) return null;
|
||||||
|
|
||||||
const { transaction } = details.transaction;
|
const { transaction } = details.transaction;
|
||||||
if (transaction.instructions.length === 0) {
|
if (transaction.instructions.length === 0) {
|
||||||
return <RetryCard retry={refreshDetails} text="No instructions found" />;
|
return <RetryCard retry={refreshDetails} text="No instructions found" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = status.info.result;
|
||||||
const instructionDetails = transaction.instructions.map((ix, index) => {
|
const instructionDetails = transaction.instructions.map((ix, index) => {
|
||||||
const transfer = decodeTransfer(ix);
|
const transfer = decodeTransfer(ix);
|
||||||
if (transfer)
|
if (transfer) {
|
||||||
return <TransferDetails key={index} transfer={transfer} index={index} />;
|
return (
|
||||||
|
<InstructionCard
|
||||||
|
key={index}
|
||||||
|
title="Transfer"
|
||||||
|
result={result}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
<TransferDetails ix={ix} transfer={transfer} />
|
||||||
|
</InstructionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const create = decodeCreate(ix);
|
const create = decodeCreate(ix);
|
||||||
if (create)
|
if (create) {
|
||||||
return <CreateDetails key={index} create={create} index={index} />;
|
return (
|
||||||
return <InstructionDetails key={index} ix={ix} index={index} />;
|
<InstructionCard
|
||||||
|
key={index}
|
||||||
|
title="Create Account"
|
||||||
|
result={result}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
<CreateDetails ix={ix} create={create} />
|
||||||
|
</InstructionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstructionCard key={index} title="Raw" result={result} index={index}>
|
||||||
|
<InstructionDetails ix={ix} />
|
||||||
|
</InstructionCard>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-body">Transaction Instruction(s)</div>
|
<div className="header-body">
|
||||||
|
<h3 className="mb-0">Transaction Instruction(s)</h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{instructionDetails}
|
{instructionDetails}
|
||||||
@ -320,140 +374,186 @@ function TransactionInstructionsCard({ signature }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TransferDetails({
|
function TransferDetails({
|
||||||
transfer,
|
ix,
|
||||||
index
|
transfer
|
||||||
}: {
|
}: {
|
||||||
|
ix: TransactionInstruction;
|
||||||
transfer: TransferParams;
|
transfer: TransferParams;
|
||||||
index: number;
|
|
||||||
}) {
|
}) {
|
||||||
const from = transfer.fromPubkey.toBase58();
|
const from = transfer.fromPubkey.toBase58();
|
||||||
const to = transfer.toPubkey.toBase58();
|
const to = transfer.toPubkey.toBase58();
|
||||||
|
const [fromMeta, toMeta] = ix.keys;
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<>
|
||||||
<div className="card-header">
|
<tr>
|
||||||
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
<td>Program</td>
|
||||||
<span className="badge badge-soft-dark mr-2">#{index + 1}</span>
|
<td className="text-right">
|
||||||
Transfer
|
<Copyable bottom text={SystemProgram.programId.toBase58()}>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="list-group list-group-flush my-n3">
|
|
||||||
<ListGroupItem label="Program">
|
|
||||||
<code>{displayAddress(SystemProgram.programId)}</code>
|
<code>{displayAddress(SystemProgram.programId)}</code>
|
||||||
</ListGroupItem>
|
</Copyable>
|
||||||
<ListGroupItem label="From">
|
</td>
|
||||||
<Copyable text={from}>
|
</tr>
|
||||||
<code>{from}</code>
|
|
||||||
</Copyable>
|
<tr>
|
||||||
</ListGroupItem>
|
<td>
|
||||||
<ListGroupItem label="To">
|
<div className="mr-2 d-md-inline">From Account</div>
|
||||||
<Copyable text={to}>
|
{!fromMeta.isWritable && (
|
||||||
<code>{to}</code>
|
<span className="badge badge-soft-dark mr-1">Readonly</span>
|
||||||
</Copyable>
|
)}
|
||||||
</ListGroupItem>
|
{fromMeta.isSigner && (
|
||||||
<ListGroupItem label="Amount (SOL)">
|
<span className="badge badge-soft-dark mr-1">Signer</span>
|
||||||
{lamportsToSolString(transfer.lamports)}
|
)}
|
||||||
</ListGroupItem>
|
</td>
|
||||||
</div>
|
<td className="text-right">
|
||||||
</div>
|
<Copyable text={from}>
|
||||||
</div>
|
<code>{from}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className="mr-2 d-md-inline">To Account</div>
|
||||||
|
{!toMeta.isWritable && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Readonly</span>
|
||||||
|
)}
|
||||||
|
{toMeta.isSigner && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Signer</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<Copyable text={to}>
|
||||||
|
<code>{to}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Transfer Amount (SOL)</td>
|
||||||
|
<td className="text-right">{lamportsToSolString(transfer.lamports)}</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateDetails({
|
function CreateDetails({
|
||||||
create,
|
ix,
|
||||||
index
|
create
|
||||||
}: {
|
}: {
|
||||||
|
ix: TransactionInstruction;
|
||||||
create: CreateAccountParams;
|
create: CreateAccountParams;
|
||||||
index: number;
|
|
||||||
}) {
|
}) {
|
||||||
const from = create.fromPubkey.toBase58();
|
const from = create.fromPubkey.toBase58();
|
||||||
const newKey = create.newAccountPubkey.toBase58();
|
const newKey = create.newAccountPubkey.toBase58();
|
||||||
|
const [fromMeta, newMeta] = ix.keys;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<>
|
||||||
<div className="card-header">
|
<tr>
|
||||||
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
<td>Program</td>
|
||||||
<span className="badge badge-soft-dark mr-2">#{index + 1}</span>
|
<td className="text-right">
|
||||||
Create Account
|
<Copyable bottom text={SystemProgram.programId.toBase58()}>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="list-group list-group-flush my-n3">
|
|
||||||
<ListGroupItem label="Program">
|
|
||||||
<code>{displayAddress(SystemProgram.programId)}</code>
|
<code>{displayAddress(SystemProgram.programId)}</code>
|
||||||
</ListGroupItem>
|
</Copyable>
|
||||||
<ListGroupItem label="From">
|
</td>
|
||||||
<Copyable text={from}>
|
</tr>
|
||||||
<code>{from}</code>
|
|
||||||
</Copyable>
|
<tr>
|
||||||
</ListGroupItem>
|
<td>
|
||||||
<ListGroupItem label="New Account">
|
<div className="mr-2 d-md-inline">From Account</div>
|
||||||
<Copyable text={newKey}>
|
{!fromMeta.isWritable && (
|
||||||
<code>{newKey}</code>
|
<span className="badge badge-soft-dark mr-1">Readonly</span>
|
||||||
</Copyable>
|
)}
|
||||||
</ListGroupItem>
|
{fromMeta.isSigner && (
|
||||||
<ListGroupItem label="Amount (SOL)">
|
<span className="badge badge-soft-dark mr-1">Signer</span>
|
||||||
{lamportsToSolString(create.lamports)}
|
)}
|
||||||
</ListGroupItem>
|
</td>
|
||||||
<ListGroupItem label="Data (Bytes)">{create.space}</ListGroupItem>
|
<td className="text-right">
|
||||||
<ListGroupItem label="Owner">
|
<Copyable text={from}>
|
||||||
|
<code>{from}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className="mr-2 d-md-inline">New Account</div>
|
||||||
|
{!newMeta.isWritable && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Readonly</span>
|
||||||
|
)}
|
||||||
|
{newMeta.isSigner && (
|
||||||
|
<span className="badge badge-soft-dark mr-1">Signer</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<Copyable text={newKey}>
|
||||||
|
<code>{newKey}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Transfer Amount (SOL)</td>
|
||||||
|
<td className="text-right">{lamportsToSolString(create.lamports)}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Allocated Space (Bytes)</td>
|
||||||
|
<td className="text-right">{create.space}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Assigned Owner</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<Copyable text={create.programId.toBase58()}>
|
||||||
<code>{displayAddress(create.programId)}</code>
|
<code>{displayAddress(create.programId)}</code>
|
||||||
</ListGroupItem>
|
</Copyable>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InstructionDetails({
|
function InstructionDetails({ ix }: { ix: TransactionInstruction }) {
|
||||||
ix,
|
|
||||||
index
|
|
||||||
}: {
|
|
||||||
ix: TransactionInstruction;
|
|
||||||
index: number;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<>
|
||||||
<div className="card-header">
|
<tr>
|
||||||
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
<td>Program</td>
|
||||||
<span className="badge badge-soft-dark mr-2">#{index + 1}</span>
|
<td className="text-right">
|
||||||
</h3>
|
<Copyable bottom text={ix.programId.toBase58()}>
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="list-group list-group-flush my-n3">
|
|
||||||
<ListGroupItem label="Program">
|
|
||||||
<code>{displayAddress(ix.programId)}</code>
|
<code>{displayAddress(ix.programId)}</code>
|
||||||
</ListGroupItem>
|
</Copyable>
|
||||||
{ix.keys.map(({ pubkey }, keyIndex) => (
|
</td>
|
||||||
<ListGroupItem key={keyIndex} label={`Address #${keyIndex + 1}`}>
|
</tr>
|
||||||
<Copyable text={pubkey.toBase58()}>
|
|
||||||
<code>{pubkey.toBase58()}</code>
|
|
||||||
</Copyable>
|
|
||||||
</ListGroupItem>
|
|
||||||
))}
|
|
||||||
<ListGroupItem label="Data (Bytes)">{ix.data.length}</ListGroupItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListGroupItem({
|
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => (
|
||||||
label,
|
<tr key={keyIndex}>
|
||||||
children
|
<td>
|
||||||
}: {
|
<div className="mr-2 d-md-inline">Account #{keyIndex + 1}</div>
|
||||||
label: string;
|
{!isWritable && (
|
||||||
children: React.ReactNode;
|
<span className="badge badge-soft-dark mr-1">Readonly</span>
|
||||||
}) {
|
)}
|
||||||
return (
|
{isSigner && (
|
||||||
<div className="list-group-item">
|
<span className="badge badge-soft-dark mr-1">Signer</span>
|
||||||
<div className="row align-items-center">
|
)}
|
||||||
<div className="col">
|
</td>
|
||||||
<h5 className="mb-0">{label}</h5>
|
<td className="text-right">
|
||||||
</div>
|
<Copyable text={pubkey.toBase58()}>
|
||||||
<div className="col-auto">{children}</div>
|
<code>{pubkey.toBase58()}</code>
|
||||||
</div>
|
</Copyable>
|
||||||
</div>
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Raw Data (Base58)</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<Copyable text={bs58.encode(ix.data)}>
|
||||||
|
<code>{bs58.encode(ix.data)}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,3 +580,13 @@ function RetryCard({ retry, text }: { retry: () => void; text: string }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TableCardBody({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<tbody className="list">{children}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -13,7 +13,6 @@ import bs58 from "bs58";
|
|||||||
import { assertUnreachable } from "../utils";
|
import { assertUnreachable } from "../utils";
|
||||||
import { useCluster } from "../providers/cluster";
|
import { useCluster } from "../providers/cluster";
|
||||||
import Copyable from "./Copyable";
|
import Copyable from "./Copyable";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
function TransactionsCard() {
|
function TransactionsCard() {
|
||||||
const { transactions, idCounter } = useTransactions();
|
const { transactions, idCounter } = useTransactions();
|
||||||
@ -21,7 +20,6 @@ function TransactionsCard() {
|
|||||||
const signatureInput = React.useRef<HTMLInputElement>(null);
|
const signatureInput = React.useRef<HTMLInputElement>(null);
|
||||||
const [error, setError] = React.useState("");
|
const [error, setError] = React.useState("");
|
||||||
const { url } = useCluster();
|
const { url } = useCluster();
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const onNew = (signature: string) => {
|
const onNew = (signature: string) => {
|
||||||
if (signature.length === 0) return;
|
if (signature.length === 0) return;
|
||||||
@ -101,7 +99,7 @@ function TransactionsCard() {
|
|||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
{transactions.map(transaction =>
|
{transactions.map(transaction =>
|
||||||
renderTransactionRow(transaction, dispatch, location, url)
|
renderTransactionRow(transaction, dispatch, url)
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -125,7 +123,6 @@ const renderHeader = () => {
|
|||||||
const renderTransactionRow = (
|
const renderTransactionRow = (
|
||||||
transactionStatus: TransactionStatus,
|
transactionStatus: TransactionStatus,
|
||||||
dispatch: any,
|
dispatch: any,
|
||||||
location: any,
|
|
||||||
url: string
|
url: string
|
||||||
) => {
|
) => {
|
||||||
const { fetchStatus, info, signature, id } = transactionStatus;
|
const { fetchStatus, info, signature, id } = transactionStatus;
|
||||||
@ -169,7 +166,7 @@ const renderTransactionRow = (
|
|||||||
if (info?.confirmations === "max") {
|
if (info?.confirmations === "max") {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={{ ...location, pathname: "/tx/" + signature }}
|
to={location => ({ ...location, pathname: "/tx/" + signature })}
|
||||||
className="btn btn-rounded-circle btn-white btn-sm"
|
className="btn btn-rounded-circle btn-white btn-sm"
|
||||||
>
|
>
|
||||||
<span className="fe fe-arrow-right"></span>
|
<span className="fe fe-arrow-right"></span>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { clusterApiUrl, Connection } from "@solana/web3.js";
|
import { clusterApiUrl, Connection } from "@solana/web3.js";
|
||||||
import { findGetParameter } from "../utils/url";
|
import { useQuery } from "../utils/url";
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
Connected,
|
Connected,
|
||||||
@ -22,6 +22,19 @@ export const CLUSTERS = [
|
|||||||
Cluster.Custom
|
Cluster.Custom
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function clusterSlug(cluster: Cluster): string | undefined {
|
||||||
|
switch (cluster) {
|
||||||
|
case Cluster.MainnetBeta:
|
||||||
|
return "mainnet-beta";
|
||||||
|
case Cluster.Testnet:
|
||||||
|
return "testnet";
|
||||||
|
case Cluster.Devnet:
|
||||||
|
return "devnet";
|
||||||
|
case Cluster.Custom:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function clusterName(cluster: Cluster): string {
|
export function clusterName(cluster: Cluster): string {
|
||||||
switch (cluster) {
|
switch (cluster) {
|
||||||
case Cluster.MainnetBeta:
|
case Cluster.MainnetBeta:
|
||||||
@ -77,11 +90,11 @@ function clusterReducer(state: State, action: Action): State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initState(): State {
|
function parseQuery(
|
||||||
const clusterParam =
|
query: URLSearchParams
|
||||||
findGetParameter("cluster") || findGetParameter("network");
|
): { cluster: Cluster; customUrl: string } {
|
||||||
const clusterUrlParam =
|
const clusterParam = query.get("cluster");
|
||||||
findGetParameter("clusterUrl") || findGetParameter("networkUrl");
|
const clusterUrlParam = query.get("clusterUrl");
|
||||||
|
|
||||||
let cluster;
|
let cluster;
|
||||||
let customUrl = DEFAULT_CUSTOM_URL;
|
let customUrl = DEFAULT_CUSTOM_URL;
|
||||||
@ -120,8 +133,7 @@ function initState(): State {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
cluster,
|
cluster,
|
||||||
customUrl,
|
customUrl
|
||||||
status: ClusterStatus.Connecting
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,17 +146,18 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
|||||||
|
|
||||||
type ClusterProviderProps = { children: React.ReactNode };
|
type ClusterProviderProps = { children: React.ReactNode };
|
||||||
export function ClusterProvider({ children }: ClusterProviderProps) {
|
export function ClusterProvider({ children }: ClusterProviderProps) {
|
||||||
const [state, dispatch] = React.useReducer(
|
const [state, dispatch] = React.useReducer(clusterReducer, {
|
||||||
clusterReducer,
|
cluster: DEFAULT_CLUSTER,
|
||||||
undefined,
|
customUrl: DEFAULT_CUSTOM_URL,
|
||||||
initState
|
status: ClusterStatus.Connecting
|
||||||
);
|
});
|
||||||
const [showModal, setShowModal] = React.useState(false);
|
const [showModal, setShowModal] = React.useState(false);
|
||||||
|
const { cluster, customUrl } = parseQuery(useQuery());
|
||||||
|
|
||||||
|
// Reconnect to cluster when it changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Connect to cluster immediately
|
updateCluster(dispatch, cluster, customUrl);
|
||||||
updateCluster(dispatch, state.cluster, state.customUrl);
|
}, [cluster, customUrl]);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
@ -170,7 +183,7 @@ export function clusterUrl(cluster: Cluster, customUrl: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCluster(
|
async function updateCluster(
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
cluster: Cluster,
|
cluster: Cluster,
|
||||||
customUrl: string
|
customUrl: string
|
||||||
@ -203,14 +216,6 @@ export function useCluster() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useClusterDispatch() {
|
|
||||||
const context = React.useContext(DispatchContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(`useClusterDispatch must be used within a ClusterProvider`);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useClusterModal() {
|
export function useClusterModal() {
|
||||||
const context = React.useContext(ModalContext);
|
const context = React.useContext(ModalContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
TransactionSignature,
|
TransactionSignature,
|
||||||
ConfirmedTransaction
|
ConfirmedTransaction
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { useCluster, ClusterStatus } from "../cluster";
|
import { useCluster } from "../cluster";
|
||||||
import { useTransactions, FetchStatus } from "./index";
|
import { useTransactions, FetchStatus } from "./index";
|
||||||
|
|
||||||
export interface Details {
|
export interface Details {
|
||||||
@ -94,7 +94,7 @@ export function DetailsProvider({ children }: DetailsProviderProps) {
|
|||||||
const [state, dispatch] = React.useReducer(reducer, {});
|
const [state, dispatch] = React.useReducer(reducer, {});
|
||||||
|
|
||||||
const { transactions } = useTransactions();
|
const { transactions } = useTransactions();
|
||||||
const { status, url } = useCluster();
|
const { url } = useCluster();
|
||||||
|
|
||||||
// Filter blocks for current transaction slots
|
// Filter blocks for current transaction slots
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -111,15 +111,13 @@ export function DetailsProvider({ children }: DetailsProviderProps) {
|
|||||||
removeSignatures.forEach(s => removeList.push(s));
|
removeSignatures.forEach(s => removeList.push(s));
|
||||||
dispatch({ type: ActionType.Remove, signatures: removeList });
|
dispatch({ type: ActionType.Remove, signatures: removeList });
|
||||||
|
|
||||||
if (status !== ClusterStatus.Connected) return;
|
|
||||||
|
|
||||||
const fetchList: string[] = [];
|
const fetchList: string[] = [];
|
||||||
fetchSignatures.forEach(s => fetchList.push(s));
|
fetchSignatures.forEach(s => fetchList.push(s));
|
||||||
dispatch({ type: ActionType.Add, signatures: fetchList });
|
dispatch({ type: ActionType.Add, signatures: fetchList });
|
||||||
fetchSignatures.forEach(signature => {
|
fetchSignatures.forEach(signature => {
|
||||||
fetchDetails(dispatch, signature, url);
|
fetchDetails(dispatch, signature, url);
|
||||||
});
|
});
|
||||||
}, [status, transactions]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
|
@ -6,8 +6,8 @@ import {
|
|||||||
Account,
|
Account,
|
||||||
SignatureResult
|
SignatureResult
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { findGetParameter } from "../../utils/url";
|
import { useQuery } from "../../utils/url";
|
||||||
import { useCluster, ClusterStatus } from "../cluster";
|
import { useCluster, Cluster } from "../cluster";
|
||||||
import {
|
import {
|
||||||
DetailsProvider,
|
DetailsProvider,
|
||||||
StateContext as DetailsStateContext,
|
StateContext as DetailsStateContext,
|
||||||
@ -20,7 +20,6 @@ import {
|
|||||||
Dispatch as AccountsDispatch,
|
Dispatch as AccountsDispatch,
|
||||||
ActionType as AccountsActionType
|
ActionType as AccountsActionType
|
||||||
} from "../accounts";
|
} from "../accounts";
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
export enum FetchStatus {
|
export enum FetchStatus {
|
||||||
Fetching,
|
Fetching,
|
||||||
@ -128,14 +127,12 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
|||||||
transactions: {}
|
transactions: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status, url } = useCluster();
|
const { cluster, url } = useCluster();
|
||||||
const accountsDispatch = useAccountsDispatch();
|
const accountsDispatch = useAccountsDispatch();
|
||||||
const search = useLocation().search;
|
const query = useQuery();
|
||||||
|
|
||||||
// Check transaction statuses whenever cluster updates
|
// Check transaction statuses whenever cluster updates
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status !== ClusterStatus.Connected) return;
|
|
||||||
|
|
||||||
Object.keys(state.transactions).forEach(signature => {
|
Object.keys(state.transactions).forEach(signature => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.FetchSignature,
|
type: ActionType.FetchSignature,
|
||||||
@ -146,19 +143,18 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create a test transaction
|
// Create a test transaction
|
||||||
if (findGetParameter("test") !== null) {
|
if (cluster === Cluster.Devnet && query.get("test") !== null) {
|
||||||
createTestTransaction(dispatch, accountsDispatch, url);
|
createTestTransaction(dispatch, accountsDispatch, url);
|
||||||
}
|
}
|
||||||
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [query, cluster, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Check for transactions in the url params
|
// Check for transactions in the url params
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
TX_ALIASES.flatMap(key =>
|
TX_ALIASES.flatMap(key => [query.get(key), query.get(key + "s")])
|
||||||
(findGetParameter(key)?.split(",") || []).concat(
|
.filter((value): value is string => value !== null)
|
||||||
findGetParameter(key + "s")?.split(",") || []
|
.flatMap(value => value.split(","))
|
||||||
)
|
// Remove duplicates
|
||||||
)
|
.filter((item, pos, self) => self.indexOf(item) === pos)
|
||||||
.flatMap(paramValue => paramValue?.split(",") || [])
|
|
||||||
.filter(signature => !state.transactions[signature])
|
.filter(signature => !state.transactions[signature])
|
||||||
.forEach(signature => {
|
.forEach(signature => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -168,7 +164,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
|||||||
});
|
});
|
||||||
checkTransactionStatus(dispatch, signature, url);
|
checkTransactionStatus(dispatch, signature, url);
|
||||||
});
|
});
|
||||||
}, [search]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [query]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
|
@ -12,15 +12,19 @@ code {
|
|||||||
|
|
||||||
.copyable {
|
.copyable {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: fit-content;
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
& > div:hover {
|
.popover {
|
||||||
cursor: pointer;
|
&.bs-popover-top {
|
||||||
}
|
background-color: $dark;
|
||||||
|
top: -4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.popover.bs-popover-top {
|
&.bs-popover-bottom {
|
||||||
background-color: $dark;
|
background-color: $dark;
|
||||||
top: -4rem;
|
top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.popover-body {
|
.popover-body {
|
||||||
color: white;
|
color: white;
|
||||||
@ -28,6 +32,7 @@ code {
|
|||||||
|
|
||||||
.arrow::after {
|
.arrow::after {
|
||||||
border-top-color: $dark;
|
border-top-color: $dark;
|
||||||
|
border-bottom-color: $dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
export function useQuery() {
|
||||||
|
return new URLSearchParams(useLocation().search);
|
||||||
|
}
|
||||||
|
|
||||||
export function findGetParameter(parameterName: string): string | null {
|
export function findGetParameter(parameterName: string): string | null {
|
||||||
let result = null,
|
let result = null,
|
||||||
tmp = [];
|
tmp = [];
|
||||||
|
Reference in New Issue
Block a user