Mint Token-2022 tokens from inside your program
Mint Token-2022 tokens from your own Anchor program instead of doing it from the command line.
Mint Token-2022 tokens from inside your program
New tokens do not come from nowhere. Somewhere a program holding mint authority calls into the token program, asks it to create some units, and the supply ticks up. So far you have only made that request from the command line with the SPL Token CLI. Today your own Anchor program makes it instead.
The scenario
The instruction you wrote in the previous challenge reached across to the System Program and asked it to move SOL. Today the program you call changes from the System Program to Token-2022, and the job changes from moving lamports to minting tokens. The mechanics, though, are the ones you already know: build a set of accounts, wrap them in a CpiContext, and hand it to a helper that does the cross-program invocation for you.
Here is the useful way to picture it. A cross-program invocation is one service calling another service’s API. Your program is the caller. Token-2022 is the service that owns the “create new tokens” endpoint, and that endpoint has an access rule: only the mint authority is allowed to call it. So your instruction has to carry the mint authority’s signature through to Token-2022. Today the mint authority is a plain wallet that signs the transaction, which keeps the call simple. In the next challenge you will make a PDA the authority and sign on its behalf, but that is tomorrow’s problem.
The challenge
What you’ll need
- The Anchor toolchain (this guide targets Anchor
1.0.x, the same version Day 71 set up) plus Rust and the Solana CLI -
Surfpool, the local validator
anchor testuses (installed in Day 71) - Node and a package manager (npm or yarn) for the test
- A terminal and your editor
- A fresh Anchor project, or the program you have been building this arc if you would rather add one more instruction to it
Steps
anchor init token_cpi
cd token_cpi
rm -rf programs/token_cpi/tests # remove the scaffolded Rust test that references the deleted initialize instruction
Open programs/token_cpi/Cargo.toml and replace the existing [dependencies] block with these two lines so the program can use anchor-spl’s token interface:
[dependencies]
anchor-lang = "1.0"
anchor-spl = { version = "1.0", features = ["idl-build"] }
If you are on Anchor 0.31.x, use "0.31.1" for both crates and add anchor-spl/idl-build to the idl-build feature list in [features].
Now replace programs/token_cpi/src/lib.rs with this handler. Keep the declare_id! line Anchor generated for you. Only the body changes.
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface};
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); // keep YOUR generated id
#[program]
pub mod token_cpi {
use super::*;
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
// The accounts Token-2022 needs in order to mint.
let cpi_accounts = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
};
// The program we are calling into, and the context that ties it together.
// In Anchor 1.0 the first arg to CpiContext::new is a Pubkey, so use .key().
let cpi_program = ctx.accounts.token_program.key();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
// The cross-program invocation. amount is in base units.
token_interface::mint_to(cpi_ctx, amount)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub token_program: Interface<'info, TokenInterface>,
}
A few things worth naming. MintTo is the accounts struct that the token program’s mint instruction expects: the mint to grow, the token account to receive the new units, and the authority allowed to do it. InterfaceAccount and Interface are the Token-2022 aware versions of Anchor’s account types, so the very same program would also work against the original Token Program without a rewrite. And notice that nothing here checks a signature by hand. Because signer is the mint authority and it signs the outer transaction, that signature flows through into the CPI automatically.
Build the program once so Anchor generates the IDL and TypeScript types your test will import:
anchor build
Anchor 1.0 does not auto-create a TypeScript test directory, so make one. The Anchor 1.0 JS client (@anchor-lang/core) already ships with the scaffold, so you only need to add @solana/spl-token for the mint helpers:
mkdir tests
yarn add --dev @solana/spl-token
Create tests/token_cpi.ts with these imports at the top:
import * as anchor from "@anchor-lang/core";
import { Program } from "@anchor-lang/core";
import { TokenCpi } from "../target/types/token_cpi";
import { strict as assert } from "assert";
import {
TOKEN_2022_PROGRAM_ID,
createMint,
getOrCreateAssociatedTokenAccount,
getAccount,
} from "@solana/spl-token";
Then add the body of the test: a describe block, the provider setup, the program variable, and the it(...) block that actually exercises the CPI.
describe("token_cpi", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.TokenCpi as Program<TokenCpi>;
it("mints Token-2022 tokens through the program", async () => {
const payer = (provider.wallet as anchor.Wallet).payer;
const connection = provider.connection;
// 1. Create a Token-2022 mint. Your wallet is the mint authority.
const mint = await createMint(
connection,
payer,
payer.publicKey, // mint authority
null, // no freeze authority
9, // decimals
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// 2. Create the destination token account your wallet owns.
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
payer.publicKey,
false,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// 3. Ask YOUR program to mint. It runs the mint_to CPI for you.
const amount = new anchor.BN(1_000_000_000); // 1 whole token at 9 decimals
await program.methods
.mintTokens(amount)
.accountsPartial({
signer: payer.publicKey,
mint,
tokenAccount: ata.address,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.rpc();
// 4. Read the balance straight from the chain.
const account = await getAccount(connection, ata.address, undefined, TOKEN_2022_PROGRAM_ID);
console.log("Minted base units:", account.amount.toString());
assert.equal(account.amount.toString(), amount.toString());
});
});
The first two steps are ordinary setup: a mint exists, and a token account exists to hold the result. Step three is the whole point of the day. Instead of asking Token-2022 to mint directly, you call mintTokens on your program, and your program turns around and makes the CPI. The tokenProgram account you pass in is what your handler reads as ctx.accounts.token_program, which is how the same code can target Token-2022 here and the classic Token Program elsewhere.
Run it
Wire the mocha runner into Anchor.toml (replacing the default cargo test script) so anchor test actually executes your TypeScript file:
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
anchor test then spins up surfpool, which already has Token-2022 and the Associated Token Account program loaded, deploys your program, and runs the test.
anchor test
A passing run prints the minted balance and the green check for your test. Seeing Minted base units: 1000000000 means your program just created one whole token through a cross-program invocation.
What just happened
You wrote an instruction that, mid-execution, paused and called a completely separate program to do work your program is not allowed to do itself. Your program cannot increase a token’s supply directly, in the same way that one microservice cannot reach into another service’s database and edit rows. It has to call the endpoint that owns that operation. token_interface::mint_to is that call, and CpiContext is the envelope that carries the right accounts to the right program.
The detail that makes this work without any extra ceremony is the authority. Token-2022 will only mint if the mint authority approves, and here the authority is your wallet, which already signed the outer transaction. Solana carries that signature down into the invoked program, so Token-2022 sees a valid authorization and mints. This is exactly the same CpiContext::new shape you used to move SOL through the System Program, just pointed at a different service with a different set of accounts. That repetition is the real lesson of this arc: once you know the pattern, every program on Solana becomes a function you can call.
There is one assumption baked into today’s version. The authority had to sign the transaction itself, which is fine for a wallet but useless when you want the program to be in charge of its own mint. In the next challenge you will give the mint authority to a PDA your program controls and sign the CPI with seeds, so the program can mint on its own terms without a human authority signing every time.
Resources
-
Anchor docs: Mint Tokens, the reference for the exact
token_interface::mint_topattern used here - Anchor docs: Cross Program Invocations, on how CpiContext and CPIs work in general
- @solana/spl-token: getOrCreateAssociatedTokenAccount, the client helper that sets up your destination account
-
CpiContext API reference, including the
newandnew_with_signerconstructors you will reach for next
Submission
Submit a screenshot of your passing anchor test output showing the minted balance.