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:
parent
105a6bfb46
commit
a7f8239b46
|
@ -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": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
|
"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": {
|
"methods": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
"@project-serum/serum": "^0.13.33",
|
"@project-serum/serum": "^0.13.33",
|
||||||
"@react-hook/debounce": "^3.0.0",
|
"@react-hook/debounce": "^3.0.0",
|
||||||
"@sentry/react": "^6.2.5",
|
"@sentry/react": "^6.2.5",
|
||||||
|
|
|
@ -14,9 +14,12 @@ import { Link } from "react-router-dom";
|
||||||
import { Location } from "history";
|
import { Location } from "history";
|
||||||
import { useTokenRegistry } from "providers/mints/token-registry";
|
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||||
import { BigNumber } from "bignumber.js";
|
import { BigNumber } from "bignumber.js";
|
||||||
|
import { Identicon } from "components/common/Identicon";
|
||||||
|
|
||||||
type Display = "summary" | "detail" | null;
|
type Display = "summary" | "detail" | null;
|
||||||
|
|
||||||
|
const SMALL_IDENTICON_WIDTH = 16;
|
||||||
|
|
||||||
const useQueryDisplay = (): Display => {
|
const useQueryDisplay = (): Display => {
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const filter = query.get("display");
|
const filter = query.get("display");
|
||||||
|
@ -102,12 +105,18 @@ function HoldingsDetailTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||||
<tr key={address}>
|
<tr key={address}>
|
||||||
{showLogos && (
|
{showLogos && (
|
||||||
<td className="w-1 p-0 text-center">
|
<td className="w-1 p-0 text-center">
|
||||||
{tokenDetails?.logoURI && (
|
{tokenDetails?.logoURI ? (
|
||||||
<img
|
<img
|
||||||
src={tokenDetails.logoURI}
|
src={tokenDetails.logoURI}
|
||||||
alt="token icon"
|
alt="token icon"
|
||||||
className="token-icon rounded-circle border border-4 border-gray-dark"
|
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>
|
||||||
)}
|
)}
|
||||||
|
@ -171,12 +180,18 @@ function HoldingsSummaryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||||
<tr key={mintAddress}>
|
<tr key={mintAddress}>
|
||||||
{showLogos && (
|
{showLogos && (
|
||||||
<td className="w-1 p-0 text-center">
|
<td className="w-1 p-0 text-center">
|
||||||
{tokenDetails?.logoURI && (
|
{tokenDetails?.logoURI ? (
|
||||||
<img
|
<img
|
||||||
src={tokenDetails.logoURI}
|
src={tokenDetails.logoURI}
|
||||||
alt="token icon"
|
alt="token icon"
|
||||||
className="token-icon rounded-circle border border-4 border-gray-dark"
|
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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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 React from "react";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { FetchStatus } from "providers/cache";
|
import { CacheEntry, FetchStatus } from "providers/cache";
|
||||||
import {
|
import {
|
||||||
useFetchAccountInfo,
|
useFetchAccountInfo,
|
||||||
useAccountInfo,
|
useAccountInfo,
|
||||||
|
@ -30,7 +30,9 @@ import { ConfigAccountSection } from "components/account/ConfigAccountSection";
|
||||||
import { useFlaggedAccounts } from "providers/accounts/flagged-accounts";
|
import { useFlaggedAccounts } from "providers/accounts/flagged-accounts";
|
||||||
import { UpgradeableLoaderAccountSection } from "components/account/UpgradeableLoaderAccountSection";
|
import { UpgradeableLoaderAccountSection } from "components/account/UpgradeableLoaderAccountSection";
|
||||||
import { useTokenRegistry } from "providers/mints/token-registry";
|
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||||
|
import { Identicon } from "components/common/Identicon";
|
||||||
|
|
||||||
|
const IDENTICON_WIDTH = 64;
|
||||||
const TABS_LOOKUP: { [id: string]: Tab } = {
|
const TABS_LOOKUP: { [id: string]: Tab } = {
|
||||||
"spl-token:mint": {
|
"spl-token:mint": {
|
||||||
slug: "largest",
|
slug: "largest",
|
||||||
|
@ -69,49 +71,77 @@ const TOKEN_TABS_HIDDEN = [
|
||||||
|
|
||||||
type Props = { address: string; tab?: string };
|
type Props = { address: string; tab?: string };
|
||||||
export function AccountDetailsPage({ address, tab }: Props) {
|
export function AccountDetailsPage({ address, tab }: Props) {
|
||||||
|
const fetchAccount = useFetchAccountInfo();
|
||||||
|
const { status } = useCluster();
|
||||||
|
const info = useAccountInfo(address);
|
||||||
let pubkey: PublicKey | undefined;
|
let pubkey: PublicKey | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pubkey = new PublicKey(address);
|
pubkey = new PublicKey(address);
|
||||||
} catch (err) {}
|
} 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 (
|
return (
|
||||||
<div className="container mt-n3">
|
<div className="container mt-n3">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-body">
|
<div className="header-body">
|
||||||
<AccountHeader address={address} />
|
<AccountHeader address={address} info={info} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!pubkey ? (
|
{!pubkey ? (
|
||||||
<ErrorCard text={`Address "${address}" is not valid`} />
|
<ErrorCard text={`Address "${address}" is not valid`} />
|
||||||
) : (
|
) : (
|
||||||
<DetailsSections pubkey={pubkey} tab={tab} />
|
<DetailsSections pubkey={pubkey} tab={tab} info={info} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountHeader({ address }: { address: string }) {
|
export function AccountHeader({
|
||||||
|
address,
|
||||||
|
info,
|
||||||
|
}: {
|
||||||
|
address: string;
|
||||||
|
info?: CacheEntry<Account>;
|
||||||
|
}) {
|
||||||
const { tokenRegistry } = useTokenRegistry();
|
const { tokenRegistry } = useTokenRegistry();
|
||||||
const tokenDetails = tokenRegistry.get(address);
|
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 (
|
return (
|
||||||
<div className="row align-items-end">
|
<div className="row align-items-end">
|
||||||
{tokenDetails.logoURI && (
|
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
<div className="avatar avatar-lg header-avatar-top">
|
<div className="avatar avatar-lg header-avatar-top">
|
||||||
|
{tokenDetails?.logoURI ? (
|
||||||
<img
|
<img
|
||||||
src={tokenDetails.logoURI}
|
src={tokenDetails.logoURI}
|
||||||
alt="token logo"
|
alt="token logo"
|
||||||
className="avatar-img rounded-circle border border-4 border-body"
|
className="avatar-img rounded-circle border border-4 border-body"
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
</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">
|
<div className="col mb-3 ml-n3 ml-md-n2">
|
||||||
<h6 className="header-pretitle">Token</h6>
|
<h6 className="header-pretitle">Token</h6>
|
||||||
<h2 className="header-title">{tokenDetails.name}</h2>
|
<h2 className="header-title">
|
||||||
|
{tokenDetails?.name || "Unlisted Token"}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</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 fetchAccount = useFetchAccountInfo();
|
||||||
const address = pubkey.toBase58();
|
const address = pubkey.toBase58();
|
||||||
const info = useAccountInfo(address);
|
|
||||||
const { status } = useCluster();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { flaggedAccounts } = useFlaggedAccounts();
|
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) {
|
if (!info || info.status === FetchStatus.Fetching) {
|
||||||
return <LoadingCard />;
|
return <LoadingCard />;
|
||||||
} else if (
|
} else if (
|
||||||
|
|
|
@ -382,3 +382,11 @@ p.updated-time {
|
||||||
.change-negative {
|
.change-negative {
|
||||||
color: $warning;
|
color: $warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.identicon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identicon-wrapper-small {
|
||||||
|
margin-left: .4rem;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue