Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
fc16d3f
feat: add extensions tab
rogaldh Mar 27, 2025
10fe9ae
add refresh button
rogaldh Mar 27, 2025
52eadb1
chore: disable no non-null assertions rule to increase visibility for…
rogaldh Mar 27, 2025
8c92d84
feat: add tools to generate ui components
rogaldh Mar 28, 2025
00dc068
prettify accordion
rogaldh Mar 28, 2025
4edc7ba
chore: improve prettier configuration
rogaldh Mar 28, 2025
072407b
feat: add generated accordeon with proper prefix resolving
rogaldh Mar 28, 2025
ae31c74
WIP
rogaldh Mar 31, 2025
1eb6204
WIP
rogaldh Apr 1, 2025
6c9198e
fix layout for extension
rogaldh Apr 2, 2025
529dc3e
chore: use correct prefix for generated components
rogaldh Apr 3, 2025
b90a826
downgrade to tailwind3 to cover responsive layout
rogaldh Apr 3, 2025
41fe9c9
improve adaptive layout for extensions
rogaldh Apr 3, 2025
9fcd405
parse t22 most common extensions
rogaldh Apr 3, 2025
520f6e2
wip
rogaldh Apr 3, 2025
126c1af
feat: add components
rogaldh Apr 3, 2025
b17c4cf
Downgrade to prettier@2
rogaldh Apr 4, 2025
15dc1a2
chore: improve prettier config to support plugin for tailwind
rogaldh Apr 4, 2025
954b991
sort classes
rogaldh Apr 4, 2025
f6e14f5
chore: update node engine according version at Vercel
rogaldh Apr 4, 2025
04b3d4c
feat: write test-cases for TokenExtensionRow implemetation
rogaldh Apr 4, 2025
33e3405
replace inner component with existing one to render extension data in…
rogaldh Apr 7, 2025
a2e87a5
feat: complete skeleton for extension data
rogaldh Apr 7, 2025
cdc544a
pass symbol to extensions
rogaldh Apr 7, 2025
61f20ae
remove extensions from main section
rogaldh Apr 7, 2025
327fbdb
fix accordion rotation
rogaldh Apr 7, 2025
8c1d5d1
fix typo
rogaldh Apr 7, 2025
70095dd
update version of shadcn to latest version
rogaldh Apr 8, 2025
f49ec04
feat: add active state for raw link and make it similar to other raw …
rogaldh Apr 8, 2025
5ade1b2
feat: add descriptions for extensions
rogaldh Apr 8, 2025
0310ac6
add deps to hooks to satisfy dependencies
rogaldh Apr 8, 2025
634ab6d
fix issue with overlapping columns for small displays
rogaldh Apr 8, 2025
ca33555
Merge branch 'master' into feat/t22-extensions-rework
rogaldh Apr 8, 2025
2da6609
fix ui issues
rogaldh Apr 8, 2025
a8aa778
fix issue with 404 images & remove obsolete images & optimize existin…
rogaldh Apr 8, 2025
6dd4235
reuse existong types for token extensions
rogaldh Apr 8, 2025
d26c78d
move comment inside css block
rogaldh Apr 8, 2025
1f534f0
remove unreachable code
rogaldh Apr 8, 2025
2e394c5
remove duplicated modifiers
rogaldh Apr 8, 2025
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
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"semi": ["error", "always"],
// Temporary rule to ignore any explicit any type warnings in TypeScript files
"@typescript-eslint/no-explicit-any": "off",
"object-curly-spacing": ["error", "always"]
"object-curly-spacing": ["error", "always"],
// Do not rely on non-null assertions, please. Make it off to see other errors.
"@typescript-eslint/no-non-null-assertion": "off"
},
"overrides": [
// Only uses Testing Library lint rules in test files
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,4 @@ next-env.d.ts
.swc/

# vim
*.sw*
.editorconfig
.editorconfig
7 changes: 7 additions & 0 deletions .prettierrc.cjs
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',
};
35 changes: 34 additions & 1 deletion app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -724,6 +726,23 @@ 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 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>
),
tab: extensionsTab,
});
}

const anchorProgramTab: Tab = {
path: 'anchor-program',
slug: 'anchor-program',
Expand Down Expand Up @@ -874,3 +893,17 @@ function ProgramMultisigLink({
</li>
);
}

function TokenExtensionsLink({ address, tab }: { address: string; tab: Tab }) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component duplicates the existing Tab component. Consider using Tab instead, passing tab.path and tab.title as separate parameters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's wrong. Tab has different logic for isActive inside it. Accepting this will lead to incorrect state for tabs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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>
);
}
22 changes: 22 additions & 0 deletions app/address/[address]/token-extensions/page.tsx
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} />;
}
12 changes: 12 additions & 0 deletions app/address/[address]/token-extensions/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import '../../../styles.css';

/*
* Fill default vars for transformations as without them `rotate-N` does not work
*/
:root {
--tw-translate-x: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
}
86 changes: 53 additions & 33 deletions app/components/account/TokenAccountSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ function FungibleTokenMintAccountCard({
tokenInfo?: FullLegacyTokenInfo;
}) {
const fetchInfo = useFetchAccountInfo();
const { clusterInfo } = useCluster();
const epoch = clusterInfo?.epochInfo.epoch;
const refresh = () => fetchInfo(account.pubkey, 'parsed');

const bridgeContractAddress = getEthAddress(tokenInfo?.extensions?.bridgeContract);
Expand Down Expand Up @@ -288,9 +286,6 @@ function FungibleTokenMintAccountCard({
</td>
</tr>
)}
{mintExtensions?.map(extension =>
TokenExtensionRows(extension, epoch, mintInfo.decimals, tokenInfo?.symbol)
)}
</TableCardBody>
</div>
</>
Expand Down Expand Up @@ -428,6 +423,8 @@ function TokenAccountCard({ account, info }: { account: Account; info: TokenAcco
} else {
setSymbol(tokenInfo?.symbol);
}
// TODO: Not sure at this moment whether deps are correct at this case. Figure it out later.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tokenInfo]);

return (
Expand Down Expand Up @@ -518,7 +515,7 @@ function TokenAccountCard({ account, info }: { account: Account; info: TokenAcco
</>
)}
{accountExtensions?.map(extension =>
TokenExtensionRows(extension, epoch, info.tokenAmount.decimals, symbol)
TokenExtensionRow(extension, epoch, info.tokenAmount.decimals, symbol)
)}
</TableCardBody>
</div>
Expand Down Expand Up @@ -572,9 +569,35 @@ function MultisigAccountCard({ account, info }: { account: Account; info: Multis
);
}

export type TokenExtensionType =
| 'transferFeeAmount'
| 'mintCloseAuthority'
| 'defaultAccountState'
| 'immutableOwner'
| 'memoTransfer'
| 'nonTransferable'
| 'nonTransferableAccount'
| 'cpiGuard'
| 'permanentDelegate'
| 'transferHook'
| 'transferHookAccount'
| 'metadataPointer'
| 'groupPointer'
| 'groupMemberPointer'
| 'confidentialTransferAccount'
| 'confidentialTransferFeeConfig'
| 'confidentialTransferFeeAmount'
| 'confidentialTransferMint'
| 'interestBearingConfig'
| 'transferFeeConfig'
| 'tokenGroup'
| 'tokenGroupMember'
| 'tokenMetadata'
| 'unparseableExtension';

function cmpExtension(a: TokenExtension, b: TokenExtension) {
// be sure that extensions with a header row always come later
const sortedExtensionTypes = [
const sortedExtensionTypes: TokenExtensionType[] = [
'transferFeeAmount',
'mintCloseAuthority',
'defaultAccountState',
Expand Down Expand Up @@ -605,11 +628,24 @@ function cmpExtension(a: TokenExtension, b: TokenExtension) {
return sortedExtensionTypes.indexOf(a.extension) - sortedExtensionTypes.indexOf(b.extension);
}

function TokenExtensionRows(
function HHeader({ name }: { name: string }) {
return (
<tr>
<h4>{name}</h4>
</tr>
);
}

/* Do not move component to keep commit history.
NOTE: Needs to be separated at closest refactoring
Also check whether it is needed to keep it as function and not a proper React component
*/
export function TokenExtensionRow(
tokenExtension: TokenExtension,
maybeEpoch: bigint | undefined,
decimals: number,
symbol: string | undefined
symbol: string | undefined,
headerStyle: 'header' | 'omit' = 'header'
) {
const epoch = maybeEpoch || 0n; // fallback to 0 if not provided
switch (tokenExtension.extension) {
Expand Down Expand Up @@ -645,9 +681,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, TransferFeeConfig);
return (
<>
<tr>
<h4>Transfer Fee Config</h4>
</tr>
{headerStyle == 'header' ? <HHeader name="Transfer Fee Config" /> : null}
{extension.transferFeeConfigAuthority && (
<tr>
<td>Transfer Fee Authority</td>
Expand Down Expand Up @@ -723,9 +757,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, ConfidentialTransferMint);
return (
<>
<tr>
<h4>Confidential Transfer</h4>
</tr>
{headerStyle === 'header' ? <HHeader name="Confidential Transfer" /> : null}
{extension.authority && (
<tr>
<td>Authority</td>
Expand All @@ -751,9 +783,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, ConfidentialTransferFeeConfig);
return (
<>
<tr>
<h4>Confidential Transfer Fee</h4>
</tr>
{headerStyle === 'header' ? <HHeader name="Confidential Transfer Fee" /> : null}
{extension.authority && (
<tr>
<td>Authority</td>
Expand Down Expand Up @@ -800,9 +830,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, InterestBearingConfig);
return (
<>
<tr>
<h4>Interest-Bearing</h4>
</tr>
{headerStyle === 'header' ? <HHeader name="Interest-Bearing" /> : null}
{extension.rateAuthority && (
<tr>
<td>Authority</td>
Expand Down Expand Up @@ -941,9 +969,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, TokenMetadata);
return (
<>
<tr>
<h4>Metadata</h4>
</tr>
{headerStyle === 'header' ? <HHeader name="Metadata" /> : null}
<tr>
<td>Mint</td>
<td className="text-lg-end">
Expand Down Expand Up @@ -1008,9 +1034,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, ConfidentialTransferAccount);
return (
<>
<tr>
<h4>Confidential Transfer</h4>
</tr>
{headerStyle === 'header' ? <HHeader name="Confidential Transfer" /> : null}
<tr>
<td>Status</td>
<td className="text-lg-end">{!extension.approved && 'not '}approved</td>
Expand Down Expand Up @@ -1111,9 +1135,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, TokenGroup);
return (
<>
<tr>
<h4>Group</h4>
</tr>
{headerStyle === 'header' ? <HHeader name="Group" /> : null}
<tr>
<td>Mint</td>
<td className="text-lg-end">
Expand Down Expand Up @@ -1143,9 +1165,7 @@ function TokenExtensionRows(
const extension = create(tokenExtension.state, TokenGroupMember);
return (
<>
<tr>
<h4>Group Member</h4>
</tr>
{headerStyle === 'header' ? <HHeader name="Group Member" /> : null}
<tr>
<td>Mint</td>
<td className="text-lg-end">
Expand Down
Loading