Week 4 • Spl Token And Token 2022 Client Flows
SPL Token & Token-2022 Client Flows
Build token client flows safely with explicit token program selection and validation checks.
This lesson is dev-only and educational. Do not use production funds while learning.
Step 1: 0-to-1 Theory
Primitives
mint: token definition accounttoken account: balance holder for owner+minttoken program id: classic SPL vs Token-2022
Mental Model
If SOL is “one native balance per address”, tokens are “many balances per address”.
Those balances live in token accounts. Your client must always be clear about:
- which mint you are using
- which token program owns the accounts
- how to convert UI amounts into raw integer amounts using decimals
Invariants
- Token program choice must be explicit.
- Mint/account pairing must be validated before transfer logic.
- Amount handling must respect decimals.
Quick Checks
- Why is "assume token program" dangerous?
- Why validate mint/account before send?
Step 2: Real-World Implementation (Code Solution)
Install token clients:
pnpm add @solana-program/token @solana-program/token-2022Create lib/solana/tokenFlow.ts:
// Keep token program choice explicit in your app.
// Classic SPL Token and Token-2022 are different programs.
export type TokenProgramKind = "spl" | "token2022";
export type TokenTransferInput = {
// Which token program are we using for this mint + token accounts?
programKind: TokenProgramKind;
// Base58 strings for simplicity in this beginner module.
mint: string;
sourceTokenAccount: string;
destinationTokenAccount: string;
// Raw integer amount (already scaled by decimals).
amountRaw: bigint;
// Number of decimals used by the mint (for UI conversion).
decimals: number;
};
export function validateTokenTransferInput(input: TokenTransferInput) {
// Fail early with clear messages before you ask the wallet to sign.
if (!input.mint) throw new Error("Missing mint");
if (!input.sourceTokenAccount) throw new Error("Missing source token account");
if (!input.destinationTokenAccount) throw new Error("Missing destination token account");
if (input.amountRaw <= 0n) throw new Error("Amount must be positive");
if (input.decimals < 0 || input.decimals > 18) throw new Error("Invalid decimals");
}
export async function buildTokenTransferPlan(input: TokenTransferInput) {
validateTokenTransferInput(input);
// In a full implementation, create token program instructions here.
// Keep programKind explicit to route SPL vs Token-2022 instruction builders.
return {
ok: true,
programKind: input.programKind,
summary: {
mint: input.mint,
from: input.sourceTokenAccount,
to: input.destinationTokenAccount,
amountRaw: input.amountRaw.toString(),
decimals: input.decimals,
},
};
}Create components/solana/TokenTransferPreview.tsx:
"use client";
import React, { useState } from "react";
import { buildTokenTransferPlan } from "@/lib/solana/tokenFlow";
export function TokenTransferPreview() {
// Show the plan output as JSON so beginners can see what will be signed/sent.
const [result, setResult] = useState<any>(null);
const onPreview = async () => {
const plan = await buildTokenTransferPlan({
programKind: "spl",
// These are placeholder strings. Replace with real addresses when you wire up a real mint.
mint: "Mint111111111111111111111111111111111111111",
sourceTokenAccount: "Src1111111111111111111111111111111111111111",
destinationTokenAccount: "Dst1111111111111111111111111111111111111111",
// Raw amount: for a 6-decimal mint, 1 token = 1_000_000 raw units.
amountRaw: 1_000_000n,
decimals: 6,
});
setResult(plan);
};
return (
<div>
<button onClick={onPreview} className="px-3 py-2 rounded bg-indigo-600 text-white">Preview Transfer</button>
{result && <pre className="mt-3 text-xs bg-zinc-950 p-3 rounded">{JSON.stringify(result, null, 2)}</pre>}
</div>
);
}Expected result:
- explicit token flow validation and preview output before transaction assembly.
Step 3: Mastery Test
- Easy: where do you enforce program kind selection?
- Medium: what field must be checked before decimals conversion?
- Hard: how do you prevent cross-program account mixups at client layer?