Okay, before you touch your local machine, hop into Solana Playground. It's browser based, no installs, deploys in seconds. Why? You'll screw up Rust/Anchor setup otherwise, and it's frustrating as hell. I always start there for PDA stuff.
Hit "Create a new project", name it "pda escrow test", pick Anchor (Rust). Boom. You're in. Now, delete the starter code in lib.rs. We're building from scratch.
PDAs are like magic wallets your program controls. No private keys. Deterministic - same seeds, same address every time. Perfect for escrow, vaults, game states. Think: sender + receiver + "escrow" string = unique PDA that holds funds till conditions met.
The thing is, Solana's ed25519 curve has this "line" - if your seed hash lands on it, it's a valid keypair (bad). Off curve? PDA. Anchor finds the bump (magic number 255 down to 0) to push it off curve.
Why does this matter? Your program signs for the PDA via seeds + bump. No one else can touch it unless they know the seeds. Sound familiar? Like a hashmap value, but on chain.
Caveat: Same seeds = same PDA. Can't reuse for multiple escrows between same pair. Add nonce or timestamp seed if needed.
Let's make an escrow PDA. Sender creates it, funds it with SOL. Later, you can add release logic. Start in lib.rs.
First, imports and declare_id:
rust use anchor_lang::prelude::*; use anchorlang::systemprogram; declare_id!("11111111111111111111111111111111");Now, define your Escrow account struct. Simple: store sender, receiver, amount.
rust #[account] pub struct Escrow { pub from: Pubkey, pub to: Pubkey, pub amount: u64, }Okay, the instruction context. This is where Anchor magic happens. Use #[account(init, payer = from, space = 8 + 32 + 32 + 8)] - init creates PDA, payer funds rent, space is discriminator (8 bytes) + two Pubkeys (32 each) + u64 (8).
But smarter: use anchorlang::solanaprogram::accountinfo::nextaccount_info no, Anchor's sizeof!
impl on Escrow:
rust impl Escrow { pub const INIT_SPACE: usize = 8 + 32 + 32 + 8; }Now the instruction function:
rust #[program] pub mod pdaescrow { use super::*; pub fn createescrow(ctx: ContextThat's it for the program. Hit Build in Playground. If errors, check space - rent is ~0.002 SOL for this, paid by payer.
Switch to tests/anchor.test.ts. Replace everything. We're funding the PDA post creation, but first create.
solana airdrop 5anchor deployRun anchor test. Green? PDA created. Check Solana Explorer (localnet) - escrow account owned by your program, funded with rent + whatever.
Build fails on space? Calc: 8 (disc) + 32 (pubkey) + 32 (pubkey) + 8 (u64) = 80. But pad to 88 or use sizeof<Escrow>(). Rent calc: minimum_balance(80) ~0.0015 SOL.
PDA collision? Seeds too simple. Add [u8 nonce]. Frontend generates bump too: findProgramAddressSync(seeds, programId).
Tests flop? Wrong bump. Always derive PDA in TS same as Rust seeds. Bump mismatches kill it.
In my experience, 80% issues are seeds/bump. Print 'em: msg!("PDA: {:?}, bump: {:?}", pda.(), bump);
Creation doesn't transfer SOL yet. Add transfer instruction? Nah, use SystemProgram transfer in create, but PDA needs to be mut.
Separate fund instr. New context:
rust #[derive(Accounts)] pub struct FundEscrow<'info> { #[account( mut, seeds = [b"escrow", escrow.from.asref(), escrow.to.asref()], bump, hasone = from @ ErrorCode::InvalidOwner )] pub escrow: Account<'info, Escrow>, #[account(mut)] pub from: Signer<'info>, pub systemprogram: Program<'info, System>, }Function:
rust pub fn fundescrow(ctx: ContextWait, better: Use Anchor's transfer CPI.
rust use anchorlang::systemprogram::{transfer, Transfer}; systemprogram::transfer( CpiContext::new( ctx.accounts.systemprogram.toaccountinfo(), Transfer { from: ctx.accounts.from.toaccountinfo(), to: ctx.accounts.escrow.toaccountinfo(), }, ), amount, )?; ctx.accounts.escrow.amount += amount;Test it: After create, call fund_escrow with escrowPda. Balance check: escrow.lamports increases by amount (1 lamport min, but use 0.001 SOL).
Fine, you want CLI. Install Rust, Solana CLI, Anchor.
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"cargo install --git https://github.com/coral xyz/anchor anchor cli --lockedanchor init pda localcd pda local; solana config set --url localhost; solana test validator &anchor build; anchor deploy; anchor testPro tip: WSL2 on Windows, or macOS. VSCode + rust analyzer extension. Fees? Localnet free. Devnet: ~0.000005 SOL/tx.
Escrow done? Release to receiver. New instr, sign by sender or authority.
| Action | Accounts | Seeds/Bump | Lamports Moved |
|---|---|---|---|
| Create | escrow (init), from (payer), to | "escrow" + from + to | Rent only (~0.0018 SOL) |
| Fund | escrow (mut), from (mut signer) | Same | User amount (e.g. 0.1 SOL) |
| Withdraw | escrow (mut, close?), from (signer), to (mut) | Same | escrow.lamports - rent |
Withdraw func sketch:
rust pub fn withdraw(ctx: ContextFull close PDA: Return rent to payer. Add #[account(mut, close = from)] on escrow. Lamports auto refund. Brutal efficiency.
SOL boring? PDAs hold SPL tokens too. Associated Token Account (ATA) derived from PDA owner.
Seeds: PDA seeds + token mint + program (Token2022 or whatever). Use findprogramaddress for ATA.
In experience, mix SPL + PDA for vaults. Import anchor_spl::token::{Token, TokenAccount}. InitIfNeeded for token acc.
Issue: PDA can't sign token transfers directly. CPI to Token program with PDA seeds as authority.
#[account(mut)] ata: Account<'info, TokenAccount>Why? Multisig, staking pools. Gas? Token tx ~0.00001 SOL.
Stuck? Logs: anchor logs or Playground console. PDA wrong? Log seeds, bump.
Repro: GitHub repos like chang47/solana pda example program. Fork, tweak.
Edge: Multiple PDAs per instr. Array of accounts with seeds. Or realloc for bigger data - #[account(realloc = new_space, realloc::payer = user)]. Rent diff refunded/charged.
Tests pass? solana config set --url devnet; anchor deploy. ~0.4 SOL fee first time (account rent).
Frontend? @solana/web3.js + @coral xyz/anchor. Derive PDA client side, pass accounts.
What's next? CPI to other programs - invoke System from PDA. Or your own.
Honestly, PDAs clicked for me after 3 failed tests. Persist. Build this escrow, fund/withdraw. Then NFTs, games. Solana's fast - 50k TPS possible.
Hit snags? Tweak seeds. You're good.