diff --git a/explorer/package-lock.json b/explorer/package-lock.json index fd3ee43a5..4b26a5070 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 6e302194b..c26fe48b4 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 7deb04f7b..352ac26bf 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 ? ( token icon + ) : ( + )} )} @@ -171,12 +180,18 @@ function HoldingsSummaryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) { {showLogos && ( - {tokenDetails?.logoURI && ( + {tokenDetails?.logoURI ? ( token icon + ) : ( + )} )} diff --git a/explorer/src/components/common/Identicon.tsx b/explorer/src/components/common/Identicon.tsx new file mode 100644 index 000000000..77bb02358 --- /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 e18ec7811..13cc2d603 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 logo -
+ ) : ( + + )}
- )} +
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 1d63dfc70..77ca47f6c 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; +}