Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate basic token-2022 support #311

Merged
merged 3 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/address/[address]/attributes/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { MetaplexNFTAttributesCard } from '@components/account/MetaplexNFTAttributesCard';
import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { isTokenProgramData } from '@providers/accounts';
import React from 'react';

type Props = Readonly<{
Expand All @@ -15,7 +16,7 @@ function MetaplexNFTAttributesCardRenderer({
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const parsedData = account?.data?.parsed;
if (!parsedData || parsedData.program !== 'spl-token' || parsedData.parsed.type !== 'mint' || !parsedData.nftData) {
if (!parsedData || !isTokenProgramData(parsedData) || parsedData.parsed.type !== 'mint' || !parsedData.nftData) {
return onNotFound();
}
return <MetaplexNFTAttributesCard nftData={parsedData.nftData} />;
Expand Down
37 changes: 31 additions & 6 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { LoadingCard } from '@components/common/LoadingCard';
import {
Account,
AccountsProvider,
isTokenProgramData,
TokenProgramData,
useAccountInfo,
useFetchAccountInfo,
Expand Down Expand Up @@ -76,6 +77,30 @@ const TABS_LOOKUP: { [id: string]: Tab[] } = {
title: 'Concurrent Merkle Tree',
},
],
'spl-token-2022:mint': [
{
path: 'transfers',
slug: 'transfers',
title: 'Transfers',
},
{
path: 'instructions',
slug: 'instructions',
title: 'Instructions',
},
],
'spl-token-2022:mint:metaplexNFT': [
{
path: 'metadata',
slug: 'metadata',
title: 'Metadata',
},
{
path: 'attributes',
slug: 'attributes',
title: 'Attributes',
},
],
'spl-token:mint': [
{
path: 'transfers',
Expand Down Expand Up @@ -142,7 +167,7 @@ const TABS_LOOKUP: { [id: string]: Tab[] } = {
],
};

const TOKEN_TABS_HIDDEN = ['spl-token:mint', 'config', 'vote', 'sysvar', 'config'];
const TOKEN_TABS_HIDDEN = ['spl-token:mint', 'spl-token-2022:mint', 'config', 'vote', 'sysvar', 'config'];

type Props = PropsWithChildren<{ params: { address: string } }>;

Expand All @@ -164,10 +189,10 @@ function AddressLayoutInner({ children, params: { address } }: Props) {
}

const infoStatus = info?.status;
const infoProgram = info?.data?.data.parsed?.program;
const infoParsed = info?.data?.data.parsed;

const { data: fullTokenInfo, isLoading: isFullTokenInfoLoading } = useSWRImmutable(
infoStatus === FetchStatus.Fetched && infoProgram === "spl-token" && pubkey ? ['get-full-token-info', address, cluster, url] : null,
infoStatus === FetchStatus.Fetched && infoParsed && isTokenProgramData(infoParsed) && pubkey ? ['get-full-token-info', address, cluster, url] : null,
fetchFullTokenInfo
);

Expand Down Expand Up @@ -208,7 +233,7 @@ function AccountHeader({ address, account, tokenInfo, isTokenInfoLoading }: { ad
const mintInfo = useMintAccountInfo(address);

const parsedData = account?.data.parsed;
const isToken = parsedData?.program === 'spl-token' && parsedData?.parsed.type === 'mint';
const isToken = parsedData && isTokenProgramData(parsedData) && parsedData?.parsed.type === 'mint';

if (isMetaplexNFT(parsedData, mintInfo) && parsedData.nftData) {
return <MetaplexNFTHeader nftData={parsedData.nftData} address={address} />;
Expand Down Expand Up @@ -346,7 +371,7 @@ function InfoSection({ account, tokenInfo }: { account: Account, tokenInfo?: Ful
);
} else if (account.owner.toBase58() === NFTOKEN_ADDRESS) {
return <NFTokenAccountSection account={account} />;
} else if (parsedData && parsedData.program === 'spl-token') {
} else if (parsedData && isTokenProgramData(parsedData)) {
return <TokenAccountSection account={account} tokenAccount={parsedData.parsed} tokenInfo={tokenInfo} />;
} else if (parsedData && parsedData.program === 'nonce') {
return <NonceAccountSection account={account} nonceAccount={parsedData.parsed} />;
Expand Down Expand Up @@ -447,7 +472,7 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
}

// Add the key for Metaplex NFTs
if (parsedData && programTypeKey === 'spl-token:mint' && (parsedData as TokenProgramData).nftData) {
if (parsedData && (programTypeKey === 'spl-token:mint' || programTypeKey == 'spl-token-2022:mint') && (parsedData as TokenProgramData).nftData) {
tabs.push(...TABS_LOOKUP[`${programTypeKey}:metaplexNFT`]);
}

Expand Down
3 changes: 2 additions & 1 deletion app/address/[address]/metadata/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { MetaplexMetadataCard } from '@components/account/MetaplexMetadataCard';
import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { isTokenProgramData } from '@providers/accounts';
import React from 'react';

type Props = Readonly<{
Expand All @@ -15,7 +16,7 @@ function MetaplexMetadataCardRenderer({
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const parsedData = account?.data?.parsed;
if (!parsedData || parsedData.program !== 'spl-token' || parsedData.parsed.type !== 'mint' || !parsedData.nftData) {
if (!parsedData || !isTokenProgramData(parsedData) || parsedData.parsed.type !== 'mint' || !parsedData.nftData) {
return onNotFound();
}
return <MetaplexMetadataCard nftData={parsedData.nftData} />;
Expand Down
5 changes: 3 additions & 2 deletions app/components/account/TokenAccountSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Copyable } from '@components/common/Copyable';
import { LoadingCard } from '@components/common/LoadingCard';
import { TableCardBody } from '@components/common/TableCardBody';
import { Account, NFTData, TokenProgramData, useFetchAccountInfo } from '@providers/accounts';
import { TOKEN_2022_PROGRAM_ID } from '@providers/accounts/tokens';
import isMetaplexNFT from '@providers/accounts/utils/isMetaplexNFT';
import { useCluster } from '@providers/cluster';
import { PublicKey } from '@solana/web3.js';
Expand Down Expand Up @@ -151,7 +152,7 @@ function FungibleTokenMintAccountCard({ account, mintInfo, tokenInfo }: { accoun
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
{tokenInfo ? 'Overview' : 'Token Mint'}
{tokenInfo ? 'Overview' : account.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() ? 'Token-2022 Mint' : 'Token Mint'}
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<RefreshCw className="align-text-top me-2" size={13} />
Expand Down Expand Up @@ -370,7 +371,7 @@ function TokenAccountCard({ account, info }: { account: Account; info: TokenAcco
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">Token Account</h3>
<h3 className="card-header-title mb-0 d-flex align-items-center">Token{ account.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() && "-2022" } Account</h3>
<button className="btn btn-white btn-sm" onClick={() => refresh(account.pubkey, 'parsed')}>
<RefreshCw className="align-text-top me-2" size={13} />
Refresh
Expand Down
9 changes: 5 additions & 4 deletions app/components/account/TokenHistoryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import {
parseTokenLendingInstructionTitle,
} from '@components/instruction/token-lending/types';
import { isTokenSwapInstruction, parseTokenSwapInstructionTitle } from '@components/instruction/token-swap/types';
import { isTokenProgramData } from '@providers/accounts';
import { useAccountHistories, useFetchAccountHistory } from '@providers/accounts/history';
import { TOKEN_PROGRAM_ID, TokenInfoWithPubkey, useAccountOwnedTokens } from '@providers/accounts/tokens';
import { isTokenProgramId, TokenInfoWithPubkey, useAccountOwnedTokens } from '@providers/accounts/tokens';
import { CacheEntry, FetchStatus } from '@providers/cache';
import { useCluster } from '@providers/cluster';
import { Details, useFetchTransactionDetails, useTransactionDetailsCache } from '@providers/transactions/parsed';
Expand Down Expand Up @@ -391,7 +392,7 @@ const TokenTransactionRow = React.memo(function TokenTransactionRow({
}

if ('parsed' in ix) {
if (ix.program === 'spl-token') {
if (isTokenProgramData(ix)) {
name = getTokenProgramInstructionName(ix, tx);
} else {
return undefined;
Expand Down Expand Up @@ -425,7 +426,7 @@ const TokenTransactionRow = React.memo(function TokenTransactionRow({
return undefined;
}
} else {
if (ix.accounts.findIndex(account => account.equals(TOKEN_PROGRAM_ID)) >= 0) {
if (ix.accounts.findIndex(account => isTokenProgramId(account)) >= 0) {
name = 'Unknown (Inner)';
} else {
return undefined;
Expand Down Expand Up @@ -476,7 +477,7 @@ function InstructionDetails({ instructionType, tx }: { instructionType: Instruct

const instructionTypes = instructionType.innerInstructions
.map(ix => {
if ('parsed' in ix && ix.program === 'spl-token') {
if ('parsed' in ix && isTokenProgramData(ix)) {
return getTokenProgramInstructionName(ix, tx);
}
return undefined;
Expand Down
4 changes: 2 additions & 2 deletions app/components/account/history/TokenTransfersCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ErrorCard } from '@components/common/ErrorCard';
import { LoadingCard } from '@components/common/LoadingCard';
import { Signature } from '@components/common/Signature';
import { TokenInstructionType, Transfer, TransferChecked } from '@components/instruction/token/types';
import { useAccountHistory } from '@providers/accounts';
import { isTokenProgramData, useAccountHistory } from '@providers/accounts';
import { useFetchAccountHistory } from '@providers/accounts/history';
import { FetchStatus } from '@providers/cache';
import { useCluster } from '@providers/cluster';
Expand Down Expand Up @@ -218,7 +218,7 @@ function getTransfer(
cluster: Cluster,
signature: string
): Transfer | TransferChecked | undefined {
if ('parsed' in instruction && instruction.program === 'spl-token') {
if ('parsed' in instruction && isTokenProgramData(instruction)) {
try {
const { type: rawType } = instruction.parsed;
const type = create(rawType, TokenInstructionType);
Expand Down
3 changes: 2 additions & 1 deletion app/components/common/InstructionDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isTokenProgramData } from '@providers/accounts';
import { ConfirmedSignatureInfo } from '@solana/web3.js';
import { getTokenProgramInstructionName, InstructionType } from '@utils/instruction';
import React from 'react';
Expand All @@ -14,7 +15,7 @@ export function InstructionDetails({

const instructionTypes = instructionType.innerInstructions
.map(ix => {
if ('parsed' in ix && ix.program === 'spl-token') {
if ('parsed' in ix && isTokenProgramData(ix)) {
return getTokenProgramInstructionName(ix, tx);
}
return undefined;
Expand Down
27 changes: 17 additions & 10 deletions app/components/inspector/SimulatorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ProgramLogsCardBody } from '@components/ProgramLogsCardBody';
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@providers/accounts/tokens';
import { useCluster } from '@providers/cluster';
import { AccountLayout, MintLayout, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { AccountLayout, MintLayout } from "@solana/spl-token";
import { AccountInfo, AddressLookupTableAccount, Connection, MessageAddressTableLookup, ParsedAccountData, ParsedMessageAccount, SimulatedTransactionAccountInfo, TokenBalance, VersionedMessage, VersionedTransaction } from '@solana/web3.js';
import { PublicKey } from '@solana/web3.js';
import { InstructionLogs, parseProgramLogs } from '@utils/program-logs';
Expand Down Expand Up @@ -140,9 +141,9 @@ function useSimulator(message: VersionedMessage) {
const accountOwnerPost = resp.value.accounts?.at(index)?.owner;

if (
(parsedAccountPre?.owner.toBase58() == TOKEN_PROGRAM_ID.toBase58() ||
parsedAccountPre?.owner.toBase58() == "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") &&
(parsedAccountPre?.data as ParsedAccountData).parsed.type === 'account'
parsedAccountPre &&
isTokenProgramBase58(parsedAccountPre.owner.toBase58()) &&
(parsedAccountPre.data as ParsedAccountData).parsed.type === 'account'
) {
const mint = (parsedAccountPre?.data as ParsedAccountData).parsed.info.mint;
const owner = (parsedAccountPre?.data as ParsedAccountData).parsed.info.owner;
Expand All @@ -157,8 +158,8 @@ function useSimulator(message: VersionedMessage) {
}

if (
(accountOwnerPost === TOKEN_PROGRAM_ID.toBase58() ||
accountOwnerPost === "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") &&
accountOwnerPost &&
isTokenProgramBase58(accountOwnerPost) &&
Buffer.from(accountDataPost!, 'base64').length >= 165
) {
const accountParsedPost = AccountLayout.decode(Buffer.from(accountDataPost!, 'base64'));
Expand Down Expand Up @@ -235,6 +236,10 @@ function useSimulator(message: VersionedMessage) {
};
}

function isTokenProgramBase58(programIdBase58: string): boolean {
return programIdBase58 === TOKEN_PROGRAM_ID.toBase58() || programIdBase58 === TOKEN_2022_PROGRAM_ID.toBase58();
}

function getMintDecimals(
accountKeys: PublicKey[],
parsedAccountsPre: (AccountInfo<ParsedAccountData | Buffer> | null)[],
Expand All @@ -249,16 +254,18 @@ function getMintDecimals(

// Token account before
if (
parsedAccount?.owner.toBase58() == TOKEN_PROGRAM_ID.toBase58() &&
(parsedAccount?.data as ParsedAccountData).parsed.type === 'account'
parsedAccount &&
isTokenProgramBase58(parsedAccount.owner.toBase58()) &&
(parsedAccount.data as ParsedAccountData).parsed.type === 'account'
) {
mintToDecimals[(parsedAccount?.data as ParsedAccountData).parsed.info.mint] = (
parsedAccount?.data as ParsedAccountData
).parsed.info.tokenAmount.decimals;
}
// Mint account before
if (
parsedAccount?.owner.toBase58() == TOKEN_PROGRAM_ID.toBase58() &&
parsedAccount &&
isTokenProgramBase58(parsedAccount.owner.toBase58()) &&
(parsedAccount?.data as ParsedAccountData).parsed.type === 'mint'
) {
mintToDecimals[key.toBase58()] = (parsedAccount?.data as ParsedAccountData).parsed.info.decimals;
Expand All @@ -267,7 +274,7 @@ function getMintDecimals(
// Token account after
const accountDataPost = accountDatasPost.at(index)?.data[0];
const accountOwnerPost = accountDatasPost.at(index)?.owner;
if (accountOwnerPost === TOKEN_PROGRAM_ID.toBase58() && Buffer.from(accountDataPost!, 'base64').length === 82) {
if (accountOwnerPost && isTokenProgramBase58(accountOwnerPost) && Buffer.from(accountDataPost!, 'base64').length === 82) {
const accountParsedPost = MintLayout.decode(Buffer.from(accountDataPost!, 'base64'));
mintToDecimals[key.toBase58()] = accountParsedPost.decimals;
}
Expand Down
3 changes: 2 additions & 1 deletion app/components/instruction/token/TokenDetailsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useSWR from 'swr';

import { useCluster } from '@/app/providers/cluster';
import { Cluster } from '@/app/utils/cluster';
import { TOKEN_IDS } from '@/app/utils/programs';
import { getTokenInfo, getTokenInfoSwrKey } from '@/app/utils/token-info';

import { InstructionCard } from '../InstructionCard';
Expand All @@ -27,7 +28,7 @@ export function TokenDetailsCard(props: DetailsProps) {
const parsed = create(props.ix.parsed, ParsedInfo);
const { type: rawType, info } = parsed;
const type = create(rawType, TokenInstructionType);
const title = `Token Program: ${IX_TITLES[type]}`;
const title = `${TOKEN_IDS[props.ix.programId.toString()]}: ${IX_TITLES[type]}`;
const created = create(info, IX_STRUCTS[type] as any);
return <TokenInstruction title={title} info={created} {...props} />;
}
Expand Down
18 changes: 14 additions & 4 deletions app/providers/accounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '@solana/web3.js';
import { Cluster } from '@utils/cluster';
import { pubkeyToString } from '@utils/index';
import { assertIsTokenProgram, TokenProgram } from '@utils/programs';
import { ParsedAddressLookupTableAccount } from '@validators/accounts/address-lookup-table';
import { ConfigAccount } from '@validators/accounts/config';
import { NonceAccount } from '@validators/accounts/nonce';
Expand Down Expand Up @@ -57,8 +58,16 @@ export type NFTData = {
editionInfo: EditionInfo;
};

export function isTokenProgramData(data: { program: string }): data is TokenProgramData {
try {
assertIsTokenProgram(data.program);
return true;
} catch(e) {
return false;
}
}
export type TokenProgramData = {
program: 'spl-token';
program: TokenProgram;
parsed: TokenAccount;
nftData?: NFTData;
};
Expand Down Expand Up @@ -374,7 +383,8 @@ async function handleParsedAccountData(
};
}

case 'spl-token': {
case 'spl-token':
case 'spl-token-2022': {
const parsed = create(info, TokenAccount);
let nftData;

Expand Down Expand Up @@ -484,7 +494,7 @@ export function useMintAccountInfo(address: string | undefined): MintAccountInfo
try {
const parsedData = account.data.parsed;
if (!parsedData) return;
if (parsedData.program !== 'spl-token' || parsedData.parsed.type !== 'mint') {
if (!isTokenProgramData(parsedData) || parsedData.parsed.type !== 'mint') {
return;
}

Expand All @@ -504,7 +514,7 @@ export function useTokenAccountInfo(address: string | undefined): TokenAccountIn
try {
const parsedData = account.data.parsed;
if (!parsedData) return;
if (parsedData.program !== 'spl-token' || parsedData.parsed.type !== 'account') {
if (!isTokenProgramData(parsedData) || parsedData.parsed.type !== 'account') {
return;
}

Expand Down
11 changes: 9 additions & 2 deletions app/providers/accounts/tokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export function TokensProvider({ children }: ProviderProps) {
}

export const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
export const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
export function isTokenProgramId(programId: PublicKey) {
return programId.equals(TOKEN_PROGRAM_ID) || programId.equals(TOKEN_2022_PROGRAM_ID);
}

async function fetchAccountTokens(dispatch: Dispatch, pubkey: PublicKey, cluster: Cluster, url: string) {
const key = pubkey.toBase58();
Expand All @@ -59,11 +63,14 @@ async function fetchAccountTokens(dispatch: Dispatch, pubkey: PublicKey, cluster
let status;
let data;
try {
const { value } = await new Connection(url, 'processed').getParsedTokenAccountsByOwner(pubkey, {
const { value: tokenAccounts } = await new Connection(url, 'processed').getParsedTokenAccountsByOwner(pubkey, {
programId: TOKEN_PROGRAM_ID,
});
const { value: token2022Accounts } = await new Connection(url, 'processed').getParsedTokenAccountsByOwner(pubkey, {
programId: TOKEN_2022_PROGRAM_ID,
});

const tokens: TokenInfoWithPubkey[] = value.slice(0, 101).map(accountInfo => {
const tokens: TokenInfoWithPubkey[] = tokenAccounts.concat(token2022Accounts).slice(0, 101).map(accountInfo => {
const parsedInfo = accountInfo.account.data.parsed.info;
const info = create(parsedInfo, TokenAccountInfo);
return { info, pubkey: accountInfo.pubkey };
Expand Down
Loading