Explorer: Move code to `solana-labs/explorer` (#30264)

This commit is contained in:
Noah Gundotra 2023-02-10 18:37:27 -05:00 committed by GitHub
parent 3c01f4dd76
commit 7b2e1769f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
360 changed files with 0 additions and 97307 deletions

28
explorer/.gitignore vendored
View File

@ -1,28 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/wasm/target
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*
yarn-error.log*
#comment

View File

@ -1,3 +0,0 @@
build
src/serumMarketRegistry.ts
package-lock.json

View File

@ -1,6 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}

View File

@ -1,81 +0,0 @@
<p align="center">
<img alt="Solana" src="https://i.imgur.com/IKyzQ6T.png" width="250" />
</p>
# Solana Explorer
## Development
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
# Disclaimer
All claims, content, designs, algorithms, estimates, roadmaps,
specifications, and performance measurements described in this project
are done with the Solana Foundation's ("SF") best efforts. It is up to
the reader to check and validate their accuracy and truthfulness.
Furthermore nothing in this project constitutes a solicitation for
investment.
Any content produced by SF or developer resources that SF provides, are
for educational and inspiration purposes only. SF does not encourage,
induce or sanction the deployment, integration or use of any such
applications (including the code comprising the Solana blockchain
protocol) in violation of applicable laws or regulations and hereby
prohibits any such deployment, integration or use. This includes use of
any such applications by the reader (a) in violation of export control
or sanctions laws of the United States or any other applicable
jurisdiction, (b) if the reader is located in or ordinarily resident in
a country or territory subject to comprehensive sanctions administered
by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the
reader is or is working on behalf of a Specially Designated National
(SDN) or a person subject to similar blocking or denied party
prohibitions.
The reader should be aware that U.S. export control and sanctions laws
prohibit U.S. persons (and other persons that are subject to such laws)
from transacting with persons in certain countries and territories or
that are on the SDN list. As a project based primarily on open-source
software, it is possible that such sanctioned persons may nevertheless
bypass prohibitions, obtain the code comprising the Solana blockchain
protocol (or other project code or applications) and deploy, integrate,
or otherwise use it. Accordingly, there is a risk to individuals that
other persons using the Solana blockchain protocol may be sanctioned
persons and that transactions with such persons would be a violation of
U.S. export controls and sanctions law. This risk applies to
individuals, organizations, and other ecosystem participants that
deploy, integrate, or use the Solana blockchain protocol code directly
(e.g., as a node operator), and individuals that transact on the Solana
blockchain through light clients, third party interfaces, and/or wallet
software.

48838
explorer/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,87 +0,0 @@
{
"name": "explorer",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"format": "prettier -c \"**/*.+(js|jsx|ts|tsx|json|css|md)\"",
"format:fix": "prettier --write \"**/*.+(js|jsx|ts|tsx|json|css|md)\""
},
"dependencies": {
"@blockworks-foundation/mango-client": "^3.6.7",
"@bonfida/spl-name-service": "^0.1.30",
"@cloudflare/stream-react": "^1.2.0",
"@metamask/jazzicon": "^2.0.0",
"@metaplex/js": "^4.12.0",
"@project-serum/anchor": "^0.23.0",
"@project-serum/serum": "^0.13.61",
"@react-hook/debounce": "^4.0.0",
"@sentry/react": "^7.6.0",
"@solana/buffer-layout": "^3.0.0",
"@solana/spl-token": "^0.0.13",
"@solana/spl-token-registry": "^0.2.3736",
"@solana/web3.js": "^1.66.0",
"axios": "^0.27.2",
"bignumber.js": "^9.0.2",
"bn.js": "^5.2.0",
"bootstrap": "^5.1.3",
"bs58": "^4.0.1",
"chart.js": "^2.9.4",
"classnames": "^2.3.1",
"coingecko-api": "^1.0.10",
"cross-fetch": "^3.1.4",
"humanize-duration-ts": "^2.1.1",
"p-limit": "^3.0.0",
"react": "^18.1.0",
"react-chartjs-2": "^2.11.2",
"react-content-loader": "^6.1.0",
"react-countup": "^6.4.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
"react-moment": "^1.1.1",
"react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3",
"react-select": "^4.3.1",
"superstruct": "^0.15.3",
"swr": "^1.3.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.3",
"@types/bn.js": "^5.1.0",
"@types/bs58": "^4.0.1",
"@types/chai": "^4.3.1",
"@types/chart.js": "^2.9.34",
"@types/classnames": "^2.3.1",
"@types/jest": "^28.1.4",
"@types/node": "^18.0.3",
"@types/react": "^18.0.8",
"@types/react-dom": "^18.0.6",
"@types/react-router-dom": "^5.3.3",
"@types/react-select": "^3.1.2",
"@types/socket.io-client": "^3.0.0",
"chai": "^4.3.6",
"prettier": "^2.7.1",
"sass": "^1.53.0",
"typescript": "^4.7.4"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Look up transactions and accounts on the various Solana clusters"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/rainbow192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-112467444-2"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-112467444-2');
</script>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Explorer | Solana</title>
<link
href="https://fonts.googleapis.com/css?family=Oswald|Rubik|Exo+2:300,400,700|Saira|Saira+Condensed|Saira+Extra+Condensed|Saira+Semi+Condensed&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,25 +0,0 @@
{
"short_name": "Solana Explorer",
"name": "Explorer for Solana clusters",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "rainbow192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "rainbow512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#75FBB4",
"background_color": "#ffffff"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,111 +0,0 @@
import React from "react";
import { Switch, Route, Redirect } from "react-router-dom";
import { ClusterModal } from "components/ClusterModal";
import { MessageBanner } from "components/MessageBanner";
import { Navbar } from "components/Navbar";
import { ClusterStatusBanner } from "components/ClusterStatusButton";
import { SearchBar } from "components/SearchBar";
import { AccountDetailsPage } from "pages/AccountDetailsPage";
import { TransactionInspectorPage } from "pages/inspector/InspectorPage";
import { ClusterStatsPage } from "pages/ClusterStatsPage";
import { SupplyPage } from "pages/SupplyPage";
import { TransactionDetailsPage } from "pages/TransactionDetailsPage";
import { BlockDetailsPage } from "pages/BlockDetailsPage";
import { EpochDetailsPage } from "pages/EpochDetailsPage";
const ADDRESS_ALIASES = ["account", "accounts", "addresses"];
const TX_ALIASES = ["txs", "txn", "txns", "transaction", "transactions"];
function App() {
return (
<>
<ClusterModal />
<div className="main-content pb-4">
<Navbar />
<MessageBanner />
<ClusterStatusBanner />
<SearchBar />
<Switch>
<Route exact path={["/supply", "/accounts", "accounts/top"]}>
<SupplyPage />
</Route>
<Route
exact
path={TX_ALIASES.map((tx) => `/${tx}/:signature`)}
render={({ match, location }) => {
let pathname = `/tx/${match.params.signature}`;
return <Redirect to={{ ...location, pathname }} />;
}}
/>
<Route
exact
path={["/tx/inspector", "/tx/:signature/inspect"]}
render={({ match }) => {
const signature =
"signature" in match.params
? match.params.signature
: undefined;
return <TransactionInspectorPage signature={signature} />;
}}
/>
<Route
exact
path={"/tx/:signature"}
render={({ match }) => (
<TransactionDetailsPage signature={match.params.signature} />
)}
/>
<Route
exact
path={"/epoch/:id"}
render={({ match }) => <EpochDetailsPage epoch={match.params.id} />}
/>
<Route
exact
path={["/block/:id", "/block/:id/:tab"]}
render={({ match }) => {
const tab = "tab" in match.params ? match.params.tab : undefined;
return <BlockDetailsPage slot={match.params.id} tab={tab} />;
}}
/>
<Route
exact
path={[
...ADDRESS_ALIASES.map((path) => `/${path}/:address`),
...ADDRESS_ALIASES.map((path) => `/${path}/:address/:tab`),
]}
render={({ match, location }) => {
let pathname = `/address/${match.params.address}`;
if (match.params.tab) {
pathname += `/${match.params.tab}`;
}
return <Redirect to={{ ...location, pathname }} />;
}}
/>
<Route
exact
path={["/address/:address", "/address/:address/:tab"]}
render={({ match }) => {
const tab = "tab" in match.params ? match.params.tab : undefined;
return (
<AccountDetailsPage address={match.params.address} tab={tab} />
);
}}
/>
<Route exact path="/">
<ClusterStatsPage />
</Route>
<Route
render={({ location }) => (
<Redirect to={{ ...location, pathname: "/" }} />
)}
/>
</Switch>
</div>
</>
);
}
export default App;

View File

@ -1,28 +0,0 @@
import { expect } from "chai";
import { lamportsToSol, LAMPORTS_PER_SOL } from "utils";
describe("lamportsToSol", () => {
it("0 lamports", () => {
expect(lamportsToSol(0)).to.eq(0.0);
expect(lamportsToSol(BigInt(0))).to.eq(0.0);
});
it("1 lamport", () => {
expect(lamportsToSol(1)).to.eq(0.000000001);
expect(lamportsToSol(BigInt(1))).to.eq(0.000000001);
expect(lamportsToSol(-1)).to.eq(-0.000000001);
expect(lamportsToSol(BigInt(-1))).to.eq(-0.000000001);
});
it("1 SOL", () => {
expect(lamportsToSol(LAMPORTS_PER_SOL)).to.eq(1.0);
expect(lamportsToSol(BigInt(LAMPORTS_PER_SOL))).to.eq(1.0);
expect(lamportsToSol(-LAMPORTS_PER_SOL)).to.eq(-1.0);
expect(lamportsToSol(BigInt(-LAMPORTS_PER_SOL))).to.eq(-1.0);
});
it("u64::MAX lamports", () => {
expect(lamportsToSol(2n ** 64n)).to.eq(18446744073.709551615);
expect(lamportsToSol(-(2n ** 64n))).to.eq(-18446744073.709551615);
});
});

View File

@ -1,36 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { expect } from "chai";
import { NFTOKEN_ADDRESS } from "../components/account/nftoken/nftoken";
import { parseNFTokenNFTAccount } from "../components/account/nftoken/isNFTokenAccount";
describe("parseNFTokenAccounts", () => {
it("parses an NFT", () => {
const buffer = new Uint8Array([
33, 180, 91, 53, 236, 15, 63, 97, 1, 13, 194, 212, 59, 127, 163, 1, 184,
232, 229, 196, 221, 132, 114, 202, 93, 251, 147, 255, 156, 194, 45, 162,
89, 138, 54, 129, 145, 16, 170, 225, 110, 171, 80, 175, 146, 42, 195, 197,
124, 142, 197, 32, 198, 20, 137, 26, 33, 27, 67, 163, 173, 127, 113, 232,
108, 17, 2, 184, 52, 59, 71, 87, 97, 1, 178, 138, 249, 251, 68, 1, 82,
163, 86, 56, 204, 21, 192, 126, 64, 94, 187, 81, 78, 188, 73, 85, 189,
140, 52, 199, 206, 30, 238, 117, 158, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 67, 0, 0, 0, 104, 116, 116, 112, 115, 58, 47, 47, 99, 100, 110, 46,
103, 108, 111, 119, 46, 97, 112, 112, 47, 110, 47, 56, 56, 47, 55, 56,
101, 102, 49, 55, 99, 49, 45, 50, 98, 53, 97, 45, 52, 54, 56, 101, 45, 97,
101, 56, 102, 45, 55, 52, 48, 51, 56, 53, 54, 101, 57, 102, 48, 48, 46,
106, 115, 111, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]);
const nftAccount = parseNFTokenNFTAccount({
pubkey: new PublicKey("FagABcRBhZH27JDtu6A1Jo9woXyoznP28QujLkxkN9Hj"),
space: buffer.length,
lamports: 1,
executable: false,
data: { raw: buffer as Buffer },
owner: new PublicKey(NFTOKEN_ADDRESS),
});
expect(nftAccount!.metadata_url).to.eq(
"https://cdn.glow.app/n/88/78ef17c1-2b5a-468e-ae8f-7403856e9f00.json"
);
});
});

View File

@ -1,194 +0,0 @@
import React, { ChangeEvent } from "react";
import { Link, useHistory, useLocation } from "react-router-dom";
import { useDebounceCallback } from "@react-hook/debounce";
import { Location } from "history";
import {
useCluster,
ClusterStatus,
clusterName,
clusterSlug,
CLUSTERS,
Cluster,
useClusterModal,
useUpdateCustomUrl,
} from "providers/cluster";
import { assertUnreachable, localStorageIsAvailable } from "../utils";
import { Overlay } from "./common/Overlay";
import { useQuery } from "utils/url";
export function ClusterModal() {
const [show, setShow] = useClusterModal();
const onClose = () => setShow(false);
const showDeveloperSettings = localStorageIsAvailable();
const enableCustomUrl =
showDeveloperSettings && localStorage.getItem("enableCustomUrl") !== null;
const onToggleCustomUrlFeature = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
localStorage.setItem("enableCustomUrl", "");
} else {
localStorage.removeItem("enableCustomUrl");
}
};
return (
<>
<div className={`offcanvas offcanvas-end${show ? " show" : ""}`}>
<div className="modal-body" onClick={(e) => e.stopPropagation()}>
<span className="c-pointer" onClick={onClose}>
&times;
</span>
<h2 className="text-center mb-4 mt-4">Choose a Cluster</h2>
<ClusterToggle />
{showDeveloperSettings && (
<>
<hr />
<h2 className="text-center mb-4 mt-4">Developer Settings</h2>
<div className="d-flex justify-content-between">
<span className="me-3">Enable custom url param</span>
<div className="form-check form-switch">
<input
type="checkbox"
defaultChecked={enableCustomUrl}
className="form-check-input"
id="cardToggle"
onChange={onToggleCustomUrlFeature}
/>
<label
className="form-check-label"
htmlFor="cardToggle"
></label>
</div>
</div>
<p className="text-muted font-size-sm mt-3">
Enable this setting to easily connect to a custom cluster via
the "customUrl" url param.
</p>
</>
)}
</div>
</div>
<div onClick={onClose}>
<Overlay show={show} />
</div>
</>
);
}
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 btnClass = active
? `border-${activeSuffix} text-${activeSuffix}`
: "btn-white";
const clusterLocation = (location: Location) => {
query.set("cluster", "custom");
if (customUrl.length > 0) {
query.set("customUrl", customUrl);
}
return {
...location,
search: query.toString(),
};
};
const onUrlInput = useDebounceCallback((url: string) => {
updateCustomUrl(url);
if (url.length > 0) {
query.set("customUrl", url);
history.push({ ...location, search: query.toString() });
}
}, 500);
const inputTextClass = editing ? "" : "text-muted";
return (
<>
<Link className={`btn col-12 mb-3 ${btnClass}`} to={clusterLocation}>
Custom RPC URL
</Link>
{active && (
<input
type="url"
defaultValue={customUrl}
className={`form-control ${inputTextClass}`}
onFocus={() => setEditing(true)}
onBlur={() => setEditing(false)}
onInput={(e) => onUrlInput(e.currentTarget.value)}
/>
)}
</>
);
}
function ClusterToggle() {
const { status, cluster } = useCluster();
let activeSuffix = "";
switch (status) {
case ClusterStatus.Connected:
activeSuffix = "primary";
break;
case ClusterStatus.Connecting:
activeSuffix = "warning";
break;
case ClusterStatus.Failure:
activeSuffix = "danger";
break;
default:
assertUnreachable(status);
}
return (
<div className="btn-group-toggle d-flex flex-wrap mb-4">
{CLUSTERS.map((net, index) => {
const active = net === cluster;
if (net === Cluster.Custom)
return (
<CustomClusterInput
key={index}
activeSuffix={activeSuffix}
active={active}
/>
);
const btnClass = active
? `border-${activeSuffix} text-${activeSuffix}`
: "btn-white";
const clusterLocation = (location: Location) => {
const params = new URLSearchParams(location.search);
const slug = clusterSlug(net);
if (slug !== "mainnet-beta") {
params.set("cluster", slug);
} else {
params.delete("cluster");
}
return {
...location,
search: params.toString(),
};
};
return (
<Link
key={index}
className={`btn col-12 mb-3 ${btnClass}`}
to={clusterLocation}
>
{clusterName(net)}
</Link>
);
})}
</div>
);
}

View File

@ -1,70 +0,0 @@
import React from "react";
import {
useCluster,
ClusterStatus,
Cluster,
useClusterModal,
} from "providers/cluster";
export function ClusterStatusBanner() {
const [, setShow] = useClusterModal();
return (
<div className="container d-md-none my-4">
<div onClick={() => setShow(true)}>
<Button />
</div>
</div>
);
}
export function ClusterStatusButton() {
const [, setShow] = useClusterModal();
return (
<div onClick={() => setShow(true)}>
<Button />
</div>
);
}
function Button() {
const { status, cluster, name, customUrl } = useCluster();
const statusName = cluster !== Cluster.Custom ? `${name}` : `${customUrl}`;
const btnClasses = (variant: string) => {
return `btn d-block btn-${variant}`;
};
const spinnerClasses = "spinner-grow spinner-grow-sm me-2";
switch (status) {
case ClusterStatus.Connected:
return (
<span className={btnClasses("primary")}>
<span className="fe fe-check-circle me-2"></span>
{statusName}
</span>
);
case ClusterStatus.Connecting:
return (
<span className={btnClasses("warning")}>
<span
className={spinnerClasses}
role="status"
aria-hidden="true"
></span>
{statusName}
</span>
);
case ClusterStatus.Failure:
return (
<span className={btnClasses("danger")}>
<span className="fe fe-alert-circle me-2"></span>
{statusName}
</span>
);
}
}

View File

@ -1,531 +0,0 @@
import React from "react";
import { Bar } from "react-chartjs-2";
import CountUp from "react-countup";
import {
usePerformanceInfo,
PERF_UPDATE_SEC,
ClusterStatsStatus,
} from "providers/stats/solanaClusterStats";
import classNames from "classnames";
import { TableCardBody } from "components/common/TableCardBody";
import { ChartOptions, ChartTooltipModel } from "chart.js";
import { PerformanceInfo } from "providers/stats/solanaPerformanceInfo";
import { StatsNotReady } from "pages/ClusterStatsPage";
import {
PingInfo,
PingRollupInfo,
PingStatus,
useSolanaPingInfo,
} from "providers/stats/SolanaPingProvider";
type Series = "short" | "medium" | "long";
type SetSeries = (series: Series) => void;
const SERIES: Series[] = ["short", "medium", "long"];
const SERIES_INFO = {
short: {
label: (index: number) => index,
interval: "30m",
},
medium: {
label: (index: number) => index * 4,
interval: "2h",
},
long: {
label: (index: number) => index * 12,
interval: "6h",
},
};
export function LiveTransactionStatsCard() {
const [series, setSeries] = React.useState<Series>("short");
return (
<div className="card">
<div className="card-header">
<h4 className="card-header-title">Live Transaction Stats</h4>
</div>
<TpsCardBody series={series} setSeries={setSeries} />
<PingStatsCardBody series={series} setSeries={setSeries} />
</div>
);
}
function TpsCardBody({
series,
setSeries,
}: {
series: Series;
setSeries: SetSeries;
}) {
const performanceInfo = usePerformanceInfo();
if (performanceInfo.status !== ClusterStatsStatus.Ready) {
return (
<StatsNotReady
error={performanceInfo.status === ClusterStatsStatus.Error}
/>
);
}
return (
<TpsBarChart
performanceInfo={performanceInfo}
series={series}
setSeries={setSeries}
/>
);
}
const CUSTOM_TPS_TOOLTIP = function (
this: any,
tooltipModel: ChartTooltipModel
) {
// Tooltip Element
let tooltipEl = document.getElementById("chartjs-tooltip");
// Create element on first render
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.id = "chartjs-tooltip";
tooltipEl.innerHTML = `<div class="content"></div>`;
document.body.appendChild(tooltipEl);
}
// Hide if no tooltip
if (tooltipModel.opacity === 0) {
tooltipEl.style.opacity = "0";
return;
}
// Set Text
if (tooltipModel.body) {
const { label, value } = tooltipModel.dataPoints[0];
const tooltipContent = tooltipEl.querySelector("div");
if (tooltipContent) {
let innerHtml = `<div class="value">${value} TPS</div>`;
innerHtml += `<div class="label">${label}</div>`;
tooltipContent.innerHTML = innerHtml;
}
}
// Enable tooltip and set position
const canvas: Element = this._chart.canvas;
const position = canvas.getBoundingClientRect();
tooltipEl.style.opacity = "1";
tooltipEl.style.left =
position.left + window.pageXOffset + tooltipModel.caretX + "px";
tooltipEl.style.top =
position.top + window.pageYOffset + tooltipModel.caretY + "px";
};
const TPS_CHART_OPTIONS = (historyMaxTps: number): ChartOptions => {
return {
tooltips: {
intersect: false, // Show tooltip when cursor in between bars
enabled: false, // Hide default tooltip
custom: CUSTOM_TPS_TOOLTIP,
},
legend: {
display: false,
},
scales: {
xAxes: [
{
ticks: {
display: false,
},
gridLines: {
display: false,
},
},
],
yAxes: [
{
ticks: {
stepSize: 100,
fontSize: 10,
fontColor: "#EEE",
beginAtZero: true,
display: true,
suggestedMax: historyMaxTps,
},
gridLines: {
display: false,
},
},
],
},
animation: {
duration: 0, // general animation time
},
hover: {
animationDuration: 0, // duration of animations when hovering an item
},
responsiveAnimationDuration: 0, // animation duration after a resize
};
};
type TpsBarChartProps = {
performanceInfo: PerformanceInfo;
series: Series;
setSeries: SetSeries;
};
function TpsBarChart({ performanceInfo, series, setSeries }: TpsBarChartProps) {
const { perfHistory, avgTps, historyMaxTps } = performanceInfo;
const averageTps = Math.round(avgTps).toLocaleString("en-US");
const transactionCount = <AnimatedTransactionCount info={performanceInfo} />;
const seriesData = perfHistory[series];
const chartOptions = React.useMemo(
() => TPS_CHART_OPTIONS(historyMaxTps),
[historyMaxTps]
);
const seriesLength = seriesData.length;
const chartData: Chart.ChartData = {
labels: seriesData.map((val, i) => {
return `${SERIES_INFO[series].label(seriesLength - i)}min ago`;
}),
datasets: [
{
backgroundColor: "#00D192",
hoverBackgroundColor: "#00D192",
borderWidth: 0,
data: seriesData.map((val) => val || 0),
},
],
};
return (
<>
<TableCardBody>
<tr>
<td className="w-100">Transaction count</td>
<td className="text-lg-end font-monospace">{transactionCount} </td>
</tr>
<tr>
<td className="w-100">Transactions per second (TPS)</td>
<td className="text-lg-end font-monospace">{averageTps} </td>
</tr>
</TableCardBody>
<hr className="my-0" />
<div className="card-body py-3">
<div className="align-box-row align-items-start justify-content-between">
<div className="d-flex justify-content-between w-100">
<span className="mb-0 font-size-sm">TPS history</span>
<div className="font-size-sm">
{SERIES.map((key) => (
<button
key={key}
onClick={() => setSeries(key)}
className={classNames("btn btn-sm btn-white ms-2", {
active: series === key,
})}
>
{SERIES_INFO[key].interval}
</button>
))}
</div>
</div>
<div
id="perf-history"
className="mt-3 d-flex justify-content-end flex-row w-100"
>
<div className="w-100">
<Bar data={chartData} options={chartOptions} height={80} />
</div>
</div>
</div>
</div>
</>
);
}
function AnimatedTransactionCount({ info }: { info: PerformanceInfo }) {
const txCountRef = React.useRef(0);
const countUpRef = React.useRef({ start: 0, period: 0, lastUpdate: 0 });
const countUp = countUpRef.current;
const { transactionCount: txCount, avgTps } = info;
// Track last tx count to reset count up options
if (txCount !== txCountRef.current) {
if (countUp.lastUpdate > 0) {
// Since we overshoot below, calculate the elapsed value
// and start from there.
const elapsed = Date.now() - countUp.lastUpdate;
const elapsedPeriods = elapsed / (PERF_UPDATE_SEC * 1000);
countUp.start = Math.floor(
countUp.start + elapsedPeriods * countUp.period
);
// if counter gets ahead of actual count, just hold for a bit
// until txCount catches up (this will sometimes happen when a tab is
// sent to the background and/or connection drops)
countUp.period = Math.max(txCount - countUp.start, 1);
} else {
// Since this is the first tx count value, estimate the previous
// tx count in order to have a starting point for our animation
countUp.period = PERF_UPDATE_SEC * avgTps;
countUp.start = txCount - countUp.period;
}
countUp.lastUpdate = Date.now();
txCountRef.current = txCount;
}
// Overshoot the target tx count in case the next update is delayed
const COUNT_PERIODS = 3;
const countUpEnd = countUp.start + COUNT_PERIODS * countUp.period;
return (
<CountUp
start={countUp.start}
end={countUpEnd}
duration={PERF_UPDATE_SEC * COUNT_PERIODS}
delay={0}
useEasing={false}
preserveValue={true}
separator=","
/>
);
}
function PingStatsCardBody({
series,
setSeries,
}: {
series: Series;
setSeries: SetSeries;
}) {
const pingInfo = useSolanaPingInfo();
if (pingInfo.status !== PingStatus.Ready) {
return (
<PingStatsNotReady
error={pingInfo.status === PingStatus.Error}
retry={pingInfo.retry}
/>
);
}
return (
<PingBarChart pingInfo={pingInfo} series={series} setSeries={setSeries} />
);
}
type StatsNotReadyProps = { error: boolean; retry?: Function };
function PingStatsNotReady({ error, retry }: StatsNotReadyProps) {
if (error) {
return (
<div className="card-body text-center">
There was a problem loading solana ping stats.{" "}
{retry && (
<button
className="btn btn-white btn-sm"
onClick={() => {
retry();
}}
>
<span className="fe fe-refresh-cw me-2"></span>
Try Again
</button>
)}
</div>
);
}
return (
<div className="card-body text-center">
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</div>
);
}
const CUSTOM_PING_TOOLTIP = function (
this: any,
tooltipModel: ChartTooltipModel
) {
// Tooltip Element
let tooltipEl = document.getElementById("chartjs-tooltip");
// Create element on first render
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.id = "chartjs-tooltip";
tooltipEl.innerHTML = `<div class="content"></div>`;
document.body.appendChild(tooltipEl);
}
// Hide if no tooltip
if (tooltipModel.opacity === 0) {
tooltipEl.style.opacity = "0";
return;
}
// Set Text
if (tooltipModel.body) {
const { label } = tooltipModel.dataPoints[0];
const tooltipContent = tooltipEl.querySelector("div");
if (tooltipContent) {
tooltipContent.innerHTML = `${label}`;
}
}
// Enable tooltip and set position
const canvas: Element = this._chart.canvas;
const position = canvas.getBoundingClientRect();
tooltipEl.style.opacity = "1";
tooltipEl.style.left =
position.left + window.pageXOffset + tooltipModel.caretX + "px";
tooltipEl.style.top =
position.top + window.pageYOffset + tooltipModel.caretY + "px";
};
const PING_CHART_OPTIONS: ChartOptions = {
tooltips: {
intersect: false, // Show tooltip when cursor in between bars
enabled: false, // Hide default tooltip
custom: CUSTOM_PING_TOOLTIP,
},
legend: {
display: false,
},
scales: {
xAxes: [
{
ticks: {
display: false,
},
gridLines: {
display: false,
},
},
],
yAxes: [
{
ticks: {
stepSize: 100,
fontSize: 10,
fontColor: "#EEE",
beginAtZero: true,
display: true,
},
gridLines: {
display: false,
},
},
],
},
animation: {
duration: 0, // general animation time
},
hover: {
animationDuration: 0, // duration of animations when hovering an item
},
responsiveAnimationDuration: 0, // animation duration after a resize
};
function PingBarChart({
pingInfo,
series,
setSeries,
}: {
pingInfo: PingRollupInfo;
series: Series;
setSeries: SetSeries;
}) {
const seriesData = pingInfo[series] || [];
const maxMean = seriesData.reduce((a, b) => {
return Math.max(a, b.mean);
}, 0);
const seriesLength = seriesData.length;
const backgroundColor = (val: PingInfo) => {
if (val.submitted === 0) {
return "#08a274";
}
if (val.loss >= 0.25 && val.loss <= 0.5) {
return "#FFA500";
}
return val.loss > 0.5 ? "#f00" : "#00D192";
};
const chartData: Chart.ChartData = {
labels: seriesData.map((val, i) => {
if (val.submitted === 0) {
return `
<div class="label">
<p class="mb-0">Ping statistics unavailable</p>
${SERIES_INFO[series].label(seriesLength - i)}min ago
</div>
`;
}
return `
<div class="value">${val.mean} ms</div>
<div class="label">
<p class="mb-0">${val.confirmed} of ${val.submitted} confirmed</p>
${
val.loss
? `<p class="mb-0">${val.loss.toLocaleString(undefined, {
style: "percent",
minimumFractionDigits: 2,
})} loss</p>`
: ""
}
${SERIES_INFO[series].label(seriesLength - i)}min ago
</div>
`;
}),
datasets: [
{
minBarLength: 2,
backgroundColor: seriesData.map(backgroundColor),
hoverBackgroundColor: seriesData.map(backgroundColor),
borderWidth: 0,
data: seriesData.map((val) => {
if (val.submitted === 0) {
return maxMean * 0.5;
}
return val.mean || 0;
}),
},
],
};
return (
<div className="card-body py-3">
<div className="align-box-row align-items-start justify-content-between">
<div className="d-flex justify-content-between w-100">
<span className="mb-0 font-size-sm">Average Ping Time</span>
<div className="font-size-sm">
{SERIES.map((key) => (
<button
key={key}
onClick={() => setSeries(key)}
className={classNames("btn btn-sm btn-white ms-2", {
active: series === key,
})}
>
{SERIES_INFO[key].interval}
</button>
))}
</div>
</div>
<div
id="perf-history"
className="mt-3 d-flex justify-content-end flex-row w-100"
>
<div className="w-100">
<Bar data={chartData} options={PING_CHART_OPTIONS} height={80} />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,78 +0,0 @@
import React from "react";
import { useCluster, Cluster } from "providers/cluster";
import { displayTimestamp } from "utils/date";
type Announcement = {
message: string;
estimate?: string;
start?: Date;
end?: Date;
};
const announcements = new Map<Cluster, Announcement>();
// announcements.set(Cluster.Devnet, {
// message: "Devnet API node is restarting",
// start: new Date("July 25, 2020 18:00:00 GMT+8:00"),
// estimate: "2 hours",
// });
// announcements.set(Cluster.MainnetBeta, {
// message: "Mainnet Beta upgrade in progress. Transactions disabled until epoch 62",
// start: new Date("August 2, 2020 00:00:00 GMT+0:00"),
// end: new Date("August 4, 2020 00:00:00 GMT+0:00"),
// });
// announcements.set(Cluster.MainnetBeta, {
// message:
// "Mainnet Beta upgrade in progress. Transactions disabled until epoch 62",
// });
export function MessageBanner() {
const cluster = useCluster().cluster;
const announcement = announcements.get(cluster);
if (!announcement) return null;
const { message, start, end, estimate } = announcement;
const now = new Date();
if (end && now > end) return null;
if (start && now < start) return null;
let timeframe;
if (estimate || start || end) {
timeframe = (
<div>
<hr className="text-gray-500 w-100 my-3 opacity-50" />
{estimate && (
<h5 className="font-sm text-gray-200">
<span className="text-uppercase">Estimated Duration: </span>
{estimate}
</h5>
)}
{start && (
<h5 className="font-sm text-gray-200">
<span className="text-uppercase">Started at: </span>
{displayTimestamp(start.getTime())}
</h5>
)}
{end && (
<h5 className="font-sm text-gray-200">
<span className="text-uppercase">End: </span>
{displayTimestamp(end.getTime())}
</h5>
)}
</div>
);
}
return (
<div className="bg-info">
<div className="container">
<div className="d-flex flex-column align-items-center justify-content-center text-center py-3">
<h3 className="mb-0 line-height-md">
<span className="fe fe-alert-circle me-2"></span>
{message}
</h3>
{timeframe}
</div>
</div>
</div>
);
}

View File

@ -1,56 +0,0 @@
import React from "react";
import Logo from "img/logos-solana/dark-explorer-logo.svg";
import { clusterPath } from "utils/url";
import { Link, NavLink } from "react-router-dom";
import { ClusterStatusButton } from "components/ClusterStatusButton";
export function Navbar() {
// TODO: use `collapsing` to animate collapsible navbar
const [collapse, setCollapse] = React.useState(false);
return (
<nav className="navbar navbar-expand-md navbar-light">
<div className="container">
<Link to={clusterPath("/")}>
<img src={Logo} width="250" alt="Solana Explorer" />
</Link>
<button
className="navbar-toggler"
type="button"
onClick={() => setCollapse((value) => !value)}
>
<span className="navbar-toggler-icon"></span>
</button>
<div
className={`collapse navbar-collapse ms-auto me-4 ${
collapse ? "show" : ""
}`}
>
<ul className="navbar-nav me-auto">
<li className="nav-item">
<NavLink className="nav-link" to={clusterPath("/")} exact>
Cluster Stats
</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to={clusterPath("/supply")}>
Supply
</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to={clusterPath("/tx/inspector")}>
Inspector
</NavLink>
</li>
</ul>
</div>
<div className="d-none d-md-block">
<ClusterStatusButton />
</div>
</div>
</nav>
);
}

View File

@ -1,102 +0,0 @@
import { ParsedMessage, PublicKey, VersionedMessage } from "@solana/web3.js";
import { Cluster } from "providers/cluster";
import { TableCardBody } from "components/common/TableCardBody";
import { InstructionLogs } from "utils/program-logs";
import { ProgramName } from "utils/anchor";
import React from "react";
import { Link } from "react-router-dom";
import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id";
const NATIVE_PROGRAMS_MISSING_INVOKE_LOG: string[] = [
"AddressLookupTab1e1111111111111111111111111",
"ZkTokenProof1111111111111111111111111111111",
"BPFLoader1111111111111111111111111111111111",
"BPFLoader2111111111111111111111111111111111",
"BPFLoaderUpgradeab1e11111111111111111111111",
];
export function ProgramLogsCardBody({
message,
logs,
cluster,
url,
}: {
message: VersionedMessage | ParsedMessage;
logs: InstructionLogs[];
cluster: Cluster;
url: string;
}) {
let logIndex = 0;
let instructionProgramIds: PublicKey[];
if ("compiledInstructions" in message) {
instructionProgramIds = message.compiledInstructions.map((ix) => {
return message.staticAccountKeys[ix.programIdIndex];
});
} else {
instructionProgramIds = message.instructions.map((ix) => ix.programId);
}
return (
<TableCardBody>
{instructionProgramIds.map((programId, index) => {
const programAddress = programId.toBase58();
let programLogs: InstructionLogs | undefined = logs[logIndex];
if (programLogs?.invokedProgram === programAddress) {
logIndex++;
} else if (
programLogs?.invokedProgram === null &&
programLogs.logs.length > 0 &&
NATIVE_PROGRAMS_MISSING_INVOKE_LOG.includes(programAddress)
) {
logIndex++;
} else {
programLogs = undefined;
}
let badgeColor = "white";
if (programLogs) {
badgeColor = programLogs.failed ? "warning" : "success";
}
return (
<tr key={index}>
<td>
<Link
className="d-flex align-items-center"
to={(location) => ({
...location,
hash: `#${getInstructionCardScrollAnchorId([index + 1])}`,
})}
>
<span className={`badge bg-${badgeColor}-soft me-2`}>
#{index + 1}
</span>
<span className="program-log-instruction-name">
<ProgramName
programId={programId}
cluster={cluster}
url={url}
/>{" "}
Instruction
</span>
<span className="fe fe-chevrons-up c-pointer px-2" />
</Link>
{programLogs && (
<div className="d-flex align-items-start flex-column font-monospace p-2 font-size-sm">
{programLogs.logs.map((log, key) => {
return (
<span key={key}>
<span className="text-muted">{log.prefix}</span>
<span className={`text-${log.style}`}>{log.text}</span>
</span>
);
})}
</div>
)}
</td>
</tr>
);
})}
</TableCardBody>
);
}

View File

@ -1,393 +0,0 @@
import React from "react";
import bs58 from "bs58";
import { useHistory, useLocation } from "react-router-dom";
import Select, { InputActionMeta, ActionMeta, ValueType } from "react-select";
import StateManager from "react-select";
import {
LOADER_IDS,
PROGRAM_INFO_BY_ID,
SPECIAL_IDS,
SYSVAR_IDS,
LoaderName,
} from "utils/tx";
import { Cluster, useCluster } from "providers/cluster";
import { useTokenRegistry } from "providers/mints/token-registry";
import { TokenInfoMap } from "@solana/spl-token-registry";
import { Connection } from "@solana/web3.js";
import { getDomainInfo, hasDomainSyntax } from "utils/name-service";
interface SearchOptions {
label: string;
options: {
label: string;
value: string[];
pathname: string;
}[];
}
export function SearchBar() {
const [search, setSearch] = React.useState("");
const searchRef = React.useRef("");
const [searchOptions, setSearchOptions] = React.useState<SearchOptions[]>([]);
const [loadingSearch, setLoadingSearch] = React.useState<boolean>(false);
const [loadingSearchMessage, setLoadingSearchMessage] =
React.useState<string>("loading...");
const selectRef = React.useRef<StateManager<any> | null>(null);
const history = useHistory();
const location = useLocation();
const { tokenRegistry } = useTokenRegistry();
const { url, cluster, clusterInfo } = useCluster();
const onChange = (
{ pathname }: ValueType<any, false>,
meta: ActionMeta<any>
) => {
if (meta.action === "select-option") {
history.push({ ...location, pathname });
setSearch("");
}
};
const onInputChange = (value: string, { action }: InputActionMeta) => {
if (action === "input-change") {
setSearch(value);
}
};
React.useEffect(() => {
searchRef.current = search;
setLoadingSearchMessage("Loading...");
setLoadingSearch(true);
// builds and sets local search output
const options = buildOptions(
search,
cluster,
tokenRegistry,
clusterInfo?.epochInfo.epoch
);
setSearchOptions(options);
// checking for non local search output
if (hasDomainSyntax(search)) {
// if search input is a potential domain we continue the loading state
domainSearch(options);
} else {
// if search input is not a potential domain we can conclude the search has finished
setLoadingSearch(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search]);
// appends domain lookup results to the local search state
const domainSearch = async (options: SearchOptions[]) => {
setLoadingSearchMessage("Looking up domain...");
const connection = new Connection(url);
const searchTerm = search;
const updatedOptions = await buildDomainOptions(
connection,
search,
options
);
if (searchRef.current === searchTerm) {
setSearchOptions(updatedOptions);
// after attempting to fetch the domain name we can conclude the loading state
setLoadingSearch(false);
setLoadingSearchMessage("Loading...");
}
};
const resetValue = "" as any;
return (
<div className="container my-4">
<div className="row align-items-center">
<div className="col">
<Select
autoFocus
ref={(ref) => (selectRef.current = ref)}
options={searchOptions}
noOptionsMessage={() => "No Results"}
loadingMessage={() => loadingSearchMessage}
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
value={resetValue}
inputValue={search}
blurInputOnSelect
onMenuClose={() => selectRef.current?.blur()}
onChange={onChange}
styles={{
/* work around for https://github.com/JedWatson/react-select/issues/3857 */
placeholder: (style) => ({ ...style, pointerEvents: "none" }),
input: (style) => ({ ...style, width: "100%" }),
}}
onInputChange={onInputChange}
components={{ DropdownIndicator }}
classNamePrefix="search-bar"
isLoading={loadingSearch}
/>
</div>
</div>
</div>
);
}
function buildProgramOptions(search: string, cluster: Cluster) {
const matchedPrograms = Object.entries(PROGRAM_INFO_BY_ID).filter(
([address, { name, deployments }]) => {
if (!deployments.includes(cluster)) return false;
return (
name.toLowerCase().includes(search.toLowerCase()) ||
address.includes(search)
);
}
);
if (matchedPrograms.length > 0) {
return {
label: "Programs",
options: matchedPrograms.map(([address, { name }]) => ({
label: name,
value: [name, address],
pathname: "/address/" + address,
})),
};
}
}
const SEARCHABLE_LOADERS: LoaderName[] = [
"BPF Loader",
"BPF Loader 2",
"BPF Upgradeable Loader",
];
function buildLoaderOptions(search: string) {
const matchedLoaders = Object.entries(LOADER_IDS).filter(
([address, name]) => {
return (
SEARCHABLE_LOADERS.includes(name) &&
(name.toLowerCase().includes(search.toLowerCase()) ||
address.includes(search))
);
}
);
if (matchedLoaders.length > 0) {
return {
label: "Program Loaders",
options: matchedLoaders.map(([id, name]) => ({
label: name,
value: [name, id],
pathname: "/address/" + id,
})),
};
}
}
function buildSysvarOptions(search: string) {
const matchedSysvars = Object.entries(SYSVAR_IDS).filter(
([address, name]) => {
return (
name.toLowerCase().includes(search.toLowerCase()) ||
address.includes(search)
);
}
);
if (matchedSysvars.length > 0) {
return {
label: "Sysvars",
options: matchedSysvars.map(([id, name]) => ({
label: name,
value: [name, id],
pathname: "/address/" + id,
})),
};
}
}
function buildSpecialOptions(search: string) {
const matchedSpecialIds = Object.entries(SPECIAL_IDS).filter(
([address, name]) => {
return (
name.toLowerCase().includes(search.toLowerCase()) ||
address.includes(search)
);
}
);
if (matchedSpecialIds.length > 0) {
return {
label: "Accounts",
options: matchedSpecialIds.map(([id, name]) => ({
label: name,
value: [name, id],
pathname: "/address/" + id,
})),
};
}
}
function buildTokenOptions(
search: string,
cluster: Cluster,
tokenRegistry: TokenInfoMap
) {
const matchedTokens = Array.from(tokenRegistry.entries()).filter(
([address, details]) => {
const searchLower = search.toLowerCase();
return (
details.name.toLowerCase().includes(searchLower) ||
details.symbol.toLowerCase().includes(searchLower) ||
address.includes(search)
);
}
);
if (matchedTokens.length > 0) {
return {
label: "Tokens",
options: matchedTokens.slice(0, 10).map(([id, details]) => ({
label: details.name,
value: [details.name, details.symbol, id],
pathname: "/address/" + id,
})),
};
}
}
async function buildDomainOptions(
connection: Connection,
search: string,
options: SearchOptions[]
) {
const domainInfo = await getDomainInfo(search, connection);
const updatedOptions: SearchOptions[] = [...options];
if (domainInfo && domainInfo.owner && domainInfo.address) {
updatedOptions.push({
label: "Domain Owner",
options: [
{
label: domainInfo.owner,
value: [search],
pathname: "/address/" + domainInfo.owner,
},
],
});
updatedOptions.push({
label: "Name Service Account",
options: [
{
label: search,
value: [search],
pathname: "/address/" + domainInfo.address,
},
],
});
}
return updatedOptions;
}
// builds local search options
function buildOptions(
rawSearch: string,
cluster: Cluster,
tokenRegistry: TokenInfoMap,
currentEpoch?: number
) {
const search = rawSearch.trim();
if (search.length === 0) return [];
const options = [];
const programOptions = buildProgramOptions(search, cluster);
if (programOptions) {
options.push(programOptions);
}
const loaderOptions = buildLoaderOptions(search);
if (loaderOptions) {
options.push(loaderOptions);
}
const sysvarOptions = buildSysvarOptions(search);
if (sysvarOptions) {
options.push(sysvarOptions);
}
const specialOptions = buildSpecialOptions(search);
if (specialOptions) {
options.push(specialOptions);
}
const tokenOptions = buildTokenOptions(search, cluster, tokenRegistry);
if (tokenOptions) {
options.push(tokenOptions);
}
if (!isNaN(Number(search))) {
options.push({
label: "Block",
options: [
{
label: `Slot #${search}`,
value: [search],
pathname: `/block/${search}`,
},
],
});
if (currentEpoch !== undefined && Number(search) <= currentEpoch + 1) {
options.push({
label: "Epoch",
options: [
{
label: `Epoch #${search}`,
value: [search],
pathname: `/epoch/${search}`,
},
],
});
}
}
// Prefer nice suggestions over raw suggestions
if (options.length > 0) return options;
try {
const decoded = bs58.decode(search);
if (decoded.length === 32) {
options.push({
label: "Account",
options: [
{
label: search,
value: [search],
pathname: "/address/" + search,
},
],
});
} else if (decoded.length === 64) {
options.push({
label: "Transaction",
options: [
{
label: search,
value: [search],
pathname: "/tx/" + search,
},
],
});
}
} catch (err) {}
return options;
}
function DropdownIndicator() {
return (
<div className="search-indicator">
<span className="fe fe-search"></span>
</div>
);
}

View File

@ -1,74 +0,0 @@
import React from "react";
import { useSupply, useFetchSupply, Status } from "providers/supply";
import { LoadingCard } from "./common/LoadingCard";
import { ErrorCard } from "./common/ErrorCard";
import { SolBalance } from "components/common/SolBalance";
import { TableCardBody } from "./common/TableCardBody";
export function SupplyCard() {
const supply = useSupply();
const fetchSupply = useFetchSupply();
// Fetch supply on load
React.useEffect(() => {
if (supply === Status.Idle) fetchSupply();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (supply === Status.Disconnected) {
return <ErrorCard text="Not connected to the cluster" />;
}
if (supply === Status.Idle || supply === Status.Connecting)
return <LoadingCard />;
if (typeof supply === "string") {
return <ErrorCard text={supply} retry={fetchSupply} />;
}
return (
<div className="card">
{renderHeader()}
<TableCardBody>
<tr>
<td className="w-100">Total Supply (SOL)</td>
<td className="text-lg-end">
<SolBalance lamports={supply.total} maximumFractionDigits={0} />
</td>
</tr>
<tr>
<td className="w-100">Circulating Supply (SOL)</td>
<td className="text-lg-end">
<SolBalance
lamports={supply.circulating}
maximumFractionDigits={0}
/>
</td>
</tr>
<tr>
<td className="w-100">Non-Circulating Supply (SOL)</td>
<td className="text-lg-end">
<SolBalance
lamports={supply.nonCirculating}
maximumFractionDigits={0}
/>
</td>
</tr>
</TableCardBody>
</div>
);
}
const renderHeader = () => {
return (
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Supply Overview</h4>
</div>
</div>
</div>
);
};

View File

@ -1,221 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { Location } from "history";
import { AccountBalancePair } from "@solana/web3.js";
import { useRichList, useFetchRichList, Status } from "providers/richList";
import { LoadingCard } from "./common/LoadingCard";
import { ErrorCard } from "./common/ErrorCard";
import { SolBalance } from "components/common/SolBalance";
import { useQuery } from "utils/url";
import { useSupply } from "providers/supply";
import { Address } from "./common/Address";
type Filter = "circulating" | "nonCirculating" | "all" | null;
export function TopAccountsCard() {
const supply = useSupply();
const richList = useRichList();
const fetchRichList = useFetchRichList();
const [showDropdown, setDropdown] = React.useState(false);
const filter = useQueryFilter();
if (typeof supply !== "object") return null;
if (richList === Status.Disconnected) {
return <ErrorCard text="Not connected to the cluster" />;
}
if (richList === Status.Connecting) {
return <LoadingCard />;
}
if (typeof richList === "string") {
return <ErrorCard text={richList} retry={fetchRichList} />;
}
let supplyCount: number;
let accounts, header;
if (richList !== Status.Idle) {
switch (filter) {
case "nonCirculating": {
accounts = richList.nonCirculating;
supplyCount = supply.nonCirculating;
header = "Non-Circulating";
break;
}
case "all": {
accounts = richList.total;
supplyCount = supply.total;
header = "Total";
break;
}
case "circulating":
default: {
accounts = richList.circulating;
supplyCount = supply.circulating;
header = "Circulating";
break;
}
}
}
return (
<>
{showDropdown && (
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
)}
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Largest Accounts</h4>
</div>
<div className="col-auto">
<FilterDropdown
filter={filter}
toggle={() => setDropdown((show) => !show)}
show={showDropdown}
/>
</div>
</div>
</div>
{richList === Status.Idle && (
<div className="card-body">
<span
className="btn btn-white ms-3 d-none d-md-inline"
onClick={fetchRichList}
>
Load Largest Accounts
</span>
</div>
)}
{accounts && (
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Rank</th>
<th className="text-muted">Address</th>
<th className="text-muted text-end">Balance (SOL)</th>
<th className="text-muted text-end">% of {header} Supply</th>
</tr>
</thead>
<tbody className="list">
{accounts.map((account, index) =>
renderAccountRow(account, index, supplyCount)
)}
</tbody>
</table>
</div>
)}
</div>
</>
);
}
const renderAccountRow = (
account: AccountBalancePair,
index: number,
supply: number
) => {
return (
<tr key={index}>
<td>
<span className="badge bg-gray-soft badge-pill">{index + 1}</span>
</td>
<td>
<Address pubkey={account.address} link />
</td>
<td className="text-end">
<SolBalance lamports={account.lamports} maximumFractionDigits={0} />
</td>
<td className="text-end">{`${((100 * account.lamports) / supply).toFixed(
3
)}%`}</td>
</tr>
);
};
const useQueryFilter = (): Filter => {
const query = useQuery();
const filter = query.get("filter");
if (
filter === "circulating" ||
filter === "nonCirculating" ||
filter === "all"
) {
return filter;
} else {
return null;
}
};
const filterTitle = (filter: Filter): string => {
switch (filter) {
case "nonCirculating": {
return "Non-Circulating";
}
case "all": {
return "All";
}
case "circulating":
default: {
return "Circulating";
}
}
};
type DropdownProps = {
filter: Filter;
toggle: () => void;
show: boolean;
};
const FilterDropdown = ({ filter, toggle, show }: DropdownProps) => {
const buildLocation = (location: Location, filter: Filter) => {
const params = new URLSearchParams(location.search);
if (filter === null) {
params.delete("filter");
} else {
params.set("filter", filter);
}
return {
...location,
search: params.toString(),
};
};
const FILTERS: Filter[] = ["all", null, "nonCirculating"];
return (
<div className="dropdown">
<button
className="btn btn-white btn-sm dropdown-toggle"
type="button"
onClick={toggle}
>
{filterTitle(filter)}
</button>
<div className={`dropdown-menu-end dropdown-menu${show ? " show" : ""}`}>
{FILTERS.map((filterOption) => {
return (
<Link
key={filterOption || "null"}
to={(location) => buildLocation(location, filterOption)}
className={`dropdown-item${
filterOption === filter ? " active" : ""
}`}
onClick={toggle}
>
{filterTitle(filterOption)}
</Link>
);
})}
</div>
</div>
);
};

View File

@ -1,86 +0,0 @@
import React, { useMemo } from "react";
import { Account } from "providers/accounts";
import { useCluster } from "providers/cluster";
import { BorshAccountsCoder } from "@project-serum/anchor";
import { IdlTypeDef } from "@project-serum/anchor/dist/cjs/idl";
import { getAnchorProgramName, mapAccountToRows } from "utils/anchor";
import { ErrorCard } from "components/common/ErrorCard";
import { useAnchorProgram } from "providers/anchor";
export function AnchorAccountCard({ account }: { account: Account }) {
const { lamports } = account;
const { url } = useCluster();
const anchorProgram = useAnchorProgram(account.owner.toString(), url);
const rawData = account.data.raw;
const programName = getAnchorProgramName(anchorProgram) || "Unknown Program";
const { decodedAccountData, accountDef } = useMemo(() => {
let decodedAccountData: any | null = null;
let accountDef: IdlTypeDef | undefined = undefined;
if (anchorProgram && rawData) {
const coder = new BorshAccountsCoder(anchorProgram.idl);
const accountDefTmp = anchorProgram.idl.accounts?.find(
(accountType: any) =>
(rawData as Buffer)
.slice(0, 8)
.equals(BorshAccountsCoder.accountDiscriminator(accountType.name))
);
if (accountDefTmp) {
accountDef = accountDefTmp;
try {
decodedAccountData = coder.decode(accountDef.name, rawData);
} catch (err) {
console.log(err);
}
}
}
return {
decodedAccountData,
accountDef,
};
}, [anchorProgram, rawData]);
if (lamports === undefined) return null;
if (!anchorProgram) return <ErrorCard text="No Anchor IDL found" />;
if (!decodedAccountData || !accountDef) {
return (
<ErrorCard text="Failed to decode account data according to the public Anchor interface" />
);
}
return (
<div>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">
{programName}: {accountDef.name}
</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1">Field</th>
<th className="w-1">Type</th>
<th className="w-1">Value</th>
</tr>
</thead>
<tbody>
{mapAccountToRows(
decodedAccountData,
accountDef as IdlTypeDef,
anchorProgram.idl
)}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@ -1,36 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { useAnchorProgram } from "providers/anchor";
import { useCluster } from "providers/cluster";
import ReactJson from "react-json-view";
export function AnchorProgramCard({ programId }: { programId: PublicKey }) {
const { url } = useCluster();
const program = useAnchorProgram(programId.toString(), url);
if (!program) {
return null;
}
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Anchor IDL</h3>
</div>
</div>
</div>
<div className="card metadata-json-viewer m-4">
<ReactJson
src={program.idl}
theme={"solarized"}
style={{ padding: 25 }}
collapsed={1}
/>
</div>
</div>
</>
);
}

View File

@ -1,61 +0,0 @@
import React from "react";
import {
RecentBlockhashesInfo,
RecentBlockhashesEntry,
} from "validators/accounts/sysvar";
export function BlockhashesCard({
blockhashes,
}: {
blockhashes: RecentBlockhashesInfo;
}) {
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Blockhashes</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Recency</th>
<th className="w-1 text-muted">Blockhash</th>
<th className="text-muted">Fee Calculator</th>
</tr>
</thead>
<tbody className="list">
{blockhashes.length > 0 &&
blockhashes.map((entry: RecentBlockhashesEntry, index) => {
return renderAccountRow(entry, index);
})}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{blockhashes.length > 0 ? "" : "No blockhashes found"}
</div>
</div>
</div>
</>
);
}
const renderAccountRow = (entry: RecentBlockhashesEntry, index: number) => {
return (
<tr key={index}>
<td className="w-1">{index + 1}</td>
<td className="w-1 font-monospace">{entry.blockhash}</td>
<td className="">
{entry.feeCalculator.lamportsPerSignature} lamports per signature
</td>
</tr>
);
};

View File

@ -1,158 +0,0 @@
import React from "react";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { TableCardBody } from "components/common/TableCardBody";
import {
ConfigAccount,
StakeConfigInfoAccount,
ValidatorInfoAccount,
} from "validators/accounts/config";
import {
AccountAddressRow,
AccountBalanceRow,
AccountHeader,
} from "components/common/Account";
import { PublicKey } from "@solana/web3.js";
import { Address } from "components/common/Address";
const MAX_SLASH_PENALTY = Math.pow(2, 8);
export function ConfigAccountSection({
account,
configAccount,
}: {
account: Account;
configAccount: ConfigAccount;
}) {
switch (configAccount.type) {
case "stakeConfig":
return (
<StakeConfigCard account={account} configAccount={configAccount} />
);
case "validatorInfo":
return (
<ValidatorInfoCard account={account} configAccount={configAccount} />
);
}
}
function StakeConfigCard({
account,
configAccount,
}: {
account: Account;
configAccount: StakeConfigInfoAccount;
}) {
const refresh = useFetchAccountInfo();
const warmupCooldownFormatted = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 2,
}).format(configAccount.info.warmupCooldownRate);
const slashPenaltyFormatted = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 2,
}).format(configAccount.info.slashPenalty / MAX_SLASH_PENALTY);
return (
<div className="card">
<AccountHeader
title="Stake Config"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Warmup / Cooldown Rate</td>
<td className="text-lg-end">{warmupCooldownFormatted}</td>
</tr>
<tr>
<td>Slash Penalty</td>
<td className="text-lg-end">{slashPenaltyFormatted}</td>
</tr>
</TableCardBody>
</div>
);
}
function ValidatorInfoCard({
account,
configAccount,
}: {
account: Account;
configAccount: ValidatorInfoAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Validator Info"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
{configAccount.info.configData.name && (
<tr>
<td>Name</td>
<td className="text-lg-end">
{configAccount.info.configData.name}
</td>
</tr>
)}
{configAccount.info.configData.keybaseUsername && (
<tr>
<td>Keybase Username</td>
<td className="text-lg-end">
{configAccount.info.configData.keybaseUsername}
</td>
</tr>
)}
{configAccount.info.configData.website && (
<tr>
<td>Website</td>
<td className="text-lg-end">
<a
href={configAccount.info.configData.website}
target="_blank"
rel="noopener noreferrer"
>
{configAccount.info.configData.website}
</a>
</td>
</tr>
)}
{configAccount.info.configData.details && (
<tr>
<td>Details</td>
<td className="text-lg-end">
{configAccount.info.configData.details}
</td>
</tr>
)}
{configAccount.info.keys && configAccount.info.keys.length > 1 && (
<tr>
<td>Signer</td>
<td className="text-lg-end">
<Address
pubkey={new PublicKey(configAccount.info.keys[1].pubkey)}
link
alignRight
/>
</td>
</tr>
)}
</TableCardBody>
</div>
);
}

View File

@ -1,57 +0,0 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
import { useUserDomains, DomainInfo } from "../../utils/name-service";
import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
import { Address } from "components/common/Address";
export function DomainsCard({ pubkey }: { pubkey: PublicKey }) {
const [domains, domainsLoading] = useUserDomains(pubkey);
if (domainsLoading && (!domains || domains.length === 0)) {
return <LoadingCard message="Loading domains" />;
} else if (!domains) {
return <ErrorCard text="Failed to fetch domains" />;
}
if (domains.length === 0) {
return <ErrorCard text="No domain name found" />;
}
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Owned Domain Names</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Domain Name</th>
<th className="text-muted">Name Service Account</th>
</tr>
</thead>
<tbody className="list">
{domains.map((domain) => (
<RenderDomainRow
key={domain.address.toBase58()}
domainInfo={domain}
/>
))}
</tbody>
</table>
</div>
</div>
);
}
function RenderDomainRow({ domainInfo }: { domainInfo: DomainInfo }) {
return (
<tr>
<td>{domainInfo.name}</td>
<td>
<Address pubkey={domainInfo.address} link />
</td>
</tr>
);
}

View File

@ -1,116 +0,0 @@
import React from "react";
import { ConfirmedSignatureInfo, TransactionError } from "@solana/web3.js";
export type TransactionRow = {
slot: number;
signature: string;
err: TransactionError | null;
blockTime: number | null | undefined;
statusClass: string;
statusText: string;
signatureInfo: ConfirmedSignatureInfo;
};
export function HistoryCardHeader({
title,
refresh,
fetching,
}: {
title: string;
refresh: Function;
fetching: boolean;
}) {
return (
<div className="card-header align-items-center">
<h3 className="card-header-title">{title}</h3>
<button
className="btn btn-white btn-sm"
disabled={fetching}
onClick={() => refresh()}
>
{fetching ? (
<>
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</>
) : (
<>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</>
)}
</button>
</div>
);
}
export function HistoryCardFooter({
fetching,
foundOldest,
loadMore,
}: {
fetching: boolean;
foundOldest: boolean;
loadMore: Function;
}) {
return (
<div className="card-footer">
{foundOldest ? (
<div className="text-muted text-center">Fetched full history</div>
) : (
<button
className="btn btn-primary w-100"
onClick={() => loadMore()}
disabled={fetching}
>
{fetching ? (
<>
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</>
) : (
"Load More"
)}
</button>
)}
</div>
);
}
export function getTransactionRows(
transactions: ConfirmedSignatureInfo[]
): TransactionRow[] {
const transactionRows: TransactionRow[] = [];
for (var i = 0; i < transactions.length; i++) {
const slot = transactions[i].slot;
const slotTransactions = [transactions[i]];
while (i + 1 < transactions.length) {
const nextSlot = transactions[i + 1].slot;
if (nextSlot !== slot) break;
slotTransactions.push(transactions[++i]);
}
for (let slotTransaction of slotTransactions) {
let statusText;
let statusClass;
if (slotTransaction.err) {
statusClass = "warning";
statusText = "Failed";
} else {
statusClass = "success";
statusText = "Success";
}
transactionRows.push({
slot,
signature: slotTransaction.signature,
err: slotTransaction.err,
blockTime: slotTransaction.blockTime,
statusClass,
statusText,
signatureInfo: slotTransaction,
});
}
}
return transactionRows;
}

View File

@ -1,26 +0,0 @@
import { NFTData } from "providers/accounts";
import ReactJson from "react-json-view";
export function MetaplexMetadataCard({ nftData }: { nftData: NFTData }) {
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Metaplex Metadata</h3>
</div>
</div>
</div>
<div className="card metadata-json-viewer m-4">
<ReactJson
src={nftData.metadata}
theme={"solarized"}
style={{ padding: 25 }}
/>
</div>
</div>
</>
);
}

View File

@ -1,87 +0,0 @@
import React from "react";
import { NFTData } from "providers/accounts";
import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
interface Attribute {
trait_type: string;
value: string;
}
export function MetaplexNFTAttributesCard({ nftData }: { nftData: NFTData }) {
const [attributes, setAttributes] = React.useState<Attribute[]>([]);
const [status, setStatus] = React.useState<"loading" | "success" | "error">(
"loading"
);
async function fetchMetadataAttributes() {
try {
const response = await fetch(nftData.metadata.data.uri);
const metadata = await response.json();
// Verify if the attributes value is an array
if (Array.isArray(metadata.attributes)) {
// Filter attributes to keep objects matching schema
const filteredAttributes = metadata.attributes.filter(
(attribute: any) => {
return (
typeof attribute === "object" &&
typeof attribute.trait_type === "string" &&
(typeof attribute.value === "string" ||
typeof attribute.value === "number")
);
}
);
setAttributes(filteredAttributes);
setStatus("success");
} else {
throw new Error("Attributes is not an array");
}
} catch (error) {
setStatus("error");
}
}
React.useEffect(() => {
fetchMetadataAttributes();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (status === "loading") {
return <LoadingCard />;
}
if (status === "error") {
return <ErrorCard text="Failed to fetch attributes" />;
}
const attributesList: React.ReactNode[] = attributes.map(
({ trait_type, value }) => {
return (
<tr key={`${trait_type}:${value}`}>
<td>{trait_type}</td>
<td>{value}</td>
</tr>
);
}
);
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Attributes</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted w-1">Trait type</th>
<th className="text-muted w-1">Value</th>
</tr>
</thead>
<tbody className="list">{attributesList}</tbody>
</table>
</div>
</div>
);
}

View File

@ -1,207 +0,0 @@
import React from "react";
import {
NFTData,
useFetchAccountInfo,
useMintAccountInfo,
} from "providers/accounts";
import { programs } from "@metaplex/js";
import { ArtContent } from "components/common/NFTArt";
import { InfoTooltip } from "components/common/InfoTooltip";
import { clusterPath } from "utils/url";
import { Link } from "react-router-dom";
import { EditionInfo } from "providers/accounts/utils/getEditionInfo";
import { PublicKey } from "@solana/web3.js";
export function MetaplexNFTHeader({
nftData,
address,
}: {
nftData: NFTData;
address: string;
}) {
const collectionAddress = nftData.metadata.collection?.key;
const collectionMintInfo = useMintAccountInfo(collectionAddress);
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
if (collectionAddress && !collectionMintInfo) {
fetchAccountInfo(new PublicKey(collectionAddress), "parsed");
}
}, [fetchAccountInfo, collectionAddress]); // eslint-disable-line react-hooks/exhaustive-deps
const metadata = nftData.metadata;
const data = nftData.json;
const isVerifiedCollection =
metadata.collection != null &&
metadata.collection?.verified &&
collectionMintInfo !== undefined;
return (
<div className="row">
<div className="col-auto ms-2 d-flex align-items-center">
<ArtContent metadata={metadata} pubkey={address} data={data} />
</div>
<div className="col mb-3 ms-0.5 mt-3">
{<h6 className="header-pretitle ms-1">Metaplex NFT</h6>}
<div className="d-flex align-items-center">
<h2 className="header-title ms-1 align-items-center no-overflow-with-ellipsis">
{metadata.data.name !== ""
? metadata.data.name
: "No NFT name was found"}
</h2>
{getEditionPill(nftData.editionInfo)}
{isVerifiedCollection ? getVerifiedCollectionPill() : null}
</div>
<h4 className="header-pretitle ms-1 mt-1 no-overflow-with-ellipsis">
{metadata.data.symbol !== ""
? metadata.data.symbol
: "No Symbol was found"}
</h4>
<div className="mb-2 mt-2">
{getSaleTypePill(metadata.primarySaleHappened)}
</div>
<div className="mb-3 mt-2">{getIsMutablePill(metadata.isMutable)}</div>
<div className="btn-group">
<button
className="btn btn-dark btn-sm dropdown-toggle creators-dropdown-button-width"
type="button"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Creators
</button>
<div className="dropdown-menu mt-2">
{getCreatorDropdownItems(metadata.data.creators)}
</div>
</div>
</div>
</div>
);
}
type Creator = programs.metadata.Creator;
function getCreatorDropdownItems(creators: Creator[] | null) {
const CreatorHeader = () => {
const creatorTooltip =
"Verified creators signed the metadata associated with this NFT when it was created.";
const shareTooltip =
"The percentage of the proceeds a creator receives when this NFT is sold.";
return (
<div
className={
"d-flex align-items-center dropdown-header creator-dropdown-entry"
}
>
<div className="d-flex font-monospace creator-dropdown-header">
<span>Creator Address</span>
<InfoTooltip bottom text={creatorTooltip} />
</div>
<div className="d-flex font-monospace">
<span className="font-monospace">Royalty</span>
<InfoTooltip bottom text={shareTooltip} />
</div>
</div>
);
};
const getVerifiedIcon = (isVerified: boolean) => {
const className = isVerified ? "fe fe-check" : "fe fe-alert-octagon";
return <i className={`ms-3 ${className}`}></i>;
};
const CreatorEntry = (creator: Creator) => {
return (
<div
className={
"d-flex align-items-center font-monospace creator-dropdown-entry ms-3 me-3"
}
>
{getVerifiedIcon(creator.verified)}
<Link
className="dropdown-item font-monospace creator-dropdown-entry-address"
to={clusterPath(`/address/${creator.address}`)}
>
{creator.address}
</Link>
<div className="me-3"> {`${creator.share}%`}</div>
</div>
);
};
if (creators && creators.length > 0) {
let listOfCreators: JSX.Element[] = [];
listOfCreators.push(<CreatorHeader key={"header"} />);
creators.forEach((creator) => {
listOfCreators.push(<CreatorEntry key={creator.address} {...creator} />);
});
return listOfCreators;
}
return (
<div className={"dropdown-item font-monospace"}>
<div className="me-3">No creators are associated with this NFT.</div>
</div>
);
}
function getEditionPill(editionInfo: EditionInfo) {
const masterEdition = editionInfo.masterEdition;
const edition = editionInfo.edition;
return (
<div className={"d-inline-flex ms-2"}>
<span className="badge badge-pill bg-dark">{`${
edition && masterEdition
? `Edition ${edition.edition.toNumber()} / ${masterEdition.supply.toNumber()}`
: masterEdition
? "Master Edition"
: "No Master Edition Information"
}`}</span>
</div>
);
}
function getSaleTypePill(hasPrimarySaleHappened: boolean) {
const primaryMarketTooltip =
"Creator(s) split 100% of the proceeds when this NFT is sold.";
const secondaryMarketTooltip =
"Creator(s) split the Seller Fee when this NFT is sold. The owner receives the remaining proceeds.";
return (
<div className={"d-inline-flex align-items-center"}>
<span className="badge badge-pill bg-dark">{`${
hasPrimarySaleHappened ? "Secondary Market" : "Primary Market"
}`}</span>
<InfoTooltip
bottom
text={
hasPrimarySaleHappened ? secondaryMarketTooltip : primaryMarketTooltip
}
/>
</div>
);
}
function getIsMutablePill(isMutable: boolean) {
return (
<span className="badge badge-pill bg-dark">{`${
isMutable ? "Mutable" : "Immutable"
}`}</span>
);
}
function getVerifiedCollectionPill() {
const onchainVerifiedToolTip =
"This NFT has been verified as a member of an on-chain collection. This tag guarantees authenticity.";
return (
<div className={"d-inline-flex align-items-center ms-2"}>
<span className="badge badge-pill bg-dark">{"Verified Collection"}</span>
<InfoTooltip bottom text={onchainVerifiedToolTip} />
</div>
);
}

View File

@ -1,55 +0,0 @@
import React from "react";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address";
import { NonceAccount } from "validators/accounts/nonce";
import {
AccountHeader,
AccountAddressRow,
AccountBalanceRow,
} from "components/common/Account";
export function NonceAccountSection({
account,
nonceAccount,
}: {
account: Account;
nonceAccount: NonceAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Nonce Account"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Authority</td>
<td className="text-lg-end">
<Address pubkey={nonceAccount.info.authority} alignRight raw link />
</td>
</tr>
<tr>
<td>Blockhash</td>
<td className="text-lg-end">
<code>{nonceAccount.info.blockhash}</code>
</td>
</tr>
<tr>
<td>Fee</td>
<td className="text-lg-end">
{nonceAccount.info.feeCalculator.lamportsPerSignature} lamports per
signature
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -1,280 +0,0 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
import { FetchStatus } from "providers/cache";
import {
useFetchAccountOwnedTokens,
useAccountOwnedTokens,
TokenInfoWithPubkey,
} from "providers/accounts/tokens";
import { ErrorCard } from "components/common/ErrorCard";
import { LoadingCard } from "components/common/LoadingCard";
import { Address } from "components/common/Address";
import { useQuery } from "utils/url";
import { Link } from "react-router-dom";
import { Location } from "history";
import { useTokenRegistry } from "providers/mints/token-registry";
import { BigNumber } from "bignumber.js";
import { Identicon } from "components/common/Identicon";
type Display = "summary" | "detail" | null;
const SMALL_IDENTICON_WIDTH = 16;
const useQueryDisplay = (): Display => {
const query = useQuery();
const filter = query.get("display");
if (filter === "summary" || filter === "detail") {
return filter;
} else {
return null;
}
};
export function OwnedTokensCard({ pubkey }: { pubkey: PublicKey }) {
const address = pubkey.toBase58();
const ownedTokens = useAccountOwnedTokens(address);
const fetchAccountTokens = useFetchAccountOwnedTokens();
const refresh = () => fetchAccountTokens(pubkey);
const [showDropdown, setDropdown] = React.useState(false);
const display = useQueryDisplay();
// Fetch owned tokens
React.useEffect(() => {
if (!ownedTokens) refresh();
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
if (ownedTokens === undefined) {
return null;
}
const { status } = ownedTokens;
const tokens = ownedTokens.data?.tokens;
const fetching = status === FetchStatus.Fetching;
if (fetching && (tokens === undefined || tokens.length === 0)) {
return <LoadingCard message="Loading token holdings" />;
} else if (tokens === undefined) {
return <ErrorCard retry={refresh} text="Failed to fetch token holdings" />;
}
if (tokens.length === 0) {
return (
<ErrorCard
retry={refresh}
retryText="Try Again"
text={"No token holdings found"}
/>
);
}
if (tokens.length > 100) {
return (
<ErrorCard text="Token holdings is not available for accounts with over 100 token accounts" />
);
}
return (
<>
{showDropdown && (
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
)}
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Token Holdings</h3>
<DisplayDropdown
display={display}
toggle={() => setDropdown((show) => !show)}
show={showDropdown}
/>
</div>
{display === "detail" ? (
<HoldingsDetailTable tokens={tokens} />
) : (
<HoldingsSummaryTable tokens={tokens} />
)}
</div>
</>
);
}
function HoldingsDetailTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
const detailsList: React.ReactNode[] = [];
const { tokenRegistry } = useTokenRegistry();
const showLogos = tokens.some(
(t) => tokenRegistry.get(t.info.mint.toBase58())?.logoURI !== undefined
);
tokens.forEach((tokenAccount) => {
const address = tokenAccount.pubkey.toBase58();
const mintAddress = tokenAccount.info.mint.toBase58();
const tokenDetails = tokenRegistry.get(mintAddress);
detailsList.push(
<tr key={address}>
{showLogos && (
<td className="w-1 p-0 text-center">
{tokenDetails?.logoURI ? (
<img
src={tokenDetails.logoURI}
alt="token icon"
className="token-icon rounded-circle border border-4 border-gray-dark"
/>
) : (
<Identicon
address={address}
className="avatar-img identicon-wrapper identicon-wrapper-small"
style={{ width: SMALL_IDENTICON_WIDTH }}
/>
)}
</td>
)}
<td>
<Address pubkey={tokenAccount.pubkey} link truncate />
</td>
<td>
<Address pubkey={tokenAccount.info.mint} link truncate />
</td>
<td>
{tokenAccount.info.tokenAmount.uiAmountString}{" "}
{tokenDetails && tokenDetails.symbol}
</td>
</tr>
);
});
return (
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
{showLogos && (
<th className="text-muted w-1 p-0 text-center">Logo</th>
)}
<th className="text-muted">Account Address</th>
<th className="text-muted">Mint Address</th>
<th className="text-muted">Balance</th>
</tr>
</thead>
<tbody className="list">{detailsList}</tbody>
</table>
</div>
);
}
function HoldingsSummaryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
const { tokenRegistry } = useTokenRegistry();
const mappedTokens = new Map<string, string>();
for (const { info: token } of tokens) {
const mintAddress = token.mint.toBase58();
const totalByMint = mappedTokens.get(mintAddress);
let amount = token.tokenAmount.uiAmountString;
if (totalByMint !== undefined) {
amount = new BigNumber(totalByMint)
.plus(token.tokenAmount.uiAmountString)
.toString();
}
mappedTokens.set(mintAddress, amount);
}
const detailsList: React.ReactNode[] = [];
const showLogos = tokens.some(
(t) => tokenRegistry.get(t.info.mint.toBase58())?.logoURI !== undefined
);
mappedTokens.forEach((totalByMint, mintAddress) => {
const tokenDetails = tokenRegistry.get(mintAddress);
detailsList.push(
<tr key={mintAddress}>
{showLogos && (
<td className="w-1 p-0 text-center">
{tokenDetails?.logoURI ? (
<img
src={tokenDetails.logoURI}
alt="token icon"
className="token-icon rounded-circle border border-4 border-gray-dark"
/>
) : (
<Identicon
address={mintAddress}
className="avatar-img identicon-wrapper identicon-wrapper-small"
style={{ width: SMALL_IDENTICON_WIDTH }}
/>
)}
</td>
)}
<td>
<Address pubkey={new PublicKey(mintAddress)} link useMetadata />
</td>
<td>
{totalByMint} {tokenDetails && tokenDetails.symbol}
</td>
</tr>
);
});
return (
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
{showLogos && (
<th className="text-muted w-1 p-0 text-center">Logo</th>
)}
<th className="text-muted">Mint Address</th>
<th className="text-muted">Total Balance</th>
</tr>
</thead>
<tbody className="list">{detailsList}</tbody>
</table>
</div>
);
}
type DropdownProps = {
display: Display;
toggle: () => void;
show: boolean;
};
const DisplayDropdown = ({ display, toggle, show }: DropdownProps) => {
const buildLocation = (location: Location, display: Display) => {
const params = new URLSearchParams(location.search);
if (display === null) {
params.delete("display");
} else {
params.set("display", display);
}
return {
...location,
search: params.toString(),
};
};
const DISPLAY_OPTIONS: Display[] = [null, "detail"];
return (
<div className="dropdown">
<button
className="btn btn-white btn-sm dropdown-toggle"
type="button"
onClick={toggle}
>
{display === "detail" ? "Detailed" : "Summary"}
</button>
<div className={`dropdown-menu-end dropdown-menu${show ? " show" : ""}`}>
{DISPLAY_OPTIONS.map((displayOption) => {
return (
<Link
key={displayOption || "null"}
to={(location) => buildLocation(location, displayOption)}
className={`dropdown-item${
displayOption === display ? " active" : ""
}`}
onClick={toggle}
>
{displayOption === "detail" ? "Detailed" : "Summary"}
</Link>
);
})}
</div>
</div>
);
};

View File

@ -1,133 +0,0 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
import { useFetchRewards, useRewards } from "providers/accounts/rewards";
import { LoadingCard } from "components/common/LoadingCard";
import { FetchStatus } from "providers/cache";
import { ErrorCard } from "components/common/ErrorCard";
import { Slot } from "components/common/Slot";
import { lamportsToSolString } from "utils";
import { useAccountInfo } from "providers/accounts";
import { Epoch } from "components/common/Epoch";
const U64_MAX = BigInt("0xffffffffffffffff");
export function RewardsCard({ pubkey }: { pubkey: PublicKey }) {
const address = React.useMemo(() => pubkey.toBase58(), [pubkey]);
const info = useAccountInfo(address);
const account = info?.data;
const parsedData = account?.data.parsed;
const highestEpoch = React.useMemo(() => {
if (!parsedData) return;
if (parsedData.program !== "stake") return;
const stakeInfo = parsedData.parsed.info.stake;
if (
stakeInfo !== null &&
stakeInfo.delegation.deactivationEpoch !== U64_MAX
) {
return Number(stakeInfo.delegation.deactivationEpoch);
}
}, [parsedData]);
const rewards = useRewards(address);
const fetchRewards = useFetchRewards();
const loadMore = () => fetchRewards(pubkey, highestEpoch);
React.useEffect(() => {
if (!rewards) {
fetchRewards(pubkey, highestEpoch);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (!rewards) {
return null;
}
if (rewards?.data === undefined) {
if (rewards.status === FetchStatus.Fetching) {
return <LoadingCard message="Loading rewards" />;
}
return <ErrorCard retry={loadMore} text="Failed to fetch rewards" />;
}
const rewardsList = rewards.data.rewards.map((reward) => {
if (!reward) {
return null;
}
return (
<tr key={reward.epoch}>
<td>
<Epoch epoch={reward.epoch} link />
</td>
<td>
<Slot slot={reward.effectiveSlot} link />
</td>
<td>{lamportsToSolString(reward.amount)}</td>
<td>{lamportsToSolString(reward.postBalance)}</td>
</tr>
);
});
const rewardsFound = rewardsList.some((r) => r);
const { foundOldest, lowestFetchedEpoch, highestFetchedEpoch } = rewards.data;
const fetching = rewards.status === FetchStatus.Fetching;
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Rewards</h3>
</div>
</div>
</div>
{rewardsFound ? (
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Epoch</th>
<th className="text-muted">Effective Slot</th>
<th className="text-muted">Reward Amount</th>
<th className="text-muted">Post Balance</th>
</tr>
</thead>
<tbody className="list">{rewardsList}</tbody>
</table>
</div>
) : (
<div className="card-body">
No rewards issued between epochs {lowestFetchedEpoch} and{" "}
{highestFetchedEpoch}
</div>
)}
<div className="card-footer">
{foundOldest ? (
<div className="text-muted text-center">
Fetched full reward history
</div>
) : (
<button
className="btn btn-primary w-100"
onClick={() => loadMore()}
disabled={fetching}
>
{fetching ? (
<>
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</>
) : (
"Load More"
)}
</button>
)}
</div>
</div>
</>
);
}

View File

@ -1,281 +0,0 @@
import { ErrorCard } from "components/common/ErrorCard";
import { TableCardBody } from "components/common/TableCardBody";
import { UpgradeableLoaderAccountData } from "providers/accounts";
import { fromProgramData, SecurityTXT } from "utils/security-txt";
export function SecurityCard({ data }: { data: UpgradeableLoaderAccountData }) {
if (!data.programData) {
return <ErrorCard text="Account has no data" />;
}
const { securityTXT, error } = fromProgramData(data.programData);
if (!securityTXT) {
return <ErrorCard text={error!} />;
}
return (
<div className="card security-txt">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Security.txt
</h3>
<small>
Note that this is self-reported by the author of the program and might
not be accurate.
</small>
</div>
<TableCardBody>
{ROWS.filter((x) => x.key in securityTXT).map((x, idx) => {
return (
<tr key={idx}>
<td className="w-100">{x.display}</td>
<RenderEntry value={securityTXT[x.key]} type={x.type} />
</tr>
);
})}
</TableCardBody>
</div>
);
}
enum DisplayType {
String,
URL,
Date,
Contacts,
PGP,
Auditors,
}
type TableRow = {
display: string;
key: keyof SecurityTXT;
type: DisplayType;
};
const ROWS: TableRow[] = [
{
display: "Name",
key: "name",
type: DisplayType.String,
},
{
display: "Project URL",
key: "project_url",
type: DisplayType.URL,
},
{
display: "Contacts",
key: "contacts",
type: DisplayType.Contacts,
},
{
display: "Policy",
key: "policy",
type: DisplayType.URL,
},
{
display: "Preferred Languages",
key: "preferred_languages",
type: DisplayType.String,
},
{
display: "Secure Contact Encryption",
key: "encryption",
type: DisplayType.PGP,
},
{
display: "Source Code URL",
key: "source_code",
type: DisplayType.URL,
},
{
display: "Source Code Release Version",
key: "source_release",
type: DisplayType.String,
},
{
display: "Source Code Revision",
key: "source_revision",
type: DisplayType.String,
},
{
display: "Auditors",
key: "auditors",
type: DisplayType.Auditors,
},
{
display: "Acknowledgements",
key: "acknowledgements",
type: DisplayType.URL,
},
{
display: "Expiry",
key: "expiry",
type: DisplayType.Date,
},
];
function RenderEntry({
value,
type,
}: {
value: SecurityTXT[keyof SecurityTXT];
type: DisplayType;
}) {
if (!value) {
return <></>;
}
switch (type) {
case DisplayType.String:
return <td className="text-lg-end font-monospace">{value}</td>;
case DisplayType.Contacts:
return (
<td className="text-lg-end font-monospace">
<ul>
{value?.split(",").map((c, i) => {
const idx = c.indexOf(":");
if (idx < 0) {
//invalid contact
return <li key={i}>{c}</li>;
}
const [type, information] = [c.slice(0, idx), c.slice(idx + 1)];
return (
<li key={i}>
<Contact type={type} information={information} />
</li>
);
})}
</ul>
</td>
);
case DisplayType.URL:
if (isValidLink(value)) {
return (
<td className="text-lg-end">
<span className="font-monospace">
<a rel="noopener noreferrer" target="_blank" href={value}>
{value}
<span className="fe fe-external-link ms-2"></span>
</a>
</span>
</td>
);
}
return (
<td className="text-lg-end">
<pre>{value.trim()}</pre>
</td>
);
case DisplayType.Date:
return <td className="text-lg-end font-monospace">{value}</td>;
case DisplayType.PGP:
if (isValidLink(value)) {
return (
<td className="text-lg-end">
<span className="font-monospace">
<a rel="noopener noreferrer" target="_blank" href={value}>
{value}
<span className="fe fe-external-link ms-2"></span>
</a>
</span>
</td>
);
}
return (
<td>
<code>{value.trim()}</code>
</td>
);
case DisplayType.Auditors:
if (isValidLink(value)) {
return (
<td className="text-lg-end">
<span className="font-monospace">
<a rel="noopener noreferrer" target="_blank" href={value}>
{value}
<span className="fe fe-external-link ms-2"></span>
</a>
</span>
</td>
);
}
return (
<td>
<ul>
{value?.split(",").map((c, idx) => {
return <li key={idx}>{c}</li>;
})}
</ul>
</td>
);
default:
break;
}
return <></>;
}
function isValidLink(value: string) {
try {
const url = new URL(value);
return ["http:", "https:"].includes(url.protocol);
} catch (err) {
return false;
}
}
function Contact({ type, information }: { type: string; information: string }) {
switch (type) {
case "discord":
return <>Discord: {information}</>;
case "email":
return (
<a
rel="noopener noreferrer"
target="_blank"
href={`mailto:${information}`}
>
{information}
<span className="fe fe-external-link ms-2"></span>
</a>
);
case "telegram":
return (
<a
rel="noopener noreferrer"
target="_blank"
href={`https://t.me/${information}`}
>
Telegram: {information}
<span className="fe fe-external-link ms-2"></span>
</a>
);
case "twitter":
return (
<a
rel="noopener noreferrer"
target="_blank"
href={`https://twitter.com/${information}`}
>
Twitter {information}
<span className="fe fe-external-link ms-2"></span>
</a>
);
case "link":
if (isValidLink(information)) {
return (
<a rel="noopener noreferrer" target="_blank" href={`${information}`}>
{information}
<span className="fe fe-external-link ms-2"></span>
</a>
);
}
return <>{information}</>;
case "other":
default:
return (
<>
{type}: {information}
</>
);
}
}

View File

@ -1,60 +0,0 @@
import { Slot } from "components/common/Slot";
import React from "react";
import {
SysvarAccount,
SlotHashesInfo,
SlotHashEntry,
} from "validators/accounts/sysvar";
export function SlotHashesCard({
sysvarAccount,
}: {
sysvarAccount: SysvarAccount;
}) {
const slotHashes = sysvarAccount.info as SlotHashesInfo;
return (
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Slot Hashes</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Slot</th>
<th className="text-muted">Hash</th>
</tr>
</thead>
<tbody className="list">
{slotHashes.length > 0 &&
slotHashes.map((entry: SlotHashEntry, index) => {
return renderAccountRow(entry, index);
})}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{slotHashes.length > 0 ? "" : "No hashes found"}
</div>
</div>
</div>
);
}
const renderAccountRow = (entry: SlotHashEntry, index: number) => {
return (
<tr key={index}>
<td className="w-1 font-monospace">
<Slot slot={entry.slot} link />
</td>
<td className="font-monospace">{entry.hash}</td>
</tr>
);
};

View File

@ -1,305 +0,0 @@
import React from "react";
import { TableCardBody } from "components/common/TableCardBody";
import { SolBalance } from "components/common/SolBalance";
import { displayTimestampUtc } from "utils/date";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { Address } from "components/common/Address";
import {
StakeAccountInfo,
StakeMeta,
StakeAccountType,
} from "validators/accounts/stake";
import { StakeActivationData } from "@solana/web3.js";
import { Epoch } from "components/common/Epoch";
const U64_MAX = BigInt("0xffffffffffffffff");
export function StakeAccountSection({
account,
stakeAccount,
activation,
stakeAccountType,
}: {
account: Account;
stakeAccount: StakeAccountInfo;
stakeAccountType: StakeAccountType;
activation?: StakeActivationData;
}) {
const hideDelegation =
stakeAccountType !== "delegated" ||
isFullyInactivated(stakeAccount, activation);
return (
<>
<LockupCard stakeAccount={stakeAccount} />
<OverviewCard
account={account}
stakeAccount={stakeAccount}
stakeAccountType={stakeAccountType}
activation={activation}
hideDelegation={hideDelegation}
/>
{!hideDelegation && (
<DelegationCard
stakeAccount={stakeAccount}
activation={activation}
stakeAccountType={stakeAccountType}
/>
)}
<AuthoritiesCard meta={stakeAccount.meta} />
</>
);
}
function LockupCard({ stakeAccount }: { stakeAccount: StakeAccountInfo }) {
const unixTimestamp = 1000 * (stakeAccount.meta?.lockup.unixTimestamp || 0);
if (Date.now() < unixTimestamp) {
const prettyTimestamp = displayTimestampUtc(unixTimestamp);
return (
<div className="alert alert-warning text-center">
<strong>Account is locked!</strong> Lockup expires on {prettyTimestamp}
</div>
);
} else {
return null;
}
}
const TYPE_NAMES = {
uninitialized: "Uninitialized",
initialized: "Initialized",
delegated: "Delegated",
rewardsPool: "RewardsPool",
};
function displayStatus(
stakeAccountType: StakeAccountType,
activation?: StakeActivationData
) {
let status = TYPE_NAMES[stakeAccountType];
let activationState = "";
if (stakeAccountType !== "delegated") {
status = "Not delegated";
} else {
activationState = activation ? `(${activation.state})` : "";
}
return [status, activationState].join(" ");
}
function OverviewCard({
account,
stakeAccount,
stakeAccountType,
activation,
hideDelegation,
}: {
account: Account;
stakeAccount: StakeAccountInfo;
stakeAccountType: StakeAccountType;
activation?: StakeActivationData;
hideDelegation: boolean;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Stake Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports} />
</td>
</tr>
<tr>
<td>Rent Reserve (SOL)</td>
<td className="text-lg-end">
<SolBalance lamports={stakeAccount.meta.rentExemptReserve} />
</td>
</tr>
{hideDelegation && (
<tr>
<td>Status</td>
<td className="text-lg-end">
{isFullyInactivated(stakeAccount, activation)
? "Not delegated"
: displayStatus(stakeAccountType, activation)}
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
function DelegationCard({
stakeAccount,
stakeAccountType,
activation,
}: {
stakeAccount: StakeAccountInfo;
stakeAccountType: StakeAccountType;
activation?: StakeActivationData;
}) {
let voterPubkey, activationEpoch, deactivationEpoch;
const delegation = stakeAccount?.stake?.delegation;
if (delegation) {
voterPubkey = delegation.voter;
if (delegation.activationEpoch !== U64_MAX) {
activationEpoch = delegation.activationEpoch;
}
if (delegation.deactivationEpoch !== U64_MAX) {
deactivationEpoch = delegation.deactivationEpoch;
}
}
const { stake } = stakeAccount;
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Stake Delegation
</h3>
</div>
<TableCardBody>
<tr>
<td>Status</td>
<td className="text-lg-end">
{displayStatus(stakeAccountType, activation)}
</td>
</tr>
{stake && (
<>
<tr>
<td>Delegated Stake (SOL)</td>
<td className="text-lg-end">
<SolBalance lamports={stake.delegation.stake} />
</td>
</tr>
{activation && (
<>
<tr>
<td>Active Stake (SOL)</td>
<td className="text-lg-end">
<SolBalance lamports={activation.active} />
</td>
</tr>
<tr>
<td>Inactive Stake (SOL)</td>
<td className="text-lg-end">
<SolBalance lamports={activation.inactive} />
</td>
</tr>
</>
)}
{voterPubkey && (
<tr>
<td>Delegated Vote Address</td>
<td className="text-lg-end">
<Address pubkey={voterPubkey} alignRight link />
</td>
</tr>
)}
<tr>
<td>Activation Epoch</td>
<td className="text-lg-end">
{activationEpoch !== undefined ? (
<Epoch epoch={activationEpoch} link />
) : (
"-"
)}
</td>
</tr>
<tr>
<td>Deactivation Epoch</td>
<td className="text-lg-end">
{deactivationEpoch !== undefined ? (
<Epoch epoch={deactivationEpoch} link />
) : (
"-"
)}
</td>
</tr>
</>
)}
</TableCardBody>
</div>
);
}
function AuthoritiesCard({ meta }: { meta: StakeMeta }) {
const hasLockup = meta.lockup.unixTimestamp > 0;
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Authorities
</h3>
</div>
<TableCardBody>
<tr>
<td>Stake Authority Address</td>
<td className="text-lg-end">
<Address pubkey={meta.authorized.staker} alignRight link />
</td>
</tr>
<tr>
<td>Withdraw Authority Address</td>
<td className="text-lg-end">
<Address pubkey={meta.authorized.withdrawer} alignRight link />
</td>
</tr>
{hasLockup && (
<tr>
<td>Lockup Authority Address</td>
<td className="text-lg-end">
<Address pubkey={meta.lockup.custodian} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
function isFullyInactivated(
stakeAccount: StakeAccountInfo,
activation?: StakeActivationData
): boolean {
const { stake } = stakeAccount;
if (!stake || !activation) {
return false;
}
const delegatedStake = stake.delegation.stake;
const inactiveStake = BigInt(activation.inactive);
return (
stake.delegation.deactivationEpoch !== U64_MAX &&
delegatedStake === inactiveStake
);
}

View File

@ -1,73 +0,0 @@
import React from "react";
import { SolBalance } from "components/common/SolBalance";
import { Epoch } from "components/common/Epoch";
import {
SysvarAccount,
StakeHistoryInfo,
StakeHistoryEntry,
} from "validators/accounts/sysvar";
export function StakeHistoryCard({
sysvarAccount,
}: {
sysvarAccount: SysvarAccount;
}) {
const stakeHistory = sysvarAccount.info as StakeHistoryInfo;
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Stake History</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Epoch</th>
<th className="text-muted">Effective (SOL)</th>
<th className="text-muted">Activating (SOL)</th>
<th className="text-muted">Deactivating (SOL)</th>
</tr>
</thead>
<tbody className="list">
{stakeHistory.length > 0 &&
stakeHistory.map((entry: StakeHistoryEntry, index) => {
return renderAccountRow(entry, index);
})}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{stakeHistory.length > 0 ? "" : "No stake history found"}
</div>
</div>
</div>
</>
);
}
const renderAccountRow = (entry: StakeHistoryEntry, index: number) => {
return (
<tr key={index}>
<td className="w-1 font-monospace">
<Epoch epoch={entry.epoch} link />
</td>
<td className="font-monospace">
<SolBalance lamports={entry.stakeHistory.effective} />
</td>
<td className="font-monospace">
<SolBalance lamports={entry.stakeHistory.activating} />
</td>
<td className="font-monospace">
<SolBalance lamports={entry.stakeHistory.deactivating} />
</td>
</tr>
);
};

View File

@ -1,420 +0,0 @@
import React from "react";
import { Account, useFetchAccountInfo } from "providers/accounts";
import {
SysvarAccount,
SysvarClockAccount,
SysvarEpochScheduleAccount,
SysvarFeesAccount,
SysvarRecentBlockhashesAccount,
SysvarRentAccount,
SysvarRewardsAccount,
SysvarSlotHashesAccount,
SysvarSlotHistoryAccount,
SysvarStakeHistoryAccount,
} from "validators/accounts/sysvar";
import { TableCardBody } from "components/common/TableCardBody";
import {
AccountHeader,
AccountAddressRow,
AccountBalanceRow,
} from "components/common/Account";
import { displayTimestamp } from "utils/date";
import { Slot } from "components/common/Slot";
import { Epoch } from "components/common/Epoch";
export function SysvarAccountSection({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarAccount;
}) {
switch (sysvarAccount.type) {
case "clock":
return (
<SysvarAccountClockCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "rent":
return (
<SysvarAccountRentCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "rewards":
return (
<SysvarAccountRewardsCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "epochSchedule":
return (
<SysvarAccountEpochScheduleCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "fees":
return (
<SysvarAccountFeesCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "recentBlockhashes":
return (
<SysvarAccountRecentBlockhashesCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "slotHashes":
return (
<SysvarAccountSlotHashes
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "slotHistory":
return (
<SysvarAccountSlotHistory
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "stakeHistory":
return (
<SysvarAccountStakeHistory
account={account}
sysvarAccount={sysvarAccount}
/>
);
}
}
function SysvarAccountRecentBlockhashesCard({
account,
}: {
account: Account;
sysvarAccount: SysvarRecentBlockhashesAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar: Recent Blockhashes"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
</TableCardBody>
</div>
);
}
function SysvarAccountSlotHashes({
account,
}: {
account: Account;
sysvarAccount: SysvarSlotHashesAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar: Slot Hashes"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
</TableCardBody>
</div>
);
}
function SysvarAccountSlotHistory({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarSlotHistoryAccount;
}) {
const refresh = useFetchAccountInfo();
const history = Array.from(
{
length: 100,
},
(v, k) => sysvarAccount.info.nextSlot - k
);
return (
<div className="card">
<AccountHeader
title="Sysvar: Slot History"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td className="align-top">
Slot History{" "}
<span className="text-muted">(previous 100 slots)</span>
</td>
<td className="text-lg-end font-monospace">
{history.map((val) => (
<p key={val} className="mb-0">
<Slot slot={val} link />
</p>
))}
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountStakeHistory({
account,
}: {
account: Account;
sysvarAccount: SysvarStakeHistoryAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar: Stake History"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
</TableCardBody>
</div>
);
}
function SysvarAccountFeesCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarFeesAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar: Fees"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Lamports Per Signature</td>
<td className="text-lg-end">
{sysvarAccount.info.feeCalculator.lamportsPerSignature}
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountEpochScheduleCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarEpochScheduleAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar: Epoch Schedule"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Slots Per Epoch</td>
<td className="text-lg-end">{sysvarAccount.info.slotsPerEpoch}</td>
</tr>
<tr>
<td>Leader Schedule Slot Offset</td>
<td className="text-lg-end">
{sysvarAccount.info.leaderScheduleSlotOffset}
</td>
</tr>
<tr>
<td>Epoch Warmup Enabled</td>
<td className="text-lg-end">
<code>{sysvarAccount.info.warmup ? "true" : "false"}</code>
</td>
</tr>
<tr>
<td>First Normal Epoch</td>
<td className="text-lg-end">{sysvarAccount.info.firstNormalEpoch}</td>
</tr>
<tr>
<td>First Normal Slot</td>
<td className="text-lg-end">
<Slot slot={sysvarAccount.info.firstNormalSlot} />
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountClockCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarClockAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar: Clock"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Timestamp</td>
<td className="text-lg-end font-monospace">
{displayTimestamp(sysvarAccount.info.unixTimestamp * 1000)}
</td>
</tr>
<tr>
<td>Epoch</td>
<td className="text-lg-end">
<Epoch epoch={sysvarAccount.info.epoch} link />
</td>
</tr>
<tr>
<td>Leader Schedule Epoch</td>
<td className="text-lg-end">
<Epoch epoch={sysvarAccount.info.leaderScheduleEpoch} link />
</td>
</tr>
<tr>
<td>Slot</td>
<td className="text-lg-end">
<Slot slot={sysvarAccount.info.slot} link />
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountRentCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarRentAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar: Rent"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Burn Percent</td>
<td className="text-lg-end">
{sysvarAccount.info.burnPercent + "%"}
</td>
</tr>
<tr>
<td>Exemption Threshold</td>
<td className="text-lg-end">
{sysvarAccount.info.exemptionThreshold} years
</td>
</tr>
<tr>
<td>Lamports Per Byte Year</td>
<td className="text-lg-end">
{sysvarAccount.info.lamportsPerByteYear}
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountRewardsCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarRewardsAccount;
}) {
const refresh = useFetchAccountInfo();
const validatorPointValueFormatted = new Intl.NumberFormat("en-US", {
maximumSignificantDigits: 20,
}).format(sysvarAccount.info.validatorPointValue);
return (
<div className="card">
<AccountHeader
title="Sysvar: Rewards"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Validator Point Value</td>
<td className="text-lg-end font-monospace">
{validatorPointValueFormatted} lamports
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -1,566 +0,0 @@
import {
Account,
NFTData,
TokenProgramData,
useFetchAccountInfo,
} from "providers/accounts";
import {
TokenAccount,
MintAccountInfo,
TokenAccountInfo,
MultisigAccountInfo,
} from "validators/accounts/token";
import { create } from "superstruct";
import { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address";
import { UnknownAccountCard } from "./UnknownAccountCard";
import { Cluster, useCluster } from "providers/cluster";
import { abbreviatedNumber, normalizeTokenAmount } from "utils";
import { addressLabel } from "utils/tx";
import { reportError } from "utils/sentry";
import { useTokenRegistry } from "providers/mints/token-registry";
import { BigNumber } from "bignumber.js";
import { Copyable } from "components/common/Copyable";
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
import { displayTimestampWithoutDate } from "utils/date";
import { LoadingCard } from "components/common/LoadingCard";
import { PublicKey } from "@solana/web3.js";
import isMetaplexNFT from "providers/accounts/utils/isMetaplexNFT";
const getEthAddress = (link?: string) => {
let address = "";
if (link) {
const extractEth = link.match(/0x[a-fA-F0-9]{40,64}/);
if (extractEth) {
address = extractEth[0];
}
}
return address;
};
export function TokenAccountSection({
account,
tokenAccount,
}: {
account: Account;
tokenAccount: TokenAccount;
}) {
const { cluster } = useCluster();
try {
switch (tokenAccount.type) {
case "mint": {
const info = create(tokenAccount.info, MintAccountInfo);
if (isMetaplexNFT(account.data.parsed, info)) {
return (
<NonFungibleTokenMintAccountCard
account={account}
nftData={(account.data.parsed as TokenProgramData).nftData!}
mintInfo={info}
/>
);
}
return <FungibleTokenMintAccountCard account={account} info={info} />;
}
case "account": {
const info = create(tokenAccount.info, TokenAccountInfo);
return <TokenAccountCard account={account} info={info} />;
}
case "multisig": {
const info = create(tokenAccount.info, MultisigAccountInfo);
return <MultisigAccountCard account={account} info={info} />;
}
}
} catch (err) {
if (cluster !== Cluster.Custom) {
reportError(err, {
address: account.pubkey.toBase58(),
});
}
}
return <UnknownAccountCard account={account} />;
}
function FungibleTokenMintAccountCard({
account,
info,
}: {
account: Account;
info: MintAccountInfo;
}) {
const { tokenRegistry } = useTokenRegistry();
const mintAddress = account.pubkey.toBase58();
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(account.pubkey, "parsed");
const tokenInfo = tokenRegistry.get(mintAddress);
const bridgeContractAddress = getEthAddress(
tokenInfo?.extensions?.bridgeContract
);
const assetContractAddress = getEthAddress(
tokenInfo?.extensions?.assetContract
);
const coinInfo = useCoinGecko(tokenInfo?.extensions?.coingeckoId);
let tokenPriceInfo;
let tokenPriceDecimals = 2;
if (coinInfo?.status === CoingeckoStatus.Success) {
tokenPriceInfo = coinInfo.coinInfo;
if (tokenPriceInfo && tokenPriceInfo.price < 1) {
tokenPriceDecimals = 6;
}
}
return (
<>
{tokenInfo?.extensions?.coingeckoId &&
coinInfo?.status === CoingeckoStatus.Loading && (
<LoadingCard message="Loading token price data" />
)}
{tokenPriceInfo && tokenPriceInfo.price && (
<div className="row">
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>
Price{" "}
{tokenPriceInfo.market_cap_rank && (
<span className="ms-2 badge bg-primary rank">
Rank #{tokenPriceInfo.market_cap_rank}
</span>
)}
</h4>
<h1 className="mb-0">
${tokenPriceInfo.price.toFixed(tokenPriceDecimals)}{" "}
{tokenPriceInfo.price_change_percentage_24h > 0 && (
<small className="change-positive">
&uarr;{" "}
{tokenPriceInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{tokenPriceInfo.price_change_percentage_24h < 0 && (
<small className="change-negative">
&darr;{" "}
{tokenPriceInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{tokenPriceInfo.price_change_percentage_24h === 0 && (
<small>0%</small>
)}
</h1>
</div>
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>24 Hour Volume</h4>
<h1 className="mb-0">
${abbreviatedNumber(tokenPriceInfo.volume_24)}
</h1>
</div>
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>Market Cap</h4>
<h1 className="mb-0">
${abbreviatedNumber(tokenPriceInfo.market_cap)}
</h1>
<p className="updated-time text-muted">
Updated at{" "}
{displayTimestampWithoutDate(
tokenPriceInfo.last_updated.getTime()
)}
</p>
</div>
</div>
</div>
</div>
)}
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
{tokenInfo ? "Overview" : "Token Mint"}
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>
{info.mintAuthority === null ? "Fixed Supply" : "Current Supply"}
</td>
<td className="text-lg-end">
{normalizeTokenAmount(info.supply, info.decimals).toLocaleString(
"en-US",
{
maximumFractionDigits: 20,
}
)}
</td>
</tr>
{tokenInfo?.extensions?.website && (
<tr>
<td>Website</td>
<td className="text-lg-end">
<a
rel="noopener noreferrer"
target="_blank"
href={tokenInfo.extensions.website}
>
{tokenInfo.extensions.website}
<span className="fe fe-external-link ms-2"></span>
</a>
</td>
</tr>
)}
{info.mintAuthority && (
<tr>
<td>Mint Authority</td>
<td className="text-lg-end">
<Address pubkey={info.mintAuthority} alignRight link />
</td>
</tr>
)}
{info.freezeAuthority && (
<tr>
<td>Freeze Authority</td>
<td className="text-lg-end">
<Address pubkey={info.freezeAuthority} alignRight link />
</td>
</tr>
)}
<tr>
<td>Decimals</td>
<td className="text-lg-end">{info.decimals}</td>
</tr>
{!info.isInitialized && (
<tr>
<td>Status</td>
<td className="text-lg-end">Uninitialized</td>
</tr>
)}
{tokenInfo?.extensions?.bridgeContract && bridgeContractAddress && (
<tr>
<td>Bridge Contract</td>
<td className="text-lg-end">
<Copyable text={bridgeContractAddress}>
<a
href={tokenInfo.extensions.bridgeContract}
target="_blank"
rel="noreferrer"
>
{bridgeContractAddress}
</a>
</Copyable>
</td>
</tr>
)}
{tokenInfo?.extensions?.assetContract && assetContractAddress && (
<tr>
<td>Bridged Asset Contract</td>
<td className="text-lg-end">
<Copyable text={assetContractAddress}>
<a
href={tokenInfo.extensions.bridgeContract}
target="_blank"
rel="noreferrer"
>
{assetContractAddress}
</a>
</Copyable>
</td>
</tr>
)}
</TableCardBody>
</div>
</>
);
}
function NonFungibleTokenMintAccountCard({
account,
nftData,
mintInfo,
}: {
account: Account;
nftData: NFTData;
mintInfo: MintAccountInfo;
}) {
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(account.pubkey, "parsed");
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Overview
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{nftData.editionInfo.masterEdition?.maxSupply && (
<tr>
<td>Max Total Supply</td>
<td className="text-lg-end">
{nftData.editionInfo.masterEdition.maxSupply.toNumber() === 0
? 1
: nftData.editionInfo.masterEdition.maxSupply.toNumber()}
</td>
</tr>
)}
{nftData?.editionInfo.masterEdition?.supply && (
<tr>
<td>Current Supply</td>
<td className="text-lg-end">
{nftData.editionInfo.masterEdition.supply.toNumber() === 0
? 1
: nftData.editionInfo.masterEdition.supply.toNumber()}
</td>
</tr>
)}
{!!nftData?.metadata.collection?.verified && (
<tr>
<td>Verified Collection Address</td>
<td className="text-lg-end">
<Address
pubkey={new PublicKey(nftData.metadata.collection.key)}
alignRight
link
/>
</td>
</tr>
)}
{mintInfo.mintAuthority && (
<tr>
<td>Mint Authority</td>
<td className="text-lg-end">
<Address pubkey={mintInfo.mintAuthority} alignRight link />
</td>
</tr>
)}
{mintInfo.freezeAuthority && (
<tr>
<td>Freeze Authority</td>
<td className="text-lg-end">
<Address pubkey={mintInfo.freezeAuthority} alignRight link />
</td>
</tr>
)}
<tr>
<td>Update Authority</td>
<td className="text-lg-end">
<Address
pubkey={new PublicKey(nftData.metadata.updateAuthority)}
alignRight
link
/>
</td>
</tr>
{nftData?.json && nftData.json.external_url && (
<tr>
<td>Website</td>
<td className="text-lg-end">
<a
rel="noopener noreferrer"
target="_blank"
href={nftData.json.external_url}
>
{nftData.json.external_url}
<span className="fe fe-external-link ms-2"></span>
</a>
</td>
</tr>
)}
{nftData?.metadata.data && (
<tr>
<td>Seller Fee</td>
<td className="text-lg-end">
{`${nftData?.metadata.data.sellerFeeBasisPoints / 100}%`}
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
function TokenAccountCard({
account,
info,
}: {
account: Account;
info: TokenAccountInfo;
}) {
const refresh = useFetchAccountInfo();
const { cluster } = useCluster();
const { tokenRegistry } = useTokenRegistry();
const label = addressLabel(account.pubkey.toBase58(), cluster, tokenRegistry);
let unit, balance;
if (info.isNative) {
unit = "SOL";
balance = (
<>
<span className="font-monospace">
{new BigNumber(info.tokenAmount.uiAmountString).toFormat(9)}
</span>
</>
);
} else {
balance = <>{info.tokenAmount.uiAmountString}</>;
unit = tokenRegistry.get(info.mint.toBase58())?.symbol || "tokens";
}
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Token Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{label && (
<tr>
<td>Address Label</td>
<td className="text-lg-end">{label}</td>
</tr>
)}
<tr>
<td>Mint</td>
<td className="text-lg-end">
<Address pubkey={info.mint} alignRight link />
</td>
</tr>
<tr>
<td>Owner</td>
<td className="text-lg-end">
<Address pubkey={info.owner} alignRight link />
</td>
</tr>
<tr>
<td>Token balance ({unit})</td>
<td className="text-lg-end">{balance}</td>
</tr>
{info.state === "uninitialized" && (
<tr>
<td>Status</td>
<td className="text-lg-end">Uninitialized</td>
</tr>
)}
{info.rentExemptReserve && (
<tr>
<td>Rent-exempt reserve (SOL)</td>
<td className="text-lg-end">
<>
<span className="font-monospace">
{new BigNumber(
info.rentExemptReserve.uiAmountString
).toFormat(9)}
</span>
</>
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
function MultisigAccountCard({
account,
info,
}: {
account: Account;
info: MultisigAccountInfo;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Multisig Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Required Signers</td>
<td className="text-lg-end">{info.numRequiredSigners}</td>
</tr>
<tr>
<td>Valid Signers</td>
<td className="text-lg-end">{info.numValidSigners}</td>
</tr>
{info.signers.map((signer) => (
<tr key={signer.toString()}>
<td>Signer</td>
<td className="text-lg-end">
<Address pubkey={signer} alignRight link />
</td>
</tr>
))}
{!info.isInitialized && (
<tr>
<td>Status</td>
<td className="text-lg-end">Uninitialized</td>
</tr>
)}
</TableCardBody>
</div>
);
}

View File

@ -1,612 +0,0 @@
import React from "react";
import {
PublicKey,
ConfirmedSignatureInfo,
ParsedInstruction,
PartiallyDecodedInstruction,
} from "@solana/web3.js";
import { CacheEntry, FetchStatus } from "providers/cache";
import {
useAccountHistories,
useFetchAccountHistory,
} from "providers/accounts/history";
import {
useAccountOwnedTokens,
TokenInfoWithPubkey,
TOKEN_PROGRAM_ID,
} from "providers/accounts/tokens";
import { ErrorCard } from "components/common/ErrorCard";
import { LoadingCard } from "components/common/LoadingCard";
import { Signature } from "components/common/Signature";
import { Address } from "components/common/Address";
import { Slot } from "components/common/Slot";
import {
Details,
useFetchTransactionDetails,
useTransactionDetailsCache,
} from "providers/transactions/parsed";
import { reportError } from "utils/sentry";
import { intoTransactionInstruction, displayAddress } from "utils/tx";
import {
isTokenSwapInstruction,
parseTokenSwapInstructionTitle,
} from "components/instruction/token-swap/types";
import {
isTokenLendingInstruction,
parseTokenLendingInstructionTitle,
} from "components/instruction/token-lending/types";
import {
isSerumInstruction,
parseSerumInstructionTitle,
} from "components/instruction/serum/types";
import { INNER_INSTRUCTIONS_START_SLOT } from "pages/TransactionDetailsPage";
import { useCluster, Cluster } from "providers/cluster";
import { Link } from "react-router-dom";
import { Location } from "history";
import { useQuery } from "utils/url";
import { TokenInfoMap } from "@solana/spl-token-registry";
import { useTokenRegistry } from "providers/mints/token-registry";
import { getTokenProgramInstructionName } from "utils/instruction";
import {
isMangoInstruction,
parseMangoInstructionTitle,
} from "components/instruction/mango/types";
const TRUNCATE_TOKEN_LENGTH = 10;
const ALL_TOKENS = "";
type InstructionType = {
name: string;
innerInstructions: (ParsedInstruction | PartiallyDecodedInstruction)[];
};
export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
const address = pubkey.toBase58();
const ownedTokens = useAccountOwnedTokens(address);
if (ownedTokens === undefined) {
return null;
}
const tokens = ownedTokens.data?.tokens;
if (tokens === undefined || tokens.length === 0) return null;
if (tokens.length > 25) {
return (
<ErrorCard text="Token transaction history is not available for accounts with over 25 token accounts" />
);
}
return <TokenHistoryTable tokens={tokens} />;
}
const useQueryFilter = (): string => {
const query = useQuery();
const filter = query.get("filter");
return filter || "";
};
type FilterProps = {
filter: string;
toggle: () => void;
show: boolean;
tokens: TokenInfoWithPubkey[];
};
function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
const accountHistories = useAccountHistories();
const fetchAccountHistory = useFetchAccountHistory();
const transactionDetailsCache = useTransactionDetailsCache();
const [showDropdown, setDropdown] = React.useState(false);
const filter = useQueryFilter();
const filteredTokens = React.useMemo(
() =>
tokens.filter((token) => {
if (filter === ALL_TOKENS) {
return true;
}
return token.info.mint.toBase58() === filter;
}),
[tokens, filter]
);
const fetchHistories = React.useCallback(
(refresh?: boolean) => {
filteredTokens.forEach((token) => {
fetchAccountHistory(token.pubkey, refresh);
});
},
[filteredTokens, fetchAccountHistory]
);
// Fetch histories on load
React.useEffect(() => {
filteredTokens.forEach((token) => {
const address = token.pubkey.toBase58();
if (!accountHistories[address]) {
fetchAccountHistory(token.pubkey, true);
}
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const allFoundOldest = filteredTokens.every((token) => {
const history = accountHistories[token.pubkey.toBase58()];
return history?.data?.foundOldest === true;
});
const allFetchedSome = filteredTokens.every((token) => {
const history = accountHistories[token.pubkey.toBase58()];
return history?.data !== undefined;
});
// Find the oldest slot which we know we have the full history for
let oldestSlot: number | undefined = allFoundOldest ? 0 : undefined;
if (!allFoundOldest && allFetchedSome) {
filteredTokens.forEach((token) => {
const history = accountHistories[token.pubkey.toBase58()];
if (history?.data?.foundOldest === false) {
const earliest =
history.data.fetched[history.data.fetched.length - 1].slot;
if (!oldestSlot) oldestSlot = earliest;
oldestSlot = Math.max(oldestSlot, earliest);
}
});
}
const fetching = filteredTokens.some((token) => {
const history = accountHistories[token.pubkey.toBase58()];
return history?.status === FetchStatus.Fetching;
});
const failed = filteredTokens.some((token) => {
const history = accountHistories[token.pubkey.toBase58()];
return history?.status === FetchStatus.FetchFailed;
});
const sigSet = new Set();
const mintAndTxs = filteredTokens
.map((token) => ({
mint: token.info.mint,
history: accountHistories[token.pubkey.toBase58()],
}))
.filter(({ history }) => {
return history?.data?.fetched && history.data.fetched.length > 0;
})
.flatMap(({ mint, history }) =>
(history?.data?.fetched as ConfirmedSignatureInfo[]).map((tx) => ({
mint,
tx,
}))
)
.filter(({ tx }) => {
if (sigSet.has(tx.signature)) return false;
sigSet.add(tx.signature);
return true;
})
.filter(({ tx }) => {
return oldestSlot !== undefined && tx.slot >= oldestSlot;
});
React.useEffect(() => {
if (!fetching && mintAndTxs.length < 1 && !allFoundOldest) {
fetchHistories();
}
}, [fetching, mintAndTxs, allFoundOldest, fetchHistories]);
if (mintAndTxs.length === 0) {
if (fetching) {
return <LoadingCard message="Loading history" />;
} else if (failed) {
return (
<ErrorCard
retry={() => fetchHistories(true)}
text="Failed to fetch transaction history"
/>
);
}
return (
<ErrorCard
retry={() => fetchHistories(true)}
retryText="Try again"
text="No transaction history found"
/>
);
}
mintAndTxs.sort((a, b) => {
if (a.tx.slot > b.tx.slot) return -1;
if (a.tx.slot < b.tx.slot) return 1;
return 0;
});
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Token History</h3>
<FilterDropdown
filter={filter}
toggle={() => setDropdown((show) => !show)}
show={showDropdown}
tokens={tokens}
></FilterDropdown>
<button
className="btn btn-white btn-sm"
disabled={fetching}
onClick={() => fetchHistories(true)}
>
{fetching ? (
<>
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</>
) : (
<>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</>
)}
</button>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted w-1">Slot</th>
<th className="text-muted">Result</th>
<th className="text-muted">Token</th>
<th className="text-muted">Instruction Type</th>
<th className="text-muted">Transaction Signature</th>
</tr>
</thead>
<tbody className="list">
{mintAndTxs.map(({ mint, tx }) => (
<TokenTransactionRow
key={tx.signature}
mint={mint}
tx={tx}
details={transactionDetailsCache[tx.signature]}
/>
))}
</tbody>
</table>
</div>
<div className="card-footer">
{allFoundOldest ? (
<div className="text-muted text-center">Fetched full history</div>
) : (
<button
className="btn btn-primary w-100"
onClick={() => fetchHistories()}
disabled={fetching}
>
{fetching ? (
<>
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</>
) : (
"Load More"
)}
</button>
)}
</div>
</div>
);
}
const FilterDropdown = ({ filter, toggle, show, tokens }: FilterProps) => {
const { cluster } = useCluster();
const { tokenRegistry } = useTokenRegistry();
const buildLocation = (location: Location, filter: string) => {
const params = new URLSearchParams(location.search);
if (filter === ALL_TOKENS) {
params.delete("filter");
} else {
params.set("filter", filter);
}
return {
...location,
search: params.toString(),
};
};
const filterOptions: string[] = [ALL_TOKENS];
const nameLookup: Map<string, string> = new Map();
tokens.forEach((token) => {
const address = token.info.mint.toBase58();
if (!nameLookup.has(address)) {
filterOptions.push(address);
nameLookup.set(address, formatTokenName(address, cluster, tokenRegistry));
}
});
return (
<div className="dropdown me-2">
<small className="me-2">Filter:</small>
<button
className="btn btn-white btn-sm dropdown-toggle"
type="button"
onClick={toggle}
>
{filter === ALL_TOKENS ? "All Tokens" : nameLookup.get(filter)}
</button>
<div
className={`token-filter dropdown-menu-end dropdown-menu${
show ? " show" : ""
}`}
>
{filterOptions.map((filterOption) => {
return (
<Link
key={filterOption}
to={(location: Location) => buildLocation(location, filterOption)}
className={`dropdown-item${
filterOption === filter ? " active" : ""
}`}
onClick={toggle}
>
{filterOption === ALL_TOKENS
? "All Tokens"
: formatTokenName(filterOption, cluster, tokenRegistry)}
</Link>
);
})}
</div>
</div>
);
};
const TokenTransactionRow = React.memo(
({
mint,
tx,
details,
}: {
mint: PublicKey;
tx: ConfirmedSignatureInfo;
details: CacheEntry<Details> | undefined;
}) => {
const fetchDetails = useFetchTransactionDetails();
const { cluster } = useCluster();
// Fetch details on load
React.useEffect(() => {
if (!details) fetchDetails(tx.signature);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
let statusText: string;
let statusClass: string;
if (tx.err) {
statusClass = "warning";
statusText = "Failed";
} else {
statusClass = "success";
statusText = "Success";
}
const transactionWithMeta = details?.data?.transactionWithMeta;
const instructions = transactionWithMeta?.transaction.message.instructions;
if (!instructions)
return (
<tr key={tx.signature}>
<td className="w-1">
<Slot slot={tx.slot} link />
</td>
<td>
<span className={`badge bg-${statusClass}-soft`}>{statusText}</span>
</td>
<td>
<Address pubkey={mint} link truncate />
</td>
<td>
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</td>
<td>
<Signature signature={tx.signature} link />
</td>
</tr>
);
let tokenInstructionNames: InstructionType[] = [];
if (transactionWithMeta) {
tokenInstructionNames = instructions
.map((ix, index): InstructionType | undefined => {
let name = "Unknown";
const innerInstructions: (
| ParsedInstruction
| PartiallyDecodedInstruction
)[] = [];
if (
transactionWithMeta.meta?.innerInstructions &&
(cluster !== Cluster.MainnetBeta ||
transactionWithMeta.slot >= INNER_INSTRUCTIONS_START_SLOT)
) {
transactionWithMeta.meta.innerInstructions.forEach((ix) => {
if (ix.index === index) {
ix.instructions.forEach((inner) => {
innerInstructions.push(inner);
});
}
});
}
let transactionInstruction;
if (transactionWithMeta?.transaction) {
transactionInstruction = intoTransactionInstruction(
transactionWithMeta.transaction,
ix
);
}
if ("parsed" in ix) {
if (ix.program === "spl-token") {
name = getTokenProgramInstructionName(ix, tx);
} else {
return undefined;
}
} else if (
transactionInstruction &&
isSerumInstruction(transactionInstruction)
) {
try {
name = parseSerumInstructionTitle(transactionInstruction);
} catch (error) {
reportError(error, { signature: tx.signature });
return undefined;
}
} else if (
transactionInstruction &&
isTokenSwapInstruction(transactionInstruction)
) {
try {
name = parseTokenSwapInstructionTitle(transactionInstruction);
} catch (error) {
reportError(error, { signature: tx.signature });
return undefined;
}
} else if (
transactionInstruction &&
isTokenLendingInstruction(transactionInstruction)
) {
try {
name = parseTokenLendingInstructionTitle(transactionInstruction);
} catch (error) {
reportError(error, { signature: tx.signature });
return undefined;
}
} else if (
transactionInstruction &&
isMangoInstruction(transactionInstruction)
) {
try {
name = parseMangoInstructionTitle(transactionInstruction);
} catch (error) {
reportError(error, { signature: tx.signature });
return undefined;
}
} else {
if (
ix.accounts.findIndex((account) =>
account.equals(TOKEN_PROGRAM_ID)
) >= 0
) {
name = "Unknown (Inner)";
} else {
return undefined;
}
}
return {
name,
innerInstructions,
};
})
.filter((name) => name !== undefined) as InstructionType[];
}
return (
<>
{tokenInstructionNames.map((instructionType, index) => {
return (
<tr key={index}>
<td className="w-1">
<Slot slot={tx.slot} link />
</td>
<td>
<span className={`badge bg-${statusClass}-soft`}>
{statusText}
</span>
</td>
<td className="forced-truncate">
<Address pubkey={mint} link truncateUnknown />
</td>
<td>
<InstructionDetails instructionType={instructionType} tx={tx} />
</td>
<td className="forced-truncate">
<Signature signature={tx.signature} link truncate />
</td>
</tr>
);
})}
</>
);
}
);
function InstructionDetails({
instructionType,
tx,
}: {
instructionType: InstructionType;
tx: ConfirmedSignatureInfo;
}) {
const [expanded, setExpanded] = React.useState(false);
let instructionTypes = instructionType.innerInstructions
.map((ix) => {
if ("parsed" in ix && ix.program === "spl-token") {
return getTokenProgramInstructionName(ix, tx);
}
return undefined;
})
.filter((type) => type !== undefined);
return (
<>
<p className="tree">
{instructionTypes.length > 0 && (
<span
onClick={(e) => {
e.preventDefault();
setExpanded(!expanded);
}}
className={`c-pointer fe me-2 ${
expanded ? "fe-minus-square" : "fe-plus-square"
}`}
></span>
)}
{instructionType.name}
</p>
{expanded && (
<ul className="tree">
{instructionTypes.map((type, index) => {
return <li key={index}>{type}</li>;
})}
</ul>
)}
</>
);
}
function formatTokenName(
pubkey: string,
cluster: Cluster,
tokenRegistry: TokenInfoMap
): string {
let display = displayAddress(pubkey, cluster, tokenRegistry);
if (display === pubkey) {
display = display.slice(0, TRUNCATE_TOKEN_LENGTH) + "\u2026";
}
return display;
}

View File

@ -1,149 +0,0 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
import { Address } from "components/common/Address";
import {
useTokenLargestTokens,
useFetchTokenLargestAccounts,
TokenAccountBalancePairWithOwner,
} from "providers/mints/largest";
import { FetchStatus } from "providers/cache";
import { useMintAccountInfo } from "providers/accounts";
import { normalizeTokenAmount } from "utils";
import { useTokenRegistry } from "providers/mints/token-registry";
import BigNumber from "bignumber.js";
export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
const mintAddress = pubkey.toBase58();
const mintInfo = useMintAccountInfo(mintAddress);
const largestAccounts = useTokenLargestTokens(mintAddress);
const fetchLargestAccounts = useFetchTokenLargestAccounts();
const refreshLargest = React.useCallback(
() => fetchLargestAccounts(pubkey),
[pubkey, fetchLargestAccounts]
);
const { tokenRegistry } = useTokenRegistry();
const unit = tokenRegistry.get(mintAddress)?.symbol;
const unitLabel = unit ? `(${unit})` : "";
React.useEffect(() => {
if (mintInfo) refreshLargest();
}, [mintInfo, refreshLargest]);
// Largest accounts hasn't started fetching
if (largestAccounts === undefined) return null;
// This is not a mint account
if (mintInfo === undefined) return null;
if (largestAccounts?.data === undefined) {
if (largestAccounts.status === FetchStatus.Fetching) {
return <LoadingCard message="Loading largest accounts" />;
}
return (
<ErrorCard
retry={refreshLargest}
text="Failed to fetch largest accounts"
/>
);
} else if (largestAccounts.status === FetchStatus.Fetching) {
return <LoadingCard message="Refreshing largest accounts" />;
}
const accounts = largestAccounts.data.largest;
if (accounts.length === 0) {
return <ErrorCard text="No holders found" />;
}
// Find largest fixed point in accounts array
const balanceFixedPoint = accounts.reduce(
(prev: number, current: TokenAccountBalancePairWithOwner) => {
const amount = `${current.uiAmountString}`;
const length = amount.length;
const decimalIndex = amount.indexOf(".");
if (decimalIndex >= 0 && length - decimalIndex - 1 > prev) {
return length - decimalIndex - 1;
}
return prev;
},
0
);
const supplyTotal = normalizeTokenAmount(mintInfo.supply, mintInfo.decimals);
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Largest Accounts</h4>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm card-table">
<thead>
<tr>
<th className="text-muted">Rank</th>
<th className="text-muted">Address</th>
<th className="text-muted">Owner</th>
<th className="text-muted text-end">Balance {unitLabel}</th>
<th className="text-muted text-end">% of Total Supply</th>
</tr>
</thead>
<tbody className="list">
{accounts.map((account, index) =>
renderAccountRow(account, index, balanceFixedPoint, supplyTotal)
)}
</tbody>
</table>
</div>
</div>
</>
);
}
const renderAccountRow = (
account: TokenAccountBalancePairWithOwner,
index: number,
balanceFixedPoint: number,
supply: number
) => {
let percent = "-";
if (supply > 0 && account.uiAmountString) {
let uiAmountPercent = new BigNumber(account.uiAmountString)
.times(100)
.dividedBy(supply);
percent = `${uiAmountPercent.toFormat(3)}%`;
if (
parseFloat(percent) === 0 &&
new BigNumber(account.uiAmountString).gt(0)
) {
percent = `~${percent}`;
}
}
return (
<tr key={index}>
<td>
<span className="badge bg-gray-soft badge-pill">{index + 1}</span>
</td>
<td className="td">
<Address pubkey={account.address} link truncate />
</td>
<td>
{account.owner && <Address pubkey={account.owner} link truncate />}
</td>
<td className="text-end font-monospace">
{account.uiAmountString &&
new BigNumber(account.uiAmountString).toFormat(balanceFixedPoint)}
</td>
<td className="text-end font-monospace">{percent}</td>
</tr>
);
};

View File

@ -1,66 +0,0 @@
import React from "react";
import { Account } from "providers/accounts";
import { SolBalance } from "components/common/SolBalance";
import { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address";
import { addressLabel } from "utils/tx";
import { useCluster } from "providers/cluster";
import { useTokenRegistry } from "providers/mints/token-registry";
export function UnknownAccountCard({ account }: { account: Account }) {
const { cluster } = useCluster();
const { tokenRegistry } = useTokenRegistry();
const label = addressLabel(account.pubkey.toBase58(), cluster, tokenRegistry);
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Overview</h3>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{label && (
<tr>
<td>Address Label</td>
<td className="text-lg-end">{label}</td>
</tr>
)}
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end">
{account.lamports === 0 ? (
"Account does not exist"
) : (
<SolBalance lamports={account.lamports} />
)}
</td>
</tr>
{account.space !== undefined && (
<tr>
<td>Allocated Data Size</td>
<td className="text-lg-end">{account.space} byte(s)</td>
</tr>
)}
<tr>
<td>Assigned Program Id</td>
<td className="text-lg-end">
<Address pubkey={account.owner} alignRight link />
</td>
</tr>
<tr>
<td>Executable</td>
<td className="text-lg-end">{account.executable ? "Yes" : "No"}</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -1,338 +0,0 @@
import React from "react";
import { TableCardBody } from "components/common/TableCardBody";
import { SolBalance } from "components/common/SolBalance";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { Address } from "components/common/Address";
import {
ProgramAccountInfo,
ProgramBufferAccountInfo,
ProgramDataAccountInfo,
UpgradeableLoaderAccount,
} from "validators/accounts/upgradeable-program";
import { Slot } from "components/common/Slot";
import { addressLabel } from "utils/tx";
import { useCluster } from "providers/cluster";
import { UnknownAccountCard } from "components/account/UnknownAccountCard";
import { Downloadable } from "components/common/Downloadable";
import { CheckingBadge, VerifiedBadge } from "components/common/VerifiedBadge";
import { InfoTooltip } from "components/common/InfoTooltip";
import { useVerifiableBuilds } from "utils/program-verification";
import { SecurityTXTBadge } from "components/common/SecurityTXTBadge";
export function UpgradeableLoaderAccountSection({
account,
parsedData,
programData,
}: {
account: Account;
parsedData: UpgradeableLoaderAccount;
programData: ProgramDataAccountInfo | undefined;
}) {
switch (parsedData.type) {
case "program": {
return (
<UpgradeableProgramSection
account={account}
programAccount={parsedData.info}
programData={programData}
/>
);
}
case "programData": {
return (
<UpgradeableProgramDataSection
account={account}
programData={parsedData.info}
/>
);
}
case "buffer": {
return (
<UpgradeableProgramBufferSection
account={account}
programBuffer={parsedData.info}
/>
);
}
case "uninitialized": {
return <UnknownAccountCard account={account} />;
}
}
}
export function UpgradeableProgramSection({
account,
programAccount,
programData,
}: {
account: Account;
programAccount: ProgramAccountInfo;
programData: ProgramDataAccountInfo | undefined;
}) {
const refresh = useFetchAccountInfo();
const { cluster } = useCluster();
const label = addressLabel(account.pubkey.toBase58(), cluster);
const { loading, verifiableBuilds } = useVerifiableBuilds(account.pubkey);
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
{programData === undefined && "Closed "}Program Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{label && (
<tr>
<td>Address Label</td>
<td className="text-lg-end">{label}</td>
</tr>
)}
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports} />
</td>
</tr>
<tr>
<td>Executable</td>
<td className="text-lg-end">
{programData !== undefined ? "Yes" : "No"}
</td>
</tr>
<tr>
<td>Executable Data{programData === undefined && " (Closed)"}</td>
<td className="text-lg-end">
<Address pubkey={programAccount.programData} alignRight link />
</td>
</tr>
{programData !== undefined && (
<>
<tr>
<td>Upgradeable</td>
<td className="text-lg-end">
{programData.authority !== null ? "Yes" : "No"}
</td>
</tr>
<tr>
<td>
<LastVerifiedBuildLabel />
</td>
<td className="text-lg-end">
{loading ? (
<CheckingBadge />
) : (
<>
{verifiableBuilds.map((b, i) => (
<VerifiedBadge
key={i}
verifiableBuild={b}
deploySlot={programData.slot}
/>
))}
</>
)}
</td>
</tr>
<tr>
<td>
<SecurityLabel />
</td>
<td className="text-lg-end">
<SecurityTXTBadge
programData={programData}
pubkey={account.pubkey}
/>
</td>
</tr>
<tr>
<td>Last Deployed Slot</td>
<td className="text-lg-end">
<Slot slot={programData.slot} link />
</td>
</tr>
{programData.authority !== null && (
<tr>
<td>Upgrade Authority</td>
<td className="text-lg-end">
<Address pubkey={programData.authority} alignRight link />
</td>
</tr>
)}
</>
)}
</TableCardBody>
</div>
);
}
function SecurityLabel() {
return (
<InfoTooltip text="Security.txt helps security researchers to contact developers if they find security bugs.">
<a
rel="noopener noreferrer"
target="_blank"
href="https://github.com/neodyme-labs/solana-security-txt"
>
<span className="security-txt-link-color-hack-reee">Security.txt</span>
<span className="fe fe-external-link ms-2"></span>
</a>
</InfoTooltip>
);
}
function LastVerifiedBuildLabel() {
return (
<InfoTooltip text="Indicates whether the program currently deployed on-chain is verified to match the associated published source code, when it is available.">
Verifiable Build Status (experimental)
</InfoTooltip>
);
}
export function UpgradeableProgramDataSection({
account,
programData,
}: {
account: Account;
programData: ProgramDataAccountInfo;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Program Executable Data Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports} />
</td>
</tr>
{account.space !== undefined && (
<tr>
<td>Data Size (Bytes)</td>
<td className="text-lg-end">
<Downloadable
data={programData.data[0]}
filename={`${account.pubkey.toString()}.bin`}
>
<span className="me-2">{account.space}</span>
</Downloadable>
</td>
</tr>
)}
<tr>
<td>Upgradeable</td>
<td className="text-lg-end">
{programData.authority !== null ? "Yes" : "No"}
</td>
</tr>
<tr>
<td>Last Deployed Slot</td>
<td className="text-lg-end">
<Slot slot={programData.slot} link />
</td>
</tr>
{programData.authority !== null && (
<tr>
<td>Upgrade Authority</td>
<td className="text-lg-end">
<Address pubkey={programData.authority} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
export function UpgradeableProgramBufferSection({
account,
programBuffer,
}: {
account: Account;
programBuffer: ProgramBufferAccountInfo;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Program Deploy Buffer Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports} />
</td>
</tr>
{account.space !== undefined && (
<tr>
<td>Data Size (Bytes)</td>
<td className="text-lg-end">{account.space}</td>
</tr>
)}
{programBuffer.authority !== null && (
<tr>
<td>Deploy Authority</td>
<td className="text-lg-end">
<Address pubkey={programBuffer.authority} alignRight link />
</td>
</tr>
)}
<tr>
<td>Owner</td>
<td className="text-lg-end">
<Address pubkey={account.owner} alignRight link />
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -1,87 +0,0 @@
import React from "react";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address";
import { VoteAccount } from "validators/accounts/vote";
import { displayTimestamp } from "utils/date";
import {
AccountHeader,
AccountAddressRow,
AccountBalanceRow,
} from "components/common/Account";
import { Slot } from "components/common/Slot";
export function VoteAccountSection({
account,
voteAccount,
}: {
account: Account;
voteAccount: VoteAccount;
}) {
const refresh = useFetchAccountInfo();
const rootSlot = voteAccount.info.rootSlot;
return (
<div className="card">
<AccountHeader
title="Vote Account"
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>
Authorized Voter
{voteAccount.info.authorizedVoters.length > 1 ? "s" : ""}
</td>
<td className="text-lg-end">
{voteAccount.info.authorizedVoters.map((voter) => {
return (
<Address
pubkey={voter.authorizedVoter}
key={voter.authorizedVoter.toString()}
alignRight
raw
link
/>
);
})}
</td>
</tr>
<tr>
<td>Authorized Withdrawer</td>
<td className="text-lg-end">
<Address
pubkey={voteAccount.info.authorizedWithdrawer}
alignRight
raw
link
/>
</td>
</tr>
<tr>
<td>Last Timestamp</td>
<td className="text-lg-end font-monospace">
{displayTimestamp(voteAccount.info.lastTimestamp.timestamp * 1000)}
</td>
</tr>
<tr>
<td>Commission</td>
<td className="text-lg-end">{voteAccount.info.commission + "%"}</td>
</tr>
<tr>
<td>Root Slot</td>
<td className="text-lg-end">
{rootSlot !== null ? <Slot slot={rootSlot} link /> : "N/A"}
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -1,53 +0,0 @@
import { Slot } from "components/common/Slot";
import React from "react";
import { VoteAccount, Vote } from "validators/accounts/vote";
export function VotesCard({ voteAccount }: { voteAccount: VoteAccount }) {
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Vote History</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Slot</th>
<th className="text-muted">Confirmation Count</th>
</tr>
</thead>
<tbody className="list">
{voteAccount.info.votes.length > 0 &&
voteAccount.info.votes
.reverse()
.map((vote: Vote, index) => renderAccountRow(vote, index))}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{voteAccount.info.votes.length > 0 ? "" : "No votes found"}
</div>
</div>
</div>
</>
);
}
const renderAccountRow = (vote: Vote, index: number) => {
return (
<tr key={index}>
<td className="w-1 font-monospace">
<Slot slot={vote.slot} link />
</td>
<td className="font-monospace">{vote.confirmationCount}</td>
</tr>
);
};

View File

@ -1,95 +0,0 @@
import React from "react";
import { TableCardBody } from "components/common/TableCardBody";
import { SolBalance } from "components/common/SolBalance";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { Address } from "components/common/Address";
import { AddressLookupTableAccount } from "@solana/web3.js";
import { Slot } from "components/common/Slot";
import { AddressLookupTableAccountInfo } from "validators/accounts/address-lookup-table";
export function AddressLookupTableAccountSection(
params:
| {
account: Account;
data: Uint8Array;
}
| {
account: Account;
lookupTableAccount: AddressLookupTableAccountInfo;
}
) {
const account = params.account;
const lookupTableState = React.useMemo(() => {
if ("data" in params) {
return AddressLookupTableAccount.deserialize(params.data);
} else {
return params.lookupTableAccount;
}
}, [params]);
const lookupTableAccount = new AddressLookupTableAccount({
key: account.pubkey,
state: lookupTableState,
});
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Address Lookup Table Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports} />
</td>
</tr>
<tr>
<td>Activation Status</td>
<td className="text-lg-end text-uppercase">
{lookupTableAccount.isActive() ? "Active" : "Deactivated"}
</td>
</tr>
<tr>
<td>Last Extended Slot</td>
<td className="text-lg-end">
{lookupTableAccount.state.lastExtendedSlot === 0 ? (
"None (Empty)"
) : (
<Slot slot={lookupTableAccount.state.lastExtendedSlot} link />
)}
</td>
</tr>
<tr>
<td>Authority</td>
<td className="text-lg-end">
{lookupTableAccount.state.authority === undefined ? (
"None (Frozen)"
) : (
<Address
pubkey={lookupTableAccount.state.authority}
alignRight
link
/>
)}
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -1,71 +0,0 @@
import React from "react";
import { AddressLookupTableAccount, PublicKey } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { AddressLookupTableAccountInfo } from "validators/accounts/address-lookup-table";
export function LookupTableEntriesCard(
params:
| {
parsedLookupTable: AddressLookupTableAccountInfo;
}
| {
lookupTableAccountData: Uint8Array;
}
) {
const lookupTableState = React.useMemo(() => {
if ("lookupTableAccountData" in params) {
return AddressLookupTableAccount.deserialize(
params.lookupTableAccountData
);
} else {
return params.parsedLookupTable;
}
}, [params]);
return (
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Lookup Table Entries</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Index</th>
<th className="text-muted">Address</th>
</tr>
</thead>
<tbody className="list">
{lookupTableState.addresses.length > 0 &&
lookupTableState.addresses.map((entry: PublicKey, index) => {
return renderRow(entry, index);
})}
</tbody>
</table>
</div>
{lookupTableState.addresses.length === 0 && (
<div className="card-footer">
<div className="text-muted text-center">No entries found</div>
</div>
)}
</div>
);
}
const renderRow = (entry: PublicKey, index: number) => {
return (
<tr key={index}>
<td className="w-1 font-monospace">{index}</td>
<td className="font-monospace">
<Address pubkey={entry} link />
</td>
</tr>
);
};

View File

@ -1,13 +0,0 @@
import { PublicKey } from "@solana/web3.js";
const PROGRAM_ID: string = "AddressLookupTab1e1111111111111111111111111";
export function isAddressLookupTableAccount(
accountOwner: PublicKey,
accountData: Uint8Array
): boolean {
if (accountOwner.toBase58() !== PROGRAM_ID) return false;
if (!accountData || accountData.length === 0) return false;
const LOOKUP_TABLE_ACCOUNT_TYPE = 1;
return accountData[0] === LOOKUP_TABLE_ACCOUNT_TYPE;
}

View File

@ -1,197 +0,0 @@
import React from "react";
import {
ParsedTransactionWithMeta,
ParsedInstruction,
PartiallyDecodedInstruction,
PublicKey,
} from "@solana/web3.js";
import { useAccountHistory } from "providers/accounts";
import { Signature } from "components/common/Signature";
import {
getTokenInstructionName,
InstructionContainer,
} from "utils/instruction";
import { Address } from "components/common/Address";
import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
import { FetchStatus } from "providers/cache";
import { useFetchAccountHistory } from "providers/accounts/history";
import {
getTransactionRows,
HistoryCardFooter,
HistoryCardHeader,
} from "../HistoryCardComponents";
import { extractMintDetails, MintDetails } from "./common";
import Moment from "react-moment";
export function TokenInstructionsCard({ pubkey }: { pubkey: PublicKey }) {
const address = pubkey.toBase58();
const history = useAccountHistory(address);
const fetchAccountHistory = useFetchAccountHistory();
const refresh = () => fetchAccountHistory(pubkey, true, true);
const loadMore = () => fetchAccountHistory(pubkey, true);
const transactionRows = React.useMemo(() => {
if (history?.data?.fetched) {
return getTransactionRows(history.data.fetched);
}
return [];
}, [history]);
React.useEffect(() => {
if (!history || !history.data?.transactionMap?.size) {
refresh();
}
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
const { hasTimestamps, detailsList } = React.useMemo(() => {
const detailedHistoryMap =
history?.data?.transactionMap ||
new Map<string, ParsedTransactionWithMeta>();
const hasTimestamps = transactionRows.some((element) => element.blockTime);
const detailsList: React.ReactNode[] = [];
const mintMap = new Map<string, MintDetails>();
transactionRows.forEach(
({ signatureInfo, signature, blockTime, statusClass, statusText }) => {
const transactionWithMeta = detailedHistoryMap.get(signature);
if (!transactionWithMeta) return;
extractMintDetails(transactionWithMeta, mintMap);
let instructions: (ParsedInstruction | PartiallyDecodedInstruction)[] =
[];
InstructionContainer.create(transactionWithMeta).instructions.forEach(
({ instruction, inner }) => {
if (isRelevantInstruction(pubkey, address, mintMap, instruction)) {
instructions.push(instruction);
}
instructions.push(
...inner.filter((instruction) =>
isRelevantInstruction(pubkey, address, mintMap, instruction)
)
);
}
);
instructions.forEach((ix, index) => {
const programId = ix.programId;
const instructionName = getTokenInstructionName(
transactionWithMeta,
ix,
signatureInfo
);
if (instructionName) {
detailsList.push(
<tr key={signature + index}>
<td>
<Signature signature={signature} link truncateChars={48} />
</td>
{hasTimestamps && (
<td className="text-muted">
{blockTime && <Moment date={blockTime * 1000} fromNow />}
</td>
)}
<td>{instructionName}</td>
<td>
<Address
pubkey={programId}
link
truncate
truncateChars={16}
/>
</td>
<td>
<span className={`badge bg-${statusClass}-soft`}>
{statusText}
</span>
</td>
</tr>
);
}
});
}
);
return {
hasTimestamps,
detailsList,
};
}, [history, transactionRows, address, pubkey]);
if (!history) {
return null;
}
if (history?.data === undefined) {
if (history.status === FetchStatus.Fetching) {
return <LoadingCard message="Loading token instructions" />;
}
return (
<ErrorCard retry={refresh} text="Failed to fetch token instructions" />
);
}
const fetching = history.status === FetchStatus.Fetching;
return (
<div className="card">
<HistoryCardHeader
fetching={fetching}
refresh={() => refresh()}
title="Token Instructions"
/>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted w-1">Transaction Signature</th>
{hasTimestamps && <th className="text-muted">Age</th>}
<th className="text-muted">Instruction</th>
<th className="text-muted">Program</th>
<th className="text-muted">Result</th>
</tr>
</thead>
<tbody className="list">{detailsList}</tbody>
</table>
</div>
<HistoryCardFooter
fetching={fetching}
foundOldest={history.data.foundOldest}
loadMore={() => loadMore()}
/>
</div>
);
}
function isRelevantInstruction(
pubkey: PublicKey,
address: string,
mintMap: Map<string, MintDetails>,
instruction: ParsedInstruction | PartiallyDecodedInstruction
) {
if ("accounts" in instruction) {
return instruction.accounts.some(
(account) =>
account.equals(pubkey) ||
mintMap.get(account.toBase58())?.mint === address
);
} else if (
typeof instruction.parsed === "object" &&
"info" in instruction.parsed
) {
return Object.entries(instruction.parsed.info).some(
([key, value]) =>
value === address ||
(typeof value === "string" && mintMap.get(value)?.mint === address)
);
}
return false;
}

View File

@ -1,272 +0,0 @@
import React from "react";
import {
ParsedTransactionWithMeta,
ParsedInstruction,
PartiallyDecodedInstruction,
PublicKey,
} from "@solana/web3.js";
import { useAccountHistory } from "providers/accounts";
import { useTokenRegistry } from "providers/mints/token-registry";
import { create } from "superstruct";
import {
TokenInstructionType,
Transfer,
TransferChecked,
} from "components/instruction/token/types";
import { InstructionContainer } from "utils/instruction";
import { Signature } from "components/common/Signature";
import { Address } from "components/common/Address";
import { normalizeTokenAmount } from "utils";
import {
getTransactionRows,
HistoryCardFooter,
HistoryCardHeader,
} from "../HistoryCardComponents";
import { LoadingCard } from "components/common/LoadingCard";
import { useFetchAccountHistory } from "providers/accounts/history";
import { ErrorCard } from "components/common/ErrorCard";
import { FetchStatus } from "providers/cache";
import Moment from "react-moment";
import { extractMintDetails, MintDetails } from "./common";
import { Cluster, useCluster } from "providers/cluster";
import { reportError } from "utils/sentry";
type IndexedTransfer = {
index: number;
childIndex?: number;
transfer: Transfer | TransferChecked;
};
export function TokenTransfersCard({ pubkey }: { pubkey: PublicKey }) {
const { cluster } = useCluster();
const address = pubkey.toBase58();
const history = useAccountHistory(address);
const fetchAccountHistory = useFetchAccountHistory();
const refresh = () => fetchAccountHistory(pubkey, true, true);
const loadMore = () => fetchAccountHistory(pubkey, true);
const { tokenRegistry } = useTokenRegistry();
const mintDetails = React.useMemo(
() => tokenRegistry.get(address),
[address, tokenRegistry]
);
const transactionRows = React.useMemo(() => {
if (history?.data?.fetched) {
return getTransactionRows(history.data.fetched);
}
return [];
}, [history]);
React.useEffect(() => {
if (!history || !history.data?.transactionMap?.size) {
refresh();
}
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
const { hasTimestamps, detailsList } = React.useMemo(() => {
const detailedHistoryMap =
history?.data?.transactionMap ||
new Map<string, ParsedTransactionWithMeta>();
const hasTimestamps = transactionRows.some((element) => element.blockTime);
const detailsList: React.ReactNode[] = [];
const mintMap = new Map<string, MintDetails>();
transactionRows.forEach(
({ signature, blockTime, statusText, statusClass }) => {
const transactionWithMeta = detailedHistoryMap.get(signature);
if (!transactionWithMeta) return;
// Extract mint information from token deltas
// (used to filter out non-checked tokens transfers not belonging to this mint)
extractMintDetails(transactionWithMeta, mintMap);
// Extract all transfers from transaction
let transfers: IndexedTransfer[] = [];
InstructionContainer.create(transactionWithMeta).instructions.forEach(
({ instruction, inner }, index) => {
const transfer = getTransfer(instruction, cluster, signature);
if (transfer) {
transfers.push({
transfer,
index,
});
}
inner.forEach((instruction, childIndex) => {
const transfer = getTransfer(instruction, cluster, signature);
if (transfer) {
transfers.push({
transfer,
index,
childIndex,
});
}
});
}
);
// Filter out transfers not belonging to this mint
transfers = transfers.filter(({ transfer }) => {
const sourceKey = transfer.source.toBase58();
const destinationKey = transfer.destination.toBase58();
if ("tokenAmount" in transfer && transfer.mint.equals(pubkey)) {
return true;
} else if (
mintMap.has(sourceKey) &&
mintMap.get(sourceKey)?.mint === address
) {
return true;
} else if (
mintMap.has(destinationKey) &&
mintMap.get(destinationKey)?.mint === address
) {
return true;
}
return false;
});
transfers.forEach(({ transfer, index, childIndex }) => {
let units = "Tokens";
let amountString = "";
if (mintDetails?.symbol) {
units = mintDetails.symbol;
}
if ("tokenAmount" in transfer) {
amountString = transfer.tokenAmount.uiAmountString;
} else {
let decimals = 0;
if (mintDetails?.decimals) {
decimals = mintDetails.decimals;
} else if (mintMap.has(transfer.source.toBase58())) {
decimals = mintMap.get(transfer.source.toBase58())?.decimals || 0;
} else if (mintMap.has(transfer.destination.toBase58())) {
decimals =
mintMap.get(transfer.destination.toBase58())?.decimals || 0;
}
amountString = new Intl.NumberFormat("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(normalizeTokenAmount(transfer.amount, decimals));
}
detailsList.push(
<tr key={signature + index + (childIndex || "")}>
<td>
<Signature signature={signature} link truncateChars={24} />
</td>
{hasTimestamps && (
<td className="text-muted">
{blockTime && <Moment date={blockTime * 1000} fromNow />}
</td>
)}
<td>
<Address pubkey={transfer.source} link truncateChars={16} />
</td>
<td>
<Address
pubkey={transfer.destination}
link
truncateChars={16}
/>
</td>
<td>
{amountString} {units}
</td>
<td>
<span className={`badge bg-${statusClass}-soft`}>
{statusText}
</span>
</td>
</tr>
);
});
}
);
return {
hasTimestamps,
detailsList,
};
}, [history, transactionRows, mintDetails, pubkey, address, cluster]);
if (!history) {
return null;
}
if (history?.data === undefined) {
if (history.status === FetchStatus.Fetching) {
return <LoadingCard message="Loading token transfers" />;
}
return <ErrorCard retry={refresh} text="Failed to fetch token transfers" />;
}
const fetching = history.status === FetchStatus.Fetching;
return (
<div className="card">
<HistoryCardHeader
fetching={fetching}
refresh={() => refresh()}
title="Token Transfers"
/>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Transaction Signature</th>
{hasTimestamps && <th className="text-muted">Age</th>}
<th className="text-muted">Source</th>
<th className="text-muted">Destination</th>
<th className="text-muted">Amount</th>
<th className="text-muted">Result</th>
</tr>
</thead>
<tbody className="list">{detailsList}</tbody>
</table>
</div>
<HistoryCardFooter
fetching={fetching}
foundOldest={history.data.foundOldest}
loadMore={() => loadMore()}
/>
</div>
);
}
function getTransfer(
instruction: ParsedInstruction | PartiallyDecodedInstruction,
cluster: Cluster,
signature: string
): Transfer | TransferChecked | undefined {
if ("parsed" in instruction && instruction.program === "spl-token") {
try {
const { type: rawType } = instruction.parsed;
const type = create(rawType, TokenInstructionType);
if (type === "transferChecked") {
return create(instruction.parsed.info, TransferChecked);
} else if (type === "transfer") {
return create(instruction.parsed.info, Transfer);
}
} catch (error) {
if (cluster === Cluster.MainnetBeta) {
reportError(error, {
signature,
});
}
}
}
return undefined;
}

View File

@ -1,121 +0,0 @@
import React from "react";
import { Signature } from "components/common/Signature";
import { Slot } from "components/common/Slot";
import Moment from "react-moment";
import { PublicKey } from "@solana/web3.js";
import {
useAccountHistory,
useFetchAccountHistory,
} from "providers/accounts/history";
import {
getTransactionRows,
HistoryCardFooter,
HistoryCardHeader,
} from "../HistoryCardComponents";
import { FetchStatus } from "providers/cache";
import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
import { displayTimestampUtc } from "utils/date";
export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
const address = pubkey.toBase58();
const history = useAccountHistory(address);
const fetchAccountHistory = useFetchAccountHistory();
const refresh = () => fetchAccountHistory(pubkey, false, true);
const loadMore = () => fetchAccountHistory(pubkey, false);
const transactionRows = React.useMemo(() => {
if (history?.data?.fetched) {
return getTransactionRows(history.data.fetched);
}
return [];
}, [history]);
React.useEffect(() => {
if (!history) {
refresh();
}
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
if (!history) {
return null;
}
if (history?.data === undefined) {
if (history.status === FetchStatus.Fetching) {
return <LoadingCard message="Loading history" />;
}
return (
<ErrorCard retry={refresh} text="Failed to fetch transaction history" />
);
}
const hasTimestamps = transactionRows.some((element) => element.blockTime);
const detailsList: React.ReactNode[] = transactionRows.map(
({ slot, signature, blockTime, statusClass, statusText }) => {
return (
<tr key={signature}>
<td>
<Signature signature={signature} link truncateChars={60} />
</td>
<td className="w-1">
<Slot slot={slot} link />
</td>
{hasTimestamps && (
<>
<td className="text-muted">
{blockTime ? <Moment date={blockTime * 1000} fromNow /> : "---"}
</td>
<td className="text-muted">
{blockTime
? displayTimestampUtc(blockTime * 1000, true)
: "---"}
</td>
</>
)}
<td>
<span className={`badge bg-${statusClass}-soft`}>{statusText}</span>
</td>
</tr>
);
}
);
const fetching = history.status === FetchStatus.Fetching;
return (
<div className="card">
<HistoryCardHeader
fetching={fetching}
refresh={() => refresh()}
title="Transaction History"
/>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted w-1">Transaction Signature</th>
<th className="text-muted w-1">Block</th>
{hasTimestamps && (
<>
<th className="text-muted w-1">Age</th>
<th className="text-muted w-1">Timestamp</th>
</>
)}
<th className="text-muted">Result</th>
</tr>
</thead>
<tbody className="list">{detailsList}</tbody>
</table>
</div>
<HistoryCardFooter
fetching={fetching}
foundOldest={history.data.foundOldest}
loadMore={() => loadMore()}
/>
</div>
);
}

View File

@ -1,37 +0,0 @@
import { ParsedTransactionWithMeta } from "@solana/web3.js";
export type MintDetails = {
decimals: number;
mint: string;
};
export function extractMintDetails(
transactionWithMeta: ParsedTransactionWithMeta,
mintMap: Map<string, MintDetails>
) {
if (transactionWithMeta.meta?.preTokenBalances) {
transactionWithMeta.meta.preTokenBalances.forEach((balance) => {
const account =
transactionWithMeta.transaction.message.accountKeys[
balance.accountIndex
];
mintMap.set(account.pubkey.toBase58(), {
decimals: balance.uiTokenAmount.decimals,
mint: balance.mint,
});
});
}
if (transactionWithMeta.meta?.postTokenBalances) {
transactionWithMeta.meta.postTokenBalances.forEach((balance) => {
const account =
transactionWithMeta.transaction.message.accountKeys[
balance.accountIndex
];
mintMap.set(account.pubkey.toBase58(), {
decimals: balance.uiTokenAmount.decimals,
mint: balance.mint,
});
});
}
}

View File

@ -1,120 +0,0 @@
import React, { Suspense } from "react";
import { Account } from "../../../providers/accounts";
import {
parseNFTokenCollectionAccount,
parseNFTokenNFTAccount,
} from "./isNFTokenAccount";
import { NftokenTypes } from "./nftoken-types";
import { InfoTooltip } from "../../common/InfoTooltip";
import { CachedImageContent } from "../../common/NFTArt";
import { useNftokenMetadata } from "./nftoken-hooks";
export function NFTokenAccountHeader({ account }: { account: Account }) {
const nft = parseNFTokenNFTAccount(account);
if (nft) {
return (
<Suspense fallback={<div>Loading...</div>}>
<NFTokenNFTHeader nft={nft} />
</Suspense>
);
}
const collection = parseNFTokenCollectionAccount(account);
if (collection) {
return (
<Suspense fallback={<div>Loading...</div>}>
<NFTokenCollectionHeader collection={collection} />
</Suspense>
);
}
return (
<>
<h6 className="header-pretitle">Details</h6>
<h2 className="header-title">Account</h2>
</>
);
}
export function NFTokenNFTHeader({ nft }: { nft: NftokenTypes.NftAccount }) {
const { data: metadata } = useNftokenMetadata(nft.metadata_url);
return (
<div className="row">
<div className="col-auto ms-2 d-flex align-items-center">
<CachedImageContent uri={metadata?.image} />
</div>
<div className="col mb-3 ms-0.5 mt-3">
{<h6 className="header-pretitle ms-1">NFToken NFT</h6>}
<div className="d-flex align-items-center">
<h2 className="header-title ms-1 align-items-center no-overflow-with-ellipsis">
{metadata ? metadata.name || "No NFT name was found" : "Loading..."}
</h2>
</div>
<div>
<div className={"d-inline-flex align-items-center mt-2"}>
<span className="badge badge-pill bg-dark">{`${
nft.authority_can_update ? "Mutable" : "Immutable"
}`}</span>
<InfoTooltip
bottom
text={
nft.authority_can_update
? "The authority of this NFT can update the Metadata."
: "The Metadata cannot be updated by anyone."
}
/>
</div>
</div>
</div>
</div>
);
}
export function NFTokenCollectionHeader({
collection,
}: {
collection: NftokenTypes.CollectionAccount;
}) {
const { data: metadata } = useNftokenMetadata(collection.metadata_url);
return (
<div className="row">
<div className="col-auto ms-2 d-flex align-items-center">
<CachedImageContent uri={metadata?.image} />
</div>
<div className="col mb-3 ms-0.5 mt-3">
{<h6 className="header-pretitle ms-1">NFToken Collection</h6>}
<div className="d-flex align-items-center">
<h2 className="header-title ms-1 align-items-center no-overflow-with-ellipsis">
{metadata
? metadata.name || "No collection name was found"
: "Loading..."}
</h2>
</div>
<div>
<div className={"d-inline-flex align-items-center mt-2"}>
<span className="badge badge-pill bg-dark">{`${
collection.authority_can_update ? "Mutable" : "Immutable"
}`}</span>
<InfoTooltip
bottom
text={
collection.authority_can_update
? "The authority of this Collection can update the Metadata and add NFTs."
: "The Metadata cannot be updated by anyone."
}
/>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,217 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { TableCardBody } from "components/common/TableCardBody";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { Suspense, useEffect, useState } from "react";
import {
parseNFTokenCollectionAccount,
parseNFTokenNFTAccount,
} from "./isNFTokenAccount";
import { NftokenTypes } from "./nftoken-types";
import { MAX_TIME_LOADING_IMAGE, useCachedImage } from "../../common/NFTArt";
import { useCollectionNfts } from "./nftoken-hooks";
import { UnknownAccountCard } from "../UnknownAccountCard";
export function NFTokenAccountSection({ account }: { account: Account }) {
const nft = parseNFTokenNFTAccount(account);
if (nft) {
return <NFTCard nft={nft} />;
}
const collection = parseNFTokenCollectionAccount(account);
if (collection) {
return <CollectionCard collection={collection} />;
}
return <UnknownAccountCard account={account} />;
}
const NFTCard = ({ nft }: { nft: NftokenTypes.NftAccount }) => {
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(new PublicKey(nft.address), "parsed");
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Overview
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(nft.address)} alignRight raw />
</td>
</tr>
<tr>
<td>Authority</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(nft.authority)} alignRight link />
</td>
</tr>
<tr>
<td>Holder</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(nft.holder)} alignRight link />
</td>
</tr>
<tr>
<td>Delegate</td>
<td className="text-lg-end">
{nft.delegate ? (
<Address pubkey={new PublicKey(nft.delegate)} alignRight link />
) : (
"Not Delegated"
)}
</td>
</tr>
<tr>
<td>Collection</td>
<td className="text-lg-end">
{nft.collection ? (
<Address pubkey={new PublicKey(nft.collection)} alignRight link />
) : (
"No Collection"
)}
</td>
</tr>
</TableCardBody>
</div>
);
};
export const NftokenImage = ({
url,
size,
}: {
url: string | undefined;
size: number;
}) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [showError, setShowError] = useState<boolean>(false);
const [timeout, setTimeout] = useState<NodeJS.Timeout | undefined>(undefined);
useEffect(() => {
// Set the timeout if we don't have a valid uri
if (!url && !timeout) {
setTimeout(setInterval(() => setShowError(true), MAX_TIME_LOADING_IMAGE));
}
// We have a uri - clear the timeout
if (url && timeout) {
clearInterval(timeout);
}
return () => {
if (timeout) {
clearInterval(timeout);
}
};
}, [url, setShowError, timeout, setTimeout]);
const { cachedBlob } = useCachedImage(url || "");
return (
<>
{showError ? (
<div
style={{ width: size, height: size, backgroundColor: "lightgrey" }}
/>
) : (
<>
{isLoading && (
<div
style={{
width: size,
height: size,
backgroundColor: "lightgrey",
}}
/>
)}
<div className={`${isLoading ? "d-none" : "d-block"}`}>
<img
className={`rounded mx-auto ${isLoading ? "d-none" : "d-block"}`}
src={cachedBlob}
alt={"nft"}
style={{
width: size,
height: size,
}}
onLoad={() => {
setIsLoading(false);
}}
onError={() => {
setShowError(true);
}}
/>
</div>
</>
)}
</>
);
};
const CollectionCard = ({
collection,
}: {
collection: NftokenTypes.CollectionAccount;
}) => {
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(new PublicKey(collection.address), "parsed");
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Overview
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address
pubkey={new PublicKey(collection.address)}
alignRight
raw
/>
</td>
</tr>
<tr>
<td>Authority</td>
<td className="text-lg-end">
<Address
pubkey={new PublicKey(collection.authority)}
alignRight
link
/>
</td>
</tr>
<tr>
<td>Number of NFTs</td>
<td className="text-lg-end">
<Suspense fallback={<div>Loading...</div>}>
<NumNfts collection={collection.address} />
</Suspense>
</td>
</tr>
</TableCardBody>
</div>
);
};
const NumNfts = ({ collection }: { collection: string }) => {
const { data: nfts } = useCollectionNfts({ collectionAddress: collection });
return <div>{nfts.length}</div>;
};

View File

@ -1,64 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { clusterPath } from "../../../utils/url";
import { useCollectionNfts } from "./nftoken-hooks";
import { NftokenImage } from "./NFTokenAccountSection";
export function NFTokenCollectionNFTGrid({
collection,
}: {
collection: string;
}) {
const { data: nfts, mutate } = useCollectionNfts({
collectionAddress: collection,
});
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">NFTs</h3>
<button className="btn btn-white btn-sm" onClick={() => mutate()}>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
<div className="py-4">
{nfts.length === 0 && <div className={"px-4"}>No NFTs Found</div>}
{nfts.length > 0 && (
<div
style={{
display: "grid",
/* Creates as many columns as possible that are at least 10rem wide. */
gridTemplateColumns: "repeat(auto-fill, minmax(10rem, 1fr))",
gridGap: "1.5rem",
}}
>
{nfts.map((nft) => (
<div
key={nft.address}
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
gap: "1rem",
}}
>
<NftokenImage url={nft.image} size={80} />
<div>
<Link to={clusterPath(`/address/${nft.address}`)}>
<div>{nft.name ?? "No Name"}</div>
</Link>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -1,9 +0,0 @@
# NFToken
NFToken is a cheap, simple, secure NFT standard on Solana.
You can find more information and support here:
- [Website](https://nftoken.so)
- [Twitter](https://twitter.com/nftoken_so)
- [Lead Maintainer](https://twitter.com/VictorPontis)

View File

@ -1,85 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { NFTOKEN_ADDRESS } from "./nftoken";
import { Account } from "../../../providers/accounts";
import { NftokenTypes } from "./nftoken-types";
export function isNFTokenAccount(account: Account): boolean {
return Boolean(
account.owner.toBase58() === NFTOKEN_ADDRESS && account.data.raw
);
}
const nftokenAccountDisc = "IbRbNewPP2E=";
export const parseNFTokenNFTAccount = (
account: Account
): NftokenTypes.NftAccount | null => {
if (!isNFTokenAccount(account)) {
return null;
}
try {
const parsed = NftokenTypes.nftAccountLayout.decode(account.data.raw!);
if (!parsed) {
return null;
}
if (
Buffer.from(parsed!.discriminator).toString("base64") !==
nftokenAccountDisc
) {
return null;
}
return {
address: account.pubkey.toBase58(),
holder: new PublicKey(parsed.holder).toBase58(),
authority: new PublicKey(parsed.authority).toBase58(),
authority_can_update: Boolean(parsed.authority_can_update),
collection: new PublicKey(parsed.collection).toBase58(),
delegate: new PublicKey(parsed.delegate).toBase58(),
metadata_url: parsed.metadata_url?.replace(/\0/g, "") ?? null,
};
} catch (e) {
console.error("Problem parsing NFToken NFT...", e);
return null;
}
};
const collectionAccountDisc = "RQLwA3YS2fI=";
export const parseNFTokenCollectionAccount = (
account: Account
): NftokenTypes.CollectionAccount | null => {
if (!isNFTokenAccount(account)) {
return null;
}
try {
const parsed = NftokenTypes.collectionAccountLayout.decode(
account.data.raw!
);
if (!parsed) {
return null;
}
if (
Buffer.from(parsed.discriminator).toString("base64") !==
collectionAccountDisc
) {
return null;
}
return {
address: account.pubkey.toBase58(),
authority: parsed.authority,
authority_can_update: Boolean(parsed.authority_can_update),
metadata_url: parsed.metadata_url?.replace(/\0/g, "") ?? null,
};
} catch (e) {
console.error("Problem parsing NFToken Collection...", e);
return null;
}
};

View File

@ -1,55 +0,0 @@
import useSWR, { SWRResponse } from "swr";
import { useCluster } from "../../../providers/cluster";
import { NftokenFetcher } from "./nftoken";
import { NftokenTypes } from "./nftoken-types";
const getCollectionNftsFetcher = async (
_method: string,
collectionAddress: string,
url: string
) => {
return await NftokenFetcher.getNftsInCollection({
collection: collectionAddress,
rpcUrl: url,
});
};
export const useCollectionNfts = ({
collectionAddress,
}: {
collectionAddress: string;
}): {
// We can be confident that data will be nonnull even if the request fails,
// if we defined fallbackData in the config.
data: NftokenTypes.NftInfo[];
error: any;
mutate: SWRResponse<NftokenTypes.NftInfo[], never>["mutate"];
} => {
const { url } = useCluster();
const swrKey = ["getNftsInCollection", collectionAddress, url];
const { data, error, mutate } = useSWR(swrKey, getCollectionNftsFetcher, {
suspense: true,
});
// Not nullable since we use suspense
return { data: data!, error, mutate };
};
const getMetadataFetcher = async (metadataUrl: string) => {
return await NftokenFetcher.getMetadata({ url: metadataUrl });
};
export const useNftokenMetadata = (
metadataUrl: string | null | undefined
): {
data: NftokenTypes.Metadata | null;
error: any;
mutate: SWRResponse<NftokenTypes.Metadata | null, never>["mutate"];
} => {
const swrKey = [metadataUrl];
const { data, error, mutate } = useSWR(swrKey, getMetadataFetcher, {
suspense: true,
});
// Not nullable since we use suspense
return { data: data!, error, mutate };
};

View File

@ -1,69 +0,0 @@
import * as BufferLayout from "@solana/buffer-layout";
const publicKey = (property: string) => {
return BufferLayout.blob(32, property);
};
export namespace NftokenTypes {
export type Metadata = {
name: string;
description: string | null;
image: string;
traits: any;
animation_url: string | null;
external_url: string | null;
};
export type CollectionAccount = {
address: string;
authority: string;
authority_can_update: boolean;
metadata_url: string | null;
};
export type NftAccount = {
address: string;
holder: string;
authority: string;
authority_can_update: boolean;
collection: string | null;
delegate: string | null;
metadata_url: string;
};
export type NftInfo = NftAccount & Partial<Metadata>;
export const nftAccountLayout = BufferLayout.struct([
BufferLayout.blob(8, "discriminator"),
BufferLayout.u8("version"),
publicKey("holder"),
publicKey("authority"),
BufferLayout.u8("authority_can_update"),
publicKey("collection"),
publicKey("delegate"),
BufferLayout.u8("is_frozen"),
BufferLayout.u8("unused_1"),
BufferLayout.u8("unused_2"),
BufferLayout.u8("unused_3"),
BufferLayout.u32("metadata_url_length"),
BufferLayout.utf8(400, "metadata_url"),
]);
export const collectionAccountLayout = BufferLayout.struct([
BufferLayout.blob(8, "discriminator"),
BufferLayout.u8("version"),
publicKey("authority"),
BufferLayout.u8("authority_can_update"),
BufferLayout.u8("unused_1"),
BufferLayout.u8("unused_2"),
BufferLayout.u8("unused_3"),
BufferLayout.u8("unused_4"),
BufferLayout.u32("metadata_url_length"),
BufferLayout.utf8(400, "metadata_url"),
]);
}

View File

@ -1,143 +0,0 @@
import axios from "axios";
import pLimit from "p-limit";
import { Connection, PublicKey } from "@solana/web3.js";
import bs58 from "bs58";
import { NftokenTypes } from "./nftoken-types";
export const NFTOKEN_ADDRESS = "nftokf9qcHSYkVSP3P2gUMmV6d4AwjMueXgUu43HyLL";
const nftokenAccountDiscInHex = "21b45b35ec0f3f61";
export namespace NftokenFetcher {
export const getNftsInCollection = async ({
collection,
rpcUrl,
}: {
collection: string;
rpcUrl: string;
}): Promise<NftokenTypes.NftInfo[]> => {
const connection = new Connection(rpcUrl);
const accounts = await connection.getProgramAccounts(
new PublicKey(NFTOKEN_ADDRESS),
{
filters: [
{
memcmp: {
offset: 0,
bytes: bs58.encode(Buffer.from(nftokenAccountDiscInHex, "hex")),
},
},
{
memcmp: {
offset:
8 + // discriminator
1 + // version
32 + // holder
32 + // authority
1, // authority_can_update
bytes: collection,
},
},
],
}
);
const parsed_accounts: NftokenTypes.NftAccount[] = accounts.flatMap(
(account) => {
const parsed = NftokenTypes.nftAccountLayout.decode(
account.account.data
);
if (!parsed) {
return [];
}
return {
address: account.pubkey.toBase58(),
holder: parsed.holder,
authority: parsed.authority,
authority_can_update: Boolean(parsed.authority_can_update),
collection: parsed.collection,
delegate: parsed.delegate,
metadata_url: parsed.metadata_url,
};
}
);
const metadata_urls = parsed_accounts.map((a) => a.metadata_url);
const metadataMap = await getMetadataMap({ urls: metadata_urls });
const nfts = parsed_accounts.map((account) => ({
...account,
...metadataMap.get(account.metadata_url),
}));
nfts.sort();
return nfts.sort((a, b) => {
if (a.name && b.name) {
return a.name < b.name ? -1 : 1;
}
if (a.name) {
return 1;
}
if (b.name) {
return -1;
}
return a.address < b.address ? 1 : -1;
});
};
export const getMetadata = async ({
url,
}: {
url: string | null | undefined;
}): Promise<NftokenTypes.Metadata | null> => {
if (!url) {
return null;
}
const metadataMap = await getMetadataMap({
urls: [url],
});
return metadataMap.get(url) ?? null;
};
export const getMetadataMap = async ({
urls: _urls,
}: {
urls: Array<string | null | undefined>;
}): Promise<Map<string, NftokenTypes.Metadata | null>> => {
const urls = Array.from(
new Set(_urls.filter((url): url is string => Boolean(url)))
);
const metadataMap = new Map<string, NftokenTypes.Metadata | null>();
const limit = pLimit(5);
const promises = urls.map((url) =>
limit(async () => {
try {
const { data } = await axios.get(url, {
timeout: 5_000,
});
metadataMap.set(url, {
name: data.name ?? "",
description: data.description ?? null,
image: data.image ?? "",
traits: data.traits ?? [],
animation_url: data.animation_url ?? null,
external_url: data.external_url ?? null,
});
} catch {
metadataMap.set(url, null);
}
})
);
await Promise.all(promises);
return metadataMap;
};
}

View File

@ -1,131 +0,0 @@
import React from "react";
import { PublicKey, VersionedBlockResponse } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { Link } from "react-router-dom";
import { clusterPath } from "utils/url";
type AccountStats = {
reads: number;
writes: number;
};
const PAGE_SIZE = 25;
export function BlockAccountsCard({
block,
blockSlot,
}: {
block: VersionedBlockResponse;
blockSlot: number;
}) {
const [numDisplayed, setNumDisplayed] = React.useState(10);
const totalTransactions = block.transactions.length;
const accountStats = React.useMemo(() => {
const statsMap = new Map<string, AccountStats>();
block.transactions.forEach((tx) => {
const message = tx.transaction.message;
const txSet = new Map<string, boolean>();
const accountKeys = message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
message.compiledInstructions.forEach((ix) => {
ix.accountKeyIndexes.forEach((index) => {
const address = accountKeys.get(index)!.toBase58();
txSet.set(address, message.isAccountWritable(index));
});
});
txSet.forEach((isWritable, address) => {
const stats = statsMap.get(address) || { reads: 0, writes: 0 };
if (isWritable) {
stats.writes++;
} else {
stats.reads++;
}
statsMap.set(address, stats);
});
});
const accountEntries = [];
for (let entry of statsMap) {
accountEntries.push(entry);
}
accountEntries.sort((a, b) => {
const aCount = a[1].reads + a[1].writes;
const bCount = b[1].reads + b[1].writes;
if (aCount < bCount) return 1;
if (aCount > bCount) return -1;
return 0;
});
return accountEntries;
}, [block]);
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Account Usage</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Account</th>
<th className="text-muted">Read-Write Count</th>
<th className="text-muted">Read-Only Count</th>
<th className="text-muted">Total Count</th>
<th className="text-muted">% of Transactions</th>
</tr>
</thead>
<tbody>
{accountStats
.slice(0, numDisplayed)
.map(([address, { writes, reads }]) => {
return (
<tr key={address}>
<td>
<Link
to={clusterPath(
`/block/${blockSlot}`,
new URLSearchParams(
`accountFilter=${address}&filter=all`
)
)}
>
<Address pubkey={new PublicKey(address)} />
</Link>
</td>
<td>{writes}</td>
<td>{reads}</td>
<td>{writes + reads}</td>
<td>
{((100 * (writes + reads)) / totalTransactions).toFixed(
2
)}
%
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{accountStats.length > numDisplayed && (
<div className="card-footer">
<button
className="btn btn-primary w-100"
onClick={() =>
setNumDisplayed((displayed) => displayed + PAGE_SIZE)
}
>
Load More
</button>
</div>
)}
</div>
);
}

View File

@ -1,466 +0,0 @@
import React from "react";
import { Link, useHistory, useLocation } from "react-router-dom";
import { Location } from "history";
import {
ConfirmedTransactionMeta,
TransactionSignature,
PublicKey,
VOTE_PROGRAM_ID,
VersionedBlockResponse,
} from "@solana/web3.js";
import { ErrorCard } from "components/common/ErrorCard";
import { Signature } from "components/common/Signature";
import { Address } from "components/common/Address";
import { pickClusterParams, useQuery } from "utils/url";
import { useCluster } from "providers/cluster";
import { displayAddress } from "utils/tx";
import { parseProgramLogs } from "utils/program-logs";
import { SolBalance } from "components/common/SolBalance";
const PAGE_SIZE = 25;
const useQueryProgramFilter = (query: URLSearchParams): string => {
const filter = query.get("filter");
return filter || "";
};
const useQueryAccountFilter = (query: URLSearchParams): PublicKey | null => {
const filter = query.get("accountFilter");
if (filter !== null) {
try {
return new PublicKey(filter);
} catch {}
}
return null;
};
type SortMode = "index" | "compute" | "fee";
const useQuerySort = (query: URLSearchParams): SortMode => {
const sort = query.get("sort");
if (sort === "compute") return "compute";
if (sort === "fee") return "fee";
return "index";
};
type TransactionWithInvocations = {
index: number;
signature?: TransactionSignature;
meta: ConfirmedTransactionMeta | null;
invocations: Map<string, number>;
computeUnits?: number;
logTruncated: boolean;
};
export function BlockHistoryCard({ block }: { block: VersionedBlockResponse }) {
const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE);
const [showDropdown, setDropdown] = React.useState(false);
const query = useQuery();
const programFilter = useQueryProgramFilter(query);
const accountFilter = useQueryAccountFilter(query);
const sortMode = useQuerySort(query);
const { cluster } = useCluster();
const location = useLocation();
const history = useHistory();
const { transactions, invokedPrograms } = React.useMemo(() => {
const invokedPrograms = new Map<string, number>();
const transactions: TransactionWithInvocations[] = block.transactions.map(
(tx, index) => {
let signature: TransactionSignature | undefined;
if (tx.transaction.signatures.length > 0) {
signature = tx.transaction.signatures[0];
}
let programIndexes = tx.transaction.message.compiledInstructions
.map((ix) => ix.programIdIndex)
.concat(
tx.meta?.innerInstructions?.flatMap((ix) => {
return ix.instructions.map((ix) => ix.programIdIndex);
}) || []
);
const indexMap = new Map<number, number>();
programIndexes.forEach((programIndex) => {
const count = indexMap.get(programIndex) || 0;
indexMap.set(programIndex, count + 1);
});
const invocations = new Map<string, number>();
const accountKeys = tx.transaction.message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
for (const [i, count] of indexMap.entries()) {
const programId = accountKeys.get(i)!.toBase58();
invocations.set(programId, count);
const programTransactionCount = invokedPrograms.get(programId) || 0;
invokedPrograms.set(programId, programTransactionCount + 1);
}
let logTruncated = false;
let computeUnits: number | undefined = undefined;
try {
const parsedLogs = parseProgramLogs(
tx.meta?.logMessages ?? [],
tx.meta?.err ?? null,
cluster
);
logTruncated = parsedLogs[parsedLogs.length - 1].truncated;
computeUnits = parsedLogs
.map(({ computeUnits }) => computeUnits)
.reduce((sum, next) => sum + next);
} catch (err) {
// ignore parsing errors because some old logs aren't parsable
}
return {
index,
signature,
meta: tx.meta,
invocations,
computeUnits,
logTruncated,
};
}
);
return { transactions, invokedPrograms };
}, [block, cluster]);
const [filteredTransactions, showComputeUnits] = React.useMemo((): [
TransactionWithInvocations[],
boolean
] => {
const voteFilter = VOTE_PROGRAM_ID.toBase58();
const filteredTxs: TransactionWithInvocations[] = transactions
.filter(({ invocations }) => {
if (programFilter === ALL_TRANSACTIONS) {
return true;
} else if (programFilter === HIDE_VOTES) {
// hide vote txs that don't invoke any other programs
return !(invocations.has(voteFilter) && invocations.size === 1);
}
return invocations.has(programFilter);
})
.filter(({ index }) => {
if (accountFilter === null) {
return true;
}
const tx = block.transactions[index];
const accountKeys = tx.transaction.message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
return accountKeys
.keySegments()
.flat()
.find((key) => key.equals(accountFilter));
});
const showComputeUnits = filteredTxs.every(
(tx) => tx.computeUnits !== undefined
);
if (sortMode === "compute" && showComputeUnits) {
filteredTxs.sort((a, b) => b.computeUnits! - a.computeUnits!);
} else if (sortMode === "fee") {
filteredTxs.sort((a, b) => (b.meta?.fee || 0) - (a.meta?.fee || 0));
}
return [filteredTxs, showComputeUnits];
}, [
block.transactions,
transactions,
programFilter,
accountFilter,
sortMode,
]);
if (transactions.length === 0) {
return <ErrorCard text="This block has no transactions" />;
}
let title: string;
if (filteredTransactions.length === transactions.length) {
title = `Block Transactions (${filteredTransactions.length})`;
} else {
title = `Filtered Block Transactions (${filteredTransactions.length}/${transactions.length})`;
}
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">{title}</h3>
<FilterDropdown
filter={programFilter}
toggle={() => setDropdown((show) => !show)}
show={showDropdown}
invokedPrograms={invokedPrograms}
totalTransactionCount={transactions.length}
></FilterDropdown>
</div>
{accountFilter !== null && (
<div className="card-body">
Showing transactions which load account:
<div className="d-inline-block ms-2">
<Address pubkey={accountFilter} link />
</div>
</div>
)}
{filteredTransactions.length === 0 ? (
<div className="card-body">
{accountFilter === null && programFilter === HIDE_VOTES
? "This block doesn't contain any non-vote transactions"
: "No transactions found with this filter"}
</div>
) : (
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th
className="text-muted c-pointer"
onClick={() => {
query.delete("sort");
history.push(pickClusterParams(location, query));
}}
>
#
</th>
<th className="text-muted">Result</th>
<th className="text-muted">Transaction Signature</th>
<th
className="text-muted text-end c-pointer"
onClick={() => {
query.set("sort", "fee");
history.push(pickClusterParams(location, query));
}}
>
Fee
</th>
{showComputeUnits && (
<th
className="text-muted text-end c-pointer"
onClick={() => {
query.set("sort", "compute");
history.push(pickClusterParams(location, query));
}}
>
Compute
</th>
)}
<th className="text-muted">Invoked Programs</th>
</tr>
</thead>
<tbody className="list">
{filteredTransactions.slice(0, numDisplayed).map((tx, i) => {
let statusText;
let statusClass;
let signature: React.ReactNode;
if (tx.meta?.err || !tx.signature) {
statusClass = "warning";
statusText = "Failed";
} else {
statusClass = "success";
statusText = "Success";
}
if (tx.signature) {
signature = (
<Signature
signature={tx.signature}
link
truncateChars={48}
/>
);
}
const entries = [...tx.invocations.entries()];
entries.sort();
return (
<tr key={i}>
<td>{tx.index + 1}</td>
<td>
<span className={`badge bg-${statusClass}-soft`}>
{statusText}
</span>
</td>
<td>{signature}</td>
<td className="text-end">
{tx.meta !== null ? (
<SolBalance lamports={tx.meta.fee} />
) : (
"Unknown"
)}
</td>
{showComputeUnits && (
<td className="text-end">
{tx.logTruncated && ">"}
{tx.computeUnits !== undefined
? new Intl.NumberFormat("en-US").format(
tx.computeUnits
)
: "Unknown"}
</td>
)}
<td>
{tx.invocations.size === 0
? "NA"
: entries.map(([programId, count], i) => {
return (
<div
key={i}
className="d-flex align-items-center"
>
<Address
pubkey={new PublicKey(programId)}
link
/>
<span className="ms-2 text-muted">{`(${count})`}</span>
</div>
);
})}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{filteredTransactions.length > numDisplayed && (
<div className="card-footer">
<button
className="btn btn-primary w-100"
onClick={() =>
setNumDisplayed((displayed) => displayed + PAGE_SIZE)
}
>
Load More
</button>
</div>
)}
</div>
);
}
type FilterProps = {
filter: string;
toggle: () => void;
show: boolean;
invokedPrograms: Map<string, number>;
totalTransactionCount: number;
};
const ALL_TRANSACTIONS = "all";
const HIDE_VOTES = "";
type FilterOption = {
name: string;
programId: string;
transactionCount: number;
};
const FilterDropdown = ({
filter,
toggle,
show,
invokedPrograms,
totalTransactionCount,
}: FilterProps) => {
const { cluster } = useCluster();
const buildLocation = (location: Location, filter: string) => {
const params = new URLSearchParams(location.search);
if (filter === HIDE_VOTES) {
params.delete("filter");
} else {
params.set("filter", filter);
}
return {
...location,
search: params.toString(),
};
};
let defaultFilterOption: FilterOption = {
name: "All Except Votes",
programId: HIDE_VOTES,
transactionCount:
totalTransactionCount -
(invokedPrograms.get(VOTE_PROGRAM_ID.toBase58()) || 0),
};
let allTransactionsOption: FilterOption = {
name: "All Transactions",
programId: ALL_TRANSACTIONS,
transactionCount: totalTransactionCount,
};
let currentFilterOption =
filter !== ALL_TRANSACTIONS ? defaultFilterOption : allTransactionsOption;
const filterOptions: FilterOption[] = [
defaultFilterOption,
allTransactionsOption,
];
const placeholderRegistry = new Map();
[...invokedPrograms.entries()].forEach(([programId, transactionCount]) => {
const name = displayAddress(programId, cluster, placeholderRegistry);
if (filter === programId) {
currentFilterOption = {
programId,
name: `${name} Transactions (${transactionCount})`,
transactionCount,
};
}
filterOptions.push({ name, programId, transactionCount });
});
filterOptions.sort((a, b) => {
if (a.transactionCount !== b.transactionCount) {
return b.transactionCount - a.transactionCount;
} else {
return b.name > a.name ? -1 : 1;
}
});
return (
<div className="dropdown me-2">
<button
className="btn btn-white btn-sm dropdown-toggle"
type="button"
onClick={toggle}
>
{currentFilterOption.name}
</button>
<div
className={`token-filter dropdown-menu-end dropdown-menu${
show ? " show" : ""
}`}
>
{filterOptions.map(({ name, programId, transactionCount }) => {
return (
<Link
key={programId}
to={(location: Location) => buildLocation(location, programId)}
className={`dropdown-item${
programId === filter ? " active" : ""
}`}
onClick={toggle}
>
{`${name} (${transactionCount})`}
</Link>
);
})}
</div>
</div>
);
};

View File

@ -1,246 +0,0 @@
import React from "react";
import { TableCardBody } from "components/common/TableCardBody";
import { useBlock, useFetchBlock, FetchStatus } from "providers/block";
import { ErrorCard } from "components/common/ErrorCard";
import { LoadingCard } from "components/common/LoadingCard";
import { Slot } from "components/common/Slot";
import { ClusterStatus, useCluster } from "providers/cluster";
import { BlockHistoryCard } from "./BlockHistoryCard";
import { BlockRewardsCard } from "./BlockRewardsCard";
import { VersionedBlockResponse } from "@solana/web3.js";
import { NavLink } from "react-router-dom";
import { clusterPath } from "utils/url";
import { BlockProgramsCard } from "./BlockProgramsCard";
import { BlockAccountsCard } from "./BlockAccountsCard";
import { displayTimestamp, displayTimestampUtc } from "utils/date";
import { Epoch } from "components/common/Epoch";
import { Address } from "components/common/Address";
export function BlockOverviewCard({
slot,
tab,
}: {
slot: number;
tab?: string;
}) {
const confirmedBlock = useBlock(slot);
const fetchBlock = useFetchBlock();
const { clusterInfo, status } = useCluster();
const refresh = () => fetchBlock(slot);
// Fetch block on load
React.useEffect(() => {
if (!confirmedBlock && status === ClusterStatus.Connected) refresh();
}, [slot, status]); // eslint-disable-line react-hooks/exhaustive-deps
if (!confirmedBlock || confirmedBlock.status === FetchStatus.Fetching) {
return <LoadingCard message="Loading block" />;
} else if (
confirmedBlock.data === undefined ||
confirmedBlock.status === FetchStatus.FetchFailed
) {
return <ErrorCard retry={refresh} text="Failed to fetch block" />;
} else if (confirmedBlock.data.block === undefined) {
return <ErrorCard retry={refresh} text={`Block ${slot} was not found`} />;
}
const { block, blockLeader, childSlot, childLeader, parentLeader } =
confirmedBlock.data;
const showSuccessfulCount = block.transactions.every(
(tx) => tx.meta !== null
);
const successfulTxs = block.transactions.filter(
(tx) => tx.meta?.err === null
);
const epoch = clusterInfo?.epochSchedule.getEpoch(slot);
return (
<>
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Overview
</h3>
</div>
<TableCardBody>
<tr>
<td className="w-100">Blockhash</td>
<td className="text-lg-end font-monospace">
<span>{block.blockhash}</span>
</td>
</tr>
<tr>
<td className="w-100">Slot</td>
<td className="text-lg-end font-monospace">
<Slot slot={slot} />
</td>
</tr>
{blockLeader !== undefined && (
<tr>
<td className="w-100">Slot Leader</td>
<td className="text-lg-end">
<Address pubkey={blockLeader} alignRight link />
</td>
</tr>
)}
{block.blockTime ? (
<>
<tr>
<td>Timestamp (Local)</td>
<td className="text-lg-end">
<span className="font-monospace">
{displayTimestamp(block.blockTime * 1000, true)}
</span>
</td>
</tr>
<tr>
<td>Timestamp (UTC)</td>
<td className="text-lg-end">
<span className="font-monospace">
{displayTimestampUtc(block.blockTime * 1000, true)}
</span>
</td>
</tr>
</>
) : (
<tr>
<td className="w-100">Timestamp</td>
<td className="text-lg-end">Unavailable</td>
</tr>
)}
{epoch !== undefined && (
<tr>
<td className="w-100">Epoch</td>
<td className="text-lg-end font-monospace">
<Epoch epoch={epoch} link />
</td>
</tr>
)}
<tr>
<td className="w-100">Parent Blockhash</td>
<td className="text-lg-end font-monospace">
<span>{block.previousBlockhash}</span>
</td>
</tr>
<tr>
<td className="w-100">Parent Slot</td>
<td className="text-lg-end font-monospace">
<Slot slot={block.parentSlot} link />
</td>
</tr>
{parentLeader !== undefined && (
<tr>
<td className="w-100">Parent Slot Leader</td>
<td className="text-lg-end">
<Address pubkey={parentLeader} alignRight link />
</td>
</tr>
)}
{childSlot !== undefined && (
<tr>
<td className="w-100">Child Slot</td>
<td className="text-lg-end font-monospace">
<Slot slot={childSlot} link />
</td>
</tr>
)}
{childLeader !== undefined && (
<tr>
<td className="w-100">Child Slot Leader</td>
<td className="text-lg-end">
<Address pubkey={childLeader} alignRight link />
</td>
</tr>
)}
<tr>
<td className="w-100">Processed Transactions</td>
<td className="text-lg-end font-monospace">
<span>{block.transactions.length}</span>
</td>
</tr>
{showSuccessfulCount && (
<tr>
<td className="w-100">Successful Transactions</td>
<td className="text-lg-end font-monospace">
<span>{successfulTxs.length}</span>
</td>
</tr>
)}
</TableCardBody>
</div>
<MoreSection block={block} slot={slot} tab={tab} />
</>
);
}
const TABS: Tab[] = [
{
slug: "history",
title: "Transactions",
path: "",
},
{
slug: "rewards",
title: "Rewards",
path: "/rewards",
},
{
slug: "programs",
title: "Programs",
path: "/programs",
},
{
slug: "accounts",
title: "Accounts",
path: "/accounts",
},
];
type MoreTabs = "history" | "rewards" | "programs" | "accounts";
type Tab = {
slug: MoreTabs;
title: string;
path: string;
};
function MoreSection({
slot,
block,
tab,
}: {
slot: number;
block: VersionedBlockResponse;
tab?: string;
}) {
return (
<>
<div className="container">
<div className="header">
<div className="header-body pt-0">
<ul className="nav nav-tabs nav-overflow header-tabs">
{TABS.map(({ title, slug, path }) => (
<li key={slug} className="nav-item">
<NavLink
className="nav-link"
to={clusterPath(`/block/${slot}${path}`)}
exact
>
{title}
</NavLink>
</li>
))}
</ul>
</div>
</div>
</div>
{tab === undefined && <BlockHistoryCard block={block} />}
{tab === "rewards" && <BlockRewardsCard block={block} />}
{tab === "accounts" && (
<BlockAccountsCard block={block} blockSlot={slot} />
)}
{tab === "programs" && <BlockProgramsCard block={block} />}
</>
);
}

View File

@ -1,129 +0,0 @@
import React from "react";
import { PublicKey, VersionedBlockResponse } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { TableCardBody } from "components/common/TableCardBody";
export function BlockProgramsCard({
block,
}: {
block: VersionedBlockResponse;
}) {
const totalTransactions = block.transactions.length;
const txSuccesses = new Map<string, number>();
const txFrequency = new Map<string, number>();
const ixFrequency = new Map<string, number>();
let totalInstructions = 0;
block.transactions.forEach((tx) => {
const message = tx.transaction.message;
totalInstructions += message.compiledInstructions.length;
const programUsed = new Set<string>();
const accountKeys = tx.transaction.message.getAccountKeys({
accountKeysFromLookups: tx.meta?.loadedAddresses,
});
const trackProgram = (index: number) => {
if (index >= accountKeys.length) return;
const programId = accountKeys.get(index)!;
const programAddress = programId.toBase58();
programUsed.add(programAddress);
const frequency = ixFrequency.get(programAddress);
ixFrequency.set(programAddress, frequency ? frequency + 1 : 1);
};
message.compiledInstructions.forEach((ix) =>
trackProgram(ix.programIdIndex)
);
tx.meta?.innerInstructions?.forEach((inner) => {
totalInstructions += inner.instructions.length;
inner.instructions.forEach((innerIx) =>
trackProgram(innerIx.programIdIndex)
);
});
const successful = tx.meta?.err === null;
programUsed.forEach((programId) => {
const frequency = txFrequency.get(programId);
txFrequency.set(programId, frequency ? frequency + 1 : 1);
if (successful) {
const count = txSuccesses.get(programId);
txSuccesses.set(programId, count ? count + 1 : 1);
}
});
});
const programEntries = [];
for (let entry of txFrequency) {
programEntries.push(entry);
}
programEntries.sort((a, b) => {
if (a[1] < b[1]) return 1;
if (a[1] > b[1]) return -1;
return 0;
});
const showSuccessRate = block.transactions.every((tx) => tx.meta !== null);
return (
<>
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Program Stats</h3>
</div>
<TableCardBody>
<tr>
<td className="w-100">Unique Programs Count</td>
<td className="text-lg-end font-monospace">
{programEntries.length}
</td>
</tr>
<tr>
<td className="w-100">Total Instructions</td>
<td className="text-lg-end font-monospace">{totalInstructions}</td>
</tr>
</TableCardBody>
</div>
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Programs</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Program</th>
<th className="text-muted">Transaction Count</th>
<th className="text-muted">% of Total</th>
<th className="text-muted">Instruction Count</th>
<th className="text-muted">% of Total</th>
{showSuccessRate && (
<th className="text-muted">Success Rate</th>
)}
</tr>
</thead>
<tbody>
{programEntries.map(([programId, txFreq]) => {
const ixFreq = ixFrequency.get(programId) as number;
const successes = txSuccesses.get(programId) || 0;
return (
<tr key={programId}>
<td>
<Address pubkey={new PublicKey(programId)} link />
</td>
<td>{txFreq}</td>
<td>{((100 * txFreq) / totalTransactions).toFixed(2)}%</td>
<td>{ixFreq}</td>
<td>{((100 * ixFreq) / totalInstructions).toFixed(2)}%</td>
{showSuccessRate && (
<td>{((100 * successes) / txFreq).toFixed(0)}%</td>
)}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</>
);
}

View File

@ -1,84 +0,0 @@
import React from "react";
import { SolBalance } from "components/common/SolBalance";
import { PublicKey, VersionedBlockResponse } from "@solana/web3.js";
import { Address } from "components/common/Address";
const PAGE_SIZE = 10;
export function BlockRewardsCard({ block }: { block: VersionedBlockResponse }) {
const [rewardsDisplayed, setRewardsDisplayed] = React.useState(PAGE_SIZE);
if (!block.rewards || block.rewards.length < 1) {
return null;
}
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Rewards</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Address</th>
<th className="text-muted">Type</th>
<th className="text-muted">Amount</th>
<th className="text-muted">New Balance</th>
<th className="text-muted">Percent Change</th>
</tr>
</thead>
<tbody>
{block.rewards.map((reward, index) => {
if (index >= rewardsDisplayed - 1) {
return null;
}
let percentChange;
if (reward.postBalance !== null && reward.postBalance !== 0) {
percentChange = (
(Math.abs(reward.lamports) /
(reward.postBalance - reward.lamports)) *
100
).toFixed(9);
}
return (
<tr key={reward.pubkey + reward.rewardType}>
<td>
<Address pubkey={new PublicKey(reward.pubkey)} link />
</td>
<td>{reward.rewardType}</td>
<td>
<SolBalance lamports={reward.lamports} />
</td>
<td>
{reward.postBalance ? (
<SolBalance lamports={reward.postBalance} />
) : (
"-"
)}
</td>
<td>{percentChange ? percentChange + "%" : "-"}</td>
</tr>
);
})}
</tbody>
</table>
</div>
{block.rewards.length > rewardsDisplayed && (
<div className="card-footer">
<button
className="btn btn-primary w-100"
onClick={() =>
setRewardsDisplayed((displayed) => displayed + PAGE_SIZE)
}
>
Load More
</button>
</div>
)}
</div>
);
}

View File

@ -1,48 +0,0 @@
import React from "react";
import { Address } from "./Address";
import { Account } from "providers/accounts";
import { SolBalance } from "components/common/SolBalance";
type AccountHeaderProps = {
title: string;
refresh: Function;
};
type AccountProps = {
account: Account;
};
export function AccountHeader({ title, refresh }: AccountHeaderProps) {
return (
<div className="card-header align-items-center">
<h3 className="card-header-title">{title}</h3>
<button className="btn btn-white btn-sm" onClick={() => refresh()}>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
</button>
</div>
);
}
export function AccountAddressRow({ account }: AccountProps) {
return (
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
);
}
export function AccountBalanceRow({ account }: AccountProps) {
const { lamports } = account;
return (
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase">
<SolBalance lamports={lamports} />
</td>
</tr>
);
}

View File

@ -1,120 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { PublicKey } from "@solana/web3.js";
import { clusterPath } from "utils/url";
import { displayAddress } from "utils/tx";
import { useCluster } from "providers/cluster";
import { Copyable } from "./Copyable";
import { useTokenRegistry } from "providers/mints/token-registry";
import { useState, useEffect } from "react";
import { Connection, programs } from "@metaplex/js";
type Props = {
pubkey: PublicKey;
alignRight?: boolean;
link?: boolean;
raw?: boolean;
truncate?: boolean;
truncateUnknown?: boolean;
truncateChars?: number;
useMetadata?: boolean;
overrideText?: string;
};
export function Address({
pubkey,
alignRight,
link,
raw,
truncate,
truncateUnknown,
truncateChars,
useMetadata,
overrideText,
}: Props) {
const address = pubkey.toBase58();
const { tokenRegistry } = useTokenRegistry();
const { cluster } = useCluster();
if (
truncateUnknown &&
address === displayAddress(address, cluster, tokenRegistry)
) {
truncate = true;
}
let addressLabel = raw
? address
: displayAddress(address, cluster, tokenRegistry);
var metaplexData = useTokenMetadata(useMetadata, address);
if (metaplexData && metaplexData.data)
addressLabel = metaplexData.data.data.name;
if (truncateChars && addressLabel === address) {
addressLabel = addressLabel.slice(0, truncateChars) + "…";
}
if (overrideText) {
addressLabel = overrideText;
}
const content = (
<Copyable text={address} replaceText={!alignRight}>
<span className="font-monospace">
{link ? (
<Link
className={truncate ? "text-truncate address-truncate" : ""}
to={clusterPath(`/address/${address}`)}
>
{addressLabel}
</Link>
) : (
<span className={truncate ? "text-truncate address-truncate" : ""}>
{addressLabel}
</span>
)}
</span>
</Copyable>
);
return (
<>
<div
className={`d-none d-lg-flex align-items-center ${
alignRight ? "justify-content-end" : ""
}`}
>
{content}
</div>
<div className="d-flex d-lg-none align-items-center">{content}</div>
</>
);
}
export const useTokenMetadata = (
useMetadata: boolean | undefined,
pubkey: string
) => {
const [data, setData] = useState<programs.metadata.MetadataData>();
var { url } = useCluster();
useEffect(() => {
if (!useMetadata) return;
if (pubkey && !data) {
programs.metadata.Metadata.getPDA(pubkey)
.then((pda) => {
const connection = new Connection(url);
programs.metadata.Metadata.load(connection, pda)
.then((metadata) => {
setData(metadata.data);
})
.catch(() => {
setData(undefined);
});
})
.catch(() => {
setData(undefined);
});
}
}, [useMetadata, pubkey, url, data, setData]);
return { data };
};

View File

@ -1,33 +0,0 @@
import React from "react";
import { BigNumber } from "bignumber.js";
import { SolBalance } from "components/common/SolBalance";
export function BalanceDelta({
delta,
isSol = false,
}: {
delta: BigNumber;
isSol?: boolean;
}) {
let sols;
if (isSol) {
sols = <SolBalance lamports={Math.abs(delta.toNumber())} />;
}
if (delta.gt(0)) {
return (
<span className="badge bg-success-soft">
+{isSol ? sols : delta.toString()}
</span>
);
} else if (delta.lt(0)) {
return (
<span className="badge bg-warning-soft">
{isSol ? <>-{sols}</> : delta.toString()}
</span>
);
}
return <span className="badge bg-secondary-soft">0</span>;
}

View File

@ -1,98 +0,0 @@
import React, { ReactNode, useState } from "react";
type CopyState = "copy" | "copied" | "errored";
export function Copyable({
text,
children,
replaceText,
}: {
text: string;
children: ReactNode;
replaceText?: boolean;
}) {
const [state, setState] = useState<CopyState>("copy");
const handleClick = async () => {
try {
await navigator.clipboard.writeText(text);
setState("copied");
} catch (err) {
setState("errored");
}
setTimeout(() => setState("copy"), 1000);
};
function CopyIcon() {
if (state === "copy") {
return (
<span className="fe fe-copy c-pointer" onClick={handleClick}></span>
);
} else if (state === "copied") {
return <span className="fe fe-check-circle"></span>;
} else if (state === "errored") {
return (
<span
className="fe fe-x-circle"
title="Please check your browser's copy permissions."
></span>
);
}
return null;
}
let message: string | undefined;
let textColor = "";
if (state === "copied") {
message = "Copied";
textColor = "text-info";
} else if (state === "errored") {
message = "Copy Failed";
textColor = "text-danger";
}
function PrependCopyIcon() {
return (
<>
<span className="font-size-tiny me-2">
<span className={textColor}>
{message !== undefined && <span className="me-2">{message}</span>}
<CopyIcon />
</span>
</span>
{children}
</>
);
}
function ReplaceWithMessage() {
return (
<span className="d-flex flex-column flex-nowrap">
<span className="font-size-tiny">
<span className={textColor}>
<CopyIcon />
<span className="ms-2">{message}</span>
</span>
</span>
<span className="v-hidden">{children}</span>
</span>
);
}
if (state === "copy") {
return <PrependCopyIcon />;
} else if (replaceText) {
return <ReplaceWithMessage />;
}
return (
<>
<span className="d-none d-lg-inline">
<PrependCopyIcon />
</span>
<span className="d-inline d-lg-none">
<ReplaceWithMessage />
</span>
</>
);
}

View File

@ -1,27 +0,0 @@
import { ReactNode } from "react";
export function Downloadable({
data,
filename,
children,
}: {
data: string;
filename: string;
children: ReactNode;
}) {
const handleClick = async () => {
const blob = new Blob([Buffer.from(data, "base64")]);
const fileDownloadUrl = URL.createObjectURL(blob);
const tempLink = document.createElement("a");
tempLink.href = fileDownloadUrl;
tempLink.setAttribute("download", filename);
tempLink.click();
};
return (
<>
<span className="fe fe-download c-pointer me-2" onClick={handleClick} />
{children}
</>
);
}

View File

@ -1,24 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { clusterPath } from "utils/url";
import { Copyable } from "./Copyable";
type Props = {
epoch: number | bigint;
link?: boolean;
};
export function Epoch({ epoch, link }: Props) {
return (
<span className="font-monospace">
{link ? (
<Copyable text={epoch.toString()}>
<Link to={clusterPath(`/epoch/${epoch}`)}>
{epoch.toLocaleString("en-US")}
</Link>
</Copyable>
) : (
epoch.toLocaleString("en-US")
)}
</span>
);
}

View File

@ -1,43 +0,0 @@
import React from "react";
export function ErrorCard({
retry,
retryText,
text,
subtext,
}: {
retry?: () => void;
retryText?: string;
text: string;
subtext?: string;
}) {
const buttonText = retryText || "Try Again";
return (
<div className="card">
<div className="card-body text-center">
{text}
{retry && (
<>
<span
className="btn btn-white ms-3 d-none d-md-inline"
onClick={retry}
>
{buttonText}
</span>
<div className="d-block d-md-none mt-4">
<span className="btn btn-white w-100" onClick={retry}>
{buttonText}
</span>
</div>
{subtext && (
<div className="text-muted">
<hr></hr>
{subtext}
</div>
)}
</>
)}
</div>
</div>
);
}

View File

@ -1,58 +0,0 @@
import React, { ReactNode } from "react";
import { Buffer } from "buffer";
import { Copyable } from "./Copyable";
export function HexData({ raw }: { raw: Buffer }) {
if (!raw || raw.length === 0) {
return <span>No data</span>;
}
const chunks = [];
const hexString = raw.toString("hex");
for (let i = 0; i < hexString.length; i += 2) {
chunks.push(hexString.slice(i, i + 2));
}
const SPAN_SIZE = 4;
const ROW_SIZE = 4 * SPAN_SIZE;
const divs: ReactNode[] = [];
let spans: ReactNode[] = [];
for (let i = 0; i < chunks.length; i += SPAN_SIZE) {
const color = i % (2 * SPAN_SIZE) === 0 ? "text-white" : "text-gray-500";
spans.push(
<span key={i} className={color}>
{chunks.slice(i, i + SPAN_SIZE).join(" ")}&emsp;
</span>
);
if (
i % ROW_SIZE === ROW_SIZE - SPAN_SIZE ||
i >= chunks.length - SPAN_SIZE
) {
divs.push(<div key={i / ROW_SIZE}>{spans}</div>);
// clear spans
spans = [];
}
}
function Content() {
return (
<Copyable text={hexString}>
<pre className="d-inline-block text-start mb-0">{divs}</pre>
</Copyable>
);
}
return (
<>
<div className="d-none d-lg-flex align-items-center justify-content-end">
<Content />
</div>
<div className="d-flex d-lg-none align-items-center">
<Content />
</div>
</>
);
}

View File

@ -1,34 +0,0 @@
import React, { useEffect, useRef } from "react";
// @ts-ignore
import Jazzicon from "@metamask/jazzicon";
import bs58 from "bs58";
import { PublicKey } from "@solana/web3.js";
export function Identicon(props: {
address?: string | PublicKey;
style?: React.CSSProperties;
className?: string;
}) {
const { style, className } = props;
const address =
typeof props.address === "string"
? props.address
: props.address?.toBase58();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (address && ref.current) {
ref.current.innerHTML = "";
ref.current.className = className || "";
ref.current.appendChild(
Jazzicon(
style?.width || 16,
parseInt(bs58.decode(address).toString("hex").slice(5, 15), 16)
)
);
}
}, [address, style, className]);
return <div className="identicon-wrapper" ref={ref} style={props.style} />;
}

View File

@ -1,53 +0,0 @@
import React, { useState, ReactNode } from "react";
type Props = {
text: string;
children?: ReactNode;
bottom?: boolean;
right?: boolean;
};
type State = "hide" | "show";
function Popover({
state,
bottom,
right,
text,
}: {
state: State;
bottom?: boolean;
right?: boolean;
text: string;
}) {
if (state === "hide") return null;
return (
<div
className={`popover bs-popover-${bottom ? "bottom" : "top"}${
right ? " right" : ""
} show`}
>
<div className={`arrow${right ? " right" : ""}`} />
<div className="popover-body">{text}</div>
</div>
);
}
export function InfoTooltip({ bottom, right, text, children }: Props) {
const [state, setState] = useState<State>("hide");
const justify = right ? "end" : "start";
return (
<div
className="popover-container w-100"
onMouseOver={() => setState("show")}
onMouseOut={() => setState("hide")}
>
<div className={`d-flex align-items-center justify-content-${justify}`}>
{children}
<span className="fe fe-help-circle ms-2"></span>
</div>
<Popover bottom={bottom} right={right} state={state} text={text} />
</div>
);
}

View File

@ -1,51 +0,0 @@
import React from "react";
import { ConfirmedSignatureInfo } from "@solana/web3.js";
import {
getTokenProgramInstructionName,
InstructionType,
} from "utils/instruction";
export function InstructionDetails({
instructionType,
tx,
}: {
instructionType: InstructionType;
tx: ConfirmedSignatureInfo;
}) {
const [expanded, setExpanded] = React.useState(false);
let instructionTypes = instructionType.innerInstructions
.map((ix) => {
if ("parsed" in ix && ix.program === "spl-token") {
return getTokenProgramInstructionName(ix, tx);
}
return undefined;
})
.filter((type) => type !== undefined);
return (
<>
<p className="tree">
{instructionTypes.length > 0 && (
<span
onClick={(e) => {
e.preventDefault();
setExpanded(!expanded);
}}
className={`c-pointer fe me-2 ${
expanded ? "fe-minus-square" : "fe-plus-square"
}`}
></span>
)}
{instructionType.name}
</p>
{expanded && (
<ul className="tree">
{instructionTypes.map((type, index) => {
return <li key={index}>{type}</li>;
})}
</ul>
)}
</>
);
}

View File

@ -1,12 +0,0 @@
import React from "react";
export function LoadingCard({ message }: { message?: string }) {
return (
<div className="card">
<div className="card-body text-center">
<span className="spinner-grow spinner-grow-sm me-2"></span>
{message || "Loading"}
</div>
</div>
);
}

View File

@ -1,333 +0,0 @@
import { useEffect, useState } from "react";
import { Stream } from "@cloudflare/stream-react";
import { PublicKey } from "@solana/web3.js";
import {
programs,
MetadataJson,
MetaDataJsonCategory,
MetadataJsonFile,
} from "@metaplex/js";
import ContentLoader from "react-content-loader";
import ErrorLogo from "img/logos-solana/dark-solana-logo.svg";
import { getLast } from "utils";
export const MAX_TIME_LOADING_IMAGE = 5000; /* 5 seconds */
const LoadingPlaceholder = () => (
<ContentLoader
viewBox="0 0 212 200"
height={150}
width={150}
backgroundColor="transparent"
>
<circle cx="86" cy="100" r="8" />
<circle cx="106" cy="100" r="8" />
<circle cx="126" cy="100" r="8" />
</ContentLoader>
);
const ErrorPlaceHolder = () => (
<img src={ErrorLogo} width="120" height="120" alt="Solana Logo" />
);
const ViewOriginalArtContentLink = ({ src }: { src: string }) => {
if (!src) {
return null;
}
return (
<h6 className={"header-pretitle d-flex justify-content-center mt-2"}>
<a href={src}>VIEW ORIGINAL</a>
</h6>
);
};
export const CachedImageContent = ({ uri }: { uri?: string }) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [showError, setShowError] = useState<boolean>(false);
const [timeout, setTimeout] = useState<NodeJS.Timeout | undefined>(undefined);
useEffect(() => {
// Set the timeout if we don't have a valid uri
if (!uri && !timeout) {
setTimeout(setInterval(() => setShowError(true), MAX_TIME_LOADING_IMAGE));
}
// We have a uri - clear the timeout
if (uri && timeout) {
clearInterval(timeout);
}
return () => {
if (timeout) {
clearInterval(timeout);
}
};
}, [uri, setShowError, timeout, setTimeout]);
const { cachedBlob } = useCachedImage(uri || "");
return (
<>
{showError ? (
<div className={"art-error-image-placeholder"}>
<ErrorPlaceHolder />
<h6 className={"header-pretitle mt-2"}>Error Loading Image</h6>
</div>
) : (
<>
{isLoading && <LoadingPlaceholder />}
<div className={`${isLoading ? "d-none" : "d-block"}`}>
<img
className={`rounded mx-auto ${isLoading ? "d-none" : "d-block"}`}
src={cachedBlob}
alt={"nft"}
style={{
width: 150,
maxHeight: 200,
}}
onLoad={() => {
setIsLoading(false);
}}
onError={() => {
setShowError(true);
}}
/>
{uri && <ViewOriginalArtContentLink src={uri} />}
</div>
</>
)}
</>
);
};
const VideoArtContent = ({
files,
uri,
animationURL,
}: {
files?: (MetadataJsonFile | string)[];
uri?: string;
animationURL?: string;
}) => {
const likelyVideo = (files || []).filter((f, index, arr) => {
if (typeof f !== "string") {
return false;
}
// TODO: filter by fileType
return arr.length >= 2 ? index === 1 : index === 0;
})?.[0] as string;
const content =
likelyVideo &&
likelyVideo.startsWith("https://watch.videodelivery.net/") ? (
<div className={"d-block"}>
<Stream
src={likelyVideo.replace("https://watch.videodelivery.net/", "")}
loop={true}
height={180}
width={320}
controls={false}
style={{ borderRadius: 12 }}
videoDimensions={{
videoWidth: 320,
videoHeight: 180,
}}
autoplay={true}
muted={true}
/>
<ViewOriginalArtContentLink
src={likelyVideo.replace("https://watch.videodelivery.net/", "")}
/>
</div>
) : (
<div className={"d-block"}>
<video
playsInline={true}
autoPlay={true}
muted={true}
controls={true}
controlsList="nodownload"
style={{ borderRadius: 12, width: 320, height: 180 }}
loop={true}
poster={uri}
>
{likelyVideo && <source src={likelyVideo} type="video/mp4" />}
{animationURL && <source src={animationURL} type="video/mp4" />}
{files
?.filter((f) => typeof f !== "string")
.map((f: any, index: number) => (
<source key={index} src={f.uri} type={f.type} />
))}
</video>
{(likelyVideo || animationURL) && (
<ViewOriginalArtContentLink src={(likelyVideo || animationURL)!} />
)}
</div>
);
return content;
};
const HTMLContent = ({
animationUrl,
files,
}: {
animationUrl?: string;
files?: (MetadataJsonFile | string)[];
}) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [showError, setShowError] = useState<boolean>(false);
const htmlURL =
files && files.length > 0 && typeof files[0] === "string"
? files[0]
: animationUrl;
return (
<>
{showError ? (
<div className={"art-error-image-placeholder"}>
<ErrorPlaceHolder />
<h6 className={"header-pretitle mt-2"}>Error Loading Image</h6>
</div>
) : (
<>
{!isLoading && <LoadingPlaceholder />}
<div className={`${isLoading ? "d-block" : "d-none"}`}>
<iframe
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
title={"html-content"}
sandbox="allow-scripts"
frameBorder="0"
src={htmlURL}
style={{ width: 320, height: 180, borderRadius: 12 }}
onLoad={() => {
setIsLoading(true);
}}
onError={() => {
setShowError(true);
}}
></iframe>
{htmlURL && <ViewOriginalArtContentLink src={htmlURL} />}
</div>
</>
)}
</>
);
};
export const ArtContent = ({
metadata,
category,
pubkey,
uri,
animationURL,
files,
data,
}: {
metadata: programs.metadata.MetadataData;
category?: MetaDataJsonCategory;
pubkey?: PublicKey | string;
uri?: string;
animationURL?: string;
files?: (MetadataJsonFile | string)[];
data: MetadataJson | undefined;
}) => {
if (pubkey && data) {
uri = data.image;
animationURL = data.animation_url;
}
if (pubkey && data?.properties) {
files = data.properties.files;
category = data.properties.category;
}
animationURL = animationURL || "";
const animationUrlExt = new URLSearchParams(
getLast(animationURL.split("?"))
).get("ext");
const content =
category === "video" ? (
<VideoArtContent files={files} uri={uri} animationURL={animationURL} />
) : category === "html" || animationUrlExt === "html" ? (
<HTMLContent animationUrl={animationURL} files={files} />
) : (
<CachedImageContent uri={uri} />
);
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{content}
</div>
);
};
enum ArtFetchStatus {
ReadyToFetch,
Fetching,
FetchFailed,
FetchSucceeded,
}
const cachedImages = new Map<string, string>();
export const useCachedImage = (uri: string) => {
const [cachedBlob, setCachedBlob] = useState<string | undefined>(undefined);
const [fetchStatus, setFetchStatus] = useState<ArtFetchStatus>(
ArtFetchStatus.ReadyToFetch
);
useEffect(() => {
if (!uri) {
return;
}
if (fetchStatus === ArtFetchStatus.FetchFailed) {
setCachedBlob(uri);
return;
}
const result = cachedImages.get(uri);
if (result) {
setCachedBlob(result);
return;
}
if (fetchStatus === ArtFetchStatus.ReadyToFetch) {
(async () => {
setFetchStatus(ArtFetchStatus.Fetching);
let response: Response;
try {
response = await fetch(uri, { cache: "force-cache" });
} catch {
try {
response = await fetch(uri, { cache: "reload" });
} catch {
if (uri?.startsWith("http")) {
setCachedBlob(uri);
}
setFetchStatus(ArtFetchStatus.FetchFailed);
return;
}
}
const blob = await response.blob();
const blobURI = URL.createObjectURL(blob);
cachedImages.set(uri, blobURI);
setCachedBlob(blobURI);
setFetchStatus(ArtFetchStatus.FetchSucceeded);
})();
}
}, [uri, setCachedBlob, fetchStatus, setFetchStatus]);
return { cachedBlob };
};

View File

@ -1,15 +0,0 @@
import React from "react";
type OverlayProps = {
show: boolean;
};
export function Overlay({ show }: OverlayProps) {
return (
<div
className={`modal-backdrop fade ${
show ? "show" : "disable-pointer-events"
}`}
></div>
);
}

View File

@ -1,33 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { Link } from "react-router-dom";
import { fromProgramData } from "utils/security-txt";
import { clusterPath } from "utils/url";
import { ProgramDataAccountInfo } from "validators/accounts/upgradeable-program";
export function SecurityTXTBadge({
programData,
pubkey,
}: {
programData: ProgramDataAccountInfo;
pubkey: PublicKey;
}) {
const { securityTXT, error } = fromProgramData(programData);
if (securityTXT) {
return (
<h3 className="mb-0">
<Link
className="c-pointer badge bg-success-soft rank"
to={clusterPath(`/address/${pubkey.toBase58()}/security`)}
>
Included
</Link>
</h3>
);
} else {
return (
<h3 className="mb-0">
<span className="badge bg-warning-soft rank">{error}</span>
</h3>
);
}
}

View File

@ -1,50 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { TransactionSignature } from "@solana/web3.js";
import { clusterPath } from "utils/url";
import { Copyable } from "./Copyable";
type Props = {
signature: TransactionSignature;
alignRight?: boolean;
link?: boolean;
truncate?: boolean;
truncateChars?: number;
};
export function Signature({
signature,
alignRight,
link,
truncate,
truncateChars,
}: Props) {
let signatureLabel = signature;
if (truncateChars) {
signatureLabel = signature.slice(0, truncateChars) + "…";
}
return (
<div
className={`d-flex align-items-center ${
alignRight ? "justify-content-end" : ""
}`}
>
<Copyable text={signature} replaceText={!alignRight}>
<span className="font-monospace">
{link ? (
<Link
className={truncate ? "text-truncate signature-truncate" : ""}
to={clusterPath(`/tx/${signature}`)}
>
{signatureLabel}
</Link>
) : (
signatureLabel
)}
</span>
</Copyable>
</div>
);
}

View File

@ -1,24 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { clusterPath } from "utils/url";
import { Copyable } from "./Copyable";
type Props = {
slot: number;
link?: boolean;
};
export function Slot({ slot, link }: Props) {
return (
<span className="font-monospace">
{link ? (
<Copyable text={slot.toString()}>
<Link to={clusterPath(`/block/${slot}`)}>
{slot.toLocaleString("en-US")}
</Link>
</Copyable>
) : (
slot.toLocaleString("en-US")
)}
</span>
);
}

View File

@ -1,19 +0,0 @@
import React from "react";
import { lamportsToSolString } from "utils";
export function SolBalance({
lamports,
maximumFractionDigits = 9,
}: {
lamports: number | bigint;
maximumFractionDigits?: number;
}) {
return (
<span>
<span className="font-monospace">
{lamportsToSolString(lamports, maximumFractionDigits)}
</span>
</span>
);
}

View File

@ -1,11 +0,0 @@
import React from "react";
export 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>
);
}

View File

@ -1,48 +0,0 @@
import { useState } from "react";
import { displayTimestampUtc, displayTimestamp } from "utils/date";
type State = "hide" | "show";
function Tooltip({ state }: { state: State }) {
const tooltip = {
maxWidth: "20rem",
};
if (state === "hide") return null;
return (
<div className="popover bs-popover-bottom show" style={tooltip}>
<div className="arrow" />
<div className="popover-body">
(Click to toggle between local and UTC)
</div>
</div>
);
}
export function TimestampToggle({ unixTimestamp }: { unixTimestamp: number }) {
const [isTimestampTypeUtc, toggleTimestampType] = useState(true);
const [showTooltip, toggleTooltip] = useState<State>("hide");
const toggle = () => {
toggleTimestampType(!isTimestampTypeUtc);
};
const tooltipContainer = {
cursor: "pointer",
};
return (
<div className="popover-container w-100" style={tooltipContainer}>
<span
onClick={toggle}
onMouseOver={() => toggleTooltip("show")}
onMouseOut={() => toggleTooltip("hide")}
>
{isTimestampTypeUtc === true
? displayTimestampUtc(unixTimestamp)
: displayTimestamp(unixTimestamp)}
</span>
<Tooltip state={showTooltip} />
</div>
);
}

View File

@ -1,40 +0,0 @@
import { VerifiableBuild } from "utils/program-verification";
export function VerifiedBadge({
verifiableBuild,
deploySlot,
}: {
verifiableBuild: VerifiableBuild;
deploySlot: number;
}) {
if (verifiableBuild && verifiableBuild.verified_slot === deploySlot) {
return (
<h3 className="mb-0">
<a
className="c-pointer badge bg-info-soft rank"
href={verifiableBuild.url}
target="_blank"
rel="noreferrer"
>
{verifiableBuild.label}: Verified
</a>
</h3>
);
} else {
return (
<h3 className="mb-0">
<span className="badge bg-warning-soft rank">
{verifiableBuild.label}: Unverified
</span>
</h3>
);
}
}
export function CheckingBadge() {
return (
<h3 className="mb-0">
<span className="badge bg-dark rank">Checking</span>
</h3>
);
}

View File

@ -1,46 +0,0 @@
import React from "react";
import { TransactionInstruction, SignatureResult } from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import { useCluster } from "providers/cluster";
import { reportError } from "utils/sentry";
import { parseAddressLookupTableInstructionTitle } from "./address-lookup-table/types";
export function AddressLookupTableDetailsCard({
ix,
index,
result,
signature,
innerCards,
childIndex,
}: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { url } = useCluster();
let title;
try {
title = parseAddressLookupTableInstructionTitle(ix);
} catch (error) {
reportError(error, {
url: url,
signature: signature,
});
}
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={`Address Lookup Table: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);
}

View File

@ -1,156 +0,0 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import {
Idl,
Program,
BorshInstructionCoder,
Instruction,
} from "@project-serum/anchor";
import {
getAnchorNameForInstruction,
getAnchorProgramName,
getAnchorAccountsFromInstruction,
mapIxArgsToRows,
} from "utils/anchor";
import { Address } from "components/common/Address";
import { camelToTitleCase } from "utils";
import { IdlInstruction } from "@project-serum/anchor/dist/cjs/idl";
import { useMemo } from "react";
export default function AnchorDetailsCard(props: {
key: string;
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
anchorProgram: Program<Idl>;
}) {
const { ix, anchorProgram } = props;
const programName = getAnchorProgramName(anchorProgram) ?? "Unknown Program";
const ixName =
getAnchorNameForInstruction(ix, anchorProgram) ?? "Unknown Instruction";
const cardTitle = `${camelToTitleCase(programName)}: ${camelToTitleCase(
ixName
)}`;
return (
<InstructionCard title={cardTitle} {...props}>
<AnchorDetails ix={ix} anchorProgram={anchorProgram} />
</InstructionCard>
);
}
function AnchorDetails({
ix,
anchorProgram,
}: {
ix: TransactionInstruction;
anchorProgram: Program;
}) {
const { ixAccounts, decodedIxData, ixDef } = useMemo(() => {
let ixAccounts:
| {
name: string;
isMut: boolean;
isSigner: boolean;
pda?: Object;
}[]
| null = null;
let decodedIxData: Instruction | null = null;
let ixDef: IdlInstruction | undefined;
if (anchorProgram) {
const coder = new BorshInstructionCoder(anchorProgram.idl);
decodedIxData = coder.decode(ix.data);
if (decodedIxData) {
ixDef = anchorProgram.idl.instructions.find(
(ixDef) => ixDef.name === decodedIxData?.name
);
if (ixDef) {
ixAccounts = getAnchorAccountsFromInstruction(
decodedIxData,
anchorProgram
);
}
}
}
return {
ixAccounts,
decodedIxData,
ixDef,
};
}, [anchorProgram, ix.data]);
if (!ixAccounts || !decodedIxData || !ixDef) {
return (
<tr>
<td colSpan={3} className="text-lg-center">
Failed to decode account data according to the public Anchor interface
</td>
</tr>
);
}
const programName = getAnchorProgramName(anchorProgram) ?? "Unknown Program";
return (
<>
<tr>
<td>Program</td>
<td className="text-lg-end" colSpan={2}>
<Address
pubkey={ix.programId}
alignRight
link
raw
overrideText={programName}
/>
</td>
</tr>
<tr className="table-sep">
<td>Account Name</td>
<td className="text-lg-end" colSpan={2}>
Address
</td>
</tr>
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => {
return (
<tr key={keyIndex}>
<td>
<div className="me-2 d-md-inline">
{ixAccounts
? keyIndex < ixAccounts.length
? `${camelToTitleCase(ixAccounts[keyIndex].name)}`
: `Remaining Account #${keyIndex + 1 - ixAccounts.length}`
: `Account #${keyIndex + 1}`}
</div>
{isWritable && (
<span className="badge bg-info-soft me-1">Writable</span>
)}
{isSigner && (
<span className="badge bg-info-soft me-1">Signer</span>
)}
</td>
<td className="text-lg-end" colSpan={2}>
<Address pubkey={pubkey} alignRight link />
</td>
</tr>
);
})}
{decodedIxData && ixDef && ixDef.args.length > 0 && (
<>
<tr className="table-sep">
<td>Argument Name</td>
<td>Type</td>
<td className="text-lg-end">Value</td>
</tr>
{mapIxArgsToRows(decodedIxData.data, ixDef, anchorProgram.idl)}
</>
)}
</>
);
}

View File

@ -1,58 +0,0 @@
import React from "react";
import { ParsedInstruction, PublicKey, SignatureResult } from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import { Address } from "components/common/Address";
export function AssociatedTokenDetailsCard({
ix,
index,
result,
innerCards,
childIndex,
}: {
ix: ParsedInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const info = ix.parsed.info;
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Associated Token Program: Create"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
<tr>
<td>Account</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(info.account)} alignRight link />
</td>
</tr>
<tr>
<td>Mint</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(info.mint)} alignRight link />
</td>
</tr>
<tr>
<td>Wallet</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(info.wallet)} alignRight link />
</td>
</tr>
</InstructionCard>
);
}

View File

@ -1,170 +0,0 @@
import React from "react";
import {
ComputeBudgetInstruction,
SignatureResult,
TransactionInstruction,
} from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import { SolBalance } from "components/common/SolBalance";
import { Address } from "components/common/Address";
import { reportError } from "utils/sentry";
import { microLamportsToLamportsString } from "utils";
import { useCluster } from "providers/cluster";
export function ComputeBudgetDetailsCard({
ix,
index,
result,
signature,
innerCards,
childIndex,
}: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { url } = useCluster();
try {
const type = ComputeBudgetInstruction.decodeInstructionType(ix);
switch (type) {
case "RequestUnits": {
const { units, additionalFee } =
ComputeBudgetInstruction.decodeRequestUnits(ix);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Compute Budget Program: Request Units (Deprecated)"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
<tr>
<td>Requested Compute Units</td>
<td className="text-lg-end font-monospace">{`${new Intl.NumberFormat(
"en-US"
).format(units)} compute units`}</td>
</tr>
<tr>
<td>Additional Fee (SOL)</td>
<td className="text-lg-end">
<SolBalance lamports={additionalFee} />
</td>
</tr>
</InstructionCard>
);
}
case "RequestHeapFrame": {
const { bytes } = ComputeBudgetInstruction.decodeRequestHeapFrame(ix);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Compute Budget Program: Request Heap Frame"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
<tr>
<td>Requested Heap Frame (Bytes)</td>
<td className="text-lg-end font-monospace">
{new Intl.NumberFormat("en-US").format(bytes)}
</td>
</tr>
</InstructionCard>
);
}
case "SetComputeUnitLimit": {
const { units } =
ComputeBudgetInstruction.decodeSetComputeUnitLimit(ix);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Compute Budget Program: Set Compute Unit Limit"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
<tr>
<td>Compute Unit Limit</td>
<td className="text-lg-end font-monospace">{`${new Intl.NumberFormat(
"en-US"
).format(units)} compute units`}</td>
</tr>
</InstructionCard>
);
}
case "SetComputeUnitPrice": {
const { microLamports } =
ComputeBudgetInstruction.decodeSetComputeUnitPrice(ix);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Compute Budget Program: Set Compute Unit Price"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
<tr>
<td>Compute Unit Price</td>
<td className="text-lg-end font-monospace">{`${microLamportsToLamportsString(
microLamports
)} lamports per compute unit`}</td>
</tr>
</InstructionCard>
);
}
}
} catch (error) {
reportError(error, {
url: url,
signature: signature,
});
}
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Compute Budget Program: Unknown Instruction"
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);
}

View File

@ -1,140 +0,0 @@
import React, { useContext } from "react";
import {
TransactionInstruction,
SignatureResult,
ParsedInstruction,
} from "@solana/web3.js";
import { RawDetails } from "./RawDetails";
import { RawParsedDetails } from "./RawParsedDetails";
import { SignatureContext } from "../../pages/TransactionDetailsPage";
import {
useFetchRawTransaction,
useRawTransactionDetails,
} from "providers/transactions/raw";
import { Address } from "components/common/Address";
import { useScrollAnchor } from "providers/scroll-anchor";
import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id";
type InstructionProps = {
title: string;
children?: React.ReactNode;
result: SignatureResult;
index: number;
ix: TransactionInstruction | ParsedInstruction;
defaultRaw?: boolean;
innerCards?: JSX.Element[];
childIndex?: number;
};
export function InstructionCard({
title,
children,
result,
index,
ix,
defaultRaw,
innerCards,
childIndex,
}: InstructionProps) {
const [resultClass] = ixResult(result, index);
const [showRaw, setShowRaw] = React.useState(defaultRaw || false);
const signature = useContext(SignatureContext);
const rawDetails = useRawTransactionDetails(signature);
let raw: TransactionInstruction | undefined = undefined;
if (rawDetails && childIndex === undefined) {
raw = rawDetails?.data?.raw?.transaction.instructions[index];
}
const fetchRaw = useFetchRawTransaction();
const fetchRawTrigger = () => fetchRaw(signature);
const rawClickHandler = () => {
if (!defaultRaw && !showRaw && !raw) {
fetchRawTrigger();
}
return setShowRaw((r) => !r);
};
const scrollAnchorRef = useScrollAnchor(
getInstructionCardScrollAnchorId(
childIndex != null ? [index + 1, childIndex + 1] : [index + 1]
)
);
return (
<div className="card" ref={scrollAnchorRef}>
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
<span className={`badge bg-${resultClass}-soft me-2`}>
#{index + 1}
{childIndex !== undefined ? `.${childIndex + 1}` : ""}
</span>
{title}
</h3>
<button
disabled={defaultRaw}
className={`btn btn-sm d-flex ${
showRaw ? "btn-black active" : "btn-white"
}`}
onClick={rawClickHandler}
>
<span className="fe fe-code me-1"></span>
Raw
</button>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<tbody className="list">
{showRaw ? (
<>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
{"parsed" in ix ? (
<RawParsedDetails ix={ix}>
{raw ? <RawDetails ix={raw} /> : null}
</RawParsedDetails>
) : (
<RawDetails ix={ix} />
)}
</>
) : (
children
)}
{innerCards && innerCards.length > 0 && (
<>
<tr className="table-sep">
<td colSpan={3}>Inner Instructions</td>
</tr>
<tr>
<td colSpan={3}>
<div className="inner-cards">{innerCards}</div>
</td>
</tr>
</>
)}
</tbody>
</table>
</div>
</div>
);
}
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", `Error: ${JSON.stringify(error)}`];
}
}
return ["dark"];
}
return ["success"];
}

View File

@ -1,170 +0,0 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { useCluster } from "providers/cluster";
import { reportError } from "utils/sentry";
import { InstructionCard } from "./InstructionCard";
import { AddOracleDetailsCard } from "./mango/AddOracleDetailsCard";
import { AddPerpMarketDetailsCard } from "./mango/AddPerpMarketDetailsCard";
import { AddSpotMarketDetailsCard } from "./mango/AddSpotMarketDetailsCard";
import { CancelPerpOrderDetailsCard } from "./mango/CancelPerpOrderDetailsCard";
import { CancelSpotOrderDetailsCard } from "./mango/CancelSpotOrderDetailsCard";
import { ChangePerpMarketParamsDetailsCard } from "./mango/ChangePerpMarketParamsDetailsCard";
import { ConsumeEventsDetailsCard } from "./mango/ConsumeEventsDetailsCard";
import { GenericMngoAccountDetailsCard } from "./mango/GenericMngoAccountDetailsCard";
import { GenericPerpMngoDetailsCard } from "./mango/GenericPerpMngoDetailsCard";
import { GenericSpotMngoDetailsCard } from "./mango/GenericSpotMngoDetailsCard";
import { PlacePerpOrderDetailsCard } from "./mango/PlacePerpOrderDetailsCard";
import { PlaceSpotOrderDetailsCard } from "./mango/PlaceSpotOrderDetailsCard";
import {
decodeAddPerpMarket,
decodeAddSpotMarket,
decodeCancelPerpOrder,
decodeCancelSpotOrder,
decodeChangePerpMarketParams,
decodePlacePerpOrder,
decodePlacePerpOrder2,
decodePlaceSpotOrder,
parseMangoInstructionTitle,
} from "./mango/types";
import { PlacePerpOrder2DetailsCard } from "./mango/PlacePerpOrder2DetailsCard";
export function MangoDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, signature, innerCards, childIndex } = props;
const { url } = useCluster();
let title;
try {
title = parseMangoInstructionTitle(ix);
switch (title) {
case "InitMangoAccount":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "Deposit":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "Withdraw":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "InitSpotOpenOrders":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "PlaceSpotOrder":
return (
<PlaceSpotOrderDetailsCard
info={decodePlaceSpotOrder(ix)}
{...props}
/>
);
case "CancelSpotOrder":
return (
<CancelSpotOrderDetailsCard
info={decodeCancelSpotOrder(ix)}
{...props}
/>
);
case "AddPerpMarket":
return (
<AddPerpMarketDetailsCard info={decodeAddPerpMarket(ix)} {...props} />
);
case "PlacePerpOrder":
return (
<PlacePerpOrderDetailsCard
info={decodePlacePerpOrder(ix)}
{...props}
/>
);
case "PlacePerpOrder2":
return (
<PlacePerpOrder2DetailsCard
info={decodePlacePerpOrder2(ix)}
{...props}
/>
);
case "ConsumeEvents":
return <ConsumeEventsDetailsCard {...props} />;
case "CancelPerpOrder":
return (
<CancelPerpOrderDetailsCard
info={decodeCancelPerpOrder(ix)}
{...props}
/>
);
case "SettleFunds":
return (
<GenericSpotMngoDetailsCard
accountKeyLocation={2}
spotMarketkeyLocation={5}
title={title}
{...props}
/>
);
case "RedeemMngo":
return (
<GenericPerpMngoDetailsCard
mangoAccountKeyLocation={3}
perpMarketKeyLocation={4}
title={title}
{...props}
/>
);
case "ChangePerpMarketParams":
return (
<ChangePerpMarketParamsDetailsCard
info={decodeChangePerpMarketParams(ix)}
{...props}
/>
);
case "AddOracle":
return <AddOracleDetailsCard {...props} />;
case "AddSpotMarket":
return (
<AddSpotMarketDetailsCard info={decodeAddSpotMarket(ix)} {...props} />
);
}
} catch (error) {
reportError(error, {
url: url,
signature: signature,
});
}
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={`Mango Program: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);
}

View File

@ -1,45 +0,0 @@
import React from "react";
import { ParsedInstruction, SignatureResult } from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import { wrap } from "utils";
import { Address } from "components/common/Address";
export function MemoDetailsCard({
ix,
index,
result,
innerCards,
childIndex,
}: {
ix: ParsedInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const data = wrap(ix.parsed, 50);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Memo Program: Memo"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
<tr>
<td>Data (UTF-8)</td>
<td className="text-lg-end">
<pre className="d-inline-block text-start mb-0">{data}</pre>
</td>
</tr>
</InstructionCard>
);
}

View File

@ -1,36 +0,0 @@
import React from "react";
import { TransactionInstruction } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { HexData } from "components/common/HexData";
export function RawDetails({ ix }: { ix: TransactionInstruction }) {
return (
<>
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => (
<tr key={keyIndex}>
<td>
<div className="me-2 d-md-inline">Account #{keyIndex + 1}</div>
{isWritable && (
<span className="badge bg-info-soft me-1">Writable</span>
)}
{isSigner && (
<span className="badge bg-info-soft me-1">Signer</span>
)}
</td>
<td className="text-lg-end">
<Address pubkey={pubkey} alignRight link />
</td>
</tr>
))}
<tr>
<td>
Instruction Data <span className="text-muted">(Hex)</span>
</td>
<td className="text-lg-end">
<HexData raw={ix.data} />
</td>
</tr>
</>
);
}

View File

@ -1,27 +0,0 @@
import React from "react";
import { ParsedInstruction } from "@solana/web3.js";
export function RawParsedDetails({
ix,
children,
}: {
ix: ParsedInstruction;
children?: React.ReactNode;
}) {
return (
<>
{children}
<tr>
<td>
Instruction Data <span className="text-muted">(JSON)</span>
</td>
<td className="text-lg-end">
<pre className="d-inline-block text-start json-wrap">
{JSON.stringify(ix.parsed, null, 2)}
</pre>
</td>
</tr>
</>
);
}

View File

@ -1,161 +0,0 @@
import React from "react";
import { TransactionInstruction, SignatureResult } from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import { useCluster } from "providers/cluster";
import { reportError } from "utils/sentry";
import {
decodeCancelOrder,
decodeCancelOrderByClientId,
decodeCancelOrderByClientIdV2,
decodeCancelOrderV2,
decodeCloseOpenOrders,
decodeConsumeEvents,
decodeConsumeEventsPermissioned,
decodeDisableMarket,
decodeInitializeMarket,
decodeInitOpenOrders,
decodeMatchOrders,
decodeNewOrder,
decodeNewOrderV3,
decodePrune,
decodeSettleFunds,
decodeSweepFees,
OPEN_BOOK_PROGRAM_ID,
parseSerumInstructionKey,
parseSerumInstructionTitle,
} from "./serum/types";
import { NewOrderDetailsCard } from "./serum/NewOrderDetailsCard";
import { MatchOrdersDetailsCard } from "./serum/MatchOrdersDetailsCard";
import { InitializeMarketDetailsCard } from "./serum/InitializeMarketDetailsCard";
import { ConsumeEventsDetailsCard } from "./serum/ConsumeEventsDetails";
import { CancelOrderDetailsCard } from "./serum/CancelOrderDetails";
import { CancelOrderByClientIdDetailsCard } from "./serum/CancelOrderByClientIdDetails";
import { SettleFundsDetailsCard } from "./serum/SettleFundsDetailsCard";
import { DisableMarketDetailsCard } from "./serum/DisableMarketDetails";
import { SweepFeesDetailsCard } from "./serum/SweepFeesDetails";
import { NewOrderV3DetailsCard } from "./serum/NewOrderV3DetailsCard";
import { CancelOrderV2DetailsCard } from "./serum/CancelOrderV2Details";
import { CancelOrderByClientIdV2DetailsCard } from "./serum/CancelOrderByClientIdV2Details";
import { CloseOpenOrdersDetailsCard } from "./serum/CloseOpenOrdersDetails";
import { InitOpenOrdersDetailsCard } from "./serum/InitOpenOrdersDetails";
import { PruneDetailsCard } from "./serum/PruneDetails";
import { ConsumeEventsPermissionedDetailsCard } from "./serum/ConsumeEventsPermissionedDetails";
export function SerumDetailsCard(initialProps: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, signature, innerCards, childIndex } = initialProps;
const props = React.useMemo(() => {
const programName =
initialProps.ix.programId.toBase58() === OPEN_BOOK_PROGRAM_ID
? "OpenBook"
: "Serum";
return { ...initialProps, programName };
}, [initialProps]);
const { url } = useCluster();
let title;
try {
title = parseSerumInstructionTitle(ix);
switch (parseSerumInstructionKey(ix)) {
case "initializeMarket":
return (
<InitializeMarketDetailsCard
info={decodeInitializeMarket(ix)}
{...props}
/>
);
case "newOrder":
return <NewOrderDetailsCard info={decodeNewOrder(ix)} {...props} />;
case "matchOrders":
return (
<MatchOrdersDetailsCard info={decodeMatchOrders(ix)} {...props} />
);
case "consumeEvents":
return (
<ConsumeEventsDetailsCard info={decodeConsumeEvents(ix)} {...props} />
);
case "cancelOrder":
return (
<CancelOrderDetailsCard info={decodeCancelOrder(ix)} {...props} />
);
case "settleFunds":
return (
<SettleFundsDetailsCard info={decodeSettleFunds(ix)} {...props} />
);
case "cancelOrderByClientId":
return (
<CancelOrderByClientIdDetailsCard
info={decodeCancelOrderByClientId(ix)}
{...props}
/>
);
case "disableMarket":
return (
<DisableMarketDetailsCard info={decodeDisableMarket(ix)} {...props} />
);
case "sweepFees":
return <SweepFeesDetailsCard info={decodeSweepFees(ix)} {...props} />;
case "newOrderV3":
return <NewOrderV3DetailsCard info={decodeNewOrderV3(ix)} {...props} />;
case "cancelOrderV2":
return (
<CancelOrderV2DetailsCard info={decodeCancelOrderV2(ix)} {...props} />
);
case "cancelOrderByClientIdV2":
return (
<CancelOrderByClientIdV2DetailsCard
info={decodeCancelOrderByClientIdV2(ix)}
{...props}
/>
);
case "closeOpenOrders":
return (
<CloseOpenOrdersDetailsCard
info={decodeCloseOpenOrders(ix)}
{...props}
/>
);
case "initOpenOrders":
return (
<InitOpenOrdersDetailsCard
info={decodeInitOpenOrders(ix)}
{...props}
/>
);
case "prune":
return <PruneDetailsCard info={decodePrune(ix)} {...props} />;
case "consumeEventsPermissioned":
return (
<ConsumeEventsPermissionedDetailsCard
info={decodeConsumeEventsPermissioned(ix)}
{...props}
/>
);
}
} catch (error) {
reportError(error, {
url: url,
signature: signature,
});
}
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={`${props.programName} Program: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);
}

View File

@ -1,46 +0,0 @@
import React from "react";
import { TransactionInstruction, SignatureResult } from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
import { useCluster } from "providers/cluster";
import { reportError } from "utils/sentry";
import { parseTokenLendingInstructionTitle } from "./token-lending/types";
export function TokenLendingDetailsCard({
ix,
index,
result,
signature,
innerCards,
childIndex,
}: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { url } = useCluster();
let title;
try {
title = parseTokenLendingInstructionTitle(ix);
} catch (error) {
reportError(error, {
url: url,
signature: signature,
});
}
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={`Token Lending: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);
}

Some files were not shown because too many files have changed in this diff Show More