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:
@@ -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>
|
||||
)}
|
||||
|
34
explorer/src/components/common/Identicon.tsx
Normal file
34
explorer/src/components/common/Identicon.tsx
Normal 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} />;
|
||||
}
|
@@ -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 (
|
||||
|
@@ -382,3 +382,11 @@ p.updated-time {
|
||||
.change-negative {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.identicon-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.identicon-wrapper-small {
|
||||
margin-left: .4rem;
|
||||
}
|
||||
|
Reference in New Issue
Block a user