>
@@ -348,30 +398,38 @@ async function fetchTokenInfo([_, address, cluster, url]: ['get-token-info', str
function TokenAccountCard({ account, info }: { account: Account; info: TokenAccountInfo }) {
const refresh = useFetchAccountInfo();
- const { cluster, url } = useCluster();
+ const { cluster, clusterInfo, url } = useCluster();
+ const epoch = clusterInfo?.epochInfo.epoch;
const label = addressLabel(account.pubkey.toBase58(), cluster);
const swrKey = useMemo(() => getTokenInfoSwrKey(info.mint.toString(), cluster, url), [cluster, url]);
const { data: tokenInfo } = useSWR(swrKey, fetchTokenInfo);
const [symbol, setSymbol] = useState
(undefined);
+ const accountExtensions = info.extensions?.slice();
+ accountExtensions?.sort(cmpExtension);
const balance = info.isNative ? (
<>
- {'\u25ce'}{new BigNumber(info.tokenAmount.uiAmountString).toFormat(9)}
+ {'\u25ce'}
+ {new BigNumber(info.tokenAmount.uiAmountString).toFormat(9)}
>
- ) : <>{info.tokenAmount.uiAmountString}>;
+ ) : (
+ <>{info.tokenAmount.uiAmountString}>
+ );
useEffect(() => {
if (info.isNative) {
setSymbol('SOL');
} else {
- setSymbol(tokenInfo?.symbol)
+ setSymbol(tokenInfo?.symbol);
}
- }, [tokenInfo])
+ }, [tokenInfo]);
return (
-
Token{ account.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() && "-2022" } Account
+
+ Token{account.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() && '-2022'} Account
+
);
@@ -497,3 +565,603 @@ function MultisigAccountCard({ account, info }: { account: Account; info: Multis
);
}
+
+function cmpExtension(a: TokenExtension, b: TokenExtension) {
+ // be sure that extensions with a header row always come later
+ const sortedExtensionTypes = [
+ 'transferFeeAmount',
+ 'mintCloseAuthority',
+ 'defaultAccountState',
+ 'immutableOwner',
+ 'memoTransfer',
+ 'nonTransferable',
+ 'nonTransferableAccount',
+ 'cpiGuard',
+ 'permanentDelegate',
+ 'transferHook',
+ 'transferHookAccount',
+ 'metadataPointer',
+ 'groupPointer',
+ 'groupMemberPointer',
+ // everything below this comment includes a header row
+ 'confidentialTransferAccount',
+ 'confidentialTransferFeeConfig',
+ 'confidentialTransferFeeAmount',
+ 'confidentialTransferMint',
+ 'interestBearingConfig',
+ 'transferFeeConfig',
+ 'tokenGroup',
+ 'tokenGroupMember',
+ 'tokenMetadata',
+ // always keep this last
+ 'unparseableExtension',
+ ];
+ return sortedExtensionTypes.indexOf(a.extension) - sortedExtensionTypes.indexOf(b.extension);
+}
+
+function TokenExtensionRows(
+ tokenExtension: TokenExtension,
+ maybeEpoch: bigint | undefined,
+ decimals: number,
+ symbol: string | undefined
+) {
+ const epoch = maybeEpoch || 0n; // fallback to 0 if not provided
+ switch (tokenExtension.extension) {
+ case 'mintCloseAuthority': {
+ const extension = create(tokenExtension.state, MintCloseAuthority);
+ if (extension.closeAuthority) {
+ return (
+
+ Close Authority |
+
+
+ |
+
+ );
+ } else {
+ return <>>;
+ }
+ }
+ case 'transferFeeAmount': {
+ const extension = create(tokenExtension.state, TransferFeeAmount);
+ return (
+
+ Withheld Amount {typeof symbol === 'string' && `(${symbol})`} |
+
+ {normalizeTokenAmount(extension.withheldAmount, decimals).toLocaleString('en-US', {
+ maximumFractionDigits: 20,
+ })}
+ |
+
+ );
+ }
+ case 'transferFeeConfig': {
+ const extension = create(tokenExtension.state, TransferFeeConfig);
+ return (
+ <>
+
+ Transfer Fee Config
+
+ {extension.transferFeeConfigAuthority && (
+
+ Transfer Fee Authority |
+
+
+ |
+
+ )}
+
+ {extension.newerTransferFee.epoch > epoch ? 'Current' : 'Previous'} Fee Epoch |
+ {extension.olderTransferFee.epoch} |
+
+
+
+ {extension.newerTransferFee.epoch > epoch ? 'Current' : 'Previous'} Maximum Fee{' '}
+ {typeof symbol === 'string' && `(${symbol})`}
+ |
+
+ {normalizeTokenAmount(extension.olderTransferFee.maximumFee, decimals).toLocaleString(
+ 'en-US',
+ {
+ maximumFractionDigits: 20,
+ }
+ )}
+ |
+
+
+ {extension.newerTransferFee.epoch > epoch ? 'Current' : 'Previous'} Fee Rate |
+ {`${extension.olderTransferFee.transferFeeBasisPoints / 100}%`} |
+
+
+ {extension.newerTransferFee.epoch > epoch ? 'Future' : 'Current'} Fee Epoch |
+ {extension.newerTransferFee.epoch} |
+
+
+
+ {extension.newerTransferFee.epoch > epoch ? 'Future' : 'Current'} Maximum Fee{' '}
+ {typeof symbol === 'string' && `(${symbol})`}
+ |
+
+ {normalizeTokenAmount(extension.newerTransferFee.maximumFee, decimals).toLocaleString(
+ 'en-US',
+ {
+ maximumFractionDigits: 20,
+ }
+ )}
+ |
+
+
+ {extension.newerTransferFee.epoch > epoch ? 'Future' : 'Current'} Fee Rate |
+ {`${extension.newerTransferFee.transferFeeBasisPoints / 100}%`} |
+
+ {extension.withdrawWithheldAuthority && (
+
+ Withdraw Withheld Fees Authority |
+
+
+ |
+
+ )}
+
+ Withheld Amount {typeof symbol === 'string' && `(${symbol})`} |
+
+ {normalizeTokenAmount(extension.withheldAmount, decimals).toLocaleString('en-US', {
+ maximumFractionDigits: 20,
+ })}
+ |
+
+ >
+ );
+ }
+ case 'confidentialTransferMint': {
+ const extension = create(tokenExtension.state, ConfidentialTransferMint);
+ return (
+ <>
+
+ Confidential Transfer
+
+ {extension.authority && (
+
+ Authority |
+
+
+ |
+
+ )}
+ {extension.auditorElgamalPubkey && (
+
+ Auditor Elgamal Pubkey |
+ {extension.auditorElgamalPubkey} |
+
+ )}
+
+ New Account Approval Policy |
+ {extension.autoApproveNewAccounts ? 'auto' : 'manual'} |
+
+ >
+ );
+ }
+ case 'confidentialTransferFeeConfig': {
+ const extension = create(tokenExtension.state, ConfidentialTransferFeeConfig);
+ return (
+ <>
+
+ Confidential Transfer Fee
+
+ {extension.authority && (
+
+ Authority |
+
+
+ |
+
+ )}
+ {extension.withdrawWithheldAuthorityElgamalPubkey && (
+
+ Auditor Elgamal Pubkey |
+ {extension.withdrawWithheldAuthorityElgamalPubkey} |
+
+ )}
+
+ Harvest to Mint |
+ {extension.harvestToMintEnabled ? 'enabled' : 'disabled'} |
+
+
+ Encrypted Withheld Amount {typeof symbol === 'string' && `(${symbol})`} |
+ {extension.withheldAmount} |
+
+ >
+ );
+ }
+ case 'defaultAccountState': {
+ const extension = create(tokenExtension.state, DefaultAccountState);
+ return (
+
+ DefaultAccountState |
+ {extension.accountState} |
+
+ );
+ }
+ case 'nonTransferable': {
+ return (
+
+ Non-Transferable |
+ enabled |
+
+ );
+ }
+ case 'interestBearingConfig': {
+ const extension = create(tokenExtension.state, InterestBearingConfig);
+ return (
+ <>
+
+ Interest-Bearing
+
+ {extension.rateAuthority && (
+
+ Authority |
+
+
+ |
+
+ )}
+
+ Current Rate |
+ {`${extension.currentRate / 100}%`} |
+
+
+ Pre-Current Average Rate |
+ {`${extension.preUpdateAverageRate / 100}%`} |
+
+
+ Last Update Timestamp |
+ {displayTimestamp(extension.lastUpdateTimestamp * 1000)} |
+
+
+ Initialization Timestamp |
+ {displayTimestamp(extension.initializationTimestamp * 1000)} |
+
+ >
+ );
+ }
+ case 'permanentDelegate': {
+ const extension = create(tokenExtension.state, PermanentDelegate);
+ if (extension.delegate) {
+ return (
+
+ Permanent Delegate |
+
+
+ |
+
+ );
+ } else {
+ return <>>;
+ }
+ }
+ case 'transferHook': {
+ const extension = create(tokenExtension.state, TransferHook);
+ return (
+ <>
+ {extension.programId && (
+
+ Transfer Hook Program Id |
+
+
+ |
+
+ )}
+ {extension.authority && (
+
+ Transfer Hook Authority |
+
+
+ |
+
+ )}
+ >
+ );
+ }
+ case 'metadataPointer': {
+ const extension = create(tokenExtension.state, MetadataPointer);
+ return (
+ <>
+ {extension.metadataAddress && (
+
+ Metadata |
+
+
+ |
+
+ )}
+ {extension.authority && (
+
+ Metadata Pointer Authority |
+
+
+ |
+
+ )}
+ >
+ );
+ }
+ case 'groupPointer': {
+ const extension = create(tokenExtension.state, GroupPointer);
+ return (
+ <>
+ {extension.groupAddress && (
+
+ Token Group |
+
+
+ |
+
+ )}
+ {extension.authority && (
+
+ Group Pointer Authority |
+
+
+ |
+
+ )}
+ >
+ );
+ }
+ case 'groupMemberPointer': {
+ const extension = create(tokenExtension.state, GroupMemberPointer);
+ return (
+ <>
+ {extension.memberAddress && (
+
+ Token Group Member |
+
+
+ |
+
+ )}
+ {extension.authority && (
+
+ Member Pointer Authority |
+
+
+ |
+
+ )}
+ >
+ );
+ }
+ case 'tokenMetadata': {
+ const extension = create(tokenExtension.state, TokenMetadata);
+ return (
+ <>
+
+ Metadata
+
+
+ Mint |
+
+
+ |
+
+ {extension.updateAuthority && (
+
+ Update Authority |
+
+
+ |
+
+ )}
+
+ Name |
+ {extension.name} |
+
+
+ Symbol |
+ {extension.symbol} |
+
+
+ URI |
+
+
+ {extension.uri}
+
+
+ |
+
+ {extension.additionalMetadata?.length > 0 && (
+ <>
+
+ Additional Metadata
+
+ {extension.additionalMetadata?.map(keyValuePair => (
+
+ {keyValuePair[0]} |
+ {keyValuePair[1]} |
+
+ ))}
+ >
+ )}
+ >
+ );
+ }
+ case 'cpiGuard': {
+ const extension = create(tokenExtension.state, CpiGuard);
+ return (
+
+ CPI Guard |
+ {extension.lockCpi ? 'enabled' : 'disabled'} |
+
+ );
+ }
+ case 'confidentialTransferAccount': {
+ const extension = create(tokenExtension.state, ConfidentialTransferAccount);
+ return (
+ <>
+
+ Confidential Transfer
+
+
+ Status |
+ {!extension.approved && 'not '}approved |
+
+
+ Elgamal Pubkey |
+ {extension.elgamalPubkey} |
+
+
+ Confidential Credits |
+ {extension.allowConfidentialCredits ? 'enabled' : 'disabled'} |
+
+
+ Non-confidential Credits |
+
+ {extension.allowNonConfidentialCredits ? 'enabled' : 'disabled'}
+ |
+
+
+ Available Balance |
+ {extension.availableBalance} |
+
+
+ Decryptable Available Balance |
+ {extension.decryptableAvailableBalance} |
+
+
+ Pending Balance, Low Bits |
+ {extension.pendingBalanceLo} |
+
+
+ Pending Balance, High Bits |
+ {extension.pendingBalanceHi} |
+
+
+ Pending Balance Credit Counter |
+ {extension.pendingBalanceCreditCounter} |
+
+
+ Expected Pending Balance Credit Counter |
+ {extension.expectedPendingBalanceCreditCounter} |
+
+
+ Actual Pending Balance Credit Counter |
+ {extension.actualPendingBalanceCreditCounter} |
+
+
+ Maximum Pending Balance Credit Counter |
+ {extension.maximumPendingBalanceCreditCounter} |
+
+ >
+ );
+ }
+ case 'immutableOwner': {
+ return (
+
+ Immutable Owner |
+ enabled |
+
+ );
+ }
+ case 'memoTransfer': {
+ const extension = create(tokenExtension.state, MemoTransfer);
+ return (
+
+ Require Memo on Incoming Transfers |
+ {extension.requireIncomingTransferMemos ? 'enabled' : 'disabled'} |
+
+ );
+ }
+ case 'transferHookAccount': {
+ const extension = create(tokenExtension.state, TransferHookAccount);
+ return (
+
+ Transfer Hook Status |
+ {!extension.transferring && 'not '}transferring |
+
+ );
+ }
+ case 'nonTransferableAccount': {
+ return (
+
+ Non-Transferable |
+ enabled |
+
+ );
+ }
+ case 'confidentialTransferFeeAmount': {
+ const extension = create(tokenExtension.state, ConfidentialTransferFeeAmount);
+ return (
+
+ Encrypted Withheld Amount {typeof symbol === 'string' && `(${symbol})`} |
+ {extension.withheldAmount} |
+
+ );
+ }
+ case 'tokenGroup': {
+ const extension = create(tokenExtension.state, TokenGroup);
+ return (
+ <>
+
+ Group
+
+
+ Mint |
+
+
+ |
+
+ {extension.updateAuthority && (
+
+ Update Authority |
+
+
+ |
+
+ )}
+
+ Current Size |
+ {extension.size} |
+
+
+ Max Size |
+ {extension.maxSize} |
+
+ >
+ );
+ }
+ case 'tokenGroupMember': {
+ const extension = create(tokenExtension.state, TokenGroupMember);
+ return (
+ <>
+
+ Group Member
+
+
+ Mint |
+
+
+ |
+
+
+ Group |
+
+
+ |
+
+
+ Member Number |
+ {extension.memberNumber} |
+
+ >
+ );
+ }
+ case 'unparseableExtension':
+ default:
+ return (
+
+ Unknown Extension |
+ unparseable |
+
+ );
+ }
+}
diff --git a/app/validators/accounts/token-extension.ts b/app/validators/accounts/token-extension.ts
new file mode 100644
index 00000000..c8973012
--- /dev/null
+++ b/app/validators/accounts/token-extension.ts
@@ -0,0 +1,161 @@
+import { PublicKeyFromString } from '@validators/pubkey';
+import { any, array, boolean, enums, Infer, nullable, number, string, type } from 'superstruct';
+
+export type TokenExtensionType = Infer;
+const ExtensionType = enums([
+ 'transferFeeConfig',
+ 'transferFeeAmount',
+ 'mintCloseAuthority',
+ 'confidentialTransferMint',
+ 'confidentialTransferAccount',
+ 'defaultAccountState',
+ 'immutableOwner',
+ 'memoTransfer',
+ 'nonTransferable',
+ 'interestBearingConfig',
+ 'cpiGuard',
+ 'permanentDelegate',
+ 'nonTransferableAccount',
+ 'confidentialTransferFeeConfig',
+ 'confidentialTransferFeeAmount',
+ 'transferHook',
+ 'transferHookAccount',
+ 'metadataPointer',
+ 'tokenMetadata',
+ 'groupPointer',
+ 'groupMemberPointer',
+ 'tokenGroup',
+ 'tokenGroupMember',
+ 'unparseableExtension',
+]);
+
+export type TokenExtension = Infer;
+export const TokenExtension = type({
+ extension: ExtensionType,
+ state: any(),
+});
+
+const TransferFee = type({
+ epoch: number(),
+ maximumFee: number(),
+ transferFeeBasisPoints: number(),
+});
+
+export const TransferFeeConfig = type({
+ newerTransferFee: TransferFee,
+ olderTransferFee: TransferFee,
+ transferFeeConfigAuthority: nullable(PublicKeyFromString),
+ withdrawWithheldAuthority: nullable(PublicKeyFromString),
+ withheldAmount: number(),
+});
+
+export const TransferFeeAmount = type({
+ withheldAmount: number(),
+});
+
+export const MintCloseAuthority = type({
+ closeAuthority: nullable(PublicKeyFromString),
+});
+
+const AccountState = enums(['initialized', 'frozen']);
+export const DefaultAccountState = type({
+ accountState: AccountState,
+});
+
+export const MemoTransfer = type({
+ requireIncomingTransferMemos: boolean(),
+});
+
+export const CpiGuard = type({
+ lockCpi: boolean(),
+});
+
+export const PermanentDelegate = type({
+ delegate: nullable(PublicKeyFromString),
+});
+
+export const InterestBearingConfig = type({
+ currentRate: number(),
+ initializationTimestamp: number(),
+ lastUpdateTimestamp: number(),
+ preUpdateAverageRate: number(),
+ rateAuthority: nullable(PublicKeyFromString),
+});
+
+export const ConfidentialTransferMint = type({
+ auditorElgamalPubkey: nullable(string()),
+ authority: nullable(PublicKeyFromString),
+ autoApproveNewAccounts: boolean(),
+});
+
+export const ConfidentialTransferFeeConfig = type({
+ authority: nullable(PublicKeyFromString),
+ harvestToMintEnabled: boolean(),
+ withdrawWithheldAuthorityElgamalPubkey: nullable(string()),
+ withheldAmount: string(),
+});
+
+export const ConfidentialTransferAccount = type({
+ actualPendingBalanceCreditCounter: number(),
+ allowConfidentialCredits: boolean(),
+ allowNonConfidentialCredits: boolean(),
+ approved: boolean(),
+ availableBalance: string(),
+ decryptableAvailableBalance: string(),
+ elgamalPubkey: string(),
+ expectedPendingBalanceCreditCounter: number(),
+ maximumPendingBalanceCreditCounter: number(),
+ pendingBalanceCreditCounter: number(),
+ pendingBalanceHi: string(),
+ pendingBalanceLo: string(),
+});
+
+export const ConfidentialTransferFeeAmount = type({
+ withheldAmount: string(),
+});
+
+export const MetadataPointer = type({
+ authority: nullable(PublicKeyFromString),
+ metadataAddress: nullable(PublicKeyFromString),
+});
+
+export const TokenMetadata = type({
+ additionalMetadata: array(array(string())),
+ mint: PublicKeyFromString,
+ name: string(),
+ symbol: string(),
+ updateAuthority: nullable(PublicKeyFromString),
+ uri: string(),
+});
+
+export const TransferHook = type({
+ authority: nullable(PublicKeyFromString),
+ programId: nullable(PublicKeyFromString),
+});
+
+export const TransferHookAccount = type({
+ transferring: boolean(),
+});
+
+export const GroupPointer = type({
+ authority: nullable(PublicKeyFromString),
+ groupAddress: nullable(PublicKeyFromString),
+});
+
+export const GroupMemberPointer = type({
+ authority: nullable(PublicKeyFromString),
+ memberAddress: nullable(PublicKeyFromString),
+});
+
+export const TokenGroup = type({
+ maxSize: number(),
+ mint: PublicKeyFromString,
+ size: number(),
+ updateAuthority: nullable(PublicKeyFromString),
+});
+
+export const TokenGroupMember = type({
+ group: PublicKeyFromString,
+ memberNumber: number(),
+ mint: PublicKeyFromString,
+});
diff --git a/app/validators/accounts/token.ts b/app/validators/accounts/token.ts
index 1daaa504..686f4d39 100644
--- a/app/validators/accounts/token.ts
+++ b/app/validators/accounts/token.ts
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-redeclare */
+import { TokenExtension } from '@validators/accounts/token-extension';
import { PublicKeyFromString } from '@validators/pubkey';
import { any, array, boolean, enums, Infer, nullable, number, optional, string, type } from 'superstruct';
@@ -20,6 +21,7 @@ export const TokenAccountInfo = type({
closeAuthority: optional(PublicKeyFromString),
delegate: optional(PublicKeyFromString),
delegatedAmount: optional(TokenAmount),
+ extensions: optional(array(TokenExtension)),
isNative: boolean(),
mint: PublicKeyFromString,
owner: PublicKeyFromString,
@@ -31,6 +33,7 @@ export const TokenAccountInfo = type({
export type MintAccountInfo = Infer;
export const MintAccountInfo = type({
decimals: number(),
+ extensions: optional(array(TokenExtension)),
freezeAuthority: nullable(PublicKeyFromString),
isInitialized: boolean(),
mintAuthority: nullable(PublicKeyFromString),