diff --git a/explorer/package-lock.json b/explorer/package-lock.json
index fd3ee43a53..4b26a5070c 100644
--- a/explorer/package-lock.json
+++ b/explorer/package-lock.json
@@ -2342,6 +2342,35 @@
}
}
},
+ "@metamask/jazzicon": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@metamask/jazzicon/-/jazzicon-2.0.0.tgz",
+ "integrity": "sha512-7M+WSZWKcQAo0LEhErKf1z+D3YX0tEDAcGvcKbDyvDg34uvgeKR00mFNIYwAhdAS9t8YXxhxZgsrRBBg6X8UQg==",
+ "requires": {
+ "color": "^0.11.3",
+ "mersenne-twister": "^1.1.0"
+ },
+ "dependencies": {
+ "color": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz",
+ "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=",
+ "requires": {
+ "clone": "^1.0.2",
+ "color-convert": "^1.3.0",
+ "color-string": "^0.3.0"
+ }
+ },
+ "color-string": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz",
+ "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=",
+ "requires": {
+ "color-name": "^1.0.0"
+ }
+ }
+ }
+ },
"@nodelib/fs.scandir": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
@@ -12967,6 +12996,11 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
},
+ "mersenne-twister": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz",
+ "integrity": "sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o="
+ },
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
diff --git a/explorer/package.json b/explorer/package.json
index 6e302194b9..c26fe48b49 100644
--- a/explorer/package.json
+++ b/explorer/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@metamask/jazzicon": "^2.0.0",
"@project-serum/serum": "^0.13.33",
"@react-hook/debounce": "^3.0.0",
"@sentry/react": "^6.2.5",
diff --git a/explorer/src/components/account/OwnedTokensCard.tsx b/explorer/src/components/account/OwnedTokensCard.tsx
index 7deb04f7b7..352ac26bf9 100644
--- a/explorer/src/components/account/OwnedTokensCard.tsx
+++ b/explorer/src/components/account/OwnedTokensCard.tsx
@@ -14,9 +14,12 @@ 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");
@@ -102,12 +105,18 @@ function HoldingsDetailTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
{showLogos && (
- {tokenDetails?.logoURI && (
+ {tokenDetails?.logoURI ? (
+ ) : (
+
)}
|
)}
@@ -171,12 +180,18 @@ function HoldingsSummaryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
{showLogos && (
- {tokenDetails?.logoURI && (
+ {tokenDetails?.logoURI ? (
+ ) : (
+
)}
|
)}
diff --git a/explorer/src/components/common/Identicon.tsx b/explorer/src/components/common/Identicon.tsx
new file mode 100644
index 0000000000..77bb023585
--- /dev/null
+++ b/explorer/src/components/common/Identicon.tsx
@@ -0,0 +1,34 @@
+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(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 ;
+}
diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx
index e18ec78116..13cc2d6033 100644
--- a/explorer/src/pages/AccountDetailsPage.tsx
+++ b/explorer/src/pages/AccountDetailsPage.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
-import { FetchStatus } from "providers/cache";
+import { CacheEntry, FetchStatus } from "providers/cache";
import {
useFetchAccountInfo,
useAccountInfo,
@@ -30,7 +30,9 @@ import { ConfigAccountSection } from "components/account/ConfigAccountSection";
import { useFlaggedAccounts } from "providers/accounts/flagged-accounts";
import { UpgradeableLoaderAccountSection } from "components/account/UpgradeableLoaderAccountSection";
import { useTokenRegistry } from "providers/mints/token-registry";
+import { Identicon } from "components/common/Identicon";
+const IDENTICON_WIDTH = 64;
const TABS_LOOKUP: { [id: string]: Tab } = {
"spl-token:mint": {
slug: "largest",
@@ -69,49 +71,77 @@ const TOKEN_TABS_HIDDEN = [
type Props = { address: string; tab?: string };
export function AccountDetailsPage({ address, tab }: Props) {
+ const fetchAccount = useFetchAccountInfo();
+ const { status } = useCluster();
+ const info = useAccountInfo(address);
let pubkey: PublicKey | undefined;
try {
pubkey = new PublicKey(address);
} catch (err) {}
+ // Fetch account on load
+ React.useEffect(() => {
+ if (!info && status === ClusterStatus.Connected && pubkey) {
+ fetchAccount(pubkey);
+ }
+ }, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
+
return (
{!pubkey ? (
) : (
-
+
)}
);
}
-export function AccountHeader({ address }: { address: string }) {
+export function AccountHeader({
+ address,
+ info,
+}: {
+ address: string;
+ info?: CacheEntry;
+}) {
const { tokenRegistry } = useTokenRegistry();
const tokenDetails = tokenRegistry.get(address);
- if (tokenDetails) {
+ const account = info?.data;
+ const data = account?.details?.data;
+ const isToken = data?.program === "spl-token" && data?.parsed.type === "mint";
+
+ if (tokenDetails || isToken) {
return (
- {tokenDetails.logoURI && (
-
-
+
+
+ {tokenDetails?.logoURI ? (

-
+ ) : (
+
+ )}
- )}
+
Token
- {tokenDetails.name}
+
+ {tokenDetails?.name || "Unlisted Token"}
+
);
@@ -125,19 +155,20 @@ export function AccountHeader({ address }: { address: string }) {
);
}
-function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
+function DetailsSections({
+ pubkey,
+ tab,
+ info,
+}: {
+ pubkey: PublicKey;
+ tab?: string;
+ info?: CacheEntry
;
+}) {
const fetchAccount = useFetchAccountInfo();
const address = pubkey.toBase58();
- const info = useAccountInfo(address);
- const { status } = useCluster();
const location = useLocation();
const { flaggedAccounts } = useFlaggedAccounts();
- // Fetch account on load
- React.useEffect(() => {
- if (!info && status === ClusterStatus.Connected) fetchAccount(pubkey);
- }, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
-
if (!info || info.status === FetchStatus.Fetching) {
return ;
} else if (
diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss
index 1d63dfc701..77ca47f6cc 100644
--- a/explorer/src/scss/_solana.scss
+++ b/explorer/src/scss/_solana.scss
@@ -382,3 +382,11 @@ p.updated-time {
.change-negative {
color: $warning;
}
+
+.identicon-wrapper {
+ display: flex;
+}
+
+.identicon-wrapper-small {
+ margin-left: .4rem;
+}