Okay, first off, if you're brand new and don't wanna mess with installing a ton of stuff on your machine, head to Solana Playground. Click "Create a new project," name it something like "my first anchor," pick Anchor (Rust), and hit Create. Boom. You're in. No Rust, no CLI drama. Why? It compiles and deploys right in your browser. Saved me hours my first time.
The thing is, Anchor's this framework that makes writing Solana programs way less painful. Like, Solana's fast as hell, but native Rust for programs? Kinda brutal with all the account validation boilerplate. Anchor handles that crap for you with macros and traits. You'll see.
Picture this: every Solana program needs a program ID-that's like its address on the chain. Then you've got instructions (your functions) and accounts (where data lives). Anchor wraps it all nice.
Core pieces? #[program] module for your logic. #[derive(Accounts)] structs to define what accounts your instruction touches. And declare_id! macro up top for the program's.
In my experience, once you get this structure down, everything clicks. No more staring at blank Rust files wondering where to start.
Open that lib.rs file Playground gives you. It looks scary? Nah. Strip it to basics for a Hello World:
use anchorlang::prelude::*; declareid!("11111111111111111111111111111111"); // Gets auto updated on build #[program]
mod helloworld { use super::*; pub fn hello(ctx: Context<Hello>) -> Result<()> { msg!("Hello, World!"); Ok(()) }
} #[derive(Accounts)]
pub struct Hello {}
That's it. msg! logs to the transaction output. Build it (Tools > Build), and watch the console say "Build successful." Fees? Like 0.000005 SOL on devnet. Negligible.
But say you want full control. Install Anchor CLI first. I usually do it via Anchor Version Manager-avm. Grab it from their docs, then avm install latest and avm use latest.
sh -c "$(curl -sSfL https://release.solana.com/stable/install)".anchor init myproject. Cd in.solana config set --url devnet or localnet for testing.anchor build. Deploys a keypair to target/deploy/.Potential issue? Rust version mismatch. Run rustup update. Fixed it for me every time. Why local? Faster, no browser lag.
Now, let's make it do something. Update that hello function to take a counter. Add an account to track calls.
Replace with this:
use anchorlang::prelude::*; declareid!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q"); #[program]
mod counter { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = 0; Ok(()) } pub fn increment(ctx: Context<Increment>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; msg!("Count now: {}", counter.count); Ok(()) }
} #[derive(Accounts)]
pub struct Initialize<'info> { #[account(init, payer = user, space = 8 + 8)] pub counter: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>,
} #[derive(Accounts)]
pub struct Increment<'info> { #[account(mut)] pub counter: Account<'info, Counter>,
} #[account]
pub struct Counter { pub count: u64,
}
What's next? Build and deploy. Playground: Tools > Deploy to Devnet (connect your wallet, Phantom works). Local: anchor deploy. Grab ~0.5 SOL on devnet from a faucet if needed.
Why space = 8 + 8? First 8 bytes discriminator (Anchor magic), next 8 for u64 count. Run out? Realloc later, but start small.
Accounts are everything in Solana. Like files owned by programs. Anchor's #[account] validates them automatically-signer checks, ownership, etc.
Common attrs? init to create new ones (needs payer and space). mut if you're changing data. hasone = field to link accounts, like ensuring a token account matches a mint.
Example snag: Forgetting systemprogram in init. Tx fails with "invalid account data." Add it: pub system_program: Program<'info, System>,. Done.
Seeds and bumps for PDAs (program derived addresses)? Super common for unique accounts.
#[account( seeds = [b"counter", user.().as_ref()], bump,
)]
pub my_pda: Account<'info, Counter>,
Bump finds the off curve address. No keypair needed. Why? Deterministic, secure.
Anchor shines here. Define errors:
#[error_code]
pub enum MyError { #[msg("Unauthorized")] Unauthorized,
}
Then in fn: return err!(MyError::Unauthorized);.
Constraints in accounts: #[account(constraint = counter.count < 100)]. Tries to overflow? Boom, custom error. Saved my ass debugging once.
Use #[derive(InitSpace)] on your account structs.
#[account]
#[derive(InitSpace)]
pub struct MyData { pub count: u64, #[max_len(100)] pub name: String,
}
Then space = 8 + MyData::INIT_SPACE. Auto computes. No math headaches.
Anchor generates tests in tests/ folder. Run anchor test. It spins a local validator.
Edit myproject.test.ts (TypeScript client):
import * as anchor from "@coral xyz/anchor"; it("Initializes", async () => { const counter = anchor.web3.Keypair.generate(); await program.methods .initialize() .accounts({ counter: counter.publicKey, user: provider.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([counter]) .rpc(); const account = await program.account.counter.fetch(counter.publicKey); console.log("Count:", account.count); // 0
});
Fails? Check logs. Usually account space or signer missing. Tweak and re run. Tests catch 90% of dumb errors before deploy.
Configure Anchor.toml:
[programs.devnet]
counter = "YourProgramIdHere" # From build [provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"
anchor deploy --provider.cluster devnet. Costs ~0.01 SOL rent + tx fees. Update declare_id! with the new.
Client side? Anchor spits IDL (JSON interface). Use it to generate TS client: anchor idl fetch YourId --provider.cluster devnet.
| Cluster | URL | Faucet | Fees (approx) |
|---|---|---|---|
| Localnet | http://127.0.0.1:8899 | N/A (unlimited) | 0 |
| Devnet | https://api.devnet.solana.com | solfaucet.com | 0.000005 SOL/tx |
| Mainnet | https://api.mainnet beta.solana.com | Buy SOL | 0.000005 SOL/tx |
Devnet's perfect for beginners. Feels real, but free ish.
#[account(mut, realloc = new_size, realloc::payer = payer)]. Zero init with realloc::zero.Honestly, log everything with msg!. Shows in explorer.solana.com under tx details. Gold for debugging.
Grab anchor spl for tokens: Add to Cargo.toml anchor spl = "0.29.0".
Transfer example:
use anchorspl::token::{Token, TokenAccount, Transfer}; pub fn transfer(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { let cpiaccounts = Transfer { from: ctx.accounts.from.toaccountinfo(), to: ctx.accounts.to.toaccountinfo(), authority: ctx.accounts.authority.toaccountinfo(), }; let cpiprogram = ctx.accounts.tokenprogram.toaccountinfo(); let cpictx = CpiContext::new(cpiprogram, cpiaccounts); anchorspl::token::transfer(cpi_ctx, amount)?; Ok(())
}
Accounts struct links mints, authorities. Constraints like associated_token::mint = mint auto check ATA.
Why does this matter? Most dApps touch tokens. Nail this, you're golden.
IDL generates client. In JS/TS:
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const idl = JSON.parse(require('./counter.json'));
const programId = new anchor.web3.PublicKey('YourId');
const program = new anchor.Program(idl, programId, provider); await program.methods.increment().rpc();
Hook to Phantom: window.solana.connect(). Sound familiar from other chains? Yeah, similar.
Issue? Wallet not connected. Always check provider.wallet.publicKey.
Events? emit!(MyEvent { count: 42 });. Clients listen via program.account.fetch.
Zero copy for big data: Box<Account> or loader accounts. But for beginners? Stick to under 10KB.