Explorer: use identicon and token layout for unlisted tokens (#16347)

* feat: use identicon and token layout for unlisted tokens

* feat: add identicon to smaller icons and change dependency to current package

* fix: add proper library
This commit is contained in:
Josh 2021-04-12 11:25:51 -07:00 committed by GitHub
parent 105a6bfb46
commit a7f8239b46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 21 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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[] }) {
<tr key={address}>
{showLogos && (
<td className="w-1 p-0 text-center">
{tokenDetails?.logoURI && (
{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>
)}
@ -171,12 +180,18 @@ function HoldingsSummaryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
<tr key={mintAddress}>
{showLogos && (
<td className="w-1 p-0 text-center">
{tokenDetails?.logoURI && (
{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>
)}

View File

@ -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<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,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 (
<div className="container mt-n3">
<div className="header">
<div className="header-body">
<AccountHeader address={address} />
<AccountHeader address={address} info={info} />
</div>
</div>
{!pubkey ? (
<ErrorCard text={`Address "${address}" is not valid`} />
) : (
<DetailsSections pubkey={pubkey} tab={tab} />
<DetailsSections pubkey={pubkey} tab={tab} info={info} />
)}
</div>
);
}
export function AccountHeader({ address }: { address: string }) {
export function AccountHeader({
address,
info,
}: {
address: string;
info?: CacheEntry<Account>;
}) {
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 (
<div className="row align-items-end">
{tokenDetails.logoURI && (
<div className="col-auto">
<div className="avatar avatar-lg header-avatar-top">
<div className="col-auto">
<div className="avatar avatar-lg header-avatar-top">
{tokenDetails?.logoURI ? (
<img
src={tokenDetails.logoURI}
alt="token logo"
className="avatar-img rounded-circle border border-4 border-body"
/>
</div>
) : (
<Identicon
address={address}
className="avatar-img rounded-circle border border-body identicon-wrapper"
style={{ width: IDENTICON_WIDTH }}
/>
)}
</div>
)}
</div>
<div className="col mb-3 ml-n3 ml-md-n2">
<h6 className="header-pretitle">Token</h6>
<h2 className="header-title">{tokenDetails.name}</h2>
<h2 className="header-title">
{tokenDetails?.name || "Unlisted Token"}
</h2>
</div>
</div>
);
@ -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<Account>;
}) {
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 <LoadingCard />;
} else if (

View File

@ -382,3 +382,11 @@ p.updated-time {
.change-negative {
color: $warning;
}
.identicon-wrapper {
display: flex;
}
.identicon-wrapper-small {
margin-left: .4rem;
}