DEV Community

Wen cat
Wen cat

Posted on

How to read a Token-2022 transfer hook before you move an NFT

A creator sells a drawing as an NFT. Months later it changes hands on a marketplace the creator has never heard of. Does the creator still get a share?

For years, the honest answer on most chains was "only if the marketplace chooses to pay". Solana's Token-2022 program changes where that rule lives. A mint can carry a Transfer Hook: the address of a program that Token-2022 calls on every transfer. If that program says no, the transfer does not happen, whichever wallet or marketplace started it.

The Wen New Standard (WNS), the open NFT standard that grew out of the Wen community on Solana, uses exactly this extension for creator royalties. This walkthrough shows the part every buyer, wallet and marketplace should do first: read the rule before moving the token.

⚠️ Educational example, not audited. Devnet only, with a new, empty wallet. Never paste a real seed phrase into a script.

1. What a transfer hook is, in one table

Piece Where it lives What it does
TransferHook extension inside the mint stores the hook program id and the authority that may change it
Hook program its own on-chain program runs on every transfer; can reject it
Extra account list a PDA of the hook program tells clients which extra accounts the hook needs
TransferChecked Token-2022 performs the transfer and calls the hook

A plain Transfer is not enough for a mint with a hook. Clients must use TransferChecked and add the extra accounts the hook asks for.

2. Step one: is this even a Token-2022 mint?

Never assume. The owner of the mint account tells you which token program it belongs to:

export async function detectTokenProgram(connection: Connection, mint: PublicKey): Promise<PublicKey> {
  const info = await connection.getAccountInfo(mint);
  if (!info) throw new Error(`Mint ${mint.toBase58()} not found on this cluster`);
  if (info.owner.equals(TOKEN_2022_PROGRAM_ID)) return TOKEN_2022_PROGRAM_ID;
  if (info.owner.equals(TOKEN_PROGRAM_ID)) return TOKEN_PROGRAM_ID;
  throw new Error(`Account ${mint.toBase58()} is not a token mint`);
}
Enter fullscreen mode Exit fullscreen mode

Classic SPL tokens have no extensions at all, so a classic mint simply has no hook.

3. Step two: read the hook

export async function inspectRoyaltyHook(mintAddress: string) {
  const connection = getConnection();
  const mint = new PublicKey(mintAddress);
  const program = await detectTokenProgram(connection, mint);
  if (!program.equals(TOKEN_2022_PROGRAM_ID)) {
    return { mint: mintAddress, hook: null, note: 'Classic SPL token: no transfer hooks' };
  }
  const info = await getMint(connection, mint, 'confirmed', TOKEN_2022_PROGRAM_ID);
  const hook = getTransferHook(info);
  const hookProgram = hook?.programId ?? null;
  return {
    mint: mintAddress,
    hook: hookProgram?.toBase58() ?? null,
    isWnsRoyalty: hookProgram ? hookProgram.equals(WNS_ROYALTY_PROGRAM_ID) : false,
  };
}
Enter fullscreen mode Exit fullscreen mode

WNS_ROYALTY_PROGRAM_ID is diste3nXmK7ddDTs1zb6uday6j4etCa9RChD8fJ1xay, taken from the official Wen Program Library README. Always copy program ids from the source, never from a screenshot or a chat.

Run it against any mint:

npx tsx src/02-royalty-transfer.ts <MINT_ADDRESS>
Enter fullscreen mode Exit fullscreen mode

4. Look at the authority too

getTransferHook returns two fields: programId and authority. The program id is today's rule. The authority is who can change the rule tomorrow.

  • authority set → someone can point the hook to another program later;
  • authority null → the hook is fixed for the life of the mint.

Neither is "good" or "bad" in itself; a collection may need upgrades. But a buyer deserves to know which one it is, and a wallet can show it in one line.

5. Step three: transfer so that the hook runs

export async function transferWithRoyalties(mintAddress: string, to: string) {
  const connection = getConnection();
  const owner = loadKeypair();
  const mint = new PublicKey(mintAddress);
  const recipient = new PublicKey(to);
  const source = getAssociatedTokenAddressSync(mint, owner.publicKey, false, TOKEN_2022_PROGRAM_ID);
  const destination = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_2022_PROGRAM_ID);

  // resolves the hook's extra accounts on-chain
  const transferIx = await createTransferCheckedWithTransferHookInstruction(
    connection, source, mint, destination, owner.publicKey, 1n, 0, [], 'confirmed', TOKEN_2022_PROGRAM_ID,
  );
  const tx = new Transaction().add(
    createAssociatedTokenAccountIdempotentInstruction(owner.publicKey, destination, recipient, mint, TOKEN_2022_PROGRAM_ID),
    transferIx,
  );
  // if the hook rejects the transfer, the whole transaction rolls back
  return sendAndConfirmTransaction(connection, tx, [owner]);
}
Enter fullscreen mode Exit fullscreen mode

The helper createTransferCheckedWithTransferHookInstruction reads the hook's extra account list from the chain and appends those accounts for you. If you build TransferChecked by hand and forget them, the transfer fails: that is the hook doing its job, not a bug.

6. Proof, locally

The example repository checks the reading step inside LiteSVM, a local Solana VM with the real Token-2022 program: no network, no funds. It creates an NFT mint with a TransferHook pointing to the WNS royalty program, then reads it back:

PASS  M2 NFT mint created with a transfer hook
PASS  M2 hook program read from the mint = WNS royalty distribution — diste3nXmK7ddDTs1zb6uday6j4etCa9RChD8fJ1xay
Enter fullscreen mode Exit fullscreen mode

What the test does not do: it does not run the WNS royalty program itself (it is not loaded into the local VM). It proves that the rule is stored in the mint and can be read by anyone, which is the step a wallet or marketplace needs before every transfer.

Why this matters beyond NFTs

  • Creators keep a rule that travels with their work, instead of a promise that depends on each marketplace.
  • Buyers can see the rule before they pay, in one RPC call.
  • Wallets and agents can check it automatically: a transfer hook is machine-readable, a screenshot is not.

The same pattern fits any token whose transfers must follow a rule: tickets that only move between verified holders, licences, or royalties on music and photos.

Limits worth stating plainly

  • A hook enforces whatever its program says, including mistakes. Read the program's source and audits, not just its name.
  • Hooks add accounts and compute to every transfer; some older tools still use plain Transfer and will fail on hooked mints.
  • Not audited. Treat this as a model to learn from and test.

The code (Apache-2.0), the LiteSVM tests and the other Token-2022 models (a soulbound warranty card, B2B invoices, a payment link bridge) are open in one place:
https://github.com/wen-watt/wen-watt.github.io/tree/main/lab/code

Top comments (0)