Explorer: Refactor account provider data types (#28390)

This commit is contained in:
Justin Starry 2022-10-14 14:41:32 +08:00 committed by GitHub
parent d40875fbec
commit c38bca9932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 243 additions and 232 deletions

View File

@ -23,8 +23,12 @@ describe("parseNFTokenAccounts", () => {
]); ]);
const nftAccount = parseNFTokenNFTAccount({ const nftAccount = parseNFTokenNFTAccount({
pubkey: new PublicKey("FagABcRBhZH27JDtu6A1Jo9woXyoznP28QujLkxkN9Hj"), pubkey: new PublicKey("FagABcRBhZH27JDtu6A1Jo9woXyoznP28QujLkxkN9Hj"),
details: { rawData: buffer, owner: new PublicKey(NFTOKEN_ADDRESS) }, space: buffer.length,
} as any); lamports: 1,
executable: false,
data: { raw: buffer as Buffer },
owner: new PublicKey(NFTOKEN_ADDRESS),
});
expect(nftAccount!.metadata_url).to.eq( expect(nftAccount!.metadata_url).to.eq(
"https://cdn.glow.app/n/88/78ef17c1-2b5a-468e-ae8f-7403856e9f00.json" "https://cdn.glow.app/n/88/78ef17c1-2b5a-468e-ae8f-7403856e9f00.json"
); );

View File

@ -10,11 +10,8 @@ import { useAnchorProgram } from "providers/anchor";
export function AnchorAccountCard({ account }: { account: Account }) { export function AnchorAccountCard({ account }: { account: Account }) {
const { lamports } = account; const { lamports } = account;
const { url } = useCluster(); const { url } = useCluster();
const anchorProgram = useAnchorProgram( const anchorProgram = useAnchorProgram(account.owner.toString(), url);
account.details?.owner.toString() || "", const rawData = account.data.raw;
url
);
const rawData = account?.details?.rawData;
const programName = getAnchorProgramName(anchorProgram) || "Unknown Program"; const programName = getAnchorProgramName(anchorProgram) || "Unknown Program";
const { decodedAccountData, accountDef } = useMemo(() => { const { decodedAccountData, accountDef } = useMemo(() => {

View File

@ -7,22 +7,27 @@ import { ErrorCard } from "components/common/ErrorCard";
import { Slot } from "components/common/Slot"; import { Slot } from "components/common/Slot";
import { lamportsToSolString } from "utils"; import { lamportsToSolString } from "utils";
import { useAccountInfo } from "providers/accounts"; import { useAccountInfo } from "providers/accounts";
import BN from "bn.js";
import { Epoch } from "components/common/Epoch"; import { Epoch } from "components/common/Epoch";
const MAX_EPOCH = new BN(2).pow(new BN(64)).sub(new BN(1)); const U64_MAX = BigInt("0xffffffffffffffff");
export function RewardsCard({ pubkey }: { pubkey: PublicKey }) { export function RewardsCard({ pubkey }: { pubkey: PublicKey }) {
const address = React.useMemo(() => pubkey.toBase58(), [pubkey]); const address = React.useMemo(() => pubkey.toBase58(), [pubkey]);
const info = useAccountInfo(address); const info = useAccountInfo(address);
const account = info?.data; const account = info?.data;
const data = account?.details?.data?.parsed.info; const parsedData = account?.data.parsed;
const highestEpoch = React.useMemo(() => { const highestEpoch = React.useMemo(() => {
if (data.stake && !data.stake.delegation.deactivationEpoch.eq(MAX_EPOCH)) { if (!parsedData) return;
return data.stake.delegation.deactivationEpoch.toNumber(); if (parsedData.program !== "stake") return;
const stakeInfo = parsedData.parsed.info.stake;
if (
stakeInfo !== null &&
stakeInfo.delegation.deactivationEpoch !== U64_MAX
) {
return Number(stakeInfo.delegation.deactivationEpoch);
} }
}, [data]); }, [parsedData]);
const rewards = useRewards(address); const rewards = useRewards(address);
const fetchRewards = useFetchRewards(); const fetchRewards = useFetchRewards();

View File

@ -125,7 +125,7 @@ function OverviewCard({
<tr> <tr>
<td>Balance (SOL)</td> <td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase"> <td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports || 0} /> <SolBalance lamports={account.lamports} />
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -54,11 +54,11 @@ export function TokenAccountSection({
case "mint": { case "mint": {
const info = create(tokenAccount.info, MintAccountInfo); const info = create(tokenAccount.info, MintAccountInfo);
if (isMetaplexNFT(account.details?.data, info)) { if (isMetaplexNFT(account.data.parsed, info)) {
return ( return (
<NonFungibleTokenMintAccountCard <NonFungibleTokenMintAccountCard
account={account} account={account}
nftData={(account.details!.data as TokenProgramData).nftData!} nftData={(account.data.parsed as TokenProgramData).nftData!}
mintInfo={info} mintInfo={info}
/> />
); );

View File

@ -8,10 +8,8 @@ import { useCluster } from "providers/cluster";
import { useTokenRegistry } from "providers/mints/token-registry"; import { useTokenRegistry } from "providers/mints/token-registry";
export function UnknownAccountCard({ account }: { account: Account }) { export function UnknownAccountCard({ account }: { account: Account }) {
const { details, lamports } = account;
const { cluster } = useCluster(); const { cluster } = useCluster();
const { tokenRegistry } = useTokenRegistry(); const { tokenRegistry } = useTokenRegistry();
if (lamports === undefined) return null;
const label = addressLabel(account.pubkey.toBase58(), cluster, tokenRegistry); const label = addressLabel(account.pubkey.toBase58(), cluster, tokenRegistry);
return ( return (
@ -36,32 +34,26 @@ export function UnknownAccountCard({ account }: { account: Account }) {
<tr> <tr>
<td>Balance (SOL)</td> <td>Balance (SOL)</td>
<td className="text-lg-end"> <td className="text-lg-end">
<SolBalance lamports={lamports} /> <SolBalance lamports={account.lamports} />
</td> </td>
</tr> </tr>
{details?.space !== undefined && ( <tr>
<tr> <td>Allocated Data Size</td>
<td>Allocated Data Size</td> <td className="text-lg-end">{account.space} byte(s)</td>
<td className="text-lg-end">{details.space} byte(s)</td> </tr>
</tr>
)}
{details && ( <tr>
<tr> <td>Assigned Program Id</td>
<td>Assigned Program Id</td> <td className="text-lg-end">
<td className="text-lg-end"> <Address pubkey={account.owner} alignRight link />
<Address pubkey={details.owner} alignRight link /> </td>
</td> </tr>
</tr>
)}
{details && ( <tr>
<tr> <td>Executable</td>
<td>Executable</td> <td className="text-lg-end">{account.executable ? "Yes" : "No"}</td>
<td className="text-lg-end">{details.executable ? "Yes" : "No"}</td> </tr>
</tr>
)}
</TableCardBody> </TableCardBody>
</div> </div>
); );

View File

@ -104,7 +104,7 @@ export function UpgradeableProgramSection({
<tr> <tr>
<td>Balance (SOL)</td> <td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase"> <td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports || 0} /> <SolBalance lamports={account.lamports} />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -235,22 +235,20 @@ export function UpgradeableProgramDataSection({
<tr> <tr>
<td>Balance (SOL)</td> <td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase"> <td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports || 0} /> <SolBalance lamports={account.lamports} />
</td>
</tr>
<tr>
<td>Data Size (Bytes)</td>
<td className="text-lg-end">
<Downloadable
data={programData.data[0]}
filename={`${account.pubkey.toString()}.bin`}
>
<span className="me-2">{account.space}</span>
</Downloadable>
</td> </td>
</tr> </tr>
{account.details?.space !== undefined && (
<tr>
<td>Data (Bytes)</td>
<td className="text-lg-end">
<Downloadable
data={programData.data[0]}
filename={`${account.pubkey.toString()}.bin`}
>
<span className="me-2">{account.details.space}</span>
</Downloadable>
</td>
</tr>
)}
<tr> <tr>
<td>Upgradeable</td> <td>Upgradeable</td>
<td className="text-lg-end"> <td className="text-lg-end">
@ -309,15 +307,13 @@ export function UpgradeableProgramBufferSection({
<tr> <tr>
<td>Balance (SOL)</td> <td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase"> <td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports || 0} /> <SolBalance lamports={account.lamports} />
</td> </td>
</tr> </tr>
{account.details?.space !== undefined && ( <tr>
<tr> <td>Data Size (Bytes)</td>
<td>Data (Bytes)</td> <td className="text-lg-end">{account.space}</td>
<td className="text-lg-end">{account.details.space}</td> </tr>
</tr>
)}
{programBuffer.authority !== null && ( {programBuffer.authority !== null && (
<tr> <tr>
<td>Deploy Authority</td> <td>Deploy Authority</td>
@ -326,14 +322,12 @@ export function UpgradeableProgramBufferSection({
</td> </td>
</tr> </tr>
)} )}
{account.details && ( <tr>
<tr> <td>Owner</td>
<td>Owner</td> <td className="text-lg-end">
<td className="text-lg-end"> <Address pubkey={account.owner} alignRight link />
<Address pubkey={account.details.owner} alignRight link /> </td>
</td> </tr>
</tr>
)}
</TableCardBody> </TableCardBody>
</div> </div>
); );

View File

@ -56,7 +56,7 @@ export function AddressLookupTableAccountSection(
<tr> <tr>
<td>Balance (SOL)</td> <td>Balance (SOL)</td>
<td className="text-lg-end text-uppercase"> <td className="text-lg-end text-uppercase">
<SolBalance lamports={account.lamports || 0} /> <SolBalance lamports={account.lamports} />
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -5,8 +5,7 @@ import { NftokenTypes } from "./nftoken-types";
export function isNFTokenAccount(account: Account): boolean { export function isNFTokenAccount(account: Account): boolean {
return Boolean( return Boolean(
account.details?.owner.toBase58() === NFTOKEN_ADDRESS && account.owner.toBase58() === NFTOKEN_ADDRESS && account.data.raw
account.details.rawData
); );
} }
@ -20,9 +19,7 @@ export const parseNFTokenNFTAccount = (
} }
try { try {
const parsed = NftokenTypes.nftAccountLayout.decode( const parsed = NftokenTypes.nftAccountLayout.decode(account.data.raw!);
account!.details!.rawData!
);
if (!parsed) { if (!parsed) {
return null; return null;
@ -36,7 +33,7 @@ export const parseNFTokenNFTAccount = (
} }
return { return {
address: account!.pubkey.toBase58(), address: account.pubkey.toBase58(),
holder: new PublicKey(parsed.holder).toBase58(), holder: new PublicKey(parsed.holder).toBase58(),
authority: new PublicKey(parsed.authority).toBase58(), authority: new PublicKey(parsed.authority).toBase58(),
authority_can_update: Boolean(parsed.authority_can_update), authority_can_update: Boolean(parsed.authority_can_update),
@ -62,7 +59,7 @@ export const parseNFTokenCollectionAccount = (
try { try {
const parsed = NftokenTypes.collectionAccountLayout.decode( const parsed = NftokenTypes.collectionAccountLayout.decode(
account!.details!.rawData! account.data.raw!
); );
if (!parsed) { if (!parsed) {
@ -76,7 +73,7 @@ export const parseNFTokenCollectionAccount = (
} }
return { return {
address: account!.pubkey.toBase58(), address: account.pubkey.toBase58(),
authority: parsed.authority, authority: parsed.authority,
authority_can_update: Boolean(parsed.authority_can_update), authority_can_update: Boolean(parsed.authority_can_update),
metadata_url: parsed.metadata_url?.replace(/\0/g, "") ?? null, metadata_url: parsed.metadata_url?.replace(/\0/g, "") ?? null,

View File

@ -181,7 +181,7 @@ export function AccountDetailsPage({ address, tab }: Props) {
<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} info={info} /> <AccountHeader address={address} account={info?.data} />
</div> </div>
</div> </div>
{!pubkey ? ( {!pubkey ? (
@ -195,22 +195,22 @@ export function AccountDetailsPage({ address, tab }: Props) {
export function AccountHeader({ export function AccountHeader({
address, address,
info, account,
}: { }: {
address: string; address: string;
info?: CacheEntry<Account>; account?: Account;
}) { }) {
const { tokenRegistry } = useTokenRegistry(); const { tokenRegistry } = useTokenRegistry();
const tokenDetails = tokenRegistry.get(address); const tokenDetails = tokenRegistry.get(address);
const mintInfo = useMintAccountInfo(address); const mintInfo = useMintAccountInfo(address);
const account = info?.data; const parsedData = account?.data.parsed;
const data = account?.details?.data; const isToken =
const isToken = data?.program === "spl-token" && data?.parsed.type === "mint"; parsedData?.program === "spl-token" && parsedData?.parsed.type === "mint";
if (isMetaplexNFT(data, mintInfo)) { if (isMetaplexNFT(parsedData, mintInfo)) {
return ( return (
<MetaplexNFTHeader <MetaplexNFTHeader
nftData={(data as TokenProgramData).nftData!} nftData={(parsedData as TokenProgramData).nftData!}
address={address} address={address}
/> />
); );
@ -226,12 +226,14 @@ export function AccountHeader({
let unverified = false; let unverified = false;
// Fall back to legacy token list when there is stub metadata (blank uri), updatable by default by the mint authority // Fall back to legacy token list when there is stub metadata (blank uri), updatable by default by the mint authority
if (!data?.nftData?.metadata.data.uri && tokenDetails) { if (!parsedData?.nftData?.metadata.data.uri && tokenDetails) {
token = tokenDetails; token = tokenDetails;
} else if (data?.nftData) { } else if (parsedData?.nftData) {
token = { token = {
logoURI: data?.nftData?.json?.image, logoURI: parsedData?.nftData?.json?.image,
name: data?.nftData?.json?.name ?? data?.nftData.metadata.data.name, name:
parsedData?.nftData?.json?.name ??
parsedData?.nftData.metadata.data.name,
}; };
unverified = true; unverified = true;
} else if (tokenDetails) { } else if (tokenDetails) {
@ -339,62 +341,68 @@ function DetailsSections({
} }
function InfoSection({ account }: { account: Account }) { function InfoSection({ account }: { account: Account }) {
const details = account?.details; const parsedData = account.data.parsed;
const data = details?.data; const rawData = account.data.raw;
if (data && data.program === "bpf-upgradeable-loader") { if (parsedData && parsedData.program === "bpf-upgradeable-loader") {
return ( return (
<UpgradeableLoaderAccountSection <UpgradeableLoaderAccountSection
account={account} account={account}
parsedData={data.parsed} parsedData={parsedData.parsed}
programData={data.programData} programData={parsedData.programData}
/> />
); );
} else if (data && data.program === "stake") { } else if (parsedData && parsedData.program === "stake") {
return ( return (
<StakeAccountSection <StakeAccountSection
account={account} account={account}
stakeAccount={data.parsed.info} stakeAccount={parsedData.parsed.info}
activation={data.activation} activation={parsedData.activation}
stakeAccountType={data.parsed.type} stakeAccountType={parsedData.parsed.type}
/> />
); );
} else if (account.details?.owner.toBase58() === NFTOKEN_ADDRESS) { } else if (account.owner.toBase58() === NFTOKEN_ADDRESS) {
return <NFTokenAccountSection account={account} />; return <NFTokenAccountSection account={account} />;
} else if (data && data.program === "spl-token") { } else if (parsedData && parsedData.program === "spl-token") {
return <TokenAccountSection account={account} tokenAccount={data.parsed} />;
} else if (data && data.program === "nonce") {
return <NonceAccountSection account={account} nonceAccount={data.parsed} />;
} else if (data && data.program === "vote") {
return <VoteAccountSection account={account} voteAccount={data.parsed} />;
} else if (data && data.program === "sysvar") {
return ( return (
<SysvarAccountSection account={account} sysvarAccount={data.parsed} /> <TokenAccountSection account={account} tokenAccount={parsedData.parsed} />
); );
} else if (data && data.program === "config") { } else if (parsedData && parsedData.program === "nonce") {
return ( return (
<ConfigAccountSection account={account} configAccount={data.parsed} /> <NonceAccountSection account={account} nonceAccount={parsedData.parsed} />
);
} else if (parsedData && parsedData.program === "vote") {
return (
<VoteAccountSection account={account} voteAccount={parsedData.parsed} />
);
} else if (parsedData && parsedData.program === "sysvar") {
return (
<SysvarAccountSection
account={account}
sysvarAccount={parsedData.parsed}
/>
);
} else if (parsedData && parsedData.program === "config") {
return (
<ConfigAccountSection
account={account}
configAccount={parsedData.parsed}
/>
); );
} else if ( } else if (
data && parsedData &&
data.program === "address-lookup-table" && parsedData.program === "address-lookup-table" &&
data.parsed.type === "lookupTable" parsedData.parsed.type === "lookupTable"
) { ) {
return ( return (
<AddressLookupTableAccountSection <AddressLookupTableAccountSection
account={account} account={account}
lookupTableAccount={data.parsed.info} lookupTableAccount={parsedData.parsed.info}
/> />
); );
} else if ( } else if (rawData && isAddressLookupTableAccount(account.owner, rawData)) {
details?.rawData &&
isAddressLookupTableAccount(details.owner, details.rawData)
) {
return ( return (
<AddressLookupTableAccountSection <AddressLookupTableAccountSection account={account} data={rawData} />
account={account}
data={details.rawData}
/>
); );
} else { } else {
return <UnknownAccountCard account={account} />; return <UnknownAccountCard account={account} />;
@ -442,8 +450,8 @@ function MoreSection({
tabs: (JSX.Element | null)[]; tabs: (JSX.Element | null)[];
}) { }) {
const pubkey = account.pubkey; const pubkey = account.pubkey;
const details = account?.details; const parsedData = account.data.parsed;
const data = details?.data; const rawData = account.data.raw;
return ( return (
<> <>
@ -465,27 +473,27 @@ function MoreSection({
{tab === "instructions" && <TokenInstructionsCard pubkey={pubkey} />} {tab === "instructions" && <TokenInstructionsCard pubkey={pubkey} />}
{tab === "largest" && <TokenLargestAccountsCard pubkey={pubkey} />} {tab === "largest" && <TokenLargestAccountsCard pubkey={pubkey} />}
{tab === "rewards" && <RewardsCard pubkey={pubkey} />} {tab === "rewards" && <RewardsCard pubkey={pubkey} />}
{tab === "vote-history" && data?.program === "vote" && ( {tab === "vote-history" && parsedData?.program === "vote" && (
<VotesCard voteAccount={data.parsed} /> <VotesCard voteAccount={parsedData.parsed} />
)} )}
{tab === "slot-hashes" && {tab === "slot-hashes" &&
data?.program === "sysvar" && parsedData?.program === "sysvar" &&
data.parsed.type === "slotHashes" && ( parsedData.parsed.type === "slotHashes" && (
<SlotHashesCard sysvarAccount={data.parsed} /> <SlotHashesCard sysvarAccount={parsedData.parsed} />
)} )}
{tab === "stake-history" && {tab === "stake-history" &&
data?.program === "sysvar" && parsedData?.program === "sysvar" &&
data.parsed.type === "stakeHistory" && ( parsedData.parsed.type === "stakeHistory" && (
<StakeHistoryCard sysvarAccount={data.parsed} /> <StakeHistoryCard sysvarAccount={parsedData.parsed} />
)} )}
{tab === "blockhashes" && {tab === "blockhashes" &&
data?.program === "sysvar" && parsedData?.program === "sysvar" &&
data.parsed.type === "recentBlockhashes" && ( parsedData.parsed.type === "recentBlockhashes" && (
<BlockhashesCard blockhashes={data.parsed.info} /> <BlockhashesCard blockhashes={parsedData.parsed.info} />
)} )}
{tab === "metadata" && ( {tab === "metadata" && (
<MetaplexMetadataCard <MetaplexMetadataCard
nftData={(account.details?.data as TokenProgramData).nftData!} nftData={(account.data.parsed as TokenProgramData).nftData!}
/> />
)} )}
{tab === "nftoken-collection-nfts" && ( {tab === "nftoken-collection-nfts" && (
@ -497,13 +505,14 @@ function MoreSection({
)} )}
{tab === "attributes" && ( {tab === "attributes" && (
<MetaplexNFTAttributesCard <MetaplexNFTAttributesCard
nftData={(account.details?.data as TokenProgramData).nftData!} nftData={(account.data.parsed as TokenProgramData).nftData!}
/> />
)} )}
{tab === "domains" && <DomainsCard pubkey={pubkey} />} {tab === "domains" && <DomainsCard pubkey={pubkey} />}
{tab === "security" && data?.program === "bpf-upgradeable-loader" && ( {tab === "security" &&
<SecurityCard data={data} /> parsedData?.program === "bpf-upgradeable-loader" && (
)} <SecurityCard data={parsedData} />
)}
{tab === "anchor-program" && ( {tab === "anchor-program" && (
<React.Suspense <React.Suspense
fallback={<LoadingCard message="Loading anchor program IDL" />} fallback={<LoadingCard message="Loading anchor program IDL" />}
@ -521,14 +530,14 @@ function MoreSection({
</React.Suspense> </React.Suspense>
)} )}
{tab === "entries" && {tab === "entries" &&
details?.rawData && rawData &&
isAddressLookupTableAccount(details.owner, details.rawData) && ( isAddressLookupTableAccount(account.owner, rawData) && (
<LookupTableEntriesCard lookupTableAccountData={details?.rawData} /> <LookupTableEntriesCard lookupTableAccountData={rawData} />
)} )}
{tab === "entries" && {tab === "entries" &&
data?.program === "address-lookup-table" && parsedData?.program === "address-lookup-table" &&
data.parsed.type === "lookupTable" && ( parsedData.parsed.type === "lookupTable" && (
<LookupTableEntriesCard parsedLookupTable={data.parsed.info} /> <LookupTableEntriesCard parsedLookupTable={parsedData.parsed.info} />
)} )}
</> </>
); );
@ -536,7 +545,7 @@ function MoreSection({
function getTabs(pubkey: PublicKey, account: Account): TabComponent[] { function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
const address = pubkey.toBase58(); const address = pubkey.toBase58();
const data = account.details?.data; const parsedData = account.data.parsed;
const tabs: Tab[] = [ const tabs: Tab[] = [
{ {
slug: "history", slug: "history",
@ -546,31 +555,31 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
]; ];
let programTypeKey = ""; let programTypeKey = "";
if (data && "parsed" in data && "type" in data.parsed) { if (parsedData) {
programTypeKey = `${data.program}:${data.parsed.type}`; programTypeKey = `${parsedData.program}:${parsedData.parsed.type}`;
} }
if (data && data.program in TABS_LOOKUP) { if (parsedData && parsedData.program in TABS_LOOKUP) {
tabs.push(...TABS_LOOKUP[data.program]); tabs.push(...TABS_LOOKUP[parsedData.program]);
} }
if (data && programTypeKey in TABS_LOOKUP) { if (parsedData && programTypeKey in TABS_LOOKUP) {
tabs.push(...TABS_LOOKUP[programTypeKey]); tabs.push(...TABS_LOOKUP[programTypeKey]);
} }
// Add the key for address lookup tables // Add the key for address lookup tables
if ( if (
account.details?.rawData && account.data.raw &&
isAddressLookupTableAccount(account.details.owner, account.details.rawData) isAddressLookupTableAccount(account.owner, account.data.raw)
) { ) {
tabs.push(...TABS_LOOKUP["address-lookup-table"]); tabs.push(...TABS_LOOKUP["address-lookup-table"]);
} }
// Add the key for Metaplex NFTs // Add the key for Metaplex NFTs
if ( if (
data && parsedData &&
programTypeKey === "spl-token:mint" && programTypeKey === "spl-token:mint" &&
(data as TokenProgramData).nftData (parsedData as TokenProgramData).nftData
) { ) {
tabs.push(...TABS_LOOKUP[`${programTypeKey}:metaplexNFT`]); tabs.push(...TABS_LOOKUP[`${programTypeKey}:metaplexNFT`]);
} }
@ -589,9 +598,9 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
if ( if (
!isNFToken && !isNFToken &&
(!data || (!parsedData ||
!( !(
TOKEN_TABS_HIDDEN.includes(data.program) || TOKEN_TABS_HIDDEN.includes(parsedData.program) ||
TOKEN_TABS_HIDDEN.includes(programTypeKey) TOKEN_TABS_HIDDEN.includes(programTypeKey)
)) ))
) { ) {
@ -657,7 +666,7 @@ function getAnchorTabs(pubkey: PublicKey, account: Account) {
<AccountDataLink <AccountDataLink
tab={accountDataTab} tab={accountDataTab}
address={pubkey.toString()} address={pubkey.toString()}
programId={account.details?.owner} programId={account.owner}
/> />
</React.Suspense> </React.Suspense>
), ),
@ -676,7 +685,7 @@ function AnchorProgramLink({
pubkey: PublicKey; pubkey: PublicKey;
}) { }) {
const { url } = useCluster(); const { url } = useCluster();
const anchorProgram = useAnchorProgram(pubkey.toString() ?? "", url); const anchorProgram = useAnchorProgram(pubkey.toString(), url);
if (!anchorProgram) { if (!anchorProgram) {
return null; return null;
@ -702,13 +711,10 @@ function AccountDataLink({
}: { }: {
address: string; address: string;
tab: Tab; tab: Tab;
programId: PublicKey | undefined; programId: PublicKey;
}) { }) {
const { url } = useCluster(); const { url } = useCluster();
const accountAnchorProgram = useAnchorProgram( const accountAnchorProgram = useAnchorProgram(programId.toString(), url);
programId?.toString() ?? "",
url
);
if (!accountAnchorProgram) { if (!accountAnchorProgram) {
return null; return null;

View File

@ -17,12 +17,11 @@ export const createFeePayerValidator = (
feeLamports: number feeLamports: number
): AccountValidator => { ): AccountValidator => {
return (account: Account): string | undefined => { return (account: Account): string | undefined => {
if (account.details === undefined) return "Account doesn't exist"; if (account.lamports === 0) return "Account doesn't exist";
if (!account.details.owner.equals(SystemProgram.programId)) if (!account.owner.equals(SystemProgram.programId))
return "Only system-owned accounts can pay fees"; return "Only system-owned accounts can pay fees";
// TODO: Actually nonce accounts can pay fees too // TODO: Actually nonce accounts can pay fees too
if (account.details.space > 0) if (account.space > 0) return "Only unallocated accounts can pay fees";
return "Only unallocated accounts can pay fees";
if (account.lamports < feeLamports) { if (account.lamports < feeLamports) {
return "Insufficient funds for fees"; return "Insufficient funds for fees";
} }
@ -31,9 +30,8 @@ export const createFeePayerValidator = (
}; };
export const programValidator = (account: Account): string | undefined => { export const programValidator = (account: Account): string | undefined => {
if (account.details === undefined) return "Account doesn't exist"; if (account.lamports === 0) return "Account doesn't exist";
if (!account.details.executable) if (!account.executable) return "Only executable accounts can be invoked";
return "Only executable accounts can be invoked";
return; return;
}; };
@ -108,7 +106,8 @@ function AccountInfo({
} }
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps }, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
if (!info?.data) const account = info?.data;
if (!account)
return ( return (
<span className="text-muted"> <span className="text-muted">
<span className="spinner-grow spinner-grow-sm me-2"></span> <span className="spinner-grow spinner-grow-sm me-2"></span>
@ -116,23 +115,22 @@ function AccountInfo({
</span> </span>
); );
const errorMessage = validator && validator(info.data); const errorMessage = validator && validator(account);
if (errorMessage) return <span className="text-warning">{errorMessage}</span>; if (errorMessage) return <span className="text-warning">{errorMessage}</span>;
if (!info.data.details) { if (account.lamports === 0) {
return <span className="text-muted">Account doesn't exist</span>; return <span className="text-muted">Account doesn't exist</span>;
} }
const owner = info.data.details.owner; const ownerAddress = account.owner.toBase58();
const ownerAddress = owner.toBase58();
const ownerLabel = addressLabel(ownerAddress, cluster); const ownerLabel = addressLabel(ownerAddress, cluster);
return ( return (
<span className="text-muted"> <span className="text-muted">
{`Owned by ${ownerLabel || ownerAddress}.`} {`Owned by ${ownerLabel || ownerAddress}.`}
{` Balance is ${lamportsToSolString(info.data.lamports)} SOL.`} {` Balance is ${lamportsToSolString(account.lamports)} SOL.`}
{` Size is ${new Intl.NumberFormat("en-US").format( {` Size is ${new Intl.NumberFormat("en-US").format(
info.data.details.space account.space
)} byte(s).`} )} byte(s).`}
</span> </span>
); );

View File

@ -6,6 +6,7 @@ import {
StakeActivationData, StakeActivationData,
AddressLookupTableAccount, AddressLookupTableAccount,
AddressLookupTableProgram, AddressLookupTableProgram,
SystemProgram,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { useCluster, Cluster } from "../cluster"; import { useCluster, Cluster } from "../cluster";
import { HistoryProvider } from "./history"; import { HistoryProvider } from "./history";
@ -88,7 +89,7 @@ export type AddressLookupTableProgramData = {
parsed: ParsedAddressLookupTableAccount; parsed: ParsedAddressLookupTableAccount;
}; };
export type ProgramData = export type ParsedData =
| UpgradeableLoaderAccountData | UpgradeableLoaderAccountData
| StakeProgramData | StakeProgramData
| TokenProgramData | TokenProgramData
@ -98,18 +99,18 @@ export type ProgramData =
| ConfigProgramData | ConfigProgramData
| AddressLookupTableProgramData; | AddressLookupTableProgramData;
export interface Details { export interface AccountData {
executable: boolean; parsed?: ParsedData;
owner: PublicKey; raw?: Buffer;
space: number;
data?: ProgramData;
rawData?: Buffer;
} }
export interface Account { export interface Account {
pubkey: PublicKey; pubkey: PublicKey;
lamports: number; lamports: number;
details?: Details; executable: boolean;
owner: PublicKey;
space: number;
data: AccountData;
} }
type State = Cache.State<Account>; type State = Cache.State<Account>;
@ -162,12 +163,17 @@ async function fetchAccountInfo(
const connection = new Connection(url, "confirmed"); const connection = new Connection(url, "confirmed");
const result = (await connection.getParsedAccountInfo(pubkey)).value; const result = (await connection.getParsedAccountInfo(pubkey)).value;
let lamports, details; let account: Account;
if (result === null) { if (result === null) {
lamports = 0; account = {
pubkey,
lamports: 0,
owner: SystemProgram.programId,
space: 0,
executable: false,
data: { raw: Buffer.alloc(0) },
};
} else { } else {
lamports = result.lamports;
// Only save data in memory if we can decode it // Only save data in memory if we can decode it
let space: number; let space: number;
if (!("parsed" in result.data)) { if (!("parsed" in result.data)) {
@ -176,7 +182,7 @@ async function fetchAccountInfo(
space = result.data.space; space = result.data.space;
} }
let data: ProgramData | undefined; let parsedData: ParsedData | undefined;
if ("parsed" in result.data) { if ("parsed" in result.data) {
try { try {
const info = create(result.data.parsed, ParsedInfo); const info = create(result.data.parsed, ParsedInfo);
@ -200,7 +206,7 @@ async function fetchAccountInfo(
} }
} }
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed, parsed,
programData, programData,
@ -215,7 +221,7 @@ async function fetchAccountInfo(
? await connection.getStakeActivation(pubkey) ? await connection.getStakeActivation(pubkey)
: undefined; : undefined;
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed, parsed,
activation, activation,
@ -223,25 +229,25 @@ async function fetchAccountInfo(
break; break;
} }
case "vote": case "vote":
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed: create(info, VoteAccount), parsed: create(info, VoteAccount),
}; };
break; break;
case "nonce": case "nonce":
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed: create(info, NonceAccount), parsed: create(info, NonceAccount),
}; };
break; break;
case "sysvar": case "sysvar":
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed: create(info, SysvarAccount), parsed: create(info, SysvarAccount),
}; };
break; break;
case "config": case "config":
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed: create(info, ConfigAccount), parsed: create(info, ConfigAccount),
}; };
@ -250,7 +256,7 @@ async function fetchAccountInfo(
case "address-lookup-table": { case "address-lookup-table": {
const parsed = create(info, ParsedAddressLookupTableAccount); const parsed = create(info, ParsedAddressLookupTableAccount);
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed, parsed,
}; };
@ -291,14 +297,14 @@ async function fetchAccountInfo(
// unable to find NFT metadata account // unable to find NFT metadata account
} }
data = { parsedData = {
program: result.data.program, program: result.data.program,
parsed, parsed,
nftData, nftData,
}; };
break; break;
default: default:
data = undefined; parsedData = undefined;
} }
} catch (error) { } catch (error) {
reportError(error, { url, address: pubkey.toBase58() }); reportError(error, { url, address: pubkey.toBase58() });
@ -308,19 +314,23 @@ async function fetchAccountInfo(
// If we cannot parse account layout as native spl account // If we cannot parse account layout as native spl account
// then keep raw data for other components to decode // then keep raw data for other components to decode
let rawData: Buffer | undefined; let rawData: Buffer | undefined;
if (!data && !("parsed" in result.data)) { if (!parsedData && !("parsed" in result.data)) {
rawData = result.data; rawData = result.data;
} }
details = { account = {
pubkey,
lamports: result.lamports,
space, space,
executable: result.executable, executable: result.executable,
owner: result.owner, owner: result.owner,
data, data: {
rawData, parsed: parsedData,
raw: rawData,
},
}; };
} }
data = { pubkey, lamports, details }; data = account;
fetchStatus = FetchStatus.Fetched; fetchStatus = FetchStatus.Fetched;
} catch (error) { } catch (error) {
if (cluster !== Cluster.Custom) { if (cluster !== Cluster.Custom) {
@ -413,16 +423,20 @@ export function useMintAccountInfo(
): MintAccountInfo | undefined { ): MintAccountInfo | undefined {
const accountInfo = useAccountInfo(address); const accountInfo = useAccountInfo(address);
return React.useMemo(() => { return React.useMemo(() => {
if (address === undefined) return; if (address === undefined || accountInfo?.data === undefined) return;
const account = accountInfo.data;
try { try {
const data = accountInfo?.data?.details?.data; const parsedData = account.data.parsed;
if (!data) return; if (!parsedData) return;
if (data.program !== "spl-token" || data.parsed.type !== "mint") { if (
parsedData.program !== "spl-token" ||
parsedData.parsed.type !== "mint"
) {
return; return;
} }
return create(data.parsed.info, MintAccountInfo); return create(parsedData.parsed.info, MintAccountInfo);
} catch (err) { } catch (err) {
reportError(err, { address }); reportError(err, { address });
} }
@ -433,16 +447,20 @@ export function useTokenAccountInfo(
address: string | undefined address: string | undefined
): TokenAccountInfo | undefined { ): TokenAccountInfo | undefined {
const accountInfo = useAccountInfo(address); const accountInfo = useAccountInfo(address);
if (address === undefined) return; if (address === undefined || accountInfo?.data === undefined) return;
const account = accountInfo.data;
try { try {
const data = accountInfo?.data?.details?.data; const parsedData = account.data.parsed;
if (!data) return; if (!parsedData) return;
if (data.program !== "spl-token" || data.parsed.type !== "account") { if (
parsedData.program !== "spl-token" ||
parsedData.parsed.type !== "account"
) {
return; return;
} }
return create(data.parsed.info, TokenAccountInfo); return create(parsedData.parsed.info, TokenAccountInfo);
} catch (err) { } catch (err) {
reportError(err, { address }); reportError(err, { address });
} }
@ -452,24 +470,24 @@ export function useAddressLookupTable(
address: string | undefined address: string | undefined
): AddressLookupTableAccount | undefined | string { ): AddressLookupTableAccount | undefined | string {
const accountInfo = useAccountInfo(address); const accountInfo = useAccountInfo(address);
if (address === undefined) return; if (address === undefined || accountInfo?.data === undefined) return;
if (accountInfo?.data?.details === undefined) return; const account = accountInfo.data;
if (accountInfo.data.lamports === 0) return "Lookup Table Not Found"; if (account.lamports === 0) return "Lookup Table Not Found";
const { data, rawData } = accountInfo.data.details; const { parsed: parsedData, raw: rawData } = account.data;
const key = new PublicKey(address); const key = new PublicKey(address);
if (data && data.program === "address-lookup-table") { if (parsedData && parsedData.program === "address-lookup-table") {
if (data.parsed.type === "lookupTable") { if (parsedData.parsed.type === "lookupTable") {
return new AddressLookupTableAccount({ return new AddressLookupTableAccount({
key, key,
state: data.parsed.info, state: parsedData.parsed.info,
}); });
} else if (data.parsed.type === "uninitialized") { } else if (parsedData.parsed.type === "uninitialized") {
return "Lookup Table Uninitialized"; return "Lookup Table Uninitialized";
} }
} else if ( } else if (
rawData && rawData &&
accountInfo.data.details.owner.equals(AddressLookupTableProgram.programId) account.owner.equals(AddressLookupTableProgram.programId)
) { ) {
try { try {
return new AddressLookupTableAccount({ return new AddressLookupTableAccount({

View File

@ -1,16 +1,16 @@
import { MintAccountInfo } from "validators/accounts/token"; import { MintAccountInfo } from "validators/accounts/token";
import { ProgramData } from ".."; import { ParsedData } from "..";
export default function isMetaplexNFT( export default function isMetaplexNFT(
data?: ProgramData, parsedData?: ParsedData,
mintInfo?: MintAccountInfo mintInfo?: MintAccountInfo
) { ) {
return ( return (
data?.program === "spl-token" && parsedData?.program === "spl-token" &&
data?.parsed.type === "mint" && parsedData?.parsed.type === "mint" &&
data?.nftData && parsedData?.nftData &&
mintInfo?.decimals === 0 && mintInfo?.decimals === 0 &&
(parseInt(mintInfo.supply) === 1 || (parseInt(mintInfo.supply) === 1 ||
data?.nftData?.metadata?.tokenStandard === 1) parsedData?.nftData?.metadata?.tokenStandard === 1)
); );
} }

View File

@ -169,7 +169,7 @@ export function abbreviatedNumber(value: number, fixed = 1) {
} }
export const pubkeyToString = (key: PublicKey | string = "") => { export const pubkeyToString = (key: PublicKey | string = "") => {
return typeof key === "string" ? key : key?.toBase58() || ""; return typeof key === "string" ? key : key.toBase58();
}; };
export const getLast = (arr: string[]) => { export const getLast = (arr: string[]) => {