diff --git a/.changeset/token-2022-support.md b/.changeset/token-2022-support.md
new file mode 100644
index 0000000..ee5929f
--- /dev/null
+++ b/.changeset/token-2022-support.md
@@ -0,0 +1,11 @@
+---
+"@solana/client": minor
+"@solana/react-hooks": minor
+---
+
+Add Token 2022 (Token Extensions) program support to SPL token helper.
+
+- New `tokenProgram: 'auto'` option to auto-detect mint program ownership
+- Explicit Token 2022 program address support via `tokenProgram` config
+- Export `TOKEN_2022_PROGRAM_ADDRESS` and `detectTokenProgram` utility
+- Backwards compatible - existing code continues to work unchanged
diff --git a/apps/docs/content/docs/client.mdx b/apps/docs/content/docs/client.mdx
index e31f125..a19d810 100644
--- a/apps/docs/content/docs/client.mdx
+++ b/apps/docs/content/docs/client.mdx
@@ -205,6 +205,48 @@ const signature = await usdc.sendTransfer({
});
```
+### Token 2022 Support
+
+The SPL token helper supports Token 2022 (Token Extensions) mints. Use the `tokenProgram` option to specify the program:
+
+```ts
+// Auto-detect program (recommended for existing mints)
+const token2022 = client.splToken({
+ mint: "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", // PYUSD
+ tokenProgram: "auto",
+});
+
+// Fetch balance works the same way
+const balance = await token2022.fetchBalance(wallet.session.account.address);
+
+// Transfer works the same way
+const signature = await token2022.sendTransfer({
+ amount: 10,
+ authority: wallet.session,
+ destinationOwner: recipientAddress,
+});
+```
+
+You can also explicitly specify the Token 2022 program address:
+
+```ts
+import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana/client";
+
+const token = client.splToken({
+ mint: mintAddress,
+ tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
+});
+```
+
+The `detectTokenProgram` utility is also available for manual program detection:
+
+```ts
+import { detectTokenProgram } from "@solana/client";
+
+const result = await detectTokenProgram(client.runtime, mintAddress);
+console.log(result.programId); // 'token' or 'token-2022'
+```
+
## Custom Transactions
Build and send arbitrary transactions:
diff --git a/apps/docs/content/docs/react-hooks.mdx b/apps/docs/content/docs/react-hooks.mdx
index af3dd7b..b871d51 100644
--- a/apps/docs/content/docs/react-hooks.mdx
+++ b/apps/docs/content/docs/react-hooks.mdx
@@ -305,6 +305,28 @@ function TokenPanel({ mint, destinationOwner }: { mint: string; destinationOwner
| `owner` | Override balance owner (defaults to connected wallet) |
| `revalidateOnFocus` | Refresh when window regains focus |
| `swr` | Additional SWR options |
+| `config.tokenProgram` | Token program: `'auto'` for detection, or explicit address |
+
+**Token 2022 Support:**
+
+The `useSplToken` hook supports Token 2022 mints via the `tokenProgram` config option:
+
+```tsx
+function Token2022Panel({ mint }: { mint: string }) {
+ const { balance, send } = useSplToken(mint, {
+ config: { tokenProgram: "auto" }, // Auto-detect Token or Token 2022
+ });
+
+ return (
+
+
Balance: {balance?.uiAmount ?? "0"}
+
+
+ );
+}
+```
### useWrapSol
diff --git a/packages/client/README.md b/packages/client/README.md
index 1558645..7ea7200 100644
--- a/packages/client/README.md
+++ b/packages/client/README.md
@@ -112,6 +112,34 @@ const signature = await usdc.sendTransfer({
console.log(signature.toString());
```
+### Token 2022 support
+
+Token 2022 mints are supported via the `tokenProgram` option:
+
+```ts
+// Auto-detect Token or Token 2022 (recommended)
+const token = client.splToken({
+ mint: "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", // PYUSD
+ tokenProgram: "auto",
+});
+
+// Or explicitly specify Token 2022 program
+import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana/client";
+
+const token2022 = client.splToken({
+ mint: mintAddress,
+ tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
+});
+
+// Balance and transfers work the same way
+const balance = await token.fetchBalance(wallet.session.account.address);
+const signature = await token.sendTransfer({
+ amount: 10,
+ authority: wallet.session,
+ destinationOwner: recipientAddress,
+});
+```
+
### Fetch address lookup tables
```ts
diff --git a/packages/client/package.json b/packages/client/package.json
index 25bd9ca..a3c7a9d 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -71,6 +71,7 @@
"@solana-program/system": "catalog:solana",
"@solana-program/compute-budget": "catalog:solana",
"@solana-program/token": "catalog:solana",
+ "@solana-program/token-2022": "catalog:solana",
"@solana-program/address-lookup-table": "catalog:solana",
"@wallet-standard/app": "catalog:solana",
"@wallet-standard/base": "catalog:solana",
diff --git a/packages/client/src/features/spl.ts b/packages/client/src/features/spl.ts
index 5669eae..d7bfc2f 100644
--- a/packages/client/src/features/spl.ts
+++ b/packages/client/src/features/spl.ts
@@ -23,18 +23,23 @@ import {
type TransactionVersion,
} from '@solana/kit';
import {
- fetchMint,
+ fetchMint as fetchMintStandard,
findAssociatedTokenPda,
getCreateAssociatedTokenInstruction,
- getTransferCheckedInstruction,
+ getTransferCheckedInstruction as getTransferCheckedStandard,
TOKEN_PROGRAM_ADDRESS,
} from '@solana-program/token';
-
+import {
+ fetchMint as fetchMintToken2022,
+ getTransferCheckedInstruction as getTransferCheckedToken2022,
+ TOKEN_2022_PROGRAM_ADDRESS,
+} from '@solana-program/token-2022';
import { createTokenAmount, type TokenAmountMath } from '../numeric/amounts';
import type { SolanaClientRuntime } from '../rpc/types';
import { createWalletTransactionSigner, isWalletSession, resolveSignerMode } from '../signers/walletTransactionSigner';
import type { WalletSession } from '../wallet/types';
import type { SolTransferSendOptions } from './sol';
+import { detectTokenProgram } from './tokenPrograms';
/**
* Blockhash and last valid block height for transaction lifetime.
@@ -75,8 +80,12 @@ export type SplTokenHelperConfig = Readonly<{
decimals?: number;
/** The SPL token mint address. */
mint: Address | string;
- /** Token Program address. Defaults to standard Token Program. */
- tokenProgram?: Address | string;
+ /**
+ * Token Program address. Defaults to standard Token Program.
+ * Set to 'auto' to automatically detect based on mint account owner.
+ * Note: Auto-detection requires the mint to already exist on-chain.
+ */
+ tokenProgram?: Address | string | 'auto';
}>;
/**
@@ -233,9 +242,10 @@ function resolveSigner(
export type SplTokenHelper = Readonly<{
/**
* Derives the Associated Token Account (ATA) address for an owner.
- * The ATA is a deterministic address based on the owner and mint.
+ * The ATA is a deterministic address based on the owner, mint, and token program.
+ * When using tokenProgram: 'auto', the commitment is used for program detection.
*/
- deriveAssociatedTokenAddress(owner: Address | string): Promise;
+ deriveAssociatedTokenAddress(owner: Address | string, commitment?: Commitment): Promise;
/**
* Fetches the token balance for an owner's Associated Token Account.
* Returns balance info including whether the account exists.
@@ -300,15 +310,43 @@ export type SplTokenHelper = Readonly<{
*/
export function createSplTokenHelper(runtime: SolanaClientRuntime, config: SplTokenHelperConfig): SplTokenHelper {
const mintAddress = ensureAddress(config.mint);
- const tokenProgram = ensureAddress(config.tokenProgram, address(TOKEN_PROGRAM_ADDRESS));
let cachedDecimals: number | undefined = config.decimals;
let cachedMath: TokenAmountMath | undefined;
+ let resolvedTokenProgram: Address | undefined =
+ config.tokenProgram && config.tokenProgram !== 'auto' ? ensureAddress(config.tokenProgram) : undefined;
+ let isToken2022: boolean | undefined =
+ config.tokenProgram === 'auto'
+ ? undefined
+ : config.tokenProgram
+ ? config.tokenProgram === TOKEN_2022_PROGRAM_ADDRESS ||
+ (typeof config.tokenProgram === 'string' && config.tokenProgram === TOKEN_2022_PROGRAM_ADDRESS)
+ : false;
+
+ async function resolveTokenProgram(commitment?: Commitment): Promise {
+ if (resolvedTokenProgram) {
+ return resolvedTokenProgram;
+ }
+
+ if (config.tokenProgram === 'auto') {
+ const result = await detectTokenProgram(runtime, mintAddress, commitment);
+ resolvedTokenProgram = result.programAddress;
+ isToken2022 = result.programId === 'token-2022';
+ } else {
+ resolvedTokenProgram = address(TOKEN_PROGRAM_ADDRESS);
+ isToken2022 = false;
+ }
+
+ return resolvedTokenProgram;
+ }
async function resolveDecimals(commitment?: Commitment): Promise {
if (cachedDecimals !== undefined) {
return cachedDecimals;
}
+
+ const tokenProgram = await resolveTokenProgram(commitment);
+ const fetchMint = tokenProgram === TOKEN_2022_PROGRAM_ADDRESS ? fetchMintToken2022 : fetchMintStandard;
const account = await fetchMint(runtime.rpc, mintAddress, { commitment });
cachedDecimals = account.data.decimals;
return cachedDecimals;
@@ -323,7 +361,8 @@ export function createSplTokenHelper(runtime: SolanaClientRuntime, config: SplTo
return cachedMath;
}
- async function deriveAssociatedTokenAddress(owner: Address | string): Promise {
+ async function deriveAssociatedTokenAddress(owner: Address | string, commitment?: Commitment): Promise {
+ const tokenProgram = await resolveTokenProgram(commitment);
const [ata] = await findAssociatedTokenPda({
mint: mintAddress,
owner: ensureAddress(owner),
@@ -333,7 +372,7 @@ export function createSplTokenHelper(runtime: SolanaClientRuntime, config: SplTo
}
async function fetchBalance(owner: Address | string, commitment?: Commitment): Promise {
- const ataAddress = await deriveAssociatedTokenAddress(owner);
+ const ataAddress = await deriveAssociatedTokenAddress(owner, commitment);
const decimals = await resolveDecimals(commitment);
try {
const { value } = await runtime.rpc.getTokenAccountBalance(ataAddress, { commitment }).send();
@@ -360,15 +399,19 @@ export function createSplTokenHelper(runtime: SolanaClientRuntime, config: SplTo
async function prepareTransfer(config: SplTransferPrepareConfig): Promise {
const commitment = config.commitment;
+ const tokenProgram = await resolveTokenProgram(commitment);
const lifetime = await resolveLifetime(runtime, commitment, config.lifetime);
const { signer, mode } = resolveSigner(config.authority, commitment);
const sourceOwner = ensureAddress(config.sourceOwner, signer.address);
const destinationOwner = ensureAddress(config.destinationOwner);
- const sourceAta = ensureAddress(config.sourceToken, await deriveAssociatedTokenAddress(sourceOwner));
+ const sourceAta = ensureAddress(
+ config.sourceToken,
+ await deriveAssociatedTokenAddress(sourceOwner, commitment),
+ );
const destinationAta = ensureAddress(
config.destinationToken,
- await deriveAssociatedTokenAddress(destinationOwner),
+ await deriveAssociatedTokenAddress(destinationOwner, commitment),
);
const math = await getTokenMath(commitment);
@@ -399,6 +442,9 @@ export function createSplTokenHelper(runtime: SolanaClientRuntime, config: SplTo
}
}
+ // Use the correct transfer instruction based on token program
+ const getTransferCheckedInstruction = isToken2022 ? getTransferCheckedToken2022 : getTransferCheckedStandard;
+
instructionList.push(
getTransferCheckedInstruction({
amount,
diff --git a/packages/client/src/features/tokenPrograms.ts b/packages/client/src/features/tokenPrograms.ts
new file mode 100644
index 0000000..d8e3558
--- /dev/null
+++ b/packages/client/src/features/tokenPrograms.ts
@@ -0,0 +1,93 @@
+import { type Address, address, type Commitment } from '@solana/kit';
+import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
+import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022';
+
+import type { SolanaClientRuntime } from '../rpc/types';
+
+/**
+ * Identifier for supported token programs.
+ */
+export type TokenProgramId = 'token' | 'token-2022';
+
+/**
+ * Result of detecting which token program owns a mint.
+ */
+export type TokenProgramDetectionResult = Readonly<{
+ /** The program identifier ('token' or 'token-2022'). */
+ programId: TokenProgramId;
+ /** The program address. */
+ programAddress: Address;
+}>;
+
+/**
+ * Map of token program IDs to their addresses.
+ */
+export const TOKEN_PROGRAMS = {
+ token: TOKEN_PROGRAM_ADDRESS,
+ 'token-2022': TOKEN_2022_PROGRAM_ADDRESS,
+} as const;
+
+// Re-export program addresses for convenience
+export { TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS };
+
+/**
+ * Detects which token program owns a mint account by examining its owner.
+ *
+ * @param runtime - The Solana client runtime with RPC connection.
+ * @param mint - The mint account address to check.
+ * @param commitment - Optional commitment level for the RPC call.
+ * @returns The detected token program information.
+ * @throws If the mint account doesn't exist or is owned by an unknown program.
+ *
+ * @example
+ * ```ts
+ * import { detectTokenProgram } from '@solana/client';
+ *
+ * const result = await detectTokenProgram(runtime, mintAddress);
+ * if (result.programId === 'token-2022') {
+ * console.log('This is a Token 2022 mint');
+ * }
+ * ```
+ */
+export async function detectTokenProgram(
+ runtime: SolanaClientRuntime,
+ mint: Address,
+ commitment?: Commitment,
+): Promise {
+ const { value: accountInfo } = await runtime.rpc
+ .getAccountInfo(mint, {
+ commitment,
+ encoding: 'base64',
+ })
+ .send();
+
+ if (!accountInfo) {
+ throw new Error(
+ `Mint account ${mint} does not exist. ` +
+ `Provide an explicit tokenProgram when working with non-existent mints.`,
+ );
+ }
+
+ const owner = accountInfo.owner;
+
+ if (owner === TOKEN_PROGRAM_ADDRESS) {
+ return { programId: 'token', programAddress: address(TOKEN_PROGRAM_ADDRESS) };
+ }
+
+ if (owner === TOKEN_2022_PROGRAM_ADDRESS) {
+ return { programId: 'token-2022', programAddress: address(TOKEN_2022_PROGRAM_ADDRESS) };
+ }
+
+ throw new Error(`Mint ${mint} is owned by unknown program ${owner}. Expected Token Program or Token 2022 Program.`);
+}
+
+/**
+ * Checks if an address is a known token program (Token or Token 2022).
+ *
+ * @param programAddress - The program address to check.
+ * @returns True if the address is a known token program.
+ */
+export function isKnownTokenProgram(programAddress: Address | string): boolean {
+ const addrStr = typeof programAddress === 'string' ? programAddress : programAddress;
+ return addrStr === TOKEN_PROGRAM_ADDRESS || addrStr === TOKEN_2022_PROGRAM_ADDRESS;
+}
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index c85a100..9e8739f 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -69,6 +69,15 @@ export {
type WithdrawPrepareConfig,
type WithdrawSendOptions,
} from './features/stake';
+export {
+ detectTokenProgram,
+ isKnownTokenProgram,
+ TOKEN_2022_PROGRAM_ADDRESS,
+ TOKEN_PROGRAM_ADDRESS,
+ TOKEN_PROGRAMS,
+ type TokenProgramDetectionResult,
+ type TokenProgramId,
+} from './features/tokenPrograms';
export {
createTransactionHelper,
createTransactionRecipe,
diff --git a/packages/react-hooks/README.md b/packages/react-hooks/README.md
index ed53e6d..2acb770 100644
--- a/packages/react-hooks/README.md
+++ b/packages/react-hooks/README.md
@@ -196,6 +196,21 @@ function TokenPanel({
- `owner` — override balance owner (defaults to connected wallet)
- `revalidateOnFocus` — refresh when window regains focus
- `swr` — additional SWR options
+- `config.tokenProgram` — token program: `'auto'` for detection, or explicit address
+
+### Token 2022 support
+
+Token 2022 mints are supported via the `tokenProgram` config option:
+
+```tsx
+// Auto-detect Token or Token 2022
+const { balance, send } = useSplToken(mint, {
+ config: { tokenProgram: "auto" },
+});
+
+// Balance and transfers work the same way
+Balance: {balance?.uiAmount ?? "0"}
+```
### Fetch address lookup tables
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9b6317d..5f2909a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -25,6 +25,9 @@ catalogs:
'@solana-program/token':
specifier: ^0.9.0
version: 0.9.0
+ '@solana-program/token-2022':
+ specifier: ^0.7.0
+ version: 0.7.0
'@solana/addresses':
specifier: ^5.0.0
version: 5.0.0
@@ -344,6 +347,9 @@ importers:
'@solana-program/token':
specifier: catalog:solana
version: 0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))
+ '@solana-program/token-2022':
+ specifier: catalog:solana
+ version: 0.7.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))
'@solana/codecs-strings':
specifier: catalog:solana
version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
@@ -2014,6 +2020,13 @@ packages:
peerDependencies:
'@solana/kit': ^5.0
+ '@solana-program/token-2022@0.7.0':
+ resolution: {integrity: sha512-ByQdTdbgyhjGf9JklqGRf3u0nbQF5Hbn8Wb2Ir0LZHCgo8lG+2PmBN8UvNY6ONVYb7CjLbApgghvBAEQK1aagg==}
+ deprecated: This package has been deprecated
+ peerDependencies:
+ '@solana/kit': ^5.0
+ '@solana/sysvars': ^4.0
+
'@solana-program/token@0.9.0':
resolution: {integrity: sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==}
peerDependencies:
@@ -6236,6 +6249,11 @@ snapshots:
dependencies:
'@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana-program/token-2022@0.7.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))':
+ dependencies:
+ '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
+ '@solana/sysvars': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+
'@solana-program/token@0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))':
dependencies:
'@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 03701ec..1eb51aa 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -22,6 +22,7 @@ catalogs:
"@solana-program/system": "^0.10.0"
"@solana-program/compute-budget": "^0.11.0"
"@solana-program/token": "^0.9.0"
+ "@solana-program/token-2022": "^0.7.0"
"@solana-program/address-lookup-table": "^0.10.0"
"@wallet-standard/app": "^1.0.1"
"@wallet-standard/base": "^1.1.0"