Anchor
How to Create a Token with Rust and Anchor
This guide demonstrates how to create a fungible token with metadata on Solana using Rust, the Anchor framework, and the Metaplex Token Metadata program via CPI.
What You'll Build
A single Anchor instruction that:
- Creates a new SPL token mint
- Creates the associated token account for the payer
- Creates a metadata account with name, symbol, and URI
- Mints an initial token supply to the payer
Summary
Create a fungible SPL token on Solana with Anchor (Rust), mint an initial supply, and attach Metaplex Token Metadata (name, symbol, URI) via CPI.
- One instruction: init mint + ATA + metadata, then mint supply
- Uses: SPL Token + Metaplex Token Metadata CPI
- Tested: Anchor 0.32.1, Solana Agave 3.1.6
- Fungible only; NFTs need Master Edition +
decimals=0+supply=1
Out of Scope
Token-2022 extensions, confidential transfers, authority revocation, metadata updates, full NFT flow, mainnet deployment.
Quick Start
Jump to: Program · Test Client · Common Errors
anchor init anchor-spl-token- Add
anchor-splwithmetadatafeature toCargo.toml - Clone Token Metadata program in
Anchor.tomlfor localnet - Paste the program code and run
anchor test
Prerequisites
- Rust installed (rustup.rs)
- Solana CLI installed (docs.solana.com)
- Anchor CLI installed (
cargo install --git https://github.com/coral-xyz/anchor anchor-cli) - Node.js and Yarn for running tests
- A Solana wallet with SOL for transaction fees
Tested Configuration
This guide was tested with the following versions:
| Tool | Version |
|---|---|
| Anchor CLI | 0.32.1 |
| Solana CLI | 3.1.6 (Agave) |
| Rust | 1.92.0 |
| Node.js | 22.15.1 |
| Yarn | 1.22.x |
Initial Setup
Start by initializing a new Anchor project:
anchor init anchor-spl-token
cd anchor-spl-token
Configure Cargo.toml
Update programs/anchor-spl-token/Cargo.toml:
1[package]
2name = "anchor-spl-token"
3version = "0.1.0"
4description = "Created with Anchor"
5edition = "2021"
6
7[lib]
8crate-type = ["cdylib", "lib"]
9name = "anchor_spl_token"
10
11[lints.rust]
12unexpected_cfgs = { level = "warn", check-cfg = [
13 'cfg(feature, values("custom-heap", "custom-panic", "anchor-debug"))'
14] }
15
16[features]
17default = []
18cpi = ["no-entrypoint"]
19no-entrypoint = []
20no-idl = []
21no-log-ix-name = []
22idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
23
24[dependencies]
25anchor-lang = "0.32.1"
26anchor-spl = { version = "0.32.1", features = ["token", "metadata", "associated_token"] }
Important
The idl-build feature must include anchor-spl/idl-build or you'll get errors like no function or associated item named 'create_type' found for struct 'anchor_spl::token::Mint'.
Configure Anchor.toml
Update Anchor.toml to clone the Token Metadata program for local testing:
1[toolchain]
2package_manager = "yarn"
3
4[features]
5resolution = true
6skip-lint = false
7
8[programs.localnet]
9anchor_spl_token = "YOUR_PROGRAM_ID_HERE"
10
11[registry]
12url = "https://api.apr.dev"
13
14[provider]
15cluster = "localnet"
16wallet = "~/.config/solana/id.json"
17
18[scripts]
19test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
20
21[test.validator]
22url = "https://api.mainnet-beta.solana.com"
23bind_address = "127.0.0.1"
24
25[[test.validator.clone]]
26address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
bind_address = "127.0.0.1"is required for Agave 3.x validators (0.0.0.0 causes a panic)- The
[[test.validator.clone]]section clones the Metaplex Token Metadata program from mainnet
Configure package.json
1{
2 "license": "ISC",
3 "scripts": {
4 "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
5 "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
6 },
7 "dependencies": {
8 "@coral-xyz/anchor": "^0.32.1",
9 "@metaplex-foundation/mpl-token-metadata": "^3.4.0",
10 "@solana/spl-token": "^0.4.9"
11 },
12 "devDependencies": {
13 "chai": "^4.3.4",
14 "mocha": "^9.0.3",
15 "ts-mocha": "^10.0.0",
16 "@types/bn.js": "^5.1.0",
17 "@types/chai": "^4.3.0",
18 "@types/mocha": "^9.0.0",
19 "typescript": "^5.7.3",
20 "prettier": "^2.6.2"
21 }
22}
The Program
Imports and Template
Here we define all the imports and create the template for the Account struct and instruction in programs/anchor-spl-token/src/lib.rs:
1use anchor_lang::prelude::*;
2use anchor_spl::{
3 associated_token::AssociatedToken,
4 metadata::{
5 create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3,
6 Metadata,
7 },
8 token::{mint_to, Mint, MintTo, Token, TokenAccount},
9};
10
11declare_id!("YOUR_PROGRAM_ID_HERE");
12
13#[program]
14pub mod anchor_spl_token {
15 use super::*;
16
17 pub fn create_token(
18 ctx: Context<CreateToken>,
19 name: String,
20 symbol: String,
21 uri: String,
22 decimals: u8,
23 amount: u64,
24 ) -> Result<()> {
25 Ok(())
26 }
27}
28
29#[derive(Accounts)]
30#[instruction(name: String, symbol: String, uri: String, decimals: u8)]
31pub struct CreateToken<'info> {
32
33}
Creating the Account Struct
The CreateToken struct defines all accounts required by the instruction and applies the necessary constraints:
1#[derive(Accounts)]
2#[instruction(name: String, symbol: String, uri: String, decimals: u8)]
3pub struct CreateToken<'info> {
4 #[account(mut)]
5 pub payer: Signer<'info>,
6
7 /// The mint account to be created
8 #[account(
9 init,
10 payer = payer,
11 mint::decimals = decimals,
12 mint::authority = payer.key(),
13 mint::freeze_authority = payer.key(),
14 )]
15 pub mint: Account<'info, Mint>,
16
17 /// The associated token account to receive minted tokens
18 #[account(
19 init,
20 payer = payer,
21 associated_token::mint = mint,
22 associated_token::authority = payer,
23 )]
24 pub token_account: Account<'info, TokenAccount>,
25
26 /// The metadata account to be created
27 /// CHECK: Validated by seeds constraint to be the correct PDA
28 #[account(
29 mut,
30 seeds = [
31 b"metadata",
32 token_metadata_program.key().as_ref(),
33 mint.key().as_ref(),
34 ],
35 bump,
36 seeds::program = token_metadata_program.key(),
37 )]
38 pub metadata_account: UncheckedAccount<'info>,
39
40 pub token_program: Program<'info, Token>,
41 pub token_metadata_program: Program<'info, Metadata>,
42 pub associated_token_program: Program<'info, AssociatedToken>,
43 pub system_program: Program<'info, System>,
44 pub rent: Sysvar<'info, Rent>,
45}
Account Types:
- The
#[instruction(...)]attribute allows using instruction arguments (likedecimals) in account constraints mintuses Anchor'sinitconstraint withmint::decimals = decimalsto create the token mint with the specified decimal placestoken_accountis initialized as an associated token account usingassociated_token::helpersmetadata_accountusesseeds::programto validate the PDA belongs to the Token Metadata program
Creating the Instruction
The create_token function creates the metadata account via CPI and mints the initial token supply:
1pub fn create_token(
2 ctx: Context<CreateToken>,
3 name: String,
4 symbol: String,
5 uri: String,
6 decimals: u8,
7 amount: u64,
8) -> Result<()> {
9 msg!("Creating token mint...");
10 msg!("Mint: {}", ctx.accounts.mint.key());
11 msg!("Creating metadata account...");
12 msg!("Metadata account address: {}", ctx.accounts.metadata_account.key());
13
14 // Cross Program Invocation (CPI) to token metadata program
15 create_metadata_accounts_v3(
16 CpiContext::new(
17 ctx.accounts.token_metadata_program.to_account_info(),
18 CreateMetadataAccountsV3 {
19 metadata: ctx.accounts.metadata_account.to_account_info(),
20 mint: ctx.accounts.mint.to_account_info(),
21 mint_authority: ctx.accounts.payer.to_account_info(),
22 update_authority: ctx.accounts.payer.to_account_info(),
23 payer: ctx.accounts.payer.to_account_info(),
24 system_program: ctx.accounts.system_program.to_account_info(),
25 rent: ctx.accounts.rent.to_account_info(),
26 },
27 ),
28 DataV2 {
29 name,
30 symbol,
31 uri,
32 seller_fee_basis_points: 0,
33 creators: None,
34 collection: None,
35 uses: None,
36 },
37 true, // is_mutable
38 true, // update_authority_is_signer
39 None, // collection_details
40 )?;
41
42 // Mint tokens to the payer's associated token account
43 msg!("Minting {} tokens to {}", amount, ctx.accounts.token_account.key());
44
45 mint_to(
46 CpiContext::new(
47 ctx.accounts.token_program.to_account_info(),
48 MintTo {
49 mint: ctx.accounts.mint.to_account_info(),
50 to: ctx.accounts.token_account.to_account_info(),
51 authority: ctx.accounts.payer.to_account_info(),
52 },
53 ),
54 amount,
55 )?;
56
57 msg!("Token created and {} tokens minted successfully.", amount);
58 Ok(())
59}
The function performs two Cross-Program Invocations:
create_metadata_accounts_v3(lines 14-40) - Creates and initializes the metadata account with name, symbol, and URImint_to(lines 43-54) - Mints the specified amount to the payer's token account
The Client
Before testing, build the program:
anchor build
Get your program ID and update it in both lib.rs and Anchor.toml:
solana address -k target/deploy/anchor_spl_token-keypair.json
Then rebuild and deploy:
anchor build
anchor deploy
Creating the Test
Create the test file at tests/anchor-spl-token.ts:
1import * as anchor from "@coral-xyz/anchor";
2import { Program } from "@coral-xyz/anchor";
3import { AnchorSplToken } from "../target/types/anchor_spl_token";
4import { Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js";
5import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token";
6import { getAssociatedTokenAddressSync } from "@solana/spl-token";
7import { BN } from "bn.js";
8
9const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
10 "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
11);
12
13describe("anchor-spl-token", () => {
14 const provider = anchor.AnchorProvider.env();
15 anchor.setProvider(provider);
16
17 const program = anchor.workspace.AnchorSplToken as Program<AnchorSplToken>;
18 const payer = provider.wallet;
19
20 it("Creates a token with metadata and mints initial supply", async () => {
21 const mintKeypair = Keypair.generate();
22
23 const tokenName = "My Token";
24 const tokenSymbol = "MYTKN";
25 const tokenUri = "https://example.com/token-metadata.json";
26 const tokenDecimals = 9;
27 const mintAmount = new BN(1_000_000).mul(new BN(10).pow(new BN(tokenDecimals)));
28
29 // Derive the metadata account PDA
30 const [metadataAccount] = PublicKey.findProgramAddressSync(
31 [
32 Buffer.from("metadata"),
33 TOKEN_METADATA_PROGRAM_ID.toBuffer(),
34 mintKeypair.publicKey.toBuffer(),
35 ],
36 TOKEN_METADATA_PROGRAM_ID
37 );
38
39 // Derive the associated token account
40 const tokenAccount = getAssociatedTokenAddressSync(
41 mintKeypair.publicKey,
42 payer.publicKey
43 );
44
45 console.log("Mint address:", mintKeypair.publicKey.toBase58());
46 console.log("Metadata address:", metadataAccount.toBase58());
47 console.log("Token account:", tokenAccount.toBase58());
48
49 const tx = await program.methods
50 .createToken(tokenName, tokenSymbol, tokenUri, tokenDecimals, mintAmount)
51 .accountsPartial({
52 payer: payer.publicKey,
53 mint: mintKeypair.publicKey,
54 tokenAccount: tokenAccount,
55 metadataAccount: metadataAccount,
56 tokenProgram: TOKEN_PROGRAM_ID,
57 tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
58 associatedTokenProgram: ASSOCIATED_PROGRAM_ID,
59 systemProgram: SystemProgram.programId,
60 rent: SYSVAR_RENT_PUBKEY,
61 })
62 .signers([mintKeypair])
63 .rpc();
64
65 console.log("Transaction signature:", tx);
66 console.log("Token created and minted successfully!");
67 });
68});
Key Points:
- The metadata account PDA is derived using seeds:
["metadata", TOKEN_METADATA_PROGRAM_ID, mint_pubkey](lines 29-36) - The associated token account is derived using
getAssociatedTokenAddressSync(lines 39-42) - The mint keypair must be passed as a signer since it's being initialized
- Use
accountsPartialto specify accounts (Anchor 0.32+ syntax) - Use
BNfor large numbers (token amounts with decimals) tokenDecimalsis passed to the instruction and used to calculate the mint amount
Running the Test
yarn install
anchor test
Expected output:
anchor-spl-token
Mint address: GpPyH2FuMcS5PcrKWtrmEkBmW8h8gSwUaxNCQkFXwifV
Metadata address: 6jskfrDAmH9d67iL37CLNBK7Hf6FRwNZbq34q4vGucDq
Token account: J3KCxCfmnK9RJ3onmiUsfBDjvKyuVsAXgWvuypsaFQ2i
Transaction signature: 36v63t5cCsXYM8ny4pgahh...
Token created and minted successfully!
✔ Creates a token with metadata and mints initial supply (243ms)
1 passing (245ms)
Metadata JSON Format
The uri field should point to a JSON file containing your token's off-chain metadata:
{
"name": "My Token",
"symbol": "MYTKN",
"description": "A description of my token",
"image": "https://example.com/token-image.png"
}
Host this JSON file on a permanent storage solution like Arweave or IPFS.
Common Errors
no function or associated item named 'create_type' found
Add "anchor-spl/idl-build" to the idl-build feature in Cargo.toml:
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
Program account is not executable
Clone the Token Metadata program in Anchor.toml:
[[test.validator.clone]]
address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
UnspecifiedIpAddr(0.0.0.0) / Validator panic
Add bind_address = "127.0.0.1" to [test.validator] in Anchor.toml.
Notes
- The
amountparameter is in base units (includes decimals). For 1 million tokens with 9 decimals, pass1_000_000 * 10^9. - This example keeps mint authority and freeze authority on the payer. Production tokens often revoke or transfer these authorities after initial minting.
- The metadata account is mutable (
is_mutable = true). Set tofalseif you want immutable metadata.
Next Steps
- Deploy to Devnet: Change
cluster = "devnet"in Anchor.toml and runanchor deploy - Create an NFT: Set
decimals = 0andsupply = 1for non-fungible tokens - Add Token Extensions: Explore SPL Token 2022 for transfer fees, interest-bearing tokens, and more
- Learn more about Token Metadata: See the Token Metadata documentation
Quick Reference
Key Program IDs
| Program | Address |
|---|---|
| Token Program | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
| Associated Token Program | ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL |
| Token Metadata Program | metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s |
| System Program | 11111111111111111111111111111111 |
Metadata PDA Seeds
Deriving the Metadata PDA
1const [metadataAccount] = PublicKey.findProgramAddressSync(
2 [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
3 TOKEN_METADATA_PROGRAM_ID
4);
Minimum Dependencies
anchor-lang = "0.32.1"
anchor-spl = { version = "0.32.1", features = ["token", "metadata", "associated_token"] }
FAQ
Terminology
- Fungible token:
decimals >= 0, unlimited supply potential - NFT:
decimals = 0,supply = 1, plus Master Edition account - Token Metadata: Metaplex program used for both fungible tokens and NFTs
- SPL: Solana Program Library, the standard token interface
What is an SPL token?
An SPL token is Solana's equivalent of an ERC-20 token on Ethereum. SPL stands for Solana Program Library. SPL tokens are fungible tokens that can represent currencies, governance tokens, stablecoins, or any other fungible asset on Solana.
What is the difference between a token mint and a token account?
- Token Mint: The factory that creates tokens. It defines the token's properties (decimals, supply, authorities). There is one mint per token type.
- Token Account: A wallet that holds tokens. Each user needs their own token account for each token type they want to hold.
What is an Associated Token Account (ATA)?
An Associated Token Account is a deterministically-derived token account for a given wallet and mint. Instead of creating random token accounts, ATAs use a standard derivation so anyone can calculate the token account address for any wallet. This is the recommended way to handle token accounts.
What is Metaplex Token Metadata?
Metaplex Token Metadata is a program that attaches metadata (name, symbol, image URI) to SPL tokens. Without it, tokens are just anonymous mints. The metadata is stored in a Program Derived Address (PDA) associated with the mint.
Why clone the Token Metadata program for local testing?
The local Solana test validator starts with a clean state and doesn't include any programs except the core Solana programs. Metaplex Token Metadata is a separate program deployed on mainnet, so you need to clone it to use it locally.
Can I use this code to create an NFT?
Yes, with modifications:
- Set
mint::decimals = 0(NFTs are indivisible) - Mint exactly 1 token
- Remove mint authority after minting (so no more can be created)
- Add a Master Edition account (for Metaplex NFT standard)
How much does it cost to create a token on Solana?
Creating a token requires rent for three accounts:
- Mint account: ~0.00145 SOL
- Token account: ~0.00203 SOL
- Metadata account: ~0.01 SOL
Total: approximately 0.015-0.02 SOL (varies with rent prices).
What's the difference between Anchor and native Solana Rust?
Anchor is a framework that simplifies Solana development by:
- Auto-generating account serialization/deserialization
- Providing declarative account validation with macros
- Generating TypeScript clients automatically
- Handling common patterns like PDAs and CPIs
Native Solana Rust requires manually handling all of these concerns.
Glossary
| Term | Definition |
|---|---|
| SPL Token | Solana Program Library token standard, equivalent to ERC-20 |
| Mint | The account that defines a token and can create new supply |
| Token Account | An account that holds a balance of a specific token |
| ATA | Associated Token Account - deterministic token account for a wallet |
| PDA | Program Derived Address - an address derived from seeds, owned by a program |
| CPI | Cross-Program Invocation - calling one Solana program from another |
| Anchor | A Rust framework for building Solana programs |
| Metaplex | Protocol for NFTs and token metadata on Solana |
| IDL | Interface Definition Language - describes a program's interface |
| Rent | SOL required to keep an account alive on Solana |
