-
Notifications
You must be signed in to change notification settings - Fork 581
feat: token 2022 page rework #516
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
Changes from 19 commits
fc16d3f
10fe9ae
52eadb1
8c92d84
00dc068
4edc7ba
072407b
ae31c74
1eb6204
6c9198e
529dc3e
b90a826
41fe9c9
9fcd405
520f6e2
126c1af
b17c4cf
15dc1a2
954b991
f6e14f5
04b3d4c
33e3405
a2e87a5
cdc544a
61f20ae
327fbdb
8c1d5d1
70095dd
f49ec04
5ade1b2
0310ac6
634ab6d
ca33555
2da6609
a8aa778
6dd4235
d26c78d
1f534f0
2e394c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,5 +38,4 @@ next-env.d.ts | |
| .swc/ | ||
|
|
||
| # vim | ||
| *.sw* | ||
| .editorconfig | ||
| .editorconfig | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| const prettierConfigSolana = require('@solana/prettier-config-solana'); | ||
|
|
||
| module.exports = { | ||
| ...prettierConfigSolana, | ||
| plugins: [prettierConfigSolana.plugins ?? []].concat(['prettier-plugin-tailwindcss']), | ||
| endOfLine: 'lf', | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ import { CacheEntry, FetchStatus } from '@providers/cache'; | |
| import { useCluster } from '@providers/cluster'; | ||
| import { PROGRAM_ID as ACCOUNT_COMPRESSION_ID } from '@solana/spl-account-compression'; | ||
| import { PublicKey } from '@solana/web3.js'; | ||
| import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; | ||
| import { Cluster, ClusterStatus } from '@utils/cluster'; | ||
| import { FEATURE_PROGRAM_ID } from '@utils/parseFeatureAccount'; | ||
| import { useClusterPath } from '@utils/url'; | ||
|
|
@@ -569,7 +570,8 @@ export type MoreTabs = | |
| | 'compression' | ||
| | 'verified-build' | ||
| | 'program-multisig' | ||
| | 'feature-gate'; | ||
| | 'feature-gate' | ||
| | 'token-extensions'; | ||
|
|
||
| function MoreSection({ children, tabs }: { children: React.ReactNode; tabs: (JSX.Element | null)[] }) { | ||
| return ( | ||
|
|
@@ -724,6 +726,27 @@ function getCustomLinkedTabs(pubkey: PublicKey, account: Account) { | |
| tab: programMultisigTab, | ||
| }); | ||
|
|
||
| // Add extensions tab for Token Extensions program accounts | ||
| if (account.owner.toBase58() === TOKEN_2022_PROGRAM_ADDRESS) { | ||
| const tokenAccount = account.data.parsed; // as TokenProgramData; | ||
|
|
||
| console.log({ tokenAccount }); | ||
|
|
||
| const extensionsTab: Tab = { | ||
| path: 'token-extensions', | ||
| slug: 'token-extensions', | ||
| title: 'Extensions', | ||
| }; | ||
| tabComponents.push({ | ||
| component: ( | ||
| <React.Suspense key={extensionsTab.slug} fallback={<></>}> | ||
| <TokenExtensionsLink tab={extensionsTab} address={pubkey.toString()} /> | ||
| </React.Suspense> | ||
rogaldh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ), | ||
| tab: extensionsTab, | ||
| }); | ||
| } | ||
|
|
||
| const anchorProgramTab: Tab = { | ||
| path: 'anchor-program', | ||
| slug: 'anchor-program', | ||
|
|
@@ -874,3 +897,17 @@ function ProgramMultisigLink({ | |
| </li> | ||
| ); | ||
| } | ||
|
|
||
| function TokenExtensionsLink({ address, tab }: { address: string; tab: Tab }) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This component duplicates the existing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's wrong. Tab has different logic for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be a good idea to rename Tab to something more specific or to bypass isActive through props |
||
| const accountDataPath = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); | ||
| const selectedLayoutSegment = useSelectedLayoutSegment(); | ||
| const isActive = selectedLayoutSegment === tab.path; | ||
|
|
||
| return ( | ||
| <li key={tab.slug} className="nav-item"> | ||
| <Link className={`${isActive ? 'active ' : ''}nav-link`} href={accountDataPath}> | ||
| {tab.title} | ||
| </Link> | ||
| </li> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import './styles.css'; | ||
|
|
||
| import { TokenExtensionsCard } from '@components/account/TokenExtensionsCard'; | ||
| import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; | ||
| import { Metadata } from 'next/types'; | ||
|
|
||
| type Props = Readonly<{ | ||
| params: { | ||
| address: string; | ||
| }; | ||
| }>; | ||
|
|
||
| export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> { | ||
| return { | ||
| description: `Token extensions information for address ${props.params.address} on Solana`, | ||
| title: `Token Extensions | ${await getReadableTitleFromAddress(props)} | Solana`, | ||
| }; | ||
| } | ||
|
|
||
| export default function TokenExtensionsPage({ params: { address: _address } }: Props) { | ||
| return <TokenExtensionsCard address={_address} />; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| @import "../../../styles.css"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| 'use client'; | ||
|
|
||
| import { PublicKey } from '@solana/web3.js'; | ||
| import { AccountHeader } from '@components/common/Account'; | ||
| import { useFetchAccountInfo, useMintAccountInfo } from '@providers/accounts'; | ||
|
|
||
| import { StatusType } from '@/app/components/shared/StatusBadge'; | ||
|
|
||
| import { TokenExtensionsSection } from './TokenExtensionsSection'; | ||
|
|
||
| export type ExtensionStatus = StatusType; | ||
|
|
||
| export interface TokenExtension { | ||
| id: string; | ||
| name: string; | ||
| tooltip?: string; | ||
| description?: string; | ||
| status: ExtensionStatus; | ||
| externalLinks: { label: string; url: string }[]; | ||
| } | ||
|
|
||
| export type ParsedTokenExtensionWithRawData = TokenExtension & { | ||
| parsed?: NonNullable<unknown> | NonNullable<unknown>[]; | ||
| raw?: string; | ||
| }; | ||
|
|
||
| export function TokenExtensionsCard({ address }: { address: string }) { | ||
| const refresh = useFetchAccountInfo(); | ||
| const mintInfo = useMintAccountInfo(address); | ||
| console.log({ mintInfo: mintInfo?.extensions }, address); | ||
ngundotra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (!mintInfo) return null; | ||
|
|
||
| const extensions = populateTokenExtensions(mintInfo.extensions ?? []); | ||
|
|
||
| return ( | ||
| <div className="card"> | ||
| <AccountHeader title="Extensions" refresh={() => refresh(new PublicKey(address), 'parsed')} /> | ||
| <div className="card-body p-0"> | ||
| <TokenExtensionsSection extensions={extensions} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| type ParsedExtension = { extension: string; state?: NonNullable<unknown> }; | ||
|
|
||
| function populateTokenExtensions(extensions: ParsedExtension[]): ParsedTokenExtensionWithRawData[] { | ||
| function populateExternalLinks(url: string) { | ||
| return [{ label: 'Docs', url }]; | ||
| } | ||
|
|
||
| function findExtensionByKeyword(keyword: string, extensions: ParsedExtension[]) { | ||
| return extensions.find(ext => ext.extension === keyword); | ||
| } | ||
|
|
||
| /** | ||
| * Sample. | ||
| * TODO: To be removed upon editing all the details for each extension | ||
|
|
||
| id: 'permanent-delegate', | ||
| name: 'Permanent Delegate', | ||
| tooltip: 'Delegates permanent authority to a specific address that can transfer or burn tokens from any account holding this token, providing centralized administrative control over the token ecosystem.', | ||
| description: 'Delegates permanent authority to a specific address that can transfer or burn tokens from any account holding this token, providing centralized administrative control over the token ecosystem.', | ||
| status: 'active', | ||
| externalLinks: populateExternalLinks('https://solana.com/developers/guides/token-extensions/permanent-delegate'), | ||
| */ | ||
| const result = extensions.reduce((acc, { extension, state }) => { | ||
| switch (extension) { | ||
| case 'mintCloseAuthority': { | ||
| acc.set(extension, { | ||
| id: extension, | ||
| name: 'Mint Close Authority', | ||
| tooltip: undefined, | ||
| description: undefined, | ||
| status: 'active', | ||
| externalLinks: populateExternalLinks( | ||
| 'https://solana.com/developers/guides/token-extensions/mint-close-authority' | ||
| ), | ||
| parsed: state, | ||
| raw: state, | ||
| }); | ||
| break; | ||
| } | ||
| case 'permanentDelegate': { | ||
| acc.set(extension, { | ||
| id: extension, | ||
| name: 'Permanent Delegate', | ||
| tooltip: | ||
| 'Delegates permanent authority to a specific address that can transfer or burn tokens from any account holding this token, providing centralized administrative control over the token ecosystem.', | ||
| description: | ||
| 'Delegates permanent authority to a specific address that can transfer or burn tokens from any account holding this token, providing centralized administrative control over the token ecosystem.', | ||
| status: 'active', | ||
| externalLinks: populateExternalLinks( | ||
| 'https://solana.com/developers/guides/token-extensions/permanent-delegate' | ||
| ), | ||
| parsed: state, | ||
| raw: state, | ||
| }); | ||
| break; | ||
| } | ||
| case 'transferFeeConfig': { | ||
| acc.set(extension, { | ||
| id: extension, | ||
| name: 'Transfer Fee', | ||
| tooltip: undefined, | ||
| description: undefined, | ||
| status: 'active', | ||
| externalLinks: populateExternalLinks( | ||
| 'https://solana.com/developers/guides/token-extensions/transfer-fee' | ||
| ), | ||
| parsed: state, | ||
| raw: state, | ||
| }); | ||
| break; | ||
| } | ||
| case 'transferHook': { | ||
| acc.set(extension, { | ||
| id: extension, | ||
| name: 'Transfer Hook', | ||
| tooltip: undefined, | ||
| description: undefined, | ||
| status: 'active', | ||
| externalLinks: populateExternalLinks( | ||
| 'https://solana.com/developers/guides/token-extensions/transfer-hook' | ||
| ), | ||
| parsed: state, | ||
| raw: state, | ||
| }); | ||
| break; | ||
| } | ||
| case 'confidentialTransferMint': | ||
| case 'confidentialTransferFeeConfig': { | ||
| const EXTENSION_NAME = 'confidentialTransfer'; | ||
|
|
||
| // find confidentialTransfer parts by searching for the index. The are not much extensions so it won't be a bottleneck | ||
|
|
||
| if (!acc.has(EXTENSION_NAME)) { | ||
| const data = [ | ||
| ['Mint', findExtensionByKeyword('confidentialTransferMint', extensions)?.state ?? {}], | ||
| [ | ||
| 'Fee Config', | ||
| findExtensionByKeyword('confidentialTransferFeeConfig', extensions)?.state ?? {}, | ||
| ], | ||
| ]; | ||
| acc.set(EXTENSION_NAME, { | ||
| id: EXTENSION_NAME, | ||
| name: 'Confidential Transfer', | ||
| tooltip: undefined, | ||
| description: undefined, | ||
| status: 'active', | ||
| externalLinks: populateExternalLinks('https://spl.solana.com/confidential-token/quickstart'), | ||
| parsed: data, | ||
| raw: data, | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| case 'metadataPointer': | ||
| case 'tokenMetadata': { | ||
| const EXTENSION_NAME = 'metadataPointer'; | ||
|
|
||
| // find confidentialTransfer parts by searching for the index. The are not much extensions so it won't be a bottleneck | ||
|
|
||
| if (!acc.has(EXTENSION_NAME)) { | ||
| const data = [ | ||
| ['Metadata Pointer', findExtensionByKeyword('metadataPointer', extensions)?.state ?? {}], | ||
| ['Token Metadata', findExtensionByKeyword('tokenMetadata', extensions)?.state ?? {}], | ||
| ]; | ||
|
|
||
| acc.set(EXTENSION_NAME, { | ||
| id: EXTENSION_NAME, | ||
| name: 'Metadata & Metadata Pointer', | ||
| tooltip: undefined, | ||
| description: undefined, | ||
| status: 'active', | ||
| externalLinks: populateExternalLinks( | ||
| 'https://solana.com/developers/guides/token-extensions/metadata-pointer' | ||
| ), | ||
| parsed: data, | ||
| raw: data, | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| default: | ||
| break; | ||
| } | ||
| return acc; | ||
| }, new Map<string, ParsedTokenExtensionWithRawData>()); | ||
|
|
||
| return Array.from(result.values()); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.