# Anchor Core Concepts URL: https://www.learnsol.site/learn/anchor-programs/anchor-core-concepts Markdown URL: https://www.learnsol.site/learn/anchor-programs/anchor-core-concepts.mdx Learn the Anchor model clearly: instructions, accounts, signers, and constraints, and how those pieces control what your program is allowed to do. Before you write a real Anchor program, you need the mental model that makes the macros readable. Without that model, Anchor looks like syntax. With that model, Anchor looks like validation plus state changes. That is the right way to think about it. ## The Four Ideas That Control Everything At the beginner level, Anchor is mostly four things working together: 1. instructions 2. accounts 3. signers 4. constraints If you understand those four clearly, the first real program lesson becomes much easier. ` enforces that requirement." }, { term: "Constraint", definition: "A validation rule like `mut`, `init`, or `has_one` that becomes part of the instruction's security model." } ]} /> B --> C --> D --> E `} /> ## 1. Instruction An instruction is one action your program exposes. In Anchor, that usually means one function inside the `#[program]` module. ```rust #[program] pub mod counter { use super::*; pub fn increment(ctx: Context) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; Ok(()) } } ``` What this means in plain language: * a client asked your program to run `increment` * the client had to provide the required accounts * Anchor validated those accounts first * only then did your Rust logic run That order matters. The validation layer runs before your handler logic. ## 2. Accounts Solana programs do not keep normal server memory between requests. They read and write accounts. In Anchor, your persistent state is usually represented with structs marked by `#[account]`. ```rust #[account] pub struct Counter { pub authority: Pubkey, pub count: u64, } ``` This struct describes on-chain data. It is not just a Rust convenience type. It is part of the actual state model your program is allowed to read and write. ## 3. Signers A signer is an account that approved the transaction. In Anchor, you often represent that with `Signer<'info>`. ```rust pub authority: Signer<'info> ``` This does one important thing. It tells Anchor to reject the instruction unless that account really signed the transaction. A signer is not automatically the same thing as an owner. That confusion causes a lot of mistakes. The signer authorizes the transaction. Ownership and stored authority are separate things your program still needs to reason about. ## 4. Constraints Constraints are the rules Anchor checks before your handler runs. This is one of the main reasons Anchor exists. Instead of pushing every validation rule deep into the function body, you can express many of them directly in the account context. ```rust #[derive(Accounts)] pub struct Update<'info> { pub authority: Signer<'info>, #[account(mut, has_one = authority)] pub counter: Account<'info, Counter>, } ``` This says: * `authority` must sign * `counter` can be mutated * the `counter.authority` field must match the provided `authority` If one of those checks fails, your handler does not run. That is the critical idea. The official Anchor docs frame account validation as a core security feature. That is the right emphasis. Most serious program bugs are not caused by missing syntax. They are caused by bad validation. ## How These Pieces Fit Together Look at this full shape: ```rust #[program] pub mod counter { use super::*; pub fn increment(ctx: Context) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; Ok(()) } } #[derive(Accounts)] pub struct Update<'info> { pub authority: Signer<'info>, #[account(mut, has_one = authority)] pub counter: Account<'info, Counter>, } #[account] pub struct Counter { pub authority: Pubkey, pub count: u64, } ``` Now read it in order: 1. the instruction is `increment` 2. the account context is `Update` 3. the required signer is `authority` 4. the target state account is `counter` 5. the constraints say who is allowed to mutate it 6. the handler changes the value only after those rules pass That is Anchor in one example. ## Why Constraints Matter More Than Beginners Expect The Anchor docs provide many built-in account constraints such as: * `signer` * `mut` * `init` * `seeds` * `bump` * `has_one` * `constraint = ...` You do not need all of them right now. But you do need the right mental model: constraints are not decoration. They are part of your security model. ## One Small Example With `init` When you create a new account, the context often looks like this: ```rust #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] pub authority: Signer<'info>, #[account( init, payer = authority, space = 8 + 40, )] pub counter: Account<'info, Counter>, pub system_program: Program<'info, System>, } ``` What Anchor is doing here: * `init` means create the account * `payer = authority` means the signer funds the rent-exempt allocation * `space = ...` tells Anchor how much data space the account needs * `system_program` is required because account creation happens through the system program This is still the same model. Anchor is expressing validation and setup rules before your instruction logic runs. ## The Most Important Beginner Distinctions ### Instruction is not the same as transaction A transaction can contain one instruction or many. Your Anchor handler represents one instruction. ### Signer is not the same as authority A signer approves a transaction. Authority is a rule your program may store in account data and enforce with constraints like `has_one`. ### `mut` is not optional noise If an account will be written to, Anchor needs to know that. Without `mut`, state changes will not be persisted correctly. ### Client checks are not enough Even if the frontend verifies something, the program still has to enforce the real rule on-chain. The program is the final authority. ## One Good Reading Habit Whenever you look at an Anchor instruction, ask these four questions in order: 1. what instruction is being called? 2. which accounts are involved? 3. who must sign? 4. what constraints must be true before the logic runs? If you make that a habit, later Anchor code becomes much easier to read. ## Quick Self-Check Before moving on, make sure you can answer these clearly: 1. If a `has_one` check fails, does the handler still run? 2. Why is `Signer<'info>` different from a stored authority field? 3. Why does `init` need a payer? 4. Why is account validation part of security, not just convenience? If any of those answers are fuzzy, reread the examples once more. ## What Comes Next The next lesson uses these ideas to build a real Anchor program slowly. That is the point where the model stops being abstract and starts feeling practical. *** ## Quick Check # Anchor Development Ergonomics URL: https://www.learnsol.site/learn/anchor-programs/anchor-ergonomics Markdown URL: https://www.learnsol.site/learn/anchor-programs/anchor-ergonomics.mdx Learn how to keep Anchor programs readable and trustworthy with stronger constraints, clearer errors, thinner handlers, better events, and a more disciplined testing strategy. By this point in the section, you have already built enough Anchor code to see the real problem. Getting a program to work once is not the hard part. Keeping it readable, reviewable, and safe as it grows is the hard part. That is what this lesson is about. ## The Real Goal Anchor ergonomics is not about making code look fancy. It is about reducing the number of places bugs can hide. Good ergonomics means: * validation rules are easy to see * authority rules are easy to explain * handlers are short enough to reason about * failures are specific * tests describe the real risk surface That is the standard you want. B[Thin handler] B --> C[Specific errors] B --> D[Meaningful events] C --> E[Failure-path tests] D --> E E --> F[More reviewable program] `} /> ## 1. Push Validation Into Account Constraints First The official Anchor account constraint system exists for a reason. If a rule can be expressed in the account context, do not bury it deep in the handler unless you have a strong reason. ```rust #[account( mut, has_one = authority, constraint = vault.mint == mint.key() @ ErrorCode::InvalidMint, )] pub vault: Account<'info, VaultState>, ``` Why this is better than manual checks hidden later: * the rule is visible near the account itself * the handler stays shorter * reviewers can see the security model faster * error locality improves This is not about style. It is about making the program easier to trust. ## 2. Keep Handlers Thin A good handler should usually do one short sequence: 1. rely on the account context for setup and validation 2. perform the minimal business logic 3. write the state change 4. emit an event if the action matters externally When handlers become long, review gets harder. That is when people start missing authority bugs and stale state bugs. A thin handler looks like this: ```rust pub fn complete_escrow(ctx: Context) -> Result<()> { let escrow = &mut ctx.accounts.escrow; require!(escrow.status == EscrowStatus::Funded, ErrorCode::InvalidState); transfer_from_vault(ctx.accounts)?; escrow.status = EscrowStatus::Completed; Ok(()) } ``` The helper functions can still be complex if needed. But the instruction entrypoint should stay easy to read. ## 3. Use Specific Error Codes Anchor's custom error system is there to make failures explicit. Use it properly. ```rust #[error_code] pub enum ErrorCode { #[msg("Caller is not authorized for this action")] Unauthorized, #[msg("Vault mint does not match expected mint")] InvalidMint, #[msg("Escrow is not in the required state")] InvalidState, } ``` Good errors do three things: * tell you what failed * tell the client what kind of failure happened * make tests easier to write and review Bad errors hide intent. Specific errors expose intent. ## 4. Emit Events For State Changes That Matter Anchor supports events directly. Use them for meaningful transitions, not for noise. ```rust #[event] pub struct EscrowCompleted { pub escrow: Pubkey, pub maker: Pubkey, pub taker: Pubkey, pub amount: u64, } ``` Events are useful when: * off-chain systems need to index state changes * you want a clean activity trail for important actions * business-critical transitions should be visible without re-parsing every account later Do not emit events just because the framework supports them. Emit them when the action actually matters to observers. ## 5. Keep Authority Rules Explainable In One Sentence This is one of the best review tests you can apply. For every mutating instruction, ask: who is allowed to do this, and why? If the answer takes a paragraph, your authority model is probably too muddy. A good answer sounds like this: * "Only the stored admin can mint because the config account has `has_one = admin`." * "Only the recorded taker can complete the escrow when the status is `Funded`." * "Only the PDA derived from the escrow key can sign the vault transfer." If you cannot explain the rule cleanly, reviewers will struggle too. ## 6. Treat PDA Seed Formulas Like Schemas The PDA lesson already covered this at the mechanics level. At the ergonomics level, the important rule is stronger: seed formulas should be stable, documented, and reused consistently. A seed formula is not a random helper expression. It is part of the contract of your program. That means: * choose the namespace deliberately * keep seed order fixed * keep the same derivation rule across creation and later validation * test the wrong-seed path explicitly This is one of the easiest places for a program to become confusing over time. ## 7. Separate Business Logic From Transport Logic Anchor instructions do two different jobs at once: * they receive accounts and instruction inputs * they express business rules and state changes Do not let those concerns get tangled more than necessary. Bad shape: * parsing inputs * validating authority * checking state * performing CPI * mutating three accounts * formatting side effects all in one long handler Better shape: * context expresses most validation * handler expresses the business step * helper functions handle repeated logic like vault transfers or state updates That makes review much easier. ## 8. Testing Should Follow The Risk, Not The Tutorial The official Anchor docs still make `anchor test` the default local integration path. That is still the right baseline. Start there. Then add more focused tools only when the program complexity justifies them. A sensible progression now looks like this: 1. `anchor test` for full local integration flow 2. LiteSVM when you want faster in-process tests in Rust, TS/JS, or Python 3. Mollusk when you want very explicit, deterministic account-by-account instruction testing in Rust The current docs describe LiteSVM as a fast in-process VM and Mollusk as a lightweight harness where accounts are defined explicitly. That should tell you when to reach for them. Do not replace clear integration tests with clever simulation tests too early. The point is to increase confidence, not to collect testing tools for their own sake. ## 9. Test Failure Paths As First-Class Behavior A serious Anchor program should not only prove that success works. It should prove that abuse fails correctly. For each important instruction, you should usually have tests for at least some of these: * wrong signer * wrong PDA seeds * wrong mint * wrong state transition * repeated execution attempt * overflow or cap violation That is the real shape of trust. A happy-path-only suite proves much less than people think. ## 10. Treat Upgrades And State Evolution Deliberately As soon as a program has real users or real assets, account layout changes stop being casual refactors. That means you should start thinking clearly about: * how account schemas evolve * whether migrations are needed * who controls upgrade authority * how that authority is documented operationally This lesson is not about full upgrade mechanics. It is about discipline. If state evolution is not planned, maintainability is already weak. ## A Better Review Checklist Before shipping or merging a significant Anchor change, ask: 1. can every mutating instruction explain its authority rule in one sentence? 2. are the PDA derivation rules stable and tested? 3. are important token assumptions explicit? 4. do custom errors tell you what failed clearly? 5. are critical state transitions represented on-chain, not just in the client? 6. do tests cover at least the main abuse path for each important instruction? If several answers are weak, the program is not ready just because the happy path works. ## Common Ergonomics Failures ### Failure 1: the handler is the only place the rules are visible That makes review harder than it should be. ### Failure 2: every failure becomes a generic error That slows debugging and hides intent. ### Failure 3: PDA rules are copied manually in too many places That increases the chance of drift. ### Failure 4: tests only prove the demo path That is not enough for stateful or token-moving programs. ## What Comes Next At this point, the main teaching spine of the Anchor section is in place. The remaining capstone should only make sense if all of these habits are already normal: * clear authority rules * clear state transitions * clear PDA derivation * clear CPI structure * clear failure testing ## Quick Check # Anchor Local Setup URL: https://www.learnsol.site/learn/anchor-programs/anchor-local-setup Markdown URL: https://www.learnsol.site/learn/anchor-programs/anchor-local-setup.mdx Install the current Anchor toolchain, create a workspace, and get to one clean local build-and-test loop before writing program logic. This lesson has one job. Get you to a working Anchor workspace that builds and tests locally without mystery. If this part is shaky, every later lesson turns into environment debugging. ## The Goal By the end of this lesson, you should be able to: * verify that Rust, Solana CLI, and Anchor CLI are installed * create a new Anchor workspace * run `anchor build` * run `anchor test` * explain what those two commands actually do That is enough for the first step. B --> C --> D --> E `} /> ## The Current Installation Path The official Solana docs now provide a one-command installer that sets up the standard local toolchain. ```bash curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash ``` That is the fastest way to get the full environment onto your machine. After the install finishes, restart your terminal. Then verify the tools actually exist: ```bash rustc --version solana --version anchor --version node --version yarn --version ``` Why this matters: * Rust compiles your program * Solana CLI manages wallets, clusters, and validator commands * Anchor CLI creates, builds, deploys, and tests Anchor workspaces * Node and Yarn are needed for the default TypeScript test template created by `anchor init` The official Anchor docs still recommend AVM, the Anchor Version Manager, when you need to manage multiple Anchor versions across projects. That matters once you start working across repos or need reproducible builds. ## When To Use AVM If you only want a fast first setup, the installer above is enough. If you expect to work across multiple Anchor projects, install AVM as well. ```bash cargo install --git https://github.com/coral-xyz/anchor avm --force avm --version avm install latest avm use latest anchor --version ``` What AVM changes: * you can pin a specific Anchor version per machine workflow * switching Anchor versions stops being manual * reproducing an older project becomes much easier You do not need to overthink this yet. Just remember the rule: if version drift becomes a problem, use AVM. ## Create Your First Workspace Now create a project. ```bash anchor init anchor_starter cd anchor_starter ``` This gives you a default Anchor workspace with: * `Anchor.toml` for project configuration * `programs/` for on-chain Rust code * `tests/` for integration tests * `migrations/` for deployment scripting * `target/` after builds If you want Rust-based tests instead of the default TypeScript template, Anchor now supports that directly at project creation time: ```bash anchor init --test-template rust anchor_starter_rust ``` For this curriculum, the default test template is fine for the first setup pass. The goal right now is not to choose the perfect test stack. The goal is to get one clean loop working. ## Look At `Anchor.toml` Before You Run Anything Open `Anchor.toml` once. You are looking for one thing first: the cluster configuration. In a new local project, you generally want to stay on `localnet` while learning. That matters because `anchor test` behaves differently depending on the configured cluster. When the workspace is set to localnet, Anchor can: * start a local validator * deploy your program into that local validator * run your tests * shut the validator down after the test run That is your main learning loop. ## Run `anchor build` First Do not jump straight to tests. Build first. ```bash anchor build ``` What this does: * compiles your Rust program * generates build artifacts in `target/` * updates the IDL output for your program If `anchor build` fails, that usually means one of these is wrong: * Rust toolchain * Anchor installation * local dependency setup * workspace configuration Fix build issues before you move to tests. ## Run `anchor test` Once the build works, run: ```bash anchor test ``` What this does in the normal local workflow: * builds the workspace * starts localnet if needed * deploys your program locally * runs the generated integration tests If this passes, your setup is good enough for the rest of the section. That is the baseline you want. ## What Success Looks Like A successful setup means all of these are true: * `anchor --version` works * `anchor init` created the workspace without errors * `anchor build` finishes successfully * `anchor test` finishes successfully * you understand where the program code and tests live If one of those is missing, you are not fully set up yet. ## Common Failures And The Real Cause ### `anchor: command not found` Your shell cannot find the Anchor binary. Check: ```bash anchor --version avm --version ``` If AVM is installed but `anchor` is missing, restart your terminal and run: ```bash avm use latest ``` ### `could not exec the linker cc` The Anchor docs call this out on Linux and WSL. This usually means system build dependencies are missing or not available to the Rust toolchain. The right fix is not random cargo commands. The right fix is to install the missing Linux dependencies and then retry the Anchor install. ### `anchor test` fails before tests run Do not assume your program logic is wrong. Very often the problem is one of these: * another local validator is already running * Node or Yarn is missing for the default test template * the workspace did not build cleanly first The official CLI docs note that `anchor test` can fail if another local validator is already running. If that happens, shut it down and rerun the test. ### Wallet or keypair problems If Solana CLI is installed but your keypair setup is missing, create one: ```bash solana-keygen new solana address ``` You do not need devnet SOL for the local test flow. For localnet, the validator controls the test environment. ## What To Check In The Generated Project Before moving on, open these files once: * `Anchor.toml` * `programs//src/lib.rs` * `tests/.ts` You are not studying the code deeply yet. You are just building a map of the workspace. That will matter in the next lesson. ## Fast Verification Checklist Run this sequence in order: ```bash anchor --version anchor init anchor_starter cd anchor_starter anchor build anchor test ``` If those commands all work, your local setup is ready. ## What You Should Know Before Moving On Before leaving this lesson, you should be able to answer these two questions clearly: 1. What is the difference between `anchor build` and `anchor test`? 2. Why is localnet the safest place to learn first? If those answers are still fuzzy, rerun the workflow once more. ## What Comes Next The next lesson is not more setup. It is learning what the generated Anchor workspace actually contains, and why those files exist. *** ## Quick Check # Anchor Program Anatomy URL: https://www.learnsol.site/learn/anchor-programs/anchor-program-anatomy Markdown URL: https://www.learnsol.site/learn/anchor-programs/anchor-program-anatomy.mdx Understand what each file in an Anchor workspace does, how build artifacts are generated, and how a client call becomes an on-chain state change. After `anchor init`, most beginners see the file tree and immediately lose the thread. That is normal. Anchor generates several files at once, and if you do not know what each one is for, every later lesson feels more complicated than it really is. This lesson fixes that. ## The Mental Model An Anchor workspace is not one thing. It is a small system with four separate jobs: * program code in Rust * workspace configuration * test code * generated build artifacts If you keep those four buckets clear in your head, the file tree stops looking random. B[anchor build] B --> C[target/idl] B --> D[target/deploy] B --> E[target/types] F[programs/.../src/lib.rs] --> B G[tests/...ts] --> H[anchor test] H --> B H --> I[local validator] I --> J[program state changes] `} /> ## What `anchor init` Creates A default Anchor workspace includes files like these: ```txt anchor_starter/ Anchor.toml Cargo.toml package.json programs/ anchor_starter/ src/ lib.rs migrations/ tests/ anchor_starter.ts target/ ``` You may also see additional generated folders such as `.anchor/` after running tests. The exact contents can grow over time, but the important parts stay the same. ## Start With `Anchor.toml` `Anchor.toml` is the workspace control file. It tells Anchor things like: * which cluster to use * which wallet to use * which program addresses belong to the workspace * which script should run during `anchor test` A typical shape looks like this: ```toml [provider] cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" [programs.localnet] anchor_starter = "YourProgramPubkeyHere" ``` Why this file matters: * `anchor build` and `anchor test` both depend on it * cluster mistakes often start here * test behavior is partly controlled here * local program addresses are recorded here If something feels wrong at the workspace level, open `Anchor.toml` first. ## The `programs/` Directory This is where your on-chain code lives. For a normal project, you will spend most of your time in: * `programs//src/lib.rs` That file usually contains: * your instruction handlers inside `#[program]` * account validation structs with `#[derive(Accounts)]` * state structs with `#[account]` * custom errors * helper logic This is the security-critical part of the workspace. If you want to understand what a program really does, start here, not in the frontend and not in the tests. ## The Root `Cargo.toml` Do not confuse the workspace `Cargo.toml` with the program crate's `Cargo.toml`. At the root, `Cargo.toml` describes the Rust workspace itself. Inside `programs//Cargo.toml`, you configure the actual program crate. That distinction matters because Anchor workspaces can contain more than one program. ## The `tests/` Directory This is where the integration tests live. In a default setup, Anchor creates a TypeScript test file such as: * `tests/anchor_starter.ts` That file does not replace program logic. It does something different. It proves that the program behaves correctly when called through real transactions. In other words: * Rust program code defines behavior * tests prove that behavior from the outside That separation matters. You should not treat tests as decoration. ## The `migrations/` Directory This directory is for deployment scripting. Most beginners do not need to spend much time here yet. But you should know why it exists. As your project grows, deployment stops being just one command. You may need scripts that coordinate program deploys, account initialization, or environment-specific setup. That is the role this folder grows into. ## The `target/` Directory This is generated output. Do not confuse it with source code. The most important subfolders are: * `target/deploy/` * `target/idl/` * `target/types/` ### `target/deploy/` This contains build artifacts such as the compiled program binary and deployment keypair material. ### `target/idl/` This contains the program IDL JSON. Anchor generates that file during `anchor build`. The official docs describe the IDL as the standardized description of your program's instructions and accounts. That matters because clients use it to understand: * instruction names * required accounts * argument types * account data layouts ### `target/types/` This contains generated TypeScript types for the IDL. These are useful for TypeScript tooling and client-side integration. ## What `anchor build` Actually Changes When you run: ```bash anchor build ``` Anchor does more than compile Rust. It also: * emits the IDL into `target/idl/` * updates generated TypeScript types in `target/types/` * prepares deploy artifacts in `target/deploy/` That is why build output matters beyond just “did the compiler pass.” If your instruction signatures changed, the generated artifacts should change too. ## How A Client Call Becomes A State Change Now connect the files together. When a client calls your program, the flow looks like this: 1. the client uses the program interface and account list 2. the transaction is sent with the required accounts and signers 3. Anchor validates the account context in your Rust program 4. your instruction handler runs 5. account data is written back on-chain 6. the client or test fetches the updated state That is the whole system. The workspace exists to support that path. ## One Concrete Mapping Exercise Open these three files at the same time: * `programs//src/lib.rs` * `tests/.ts` * `target/idl/.json` Then pick one instruction and answer: 1. where is the Rust handler? 2. where is the test that calls it? 3. where is that instruction represented in the IDL? If you can map one instruction across those three files, you are no longer guessing how the workspace fits together. ## Common Confusions ### Confusion 1: `target/` is source code It is not. It is generated output. Read it when useful, but do not treat it as the place where program logic should be edited. ### Confusion 2: the IDL is optional trivia It is not. The IDL is one of the key contracts between your program and the code that calls it. ### Confusion 3: `Anchor.toml` is only for deployment It is not. It affects provider settings, test scripts, and local program addresses too. ### Confusion 4: the tests folder is separate from the real program The tests are separate code, but not separate behavior. They are how you prove the real behavior from the outside. ## What You Should Know Before Moving On Before the next lesson, you should be able to explain: * what `Anchor.toml` controls * where the Rust program actually lives * what the IDL is for * why `target/` is generated output, not source code * how one instruction shows up in program code, tests, and the IDL If those answers are still fuzzy, open the file tree again and trace one instruction manually. ## What Comes Next Now that the workspace layout is clear, the next lesson can focus on the core Anchor model itself: * instructions * accounts * signers * constraints ## Quick Check /src/lib.rs`", "`tests/.ts`", "`Anchor.toml`", ]} correctIndex={0} explanation="`target/` is build output. It is useful to inspect, but not the place where you write the program's real logic." /> # Cross-Program Invocations URL: https://www.learnsol.site/learn/anchor-programs/cpi-cross-program Markdown URL: https://www.learnsol.site/learn/anchor-programs/cpi-cross-program.mdx Learn how one Anchor program calls another program safely, how to build a CpiContext, and how PDA signer seeds change the authority model. CPI is the point where Solana composability becomes real. It is also the point where many learners start treating code they do not understand as magic. Do not do that. A CPI is simpler than it looks once you reduce it to the actual moving parts. ## What A CPI Really Is A cross-program invocation means your program calls an instruction on another Solana program. That is all. Your program becomes the caller. The other program becomes the callee. The current Anchor docs describe the same basic ingredients every time: 1. the program ID being called 2. the accounts required by that instruction 3. the instruction data or arguments If those three things are clear, the CPI pattern is clear. ## Why CPI Exists Without CPI, every program would be isolated. With CPI, one program can reuse another program's logic. That is how Solana programs compose. Common examples: * call the system program to transfer SOL or create accounts * call the token program to mint or transfer tokens * call another protocol program as part of a larger flow This is why CPI matters. A serious Solana program rarely lives alone. ## Start With The Simplest Mental Model Do not start with token vaults. Start with a plain system-program transfer. The official Anchor CPI docs use exactly that move for a reason. It lets you see the pattern without extra token complexity. B[Build CPI account struct] B --> C[Create CpiContext] C --> D[Optional with_signer seeds] D --> E[Call callee program] E --> F[State changes in callee] `} /> ```rust use anchor_lang::prelude::*; use anchor_lang::system_program::{transfer, Transfer}; #[derive(Accounts)] pub struct SolTransfer<'info> { #[account(mut)] pub sender: Signer<'info>, #[account(mut)] pub recipient: SystemAccount<'info>, pub system_program: Program<'info, System>, } pub fn sol_transfer(ctx: Context, amount: u64) -> Result<()> { let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), Transfer { from: ctx.accounts.sender.to_account_info(), to: ctx.accounts.recipient.to_account_info(), }, ); transfer(cpi_context, amount)?; Ok(()) } ``` ## Read That CPI In Order ### The target program ```rust ctx.accounts.system_program.to_account_info() ``` This is the program being called. That matters because CPI is not a vague helper action. It is a concrete instruction invocation against a specific program. ### The CPI account struct ```rust Transfer { from: ctx.accounts.sender.to_account_info(), to: ctx.accounts.recipient.to_account_info(), } ``` This is the account set the system program expects for its transfer instruction. That is the second core CPI idea. You must build the account layout expected by the callee. ### The `CpiContext` ```rust let cpi_context = CpiContext::new(program_id, cpi_accounts); ``` This packages: * which program is being called * which accounts are being passed into that call Then the helper function like `transfer(...)` actually performs the invocation. ## What Changes When A PDA Must Sign The basic CPI shape stays the same. The authority model changes. If the account that needs to authorize the CPI is a PDA, the program cannot sign the way a wallet signs. Instead, the runtime allows the program to sign for the PDA by verifying the PDA seeds. That is where `with_signer(...)` comes in. ## A CPI With PDA Signer Seeds The official Anchor docs show this pattern with a PDA sending SOL through the system program. That is the right next step to learn. ```rust use anchor_lang::prelude::*; use anchor_lang::system_program::{transfer, Transfer}; #[derive(Accounts)] pub struct SolTransferWithPda<'info> { #[account( mut, seeds = [b"vault", recipient.key().as_ref()], bump, )] pub vault: SystemAccount<'info>, #[account(mut)] pub recipient: SystemAccount<'info>, pub system_program: Program<'info, System>, } pub fn sol_transfer_with_pda(ctx: Context, amount: u64) -> Result<()> { let signer_seeds: &[&[&[u8]]] = &[&[ b"vault", ctx.accounts.recipient.key().as_ref(), &[ctx.bumps.vault], ]]; let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.recipient.to_account_info(), }, ) .with_signer(signer_seeds); transfer(cpi_context, amount)?; Ok(()) } ``` ## The Most Important CPI Rule In This Lesson The PDA signer seeds in the CPI must match the PDA derivation rule you validated in the account context. That is the whole game. Here, the account context says: ```rust seeds = [b"vault", recipient.key().as_ref()] bump ``` So the signer seeds passed into `with_signer(...)` must reflect that same rule. If those rules drift apart, the PDA cannot sign correctly. That is why CPI signer logic is really just another validation consistency problem. ## `with_signer(...)` Does Not Mean "Bypass Security" This is a common beginner misunderstanding. `with_signer(...)` does not create fake authority. It tells the runtime: * here are the seeds * here is the bump * if these really derive a PDA owned by this program, treat that PDA as a signer for this invocation So the runtime still verifies the derivation. This is not a shortcut. It is a controlled PDA-signing mechanism. If you do not understand why a PDA is allowed to sign in a CPI, go back to the PDA lesson first. CPI signer seeds only make sense once PDA derivation itself is clear. ## Where Token CPI Fits In The exact same CPI structure appears in token flows. For example, a token transfer via CPI still needs: * the token program as the callee * a CPI account struct like `TransferChecked` * any instruction data such as amount and decimals * signer seeds if a PDA is the token authority That is why the token lesson came before this one. The token account model is one layer. CPI is the invocation layer on top of it. ## One Token CPI Shape To Recognize This is the pattern you should be able to read now: ```rust let signer_seeds: &[&[&[u8]]] = &[&[ b"vault-authority", escrow.key().as_ref(), &[ctx.bumps.vault_authority], ]]; let cpi_accounts = TransferChecked { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.vault_ata.to_account_info(), to: ctx.accounts.receiver_ata.to_account_info(), authority: ctx.accounts.vault_authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ) .with_signer(signer_seeds); ``` Even if you have not built escrow yet, you should now understand the structure: * a PDA is the authority * the token program is the callee * the CPI account struct names the required accounts * `with_signer(...)` lets the PDA authorize the transfer ## The Real Security Questions For CPI Whenever you see a CPI instruction, ask these questions: 1. which program is being called? 2. which accounts is that program actually being given? 3. who is authorizing the CPI? 4. if a PDA signs, are the seeds exactly correct? 5. what happens if this instruction is called twice or with the wrong accounts? Those questions matter much more than memorizing helper function names. ## Common CPI Mistakes ### Mistake 1: focusing only on the helper call The helper call is often the easy part. The real risk is passing the wrong accounts or wrong authority model. ### Mistake 2: treating signer seeds like random syntax They are not syntax noise. They are the rule that tells the runtime whether your PDA is allowed to sign. ### Mistake 3: validating too little before the CPI Most CPI bugs are validation bugs. You should know exactly why every mutable account is present before the CPI happens. ### Mistake 4: forgetting that the callee has its own rules Your program does not get to redefine what the callee expects. You must satisfy the callee's instruction contract correctly. ## Suggested Test Cases For a first serious CPI flow, test these: 1. the happy path succeeds with the correct accounts 2. the wrong target program fails 3. the wrong PDA seeds fail 4. the wrong authority fails 5. a mismatched account relationship fails That is how you move from “it worked once” to “the authority model is actually correct.” ## What Comes Next The next lesson should use this exact CPI mental model in a guided project where PDAs, token authority, and on-chain rules all meet in one place. ## Quick Check # DeFi Integration Capstone URL: https://www.learnsol.site/learn/anchor-programs/defi-integration-challenge Markdown URL: https://www.learnsol.site/learn/anchor-programs/defi-integration-challenge.mdx Build one optional Anchor capstone that routes token settlement through a program-controlled vault, enforces a clear authority model, and proves the full flow with failure-path tests. This page should not be treated like the main teaching spine. It is an optional capstone. That means it assumes the rest of the Anchor section already feels normal: * PDA design * token account wiring * CPI * signer seeds * state transitions * failure-path testing If those still feel shaky, go back and strengthen them first. ## The One Project Build a vault-routed settlement module. The flow is simple: 1. a payer deposits tokens into a program-controlled vault 2. the program records a settlement agreement on-chain 3. an authorized settlement step releases tokens to a recipient 4. an optional fee cut is routed to a treasury token account 5. the settlement cannot be executed twice That is the only project in this capstone. No menu. No branching options. ## Why This Is The Right Capstone This project forces you to combine the exact ideas the section was building toward: * PDA state * PDA authority * token CPI * explicit settlement states * one real trust boundary It also feels closer to a real protocol module than the earlier training projects. You are no longer just teaching one token mint or one escrow. You are teaching controlled settlement logic. ## What The Program Must Guarantee At minimum, your program should enforce these rules on-chain: * only the intended payer can fund the vault * only the expected recipient can receive settlement proceeds * settlement can only happen once * fee routing, if present, must use the exact expected treasury account * token accounts must match the intended mint * PDA signer seeds must match the vault authority derivation rule exactly If those rules are not enforced on-chain, the capstone is weak. ## The Core Architecture Use four main pieces: 1. a `Settlement` PDA for business state 2. a `vault-authority` PDA tied to that settlement 3. a vault token account owned by the vault-authority PDA 4. one settlement instruction that performs the token CPI and updates state This should look familiar by now. That is intentional. A capstone should combine the earlier patterns, not invent a totally different mental model. B[Settlement PDA becomes Funded] B --> C[Program computes fee and net amount] C --> D[Vault authority PDA signs token CPIs] D --> E[Treasury receives fee] D --> F[Recipient receives proceeds] E --> G[Settlement marked Settled] F --> G `} /> ## Suggested Settlement State Keep the first version compact. ```rust #[account] #[derive(InitSpace)] pub struct Settlement { pub payer: Pubkey, pub recipient: Pubkey, pub treasury: Pubkey, pub mint: Pubkey, pub amount: u64, pub fee_bps: u16, pub status: SettlementStatus, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] pub enum SettlementStatus { Initialized, Funded, Settled, Cancelled, } ``` Why this is enough: * `payer` defines who supplies the tokens * `recipient` defines who should receive the main proceeds * `treasury` defines where the fee goes * `mint` fixes the asset being settled * `amount` fixes the gross settlement amount * `fee_bps` lets you compute one protocol fee deterministically * `status` prevents replay and invalid transitions That is already a real authority model. ## PDA Rules Use deterministic rules and keep them boring. ### Settlement PDA ```rust seeds = [b"settlement", payer.key().as_ref(), settlement_id.as_ref()] ``` ### Vault authority PDA ```rust seeds = [b"vault-authority", settlement.key().as_ref()] ``` Those two formulas are enough. Do not get clever here. The harder the logic becomes, the more important predictable seeds become. ## Step 1: Initialize The Settlement The initialize instruction should only write the agreement. It should not move tokens yet. That separation matters. ```rust #[derive(Accounts)] #[instruction(settlement_id: [u8; 8])] pub struct InitializeSettlement<'info> { #[account(mut)] pub payer: Signer<'info>, pub recipient: UncheckedAccount<'info>, pub treasury: UncheckedAccount<'info>, #[account( init, payer = payer, space = 8 + Settlement::INIT_SPACE, seeds = [b"settlement", payer.key().as_ref(), &settlement_id], bump, )] pub settlement: Account<'info, Settlement>, pub system_program: Program<'info, System>, } ``` The handler should only store the state terms and mark the settlement as `Initialized`. That is enough for the first step. ## Step 2: Fund The Vault The payer should move tokens into a vault token account owned by the vault-authority PDA. That is the same custody pattern you already saw in escrow. The important point is not novelty. The important point is reuse of a correct authority pattern. At the funding step, validate all of these: * settlement status is `Initialized` * payer matches the stored settlement payer * payer token account matches the expected mint * vault token account is derived for the vault-authority PDA and the correct mint Then use `transfer_checked` to move the amount into the vault. After the transfer, set the settlement status to `Funded`. ## Step 3: Settle Through The Vault Authority PDA This is the heart of the capstone. The settlement instruction should: * require the settlement to be `Funded` * compute the fee amount from `fee_bps` * compute the recipient amount as `amount - fee` * transfer the fee from the vault to the treasury token account * transfer the remainder from the vault to the recipient token account * mark the settlement as `Settled` The critical CPI pattern looks like this: ```rust let signer_seeds: &[&[&[u8]]] = &[&[ b"vault-authority", settlement.key().as_ref(), &[ctx.bumps.vault_authority], ]]; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ) .with_signer(signer_seeds); ``` That is the same signer rule you already learned. Now it is doing real settlement work. ## Step 4: Make The Fee Rule Explicit Do not hide the fee logic in the client. Do not treat treasury routing as a UI detail. If the protocol takes a fee, the program should compute and enforce it. A clear pattern is: ```rust let fee_amount = settlement .amount .checked_mul(settlement.fee_bps as u64) .ok_or(ErrorCode::Overflow)? / 10_000; let recipient_amount = settlement .amount .checked_sub(fee_amount) .ok_or(ErrorCode::Overflow)?; ``` That makes the settlement math visible and testable. ## Step 5: Block Replay By Updating State Last This is a small but important discipline point. You should update the final status only after the CPI steps succeed. ```rust settlement.status = SettlementStatus::Settled; ``` That preserves a clean rule: * unfinished CPI means unfinished settlement * finished CPI means final state transition That rule makes both reasoning and testing cleaner. ## Required Failure Paths A capstone is not complete if it only works once on the happy path. At minimum, test all of these: 1. funding succeeds for the correct payer 2. funding fails for the wrong payer 3. settlement succeeds for the correct state and accounts 4. settlement fails if the vault authority seeds are wrong 5. settlement fails if the mint is wrong 6. settlement fails if the treasury token account is not the expected one 7. settlement fails on second execution because the status is no longer `Funded` That is the minimum serious matrix. ## Risk Review Before you call this capstone done, answer these clearly: 1. who can move tokens at each step? 2. which PDA actually controls the vault? 3. what prevents double settlement? 4. what guarantees the fee goes to the intended treasury? 5. what guarantees the wrong mint cannot slip into the flow? If your answers are vague, the implementation is still weak. ## What A Good Submission Looks Like A good capstone is not the one with the most features. It is the one where: * the account model is easy to explain * the authority model is easy to explain * the seed formulas are stable * the fee logic is explicit * the failure-path tests prove the trust boundary That is the real standard. If you cannot explain exactly why the vault authority PDA is allowed to sign, do not call the design finished. That is the center of the whole module. ## One Good Extension If the core capstone works, add exactly one improvement: * add an expiry timestamp that lets the payer cancel if settlement never occurs That extension deepens the state machine without changing the core authority model. ## What Comes Next This is the end of the optional Anchor capstone path. If you can build this cleanly and explain the authority model without hand-waving, the Anchor section has done its job. ## Quick Check # Escrow with a PDA Vault URL: https://www.learnsol.site/learn/anchor-programs/escrow-mechanism-project Markdown URL: https://www.learnsol.site/learn/anchor-programs/escrow-mechanism-project.mdx Build one guided escrow project in Anchor using PDA state, a vault authority PDA, token CPI, and explicit state transitions that cannot be bypassed. Escrow is the first project in this section where your program has to do four hard things at once: * hold state over time * control token movement with a PDA * enforce who is allowed to act next * stop repeated or invalid state transitions That is why escrow matters. If token minting taught you program-controlled authority, escrow teaches you program-controlled flows. ## What You Are Building You will build one simple token escrow with this lifecycle: 1. a maker creates the escrow terms 2. the maker funds a vault token account controlled by the program 3. the taker completes the escrow and receives the tokens 4. or the maker cancels before completion and receives the tokens back That is enough. Do not add deadlines, partial fills, fees, or multi-party settlement in the first version. The point of this project is to make one state machine completely clear. ## The Core Idea The escrow has two separate responsibilities: * state tracking in your own escrow account * token custody through a vault controlled by a PDA Those are different jobs. That is why the project needs both: * an escrow state PDA * a vault authority PDA * a vault token account owned by that vault authority PDA The token program still controls token balances. Your Anchor program controls when those balances are allowed to move. Funded -> Completed` that prevents replay and invalid actions.", }, ]} /> Initialized Initialized --> Funded: maker funds vault Funded --> Completed: taker completes Initialized --> Cancelled: maker cancels Funded --> Cancelled: maker cancels before completion `} /> ## Step 1: Define The Escrow State Clearly Start with the smallest useful state machine. ```rust #[account] #[derive(InitSpace)] pub struct Escrow { pub maker: Pubkey, pub taker: Pubkey, pub mint: Pubkey, pub amount: u64, pub status: EscrowStatus, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] pub enum EscrowStatus { Initialized, Funded, Completed, Cancelled, } ``` Why each field exists: * `maker` tells the program who created the escrow * `taker` tells the program who is allowed to complete it * `mint` ties the escrow to one token mint * `amount` records the agreed token quantity * `status` prevents invalid transitions and repeated execution That is enough for the first version. ## Step 2: Choose Deterministic PDA Rules This project needs two PDA formulas. ### The escrow PDA Use one PDA for the business state itself. A clean rule is: ```rust seeds = [b"escrow", maker.key().as_ref(), escrow_id.as_ref()] ``` The exact third seed can vary. It can be a nonce, an ID, or another stable unique value. The important part is the design principle: * namespace the account * tie it to the maker * make the final seed stable enough to derive again later ### The vault authority PDA Use a second PDA for token authority. A clean rule is: ```rust seeds = [b"vault-authority", escrow.key().as_ref()] ``` This is a good pattern because the vault authority is tied directly to one escrow record. That makes the authority model easier to reason about. ## Step 3: Create The Escrow State The first instruction writes the escrow terms, but does not move tokens yet. ```rust use anchor_spl::token_interface::Mint; #[derive(Accounts)] #[instruction(escrow_id: [u8; 8])] pub struct InitializeEscrow<'info> { #[account(mut)] pub maker: Signer<'info>, pub taker: UncheckedAccount<'info>, pub mint: InterfaceAccount<'info, Mint>, #[account( init, payer = maker, space = 8 + Escrow::INIT_SPACE, seeds = [b"escrow", maker.key().as_ref(), &escrow_id], bump, )] pub escrow: Account<'info, Escrow>, pub system_program: Program<'info, System>, } ``` Then the handler stores the deal terms. ```rust pub fn initialize_escrow( ctx: Context, _escrow_id: [u8; 8], amount: u64, ) -> Result<()> { let escrow = &mut ctx.accounts.escrow; escrow.maker = ctx.accounts.maker.key(); escrow.taker = ctx.accounts.taker.key(); escrow.mint = ctx.accounts.mint.key(); escrow.amount = amount; escrow.status = EscrowStatus::Initialized; Ok(()) } ``` The important part here is not that the handler is long. The important part is that it is short. The instruction is only defining the agreement. It is not trying to define the agreement and move funds and settle the trade all at once. The chosen mint should be part of that agreement immediately. If the asset is not fixed during initialization, the escrow terms are still incomplete. ## Step 4: Define The Vault Flow Now you need a token account that actually holds the escrowed tokens. The clean beginner design is: * derive a vault authority PDA from the escrow PDA * create an ATA for that vault authority and the chosen mint * move maker tokens into that ATA during the funding step That keeps the authority model consistent with the lessons you already learned. ## Step 5: Fund The Escrow This is the instruction where the maker deposits tokens into the vault. ```rust use anchor_spl::{ associated_token::AssociatedToken, token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked}, }; #[derive(Accounts)] pub struct FundEscrow<'info> { #[account(mut)] pub maker: Signer<'info>, #[account( mut, has_one = maker, has_one = mint, constraint = escrow.status == EscrowStatus::Initialized @ EscrowError::InvalidState, )] pub escrow: Account<'info, Escrow>, pub mint: InterfaceAccount<'info, Mint>, #[account( mut, constraint = maker_token_account.mint == mint.key() @ EscrowError::InvalidMint, )] pub maker_token_account: InterfaceAccount<'info, TokenAccount>, #[account( seeds = [b"vault-authority", escrow.key().as_ref()], bump, )] pub vault_authority: UncheckedAccount<'info>, #[account( init_if_needed, payer = maker, associated_token::mint = mint, associated_token::authority = vault_authority, associated_token::token_program = token_program, )] pub vault_token_account: InterfaceAccount<'info, TokenAccount>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, } ``` Now the handler can move the tokens. ```rust pub fn fund_escrow(ctx: Context) -> Result<()> { let escrow = &mut ctx.accounts.escrow; let cpi_accounts = TransferChecked { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.maker_token_account.to_account_info(), to: ctx.accounts.vault_token_account.to_account_info(), authority: ctx.accounts.maker.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ); token_interface::transfer_checked(cpi_context, escrow.amount, ctx.accounts.mint.decimals)?; escrow.status = EscrowStatus::Funded; Ok(()) } ``` ## What This Funding Step Is Really Doing It is doing two jobs: 1. moving tokens into program-controlled custody 2. advancing the state machine from `Initialized` to `Funded` That second job matters as much as the transfer. If you move tokens and forget the state transition, your escrow model is already weak. ## Step 6: Complete The Escrow Now the taker should be able to receive the tokens, but only if the escrow is actually funded. ```rust #[derive(Accounts)] pub struct CompleteEscrow<'info> { #[account(mut)] pub taker: Signer<'info>, #[account( mut, constraint = escrow.status == EscrowStatus::Funded @ EscrowError::InvalidState, constraint = escrow.taker == taker.key() @ EscrowError::Unauthorized, has_one = mint, )] pub escrow: Account<'info, Escrow>, pub mint: InterfaceAccount<'info, Mint>, #[account( seeds = [b"vault-authority", escrow.key().as_ref()], bump, )] pub vault_authority: UncheckedAccount<'info>, #[account( mut, associated_token::mint = mint, associated_token::authority = vault_authority, associated_token::token_program = token_program, )] pub vault_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, associated_token::mint = mint, associated_token::authority = taker, associated_token::token_program = token_program, )] pub taker_token_account: InterfaceAccount<'info, TokenAccount>, pub token_program: Interface<'info, TokenInterface>, } ``` And the handler uses a PDA-signed token CPI. ```rust pub fn complete_escrow(ctx: Context) -> Result<()> { let escrow = &mut ctx.accounts.escrow; let signer_seeds: &[&[&[u8]]] = &[&[ b"vault-authority", escrow.key().as_ref(), &[ctx.bumps.vault_authority], ]]; let cpi_accounts = TransferChecked { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.vault_token_account.to_account_info(), to: ctx.accounts.taker_token_account.to_account_info(), authority: ctx.accounts.vault_authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ) .with_signer(signer_seeds); token_interface::transfer_checked(cpi_context, escrow.amount, ctx.accounts.mint.decimals)?; escrow.status = EscrowStatus::Completed; Ok(()) } ``` ## Read The Completion Flow In The Right Order ### First, confirm the state ```rust constraint = escrow.status == EscrowStatus::Funded ``` If the escrow is not funded, completion must fail. ### Then confirm the actor ```rust constraint = escrow.taker == taker.key() ``` The program decides who the taker is. The client does not get to redefine that at call time. ### Then confirm the exact token accounts ```rust associated_token::authority = vault_authority associated_token::mint = mint ``` and ```rust associated_token::authority = taker associated_token::mint = mint ``` This is important. You do not want the completion path to accept arbitrary token accounts and hope the token CPI rejects bad input later. The account context should already describe the exact custody model. ### Then let the PDA sign for the vault authority ```rust .with_signer(signer_seeds) ``` This is the same PDA-signing pattern from the CPI lesson. Now it is being used for real custody movement. ### Then update the state machine ```rust escrow.status = EscrowStatus::Completed; ``` If you forget this line, the escrow can be completed again. That is the kind of bug this project is supposed to teach you to avoid. ## Step 7: Add Cancellation Before Completion A minimal escrow also needs a cancellation path. The simplest rule is: * only the maker can cancel * cancellation is allowed only while the escrow is funded or initialized, depending on your design A clean beginner rule is to allow cancellation only before completion. The same pattern applies: * validate actor * validate current state * move tokens back with a PDA-signed CPI if needed * mark the escrow `Cancelled` ## Step 8: Add Explicit Errors A state-machine project should fail clearly. ```rust #[error_code] pub enum EscrowError { #[msg("Escrow is not in the required state")] InvalidState, #[msg("Caller is not authorized for this action")] Unauthorized, #[msg("Token mint does not match the escrow configuration")] InvalidMint, } ``` Specific errors make your tests and reviews much stronger. ## The Real Lesson In This Project Escrow is not hard because token CPI is hard. Escrow is hard because state transitions and authority rules must stay correct across time. That is the real engineering problem. The project works only if all of these remain aligned: * escrow state PDA * vault authority PDA * vault token account * current status * expected actor * expected mint If one of those drifts, the design gets unsafe quickly. ## Minimum Test Matrix Do not ship this project with one happy-path test. The minimum useful tests are: 1. maker initializes and funds successfully 2. wrong taker cannot complete 3. correct taker completes successfully 4. second completion attempt fails 5. maker cancel path works only in allowed states 6. wrong mint or wrong token account relationships fail That is the real proof that the state machine is working. ## Common Escrow Mistakes ### Mistake 1: using client-side booleans instead of on-chain status Escrow state belongs on-chain. The client can display state, but the program must enforce it. ### Mistake 2: forgetting to tie the vault authority PDA to one escrow A vague authority model creates review problems and safety problems. Tie the vault authority to the escrow directly. ### Mistake 3: moving tokens without advancing the state machine If the transfer succeeds but the status stays stale, the program logic is already inconsistent. ### Mistake 4: relying on signer checks without stored authority checks For serious state transitions, the business rule must be explicit in account data and constraints. ## One Good Extension If the core version works, add exactly one improvement: * an expiry timestamp that lets the maker cancel after timeout That is a good extension because it deepens the same state-machine logic without exploding the scope. ## What Comes Next After this project, the next lesson should move from “working flows” to “cleaner and safer program structure.” That is where ergonomics and maintainability start to matter more. ## Quick Check # First Anchor Program URL: https://www.learnsol.site/learn/anchor-programs/first-anchor-program Markdown URL: https://www.learnsol.site/learn/anchor-programs/first-anchor-program.mdx Build a small counter program in Anchor one step at a time, understand every account in the instruction context, and verify the behavior with tests. This is the lesson where Anchor stops being abstract. You are going to build one small program, read every part of it, and test it properly. The program is intentionally simple. That is a feature, not a limitation. A first Anchor program should teach the model clearly: * one state account * one authority * one initialization path * one update path * one test loop ## What You Are Building You will build a counter program with two instructions: 1. `initialize` creates a counter account 2. `increment` increases the stored value by 1 That is enough for the first real build. You do not need decrement, reset, PDA seeds, or token logic yet. The point is to understand the full path from account validation to state change. B --> C --> D --> E --> F `} /> ## Before You Touch The Code Start from a fresh Anchor workspace that already passes: ```bash anchor build anchor test ``` If the default workspace still does not pass, stop here and fix setup first. Do not layer program logic on top of a broken environment. ## Step 1: Understand The Job Of This Program This program stores two pieces of state: * `authority`: who controls the counter * `count`: the current numeric value That gives us one simple rule: only the stored authority should be allowed to increment the counter. Everything in the code will support that rule. ## Step 2: Replace The Generated Program With A Minimal Counter Open: * `programs//src/lib.rs` Replace the generated example with this structure. ```rust use anchor_lang::prelude::*; // Keep the generated program ID from your own workspace here. declare_id!(""); #[program] pub mod counter { use super::*; pub fn initialize(ctx: Context) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.authority = ctx.accounts.authority.key(); counter.count = 0; Ok(()) } pub fn increment(ctx: Context) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = counter .count .checked_add(1) .ok_or(CounterError::Overflow)?; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] pub authority: Signer<'info>, #[account( init, payer = authority, space = 8 + Counter::INIT_SPACE, )] pub counter: Account<'info, Counter>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Increment<'info> { pub authority: Signer<'info>, #[account(mut, has_one = authority)] pub counter: Account<'info, Counter>, } #[account] #[derive(InitSpace)] pub struct Counter { pub authority: Pubkey, pub count: u64, } #[error_code] pub enum CounterError { #[msg("Counter overflow")] Overflow, } ``` Keep the `declare_id!` value aligned with the program ID generated in your workspace. Do not paste a fake ID and assume Anchor will sort it out later. ## Step 3: Read The Program In The Right Order Most beginners read the file top to bottom and still miss the model. Read it in this order instead: 1. `Counter` state struct 2. `Initialize` account context 3. `Increment` account context 4. `initialize` handler 5. `increment` handler That order makes the logic easier to parse. ### First, the state ```rust #[account] #[derive(InitSpace)] pub struct Counter { pub authority: Pubkey, pub count: u64, } ``` This is the account data that will live on-chain. Why each field exists: * `authority` stores who is allowed to control this counter later * `count` stores the actual value ### Then, the initialization context ```rust #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] pub authority: Signer<'info>, #[account( init, payer = authority, space = 8 + Counter::INIT_SPACE, )] pub counter: Account<'info, Counter>, pub system_program: Program<'info, System>, } ``` This context is doing a lot of real work. What each piece means: * `authority: Signer<'info>` means the caller must sign * `#[account(mut)]` means Anchor may debit lamports from that signer to fund account creation * `init` means create the new `counter` account * `payer = authority` means the signer pays rent for that new account * `space = 8 + Counter::INIT_SPACE` allocates enough bytes for the discriminator plus the stored data * `system_program` is required because account creation happens through the system program That is one of the core Anchor ideas again. The account context expresses the setup and validation before your business logic runs. ### Then, the update context ```rust #[derive(Accounts)] pub struct Increment<'info> { pub authority: Signer<'info>, #[account(mut, has_one = authority)] pub counter: Account<'info, Counter>, } ``` This is the security rule for the whole program. What it means: * the caller must sign * the counter account can be mutated * the stored `counter.authority` must match the provided signer If that `has_one` rule fails, `increment` never runs. That is exactly what you want. ## Step 4: Understand The Handlers ### `initialize` ```rust pub fn initialize(ctx: Context) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.authority = ctx.accounts.authority.key(); counter.count = 0; Ok(()) } ``` This handler does not do much. That is good. The context already handled the heavy setup work. The handler only needs to write the initial state. ### `increment` ```rust pub fn increment(ctx: Context) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = counter .count .checked_add(1) .ok_or(CounterError::Overflow)?; Ok(()) } ``` Two important details here: * the handler only runs after the account checks pass * it uses `checked_add` instead of assuming overflow never matters That second point is small, but it is the right habit. Even a toy program should not silently wrap numeric state. ## Step 5: Build Before You Write Tests Run: ```bash anchor build ``` Do this now, before touching the test file. If the program does not compile, the test layer will only make the problem noisier. What you should check after the build: * the build succeeds * the IDL in `target/idl/` updates * no errors appear around missing derives, account space, or type mismatches ## Step 6: Replace The Default Test With A Counter Test Open: * `tests/.ts` Replace the generated test with this version. ```ts import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import type { Counter } from "../target/types/counter"; describe("counter", () => { const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.Counter as Program; const counter = anchor.web3.Keypair.generate(); it("initializes the counter", async () => { await program.methods .initialize() .accounts({ authority: provider.wallet.publicKey, counter: counter.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([counter]) .rpc(); const account = await program.account.counter.fetch(counter.publicKey); if (!account.authority.equals(provider.wallet.publicKey)) { throw new Error("expected authority to match provider wallet"); } if (account.count.toNumber() !== 0) { throw new Error("expected count to start at 0"); } }); it("increments the counter", async () => { await program.methods .increment() .accounts({ authority: provider.wallet.publicKey, counter: counter.publicKey, }) .rpc(); const account = await program.account.counter.fetch(counter.publicKey); if (account.count.toNumber() !== 1) { throw new Error("expected count to be 1 after increment"); } }); }); ``` ## Step 7: Read The Test Like A Client Flow Do not treat the test file as magic either. It is just a client calling your program. ### What happens in the initialize test ```ts await program.methods .initialize() .accounts({ authority: provider.wallet.publicKey, counter: counter.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([counter]) .rpc(); ``` This is the client saying: * call the `initialize` instruction * pass the authority signer * pass the new counter account public key * pass the system program * sign with the newly created counter keypair because the account is being created That last line matters. If you forget `signers([counter])`, account creation will fail because the new account keypair is not participating correctly in the transaction. ### What happens after the instruction runs ```ts const account = await program.account.counter.fetch(counter.publicKey); ``` This fetches the on-chain counter account and lets the test inspect the stored state. That is how you prove the handler actually changed data. ## Step 8: Run The Full Loop Now run: ```bash anchor test ``` If everything is wired correctly, the test flow should: * build the program * deploy it to localnet * run both tests * show success for initialize and increment That is your first full Anchor loop. ## Common Failures In This Lesson ### Program name mismatch in the test This line depends on the generated workspace name: ```ts const program = anchor.workspace.Counter as Program; ``` If your actual program name is different, update it. The test has to match your workspace, not a copied tutorial name. The generated type import should also match your workspace name. ### Wrong `declare_id!` If `declare_id!` is out of sync with the workspace configuration, build or deploy steps can fail in confusing ways. Keep the generated program ID consistent. ### Missing `signers([counter])` The initialize path creates a new account. If the new keypair does not sign, initialization fails. ### Missing `mut` on the counter account If the counter will be written to, the account context must mark it mutable. ### Forgetting `has_one = authority` If you remove this, anyone who can provide the account could increment it. That is exactly the kind of access-control bug Anchor is meant to help you avoid. ## Why This Lesson Matters This one small program teaches the full Anchor pattern: * define state * define account contexts * express security rules in constraints * write thin handlers * prove the behavior with tests That pattern is the foundation for everything later: * PDAs * token flows * CPI * escrow ## One Good Extension, Not Five If you want one extra exercise after this lesson, add: * `set_count(value: u64)` That extension is useful because it forces you to repeat the exact same pattern with one instruction argument. Do not add five more features yet. The point of the first program is clarity. ## What Comes Next The next lesson changes one major thing. Instead of letting accounts live at arbitrary keypairs, you will derive them deterministically with PDAs. *** ## Quick Check # Anchor Programs URL: https://www.learnsol.site/learn/anchor-programs Markdown URL: https://www.learnsol.site/learn/anchor-programs.mdx Learn how to build Solana programs with Anchor, from your first workspace and account model to PDAs, token flows, CPI, and production-minded patterns. Anchor is the framework most Solana teams use to build programs without hand-writing every low-level check. It does not remove Solana's rules. It gives you a better way to express them. This section is about learning that model properly. The goal is not to memorize macros. The goal is to understand how Anchor turns account validation, signer checks, and state transitions into code you can reason about. ## What This Section Covers You will move through Anchor in the same order real understanding tends to form: * set up a workspace that actually builds and tests * understand what each file in an Anchor project is doing * learn the core model of instructions, accounts, signers, and constraints * build one small program end to end * move into PDAs, token flows, and CPI * finish with stronger design and testing habits ## Why This Order Matters Anchor gets confusing when you learn isolated tricks. If you start with PDAs, token accounts, or CPI before the basic account model feels normal, every lesson looks like magic syntax. That is why this path stays strict: 1. workspace first 2. file layout second 3. core concepts third 4. first real program next 5. advanced composition after that ## Learning Path B --> C --> D --> E --> F --> G --> H `} /> 1. `anchor-local-setup` - install the toolchain, create a workspace, and run your first local tests 2. `anchor-program-anatomy` - understand what `Anchor.toml`, `programs/`, `tests/`, and the IDL are doing 3. `anchor-core-concepts` - learn instructions, accounts, signers, and constraints without hand-waving 4. `first-anchor-program` - build a small stateful program one instruction at a time 5. `pda-seed-derivation` - make state deterministic with program-derived addresses 6. `spl-token-integration` - work with mints, token accounts, authorities, and safer token constraints 7. `cpi-cross-program` - call other programs and reason about delegated authority safely 8. `token-minting-project` - combine PDAs, token CPI, and supply rules in one guided build 9. `escrow-mechanism-project` - build a stronger state machine with vault control and release logic 10. `anchor-ergonomics` - clean up handlers, tests, errors, and upgrade discipline 11. `defi-integration-challenge` - optional capstone once the main path feels solid ## What You Will Build By the end of this section, you should have built or understood: * a working local Anchor workspace * a tested counter-style program * PDA-backed state accounts * token mint and transfer flows * CPI calls with signer seeds * one escrow-style state machine ## Prerequisites You should already be comfortable with: * basic Rust syntax and ownership * Solana accounts and transactions * the Solana CLI If those still feel shaky, go back to Rust Foundations or Solana Foundations first. ## How To Use This Section * build as you go * run tests after each meaningful change * keep notes on every error you hit and how you fixed it * do not skip the smaller lessons just to jump into escrow or tokens A good Anchor mental model is simple: every instruction is account validation plus a state transition. When that idea becomes natural, the rest of the framework stops feeling mysterious. *** ## Quick Check ## Outcome When you finish this section, you should be able to read an Anchor instruction, explain its authority model, trace its account constraints, and test both the happy path and the abuse path. Start with: **`anchor-local-setup`**. # PDAs and Seed Derivation URL: https://www.learnsol.site/learn/anchor-programs/pda-seed-derivation Markdown URL: https://www.learnsol.site/learn/anchor-programs/pda-seed-derivation.mdx Learn how Anchor uses seeds and bumps to derive deterministic program-controlled addresses, and how to design PDA state safely. The last lesson used a normal keypair-backed account for the counter. That is useful for learning, but it is not how many real Solana programs organize state. Real programs often need addresses they can derive again later. That is where PDAs come in. ## The Core Idea A PDA is a deterministic address derived from: * seed bytes * a program ID * a bump In practice, that means your program can decide what an address should be before the account exists. That is the real power. You are not waiting for a random keypair anymore. You are defining the address from the business rule itself. ## Why Programs Need PDAs A normal keypair account is fine when a client can create any arbitrary account and hand it to the program. A PDA is better when the program needs a predictable rule like: * one profile per user * one review per `(reviewer, movie)` pair * one config account for the whole program * one vault authority for one escrow Those are not random accounts. They are accounts with a deterministic relationship to your program state. ## The Right Mental Model If a normal keypair is user-chosen state, a PDA is program-chosen state. That does not mean the program magically owns everything about it. It means the address is derived from rules your program can reproduce. That is why PDAs are so useful for: * unique records * namespaced state * authority accounts * vault patterns ## What Anchor Adds The official Anchor PDA docs emphasize that you usually define PDAs directly in the account constraints. That means the PDA derivation rule also becomes a validation rule. This is the main pattern: ```rust #[account( seeds = [b"profile", authority.key().as_ref()], bump, )] pub profile: Account<'info, Profile>, ``` Two important things are happening here at once: * Anchor derives the expected PDA from those seeds * Anchor checks that the provided account matches that expected address That is why PDAs are not just an address trick. They are part of your validation model. B[Find PDA + bump] B --> C[Create account at deterministic address] C --> D[Store authority and profile data] D --> E[Later instructions validate same seeds] `} /> ## `seeds` and `bump` Belong Together The official account constraints reference is explicit here: * `seeds` defines the PDA inputs * `bump` is used with those seeds to derive a valid PDA You should think of them as a pair. ```rust #[account( seeds = [b"profile", authority.key().as_ref()], bump, )] pub profile: Account<'info, Profile>, ``` Anchor supports: * static seeds like `b"profile"` * dynamic seeds like `authority.key().as_ref()` * combinations of both That is how you move from vague state to deterministic state. ## Your First Useful PDA Example Start with a profile account. The business rule is simple: one profile per user. That rule maps directly to a PDA formula. ```rust #[account] #[derive(InitSpace)] pub struct UserProfile { pub authority: Pubkey, #[max_len(32)] pub username: String, } ``` Now define the creation context. ```rust #[derive(Accounts)] pub struct CreateProfile<'info> { #[account(mut)] pub authority: Signer<'info>, #[account( init, payer = authority, space = 8 + UserProfile::INIT_SPACE, seeds = [b"profile", authority.key().as_ref()], bump, )] pub profile: Account<'info, UserProfile>, pub system_program: Program<'info, System>, } ``` Read that rule in plain language: * namespace this account under `b"profile"` * tie it to one authority public key * derive one deterministic address from those inputs * create the account there That means the same user cannot create ten different profile accounts under this rule. The seed formula defines uniqueness. ## Why The Namespace Seed Matters Do not start your PDA design with only dynamic values. Start with a namespace seed. Good: ```rust seeds = [b"profile", authority.key().as_ref()] ``` Weak: ```rust seeds = [authority.key().as_ref()] ``` The namespace seed does three things: * it makes the PDA purpose obvious * it avoids collisions with other PDA types in your program * it makes the design easier to review later This is a small habit, but it matters a lot once programs grow. ## Updating The Same PDA Later The next important rule is simple: if you create a PDA with one seed formula, you must validate it with the same seed formula later. Here is an update context: ```rust #[derive(Accounts)] pub struct UpdateProfile<'info> { pub authority: Signer<'info>, #[account( mut, seeds = [b"profile", authority.key().as_ref()], bump, has_one = authority, )] pub profile: Account<'info, UserProfile>, } ``` This gives you two layers of protection: * the account must be the correct PDA for that authority * the stored authority field must still match the signer That combination is stronger than signer checks alone. ## Seed Order Is Part Of The Address This is one of the easiest mistakes to make. These are not the same: ```rust seeds = [b"review", reviewer.key().as_ref(), movie.as_bytes()] ``` ```rust seeds = [b"review", movie.as_bytes(), reviewer.key().as_ref()] ``` Changing seed order changes the derived address. That means seed order is part of your state model. Pick the rule once, document it, and keep it consistent everywhere. If you ever have to ask “does seed order matter?”, the answer is yes. Treat the seed formula like a schema, not like a casual helper expression. ## Do Not Put Unstable Data In Seeds Without Thinking A seed should usually be based on data that is stable enough to derive again later. Good seed candidates: * fixed namespace bytes * public keys * stable IDs * immutable labels Risky seed candidates: * editable usernames * mutable titles * arbitrary text you may want to change later If a value can change, and you use it as a seed, your addressing scheme gets harder to reason about. That does not always mean “never do it.” It means “do it intentionally.” ## PDA Creation Versus PDA Validation These are related, but not identical. During creation, you usually have something like: ```rust #[account( init, payer = authority, space = 8 + UserProfile::INIT_SPACE, seeds = [b"profile", authority.key().as_ref()], bump, )] pub profile: Account<'info, UserProfile>, ``` Later, when the account already exists, you remove `init` and keep the validation rule: ```rust #[account( mut, seeds = [b"profile", authority.key().as_ref()], bump, has_one = authority, )] pub profile: Account<'info, UserProfile>, ``` That pattern matters. Creation is one moment. Validation is every moment after that. ## PDA Seeds In The IDL The official Anchor PDA docs point out an important detail: PDA seed information defined in account constraints is included in the program IDL. Why that matters: * clients can understand more of the account derivation rule * account resolution can be more structured * the program interface becomes less opaque That does not remove the need to understand the seeds yourself. But it does make the program contract richer and easier to work with. ## One Small Full Example Here is the minimal shape for a profile creation instruction. ```rust use anchor_lang::prelude::*; #[program] pub mod profiles { use super::*; pub fn create_profile(ctx: Context, username: String) -> Result<()> { let profile = &mut ctx.accounts.profile; profile.authority = ctx.accounts.authority.key(); profile.username = username; Ok(()) } } #[derive(Accounts)] pub struct CreateProfile<'info> { #[account(mut)] pub authority: Signer<'info>, #[account( init, payer = authority, space = 8 + UserProfile::INIT_SPACE, seeds = [b"profile", authority.key().as_ref()], bump, )] pub profile: Account<'info, UserProfile>, pub system_program: Program<'info, System>, } #[account] #[derive(InitSpace)] pub struct UserProfile { pub authority: Pubkey, #[max_len(32)] pub username: String, } ``` If you understand why that address is deterministic, and why the seed rule belongs in the account context, you understand the heart of PDA usage. ## Common PDA Mistakes ### Mistake 1: treating PDAs like random addresses They are not random. Their value comes from being reproducible. ### Mistake 2: changing the seed formula between instructions If creation uses one formula and update uses another, your validation model is broken. ### Mistake 3: relying only on the signer and ignoring stored authority A signer check alone is often not enough. Use the PDA rule and the stored authority rule together when the business logic requires both. ### Mistake 4: using mutable business data as a seed casually If a seed can change, your addressing model gets harder to preserve and review. ### Mistake 5: forgetting what the bump is doing The bump is part of deriving a valid PDA. Do not treat it like random syntax noise. ## One Good Practice Exercise Add a `UserProfile` PDA to a fresh Anchor workspace with this rule: * namespace: `b"profile"` * identity: `authority.key()` Then write two tests: 1. the correct authority can create the PDA-backed profile 2. a mismatched PDA address is rejected That is a much better PDA exercise than jumping straight into a large escrow or vault design. ## What Comes Next The next lesson moves from deterministic state to token-aware state. That is where PDAs start getting used for real authority flows, not just profile-style records. ## Quick Check # SPL Token Integration with Anchor URL: https://www.learnsol.site/learn/anchor-programs/spl-token-integration Markdown URL: https://www.learnsol.site/learn/anchor-programs/spl-token-integration.mdx Learn how Anchor works with token mints, token accounts, and token CPIs using the modern token_interface types for Token and Token-2022. After PDAs, token integration is the next big step. This is where Solana programs stop looking like generic state machines and start handling real balances, real authorities, and real asset movement. The mistake beginners make here is trying to memorize token APIs before the account model is clear. Do not do that. First understand the accounts. Then the CPI code becomes much easier to read. ## The Three Token Accounts You Must Understand At the first-pass level, token integration revolves around three things: * the mint account * the token account * the token program ### The mint account The mint defines the token itself. It stores rules like: * decimals * mint authority * optional freeze authority If you do not know which mint is involved, you do not really know what asset your program is touching. ### The token account A token account holds a balance of one mint for one owner. In most app flows, this is often an ATA, an associated token account. The important idea is simpler than the name: * a mint defines the asset * a token account holds some quantity of that asset ### The token program This is the program that actually implements token logic. Today, that may be: * the classic SPL Token program * Token-2022 That distinction matters. The current Anchor docs push you toward `token_interface` types so one account model can work across both token programs. Do not hardcode your lesson mental model around the old idea that there is only one token program. The modern Anchor pattern is to be explicit about which token program is being used. ## Why `token_interface` Is The Right Default The current Anchor token docs use: * `InterfaceAccount<'info, Mint>` * `InterfaceAccount<'info, TokenAccount>` * `Interface<'info, TokenInterface>` That is the right default because it lets your instruction accept accounts from either Token or Token-2022, as long as the account shape is compatible. That is much better than teaching a narrow legacy path first and fixing it later. B[Mint account] A --> C[Token account] A --> D[Token program] B --> E[Asset definition] C --> F[Balance storage] D --> G[CPI executes mint or transfer] `} /> ## Start With One Mint Instruction Here is the smallest useful minting example. ```rust use anchor_lang::prelude::*; use anchor_spl::token_interface::{ mint_to, Mint, MintTo, TokenAccount, TokenInterface, }; #[derive(Accounts)] pub struct MintRewards<'info> { #[account(mut)] pub authority: Signer<'info>, #[account(mut)] pub mint: InterfaceAccount<'info, Mint>, #[account(mut)] pub destination: InterfaceAccount<'info, TokenAccount>, pub token_program: Interface<'info, TokenInterface>, } pub fn mint_rewards(ctx: Context, amount: u64) -> Result<()> { let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_ctx = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ); mint_to(cpi_ctx, amount)?; Ok(()) } ``` ## Read That Example Slowly ### `mint: InterfaceAccount<'info, Mint>` This is the mint account. It tells Anchor to deserialize the account as mint data, regardless of whether the mint came from the classic token program or Token-2022. ### `destination: InterfaceAccount<'info, TokenAccount>` This is the balance-holding account that will receive the minted tokens. That account is tied to: * one mint * one owner ### `token_program: Interface<'info, TokenInterface>` This is the token program Anchor will call during CPI. That explicit account is important. Your instruction should not be vague about which token program is actually being trusted to execute the token logic. ### `MintTo` This is the CPI account struct for the token minting operation. You are building the account set the token program expects. ### `CpiContext::new(...)` This packages: * the target program * the CPI accounts Then `mint_to(...)` performs the actual token CPI. That is the pattern you will see over and over in Anchor token code. ## What This Example Still Does Not Validate The example above teaches the CPI shape, but it is not the full security story yet. Before you trust a minting flow in a real app, you still need to answer questions like: * is this the mint we intended to use? * is the destination token account for that same mint? * is the signer actually the authority that should be allowed to mint? * should the authority be a wallet signer or a PDA signer? That is why token integration is mostly an account-validation problem, not just a CPI syntax problem. ## Create A Mint With Anchor Constraints The current Anchor docs also support mint creation directly through account constraints. Here is the modern shape: ```rust use anchor_spl::token_interface::{Mint, TokenInterface}; #[derive(Accounts)] pub struct CreateMint<'info> { #[account(mut)] pub signer: Signer<'info>, #[account( init, payer = signer, mint::decimals = 6, mint::authority = signer.key(), mint::freeze_authority = signer.key(), )] pub mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } ``` This is worth reading carefully. Anchor is not just creating a generic account here. It is creating and initializing a mint account with token-specific rules. That is why the `mint::...` constraints matter. ## Token Transfers Should Usually Be Checked For transfers, the safer default is `transfer_checked`. ```rust use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked}; #[derive(Accounts)] pub struct TransferPoints<'info> { pub owner: Signer<'info>, pub mint: InterfaceAccount<'info, Mint>, #[account(mut)] pub from_ata: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub to_ata: InterfaceAccount<'info, TokenAccount>, pub token_program: Interface<'info, TokenInterface>, } pub fn transfer_points(ctx: Context, amount: u64, decimals: u8) -> Result<()> { let cpi_accounts = TransferChecked { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.from_ata.to_account_info(), to: ctx.accounts.to_ata.to_account_info(), authority: ctx.accounts.owner.to_account_info(), }; let cpi_ctx = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ); transfer_checked(cpi_ctx, amount, decimals)?; Ok(()) } ``` ## Why `transfer_checked` Is Better Than Hand-Waving The important beginner idea is not just “use this function because docs say so.” The real reason is that token amounts and decimals are part of the asset definition. A checked transfer forces the transfer logic to stay aligned with the mint’s decimal configuration. That is one more place where explicitness reduces bugs. ## The Real Validation Questions For Token Flows Whenever you read or write an Anchor token instruction, ask these questions: 1. which mint is this instruction supposed to work with? 2. do both token accounts belong to that mint? 3. who is the real authority over the action? 4. which token program is actually being called? 5. if a PDA is involved, where do the signer seeds come from? Those five questions matter more than memorizing CPI helper names. ## A Better Beginner Guardrail List For token flows, the first serious guardrails are: * validate that the token accounts match the expected mint * validate that the authority is who your business logic expects * do not assume the client passed the correct token program by accident * prefer explicit checked operations when the token API offers them * keep minting rules and supply rules on-chain, not only in the frontend ## Where PDAs Usually Enter Token Logic The lesson before this one taught PDA state. In token flows, PDAs often become: * mint authorities * vault authorities * claim record accounts * escrow state accounts That is why this lesson comes after PDAs. The PDA mental model is what makes token authority flows understandable. ## One Good Beginner Example To Keep In Mind A strong first token design usually looks like this: * one mint * one clearly defined authority model * one mint instruction or one transfer instruction * one or two failure-path tests A weak first token design tries to include: * staking * rewards * vesting * freeze logic * metadata * multiple mints * custom economics all at once. Do not do that. ## Suggested Test Cases If you are building token instructions, test these before moving on: 1. mint succeeds with the expected authority 2. mint fails with the wrong authority 3. transfer fails when the token accounts do not match the expected mint 4. transfer succeeds when the mint, accounts, and authority all line up Those tests teach much more than a giant happy-path-only example. ## Common Beginner Mistakes ### Mistake 1: treating token accounts like generic accounts They are not generic state bags. They carry mint-specific balance state governed by the token program. ### Mistake 2: ignoring the token program account If the token program is part of the instruction, it is part of the trust boundary. Treat it that way. ### Mistake 3: trusting client-provided decimals blindly If the operation depends on mint decimals, the mint itself matters. Do not treat decimals as casual UI metadata. ### Mistake 4: focusing only on CPI syntax Most token bugs are validation bugs. The CPI helper call is often the easiest part. ## What Comes Next The next lesson is where token flows and PDAs meet directly through signed CPI. That is the point where authority delegation becomes the main idea, not just token account wiring. ## Quick Check # Token Minting and Supply Control URL: https://www.learnsol.site/learn/anchor-programs/token-minting-project Markdown URL: https://www.learnsol.site/learn/anchor-programs/token-minting-project.mdx Build one Anchor token project step by step using a PDA mint authority, a config account, and on-chain supply rules that the client cannot bypass. This is the first Anchor project in this section where multiple earlier ideas finally meet in one build: * PDA design * token accounts * CPI * signer seeds * on-chain business rules That is why this project matters. It is not just “mint some tokens.” It is: make the program itself control minting, and make the rules live on-chain. ## What You Are Building You will build one reward-token system with these rules: * the mint authority is a PDA * an admin initializes the system * minting updates tracked on-chain supply state * minting fails if it would exceed a max supply cap This is the right first token project because it is small enough to finish, but serious enough to teach the real design pattern. ## The Core Idea Do not let the frontend decide who can mint. Do not let the client pretend a supply cap exists. Put the rule in the program. That means the project needs two kinds of state: 1. token state in the mint account 2. business state in your own config account The token program controls balances. Your program controls the business rule for when minting is allowed. ## The Architecture This project uses three main pieces: * a `Config` account that stores the admin and total minted amount * a mint account whose mint authority is a PDA * a mint instruction that performs a token CPI only after your own checks pass That is the cleanest way to teach program-controlled minting. B[Config PDA checks] B --> C[Projected supply check] C --> D[Mint PDA signer seeds] D --> E[Token program mint_to CPI] E --> F[Recipient token account] E --> G[Config minted_total updates] `} /> ## Step 1: Define The State You Actually Need Start with the smallest useful config state. ```rust #[account] #[derive(InitSpace)] pub struct Config { pub admin: Pubkey, pub minted_total: u64, } pub const MAX_SUPPLY: u64 = 1_000_000_000; ``` Why each field exists: * `admin` tells the program who is allowed to request minting * `minted_total` lets the program enforce a supply cap itself That is enough. Do not add pause flags, quotas, or role hierarchies in the first version. ## Step 2: Create The Mint With A PDA As Authority The current Anchor token docs explicitly support a PDA mint authority pattern. That is the correct base for this project. ```rust use anchor_lang::prelude::*; use anchor_spl::token_interface::{Mint, TokenInterface}; #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] pub admin: Signer<'info>, #[account( init, payer = admin, space = 8 + Config::INIT_SPACE, seeds = [b"config"], bump, )] pub config: Account<'info, Config>, #[account( init, payer = admin, mint::decimals = 6, mint::authority = mint, mint::freeze_authority = mint, seeds = [b"mint"], bump, )] pub mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } ``` Read the important part carefully: ```rust mint::authority = mint mint::freeze_authority = mint seeds = [b"mint"] bump ``` That means the mint account itself is also the PDA authority. This is a strong beginner pattern because it keeps the authority model simple. ## Step 3: Initialize The Config State Now write the initialize handler. ```rust pub fn initialize(ctx: Context) -> Result<()> { let config = &mut ctx.accounts.config; config.admin = ctx.accounts.admin.key(); config.minted_total = 0; Ok(()) } ``` This handler is intentionally boring. That is correct. The account constraints already did the heavy setup work. The handler only needs to store the initial business state. ## Step 4: Define The Mint Instruction Accounts Now define the instruction that will mint tokens. ```rust use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, }; #[derive(Accounts)] pub struct MintRewards<'info> { #[account(mut)] pub admin: Signer<'info>, #[account( mut, seeds = [b"config"], bump, has_one = admin, )] pub config: Account<'info, Config>, #[account( mut, seeds = [b"mint"], bump, )] pub mint: InterfaceAccount<'info, Mint>, #[account( init_if_needed, payer = admin, associated_token::mint = mint, associated_token::authority = recipient, associated_token::token_program = token_program, )] pub recipient_token_account: InterfaceAccount<'info, TokenAccount>, /// CHECK: recipient only needs to be a valid public key for ATA derivation pub recipient: UncheckedAccount<'info>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, } ``` This context enforces most of the project already. What it guarantees: * the caller must sign as `admin` * the config account must be the expected PDA * the stored config admin must match the signer * the mint must be the expected mint PDA * the recipient token account is derived for the right mint and recipient This is the real lesson again: good Anchor code pushes rules into the account context first. ## Step 5: Enforce The Supply Rule Before The CPI Now write the business logic. ```rust use anchor_spl::token_interface::{self, MintTo}; pub fn mint_rewards(ctx: Context, amount: u64) -> Result<()> { let config = &mut ctx.accounts.config; let next_total = config .minted_total .checked_add(amount) .ok_or(TokenProjectError::Overflow)?; require!(next_total <= MAX_SUPPLY, TokenProjectError::SupplyCapExceeded); let signer_seeds: &[&[&[u8]]] = &[&[b"mint", &[ctx.bumps.mint]]]; let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.recipient_token_account.to_account_info(), authority: ctx.accounts.mint.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ) .with_signer(signer_seeds); token_interface::mint_to(cpi_context, amount)?; config.minted_total = next_total; Ok(()) } ``` ## Read The Mint Logic In The Right Order ### First, update the projected supply ```rust let next_total = config .minted_total .checked_add(amount) .ok_or(TokenProjectError::Overflow)?; ``` This prevents arithmetic bugs from turning into silent bad state. ### Then enforce the cap ```rust require!(next_total <= MAX_SUPPLY, TokenProjectError::SupplyCapExceeded); ``` This is the actual business rule. The frontend cannot override it. That is the whole point of the project. ### Then prepare PDA signer seeds ```rust let signer_seeds: &[&[&[u8]]] = &[&[b"mint", &[ctx.bumps.mint]]]; ``` These seeds match the PDA rule that created the mint. That is what allows the mint PDA to act as the mint authority during CPI. ### Then perform the CPI ```rust let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, ) .with_signer(signer_seeds); ``` This is the same CPI model from the previous lesson. The only difference is that now the callee is the token program and the signer is a PDA mint authority. ### Then commit your own state update ```rust config.minted_total = next_total; ``` This is important. Your program is not only minting tokens. It is also maintaining its own rule-tracking state. That is what makes the project more than a raw token CPI wrapper. ## Step 6: Add Clear Errors The project should fail clearly when business rules are violated. ```rust #[error_code] pub enum TokenProjectError { #[msg("Minted total overflowed")] Overflow, #[msg("Minting would exceed the max supply")] SupplyCapExceeded, } ``` Specific errors make tests and client behavior much easier to reason about. ## Step 7: Build The Test Plan Before You Expand The Project Do not add more features yet. Test the core rule first. Minimum test plan: 1. initialize sets admin and zero minted total 2. mint succeeds for the admin 3. minted total updates correctly 4. non-admin mint attempt fails 5. mint over the supply cap fails That is enough to prove the design. ## Why This Project Is Strong This project teaches one of the most important real patterns in Anchor: * let the token program handle token balances * let your own program handle business rules * use a PDA when the program itself should control authority That pattern appears everywhere in real Solana apps. ## Common Mistakes ### Mistake 1: enforcing the supply cap only in the client That is not a real rule. If the rule matters, the program must enforce it. ### Mistake 2: using a wallet as the long-term mint authority when the program should control minting If the business logic belongs on-chain, the authority model should usually belong on-chain too. ### Mistake 3: forgetting that the signer seeds must match the mint PDA rule If the PDA was derived with `seeds = [b"mint"]`, your signer seeds must reflect that exact rule. ### Mistake 4: trying to add reward logic, claim logic, and quotas immediately That makes the first build harder without improving the core lesson. The first version should prove one thing clearly. ## One Good Extension If the base version works, add exactly one improvement: * a `UserQuota` PDA that limits how much one recipient can receive That is a good extension because it deepens the same mental model: * more PDA state * more business rules * same token CPI pattern ## What Comes Next The next project should take the same ideas and make the state machine more complex. That is where escrow becomes useful. ## Quick Check # Collections and String Handling Practice URL: https://www.learnsol.site/learn/rust-foundations/collections-and-string-handling-practice Markdown URL: https://www.learnsol.site/learn/rust-foundations/collections-and-string-handling-practice.mdx Practice vectors, hash maps, strings, and parsing by building one transaction log analyzer in Rust. This practice page should not split your attention across multiple projects. It should give you one realistic program that forces the collections lesson to become usable. That is what this page does. The project is small on purpose. If you can store records, parse input, group totals, and answer simple queries without getting lost, then this section is doing its job. ## The Project Build a transaction log analyzer. The program will read a small list of text commands like these: ```text send alice bob 500 send bob carol 200 send alice dave 300 send carol alice 100 ``` Then it will: * parse each line * store every valid transfer in a `Vec` * calculate per-wallet totals with `HashMap` * print a readable summary This is the right project for this lesson because it uses the exact skills you just learned: * `Vec` for ordered history * `HashMap` for lookup and aggregation * `String` for owned stored data * `&str` for borrowed parsing input * `split_whitespace`, `parse`, and `trim` for text processing ## What You Are Proving By the end, you should be able to explain: * why the transfer history belongs in a vector * why per-wallet totals belong in a hash map * why parsed input starts as `&str` and later becomes `String` * where ownership is required and where borrowing is enough If you cannot explain those four things, the code is not the real win yet. ## Step 1: Define The Core Type Start with one struct. ```rust #[derive(Debug, Clone)] struct Transfer { from: String, to: String, amount: u64, } ``` This struct is intentionally small. You do not need timestamps, memo fields, signatures, token metadata, or network state. Those would only distract from the collections lesson. ## Step 2: Parse One Line Correctly Write one function that turns a command line into a `Transfer`. ```rust fn parse_transfer(line: &str) -> Result { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() != 4 { return Err(String::from("expected format: send ")); } if parts[0] != "send" { return Err(String::from("command must start with 'send'")); } let amount = parts[3] .parse::() .map_err(|_| String::from("amount must be a positive integer"))?; Ok(Transfer { from: parts[1].to_string(), to: parts[2].to_string(), amount, }) } ``` ### What this step teaches * `line` is borrowed as `&str` because parsing does not require ownership of the whole input * `parts` becomes `Vec<&str>` because `split_whitespace` borrows slices from the original line * `from` and `to` become `String` because the `Transfer` struct needs to own them after parsing finishes That ownership shift matters. It is the main string-handling idea in this practice. ## Step 3: Store Every Parsed Transfer In A Vector Now use a vector as the ordered history. ```rust fn main() { let input = vec![ "send alice bob 500", "send bob carol 200", "send alice dave 300", "send carol alice 100", ]; let mut transfers: Vec = Vec::new(); for line in input { match parse_transfer(line) { Ok(transfer) => transfers.push(transfer), Err(error) => println!("skipping line: {} | {}", line, error), } } println!("parsed transfers: {:?}", transfers); } ``` A vector is the right tool here because: * order matters * you want to keep every record * you want to iterate through the whole history later ## Step 4: Build A Hash Map For Totals Sent Now aggregate the transfers by sender. ```rust use std::collections::HashMap; fn totals_sent(transfers: &[Transfer]) -> HashMap { let mut totals = HashMap::new(); for transfer in transfers { *totals.entry(transfer.from.clone()).or_insert(0) += transfer.amount; } totals } ``` ### Why the hash map belongs here A vector is good for the raw history. A hash map is better for questions like: * how much did Alice send in total * how much did Bob send in total That is keyed lookup. That is what `HashMap` is for. ## Step 5: Build A Hash Map For Totals Received Do the same thing for recipients. ```rust use std::collections::HashMap; fn totals_received(transfers: &[Transfer]) -> HashMap { let mut totals = HashMap::new(); for transfer in transfers { *totals.entry(transfer.to.clone()).or_insert(0) += transfer.amount; } totals } ``` This is a good checkpoint. You now have: * one vector for the full history * one hash map for sent totals * one hash map for received totals That is exactly the kind of separation a real program needs. ## Step 6: Print A Useful Summary Now make the program produce something readable. ```rust fn main() { let input = vec![ "send alice bob 500", "send bob carol 200", "send alice dave 300", "send carol alice 100", ]; let mut transfers: Vec = Vec::new(); for line in input { match parse_transfer(line) { Ok(transfer) => transfers.push(transfer), Err(error) => println!("skipping line: {} | {}", line, error), } } let sent = totals_sent(&transfers); let received = totals_received(&transfers); println!("transfers parsed: {}", transfers.len()); println!("totals sent: {:?}", sent); println!("totals received: {:?}", received); } ``` You can improve the formatting later. At this stage, correctness matters more than pretty output. ## Step 7: Add One Real Query Function Make one helper that answers a practical question. ```rust fn transfers_for_wallet<'a>(transfers: &'a [Transfer], wallet: &str) -> Vec<&'a Transfer> { transfers .iter() .filter(|transfer| transfer.from == wallet || transfer.to == wallet) .collect() } ``` This proves another useful distinction: * the history vector owns `Transfer` values * the query result can return `&Transfer` references because it only borrows from the history That is a good ownership decision. You do not need to clone the whole history just to answer one query. ## Step 8: Handle Bad Input On Purpose A practice project is weak if every line is valid. Add bad input and make sure your parser behaves correctly. ```rust let input = vec![ "send alice bob 500", "send bob carol two_hundred", "deposit alice 400", "send carol alice 100", ]; ``` What should happen: * valid `send` commands become `Transfer` records * invalid lines do not panic the program * invalid lines report a clear reason and get skipped That is a much stronger exercise than just parsing the happy path. ## Suggested Build Order Use this order and do not rush it: 1. write `Transfer` 2. write `parse_transfer` 3. parse a few valid lines into `Vec` 4. add one invalid line and verify the error path 5. write `totals_sent` 6. write `totals_received` 7. write `transfers_for_wallet` 8. print one full summary That order keeps the difficulty under control. ## Common Mistakes ### Mistake 1: using a hash map for the raw history If you store the transaction log itself in a hash map, you lose the simple ordered record list. The history is a vector. The summary lookups are hash maps. ### Mistake 2: converting everything to `String` too early When parsing, borrowed `&str` pieces are fine for temporary work. Only convert to `String` when the final stored struct needs ownership. ### Mistake 3: cloning more than necessary If a function only needs to read transfers, borrow `&[Transfer]`. Do not clone the whole vector just because borrowing feels harder. ### Mistake 4: mixing parsing logic with summary logic too early First parse the input. Then aggregate. Then print results. If all three happen in one tangled block, the lesson gets harder than it needs to be. ## What A Good Finished Version Looks Like By the end, your program should: * parse a list of transfer commands * skip invalid lines safely * store valid transfers in order * compute total sent per wallet * compute total received per wallet * answer at least one query for a specific wallet That is enough. If you can do that cleanly, you understand the point of this lesson. ## Optional Extension Only add this after the main version is clean. Add a function that builds a net balance change per wallet: * sent amount subtracts from the wallet * received amount adds to the wallet That is a good extension because it still uses the same core tools: * vectors * hash maps * string parsing * careful ownership ## What Comes Next After Rust Foundations, the next layers of the curriculum start asking you to use these same ideas in more realistic Solana code. That only works if collections and strings now feel normal instead of magical. *** ## Quick Check # Collections and String Handling in Rust URL: https://www.learnsol.site/learn/rust-foundations/collections-and-string-handling Markdown URL: https://www.learnsol.site/learn/rust-foundations/collections-and-string-handling.mdx Learn when to use vectors, hash maps, strings, and string slices so you can store, search, and transform data cleanly in Rust programs. Up to this point, most of your Rust examples have been small. A few variables. A few structs. A few enums. That is enough to learn syntax and state modeling, but it is not enough to build programs that hold more than one thing. Sooner or later, you need to answer questions like these: * how do I store many transactions * how do I look up one account by key * how do I keep a growing history * how do I parse text input without fighting ownership That is what this lesson is for. The mental model is simple: * use `Vec` when order matters and you have many items * use `HashMap` when fast lookup by key matters * use `String` when you need owned, growable text * use `&str` when you only need to borrow text temporarily Collections solve storage problems. Strings solve text ownership problems. Most real Rust programs need both at the same time. ", definition: "An ordered growable list. Use it when history, sequence, or full iteration matters." }, { term: "HashMap", definition: "A keyed lookup table. Use it when the main question is 'find the value for this key'." }, { term: "String", definition: "Owned, growable text. Use it when the program needs to keep or build text data." }, { term: "&str", definition: "A borrowed string slice. Use it for temporary read-only text access and parsing." } ]} /> B --> F[Vec] A --> C --> G[HashMap] D --> H[String] E --> I[&str] `} /> ## Start With `Vec` A vector is the default collection in Rust. Use it when: * you want to keep items in order * you expect the list to grow or shrink * you want to iterate through everything ```rust fn main() { let mut balances = Vec::new(); balances.push(500); balances.push(1_200); balances.push(900); println!("all balances: {:?}", balances); } ``` This is the right starting point because vectors are simple: * one type of item * one ordered list * easy iteration ## The Two Main Ways To Read From A Vector ```rust fn main() { let balances = vec![500, 1_200, 900]; let first = balances[0]; println!("first balance: {}", first); let maybe_second = balances.get(1); println!("second balance: {:?}", maybe_second); } ``` These two access patterns are not equally safe. * `balances[0]` will panic if the index does not exist * `balances.get(1)` returns `Option<&T>` For beginner code, `get` teaches the better habit because it forces you to handle the missing case. ```rust fn main() { let balances = vec![500, 1_200, 900]; match balances.get(3) { Some(balance) => println!("found balance: {}", balance), None => println!("no balance at that index"), } } ``` ## Iterating Over A Vector Once you have many items, iteration becomes the normal pattern. ```rust fn main() { let balances = vec![500, 1_200, 900]; for balance in &balances { println!("balance: {}", balance); } } ``` Use `&balances` when you only need to read. Use `&mut balances` when you want to change each item. ```rust fn main() { let mut balances = vec![500, 1_200, 900]; for balance in &mut balances { *balance += 100; } println!("updated balances: {:?}", balances); } ``` That `*balance` matters because the loop variable is a mutable reference, not the number itself. ## A Better Vector Example Vectors become more useful when they store structs instead of raw numbers. ```rust #[derive(Debug)] struct Transfer { to: String, amount: u64, } fn main() { let history = vec![ Transfer { to: String::from("alice"), amount: 500, }, Transfer { to: String::from("bob"), amount: 1_200, }, ]; for transfer in &history { println!("sent {} lamports to {}", transfer.amount, transfer.to); } } ``` Now the vector is not just "a list." It is a transaction history. That is the right way to think about collections. ## When `Vec` Stops Being Enough A vector is good when you want to scan through items. It is weaker when you need to find one thing by key over and over. For example, if you have many accounts and you want to look up one by address, this becomes awkward: ```rust #[derive(Debug)] struct Account { address: String, balance: u64, } fn find_account<'a>(accounts: &'a [Account], address: &str) -> Option<&'a Account> { accounts.iter().find(|account| account.address == address) } ``` This works. But every lookup scans the list. If keyed lookup is the main operation, `HashMap` is a better fit. ## Use `HashMap` For Keyed Lookup ```rust use std::collections::HashMap; fn main() { let mut balances = HashMap::new(); balances.insert(String::from("alice"), 1_500); balances.insert(String::from("bob"), 900); println!("alice balance: {:?}", balances.get("alice")); } ``` This changes the mental model. With a vector, you usually think in terms of position. With a hash map, you think in terms of keys. That is the real reason to use it. ## Reading From A HashMap `get` returns `Option<&V>` for the same reason vector `get` does. The key might not exist. ```rust use std::collections::HashMap; fn main() { let mut balances = HashMap::new(); balances.insert(String::from("alice"), 1_500); match balances.get("bob") { Some(balance) => println!("bob balance: {}", balance), None => println!("bob was not found"), } } ``` This is exactly the same pattern you learned with `Option`. Collections do not introduce a new idea here. They reuse the same type discipline. ## Updating A HashMap Cleanly The `entry` API is one of the most useful `HashMap` patterns in Rust. ```rust use std::collections::HashMap; fn main() { let mut transaction_counts: HashMap = HashMap::new(); let wallet = String::from("alice"); *transaction_counts.entry(wallet).or_insert(0) += 1; println!("counts: {:?}", transaction_counts); } ``` Why this matters: * if the key exists, use its current value * if the key does not exist, insert a default first That is cleaner than writing separate "check then insert" logic. ## `String` Versus `&str` This is the other half of the lesson. Many Rust collection problems become string problems very quickly. The short version is: * `String` owns text * `&str` borrows text ```rust fn print_label(label: &str) { println!("label: {}", label); } fn main() { let owner = String::from("alice"); let network = "devnet"; print_label(&owner); print_label(network); } ``` Why this works: * `owner` is an owned `String` * `&owner` gives a borrowed string slice * `network` is already a string slice literal ## When You Need `String` Use `String` when the program needs to own the text. Examples: * storing names inside structs * inserting keys into a `HashMap` * returning text built at runtime ```rust #[derive(Debug)] struct Wallet { owner: String, } fn build_message(owner: &str, amount: u64) -> String { format!("{} received {} lamports", owner, amount) } ``` Here: * `owner` inside `Wallet` must be owned, so it is `String` * the return value of `build_message` must also be owned, so it is `String` ## When `&str` Is Better Use `&str` when you only need to borrow text temporarily. Examples: * function parameters for read-only text input * matching command names * checking prefixes or suffixes ```rust fn parse_command(input: &str) { if input.starts_with("send") { println!("send command"); } else if input.starts_with("balance") { println!("balance command"); } else { println!("unknown command"); } } ``` This is better than taking `String` because the function does not need ownership. ## Parsing Text With `split` And `trim` You will do this constantly in CLI tools and data processing. ```rust fn main() { let input = "send,alice,500"; let parts: Vec<&str> = input.split(',').collect(); println!("parts: {:?}", parts); } ``` And for cleanup: ```rust fn main() { let input = " alice "; let cleaned = input.trim(); println!("cleaned: '{}'", cleaned); } ``` These small tools matter because messy string input is one of the fastest ways to make beginner Rust code feel confusing. ## Put Collections And Strings Together This is the pattern you will actually use. ```rust use std::collections::HashMap; fn main() { let commands = vec![ "deposit alice 500", "deposit bob 200", "deposit alice 300", ]; let mut balances: HashMap = HashMap::new(); for command in commands { let parts: Vec<&str> = command.split_whitespace().collect(); if parts.len() != 3 { continue; } let name = parts[1]; let amount = match parts[2].parse::() { Ok(value) => value, Err(_) => continue, }; *balances.entry(name.to_string()).or_insert(0) += amount; } println!("balances: {:?}", balances); } ``` This small example teaches a lot: * `Vec<&str>` from splitting input * `HashMap` for keyed storage * `parse::()` for numeric conversion * `entry(...).or_insert(...)` for updates That is much closer to real Rust work than memorizing five collection types in isolation. ## Common Mistakes ### Mistake 1: using a vector when the real operation is keyed lookup If the main question is "find this item by name or address," a `HashMap` is often the better model. ### Mistake 2: taking `String` everywhere If a function only reads text, prefer `&str`. That makes the function easier to call and avoids unnecessary ownership transfers. ### Mistake 3: indexing when `get` would teach better habits Indexing is fine when you know the position exists. When the position might be missing, `get` is the safer pattern. ### Mistake 4: cloning strings without understanding why Sometimes cloning is necessary. Often it is just a sign that ownership is still unclear. Before cloning, ask: * do I need to own this value here * or do I only need to borrow it *** ## Quick Check ## Practice Before moving on, make sure you can do these without guessing: 1. Explain when a vector is a better fit than a hash map. 2. Explain why `HashMap::get` returns `Option<&V>`. 3. Write one function that takes `&str` and one that returns `String`, and explain why each choice is correct. 4. Parse a small command string into pieces and update a `HashMap` from it. ## What You Should Know Now After this lesson, you should be able to say: * vectors are for ordered groups of items * hash maps are for lookup by key * `String` owns text * `&str` borrows text * parsing text usually means splitting, trimming, and converting carefully That is the foundation you need for the practice page. The next step is to use these tools in a more realistic exercise where you store, group, and search data without letting the program shape drift out of control. # Enums and Pattern Matching Practice URL: https://www.learnsol.site/learn/rust-foundations/enums-and-pattern-matching-practice Markdown URL: https://www.learnsol.site/learn/rust-foundations/enums-and-pattern-matching-practice.mdx Practice enums and pattern matching through a small wallet manager exercise that models account state, transaction state, and fallible actions explicitly. This practice page has one job. It should prove that you can model state with enums instead of hiding it in booleans and optional fields. If the previous lesson made sense, this page is where that understanding becomes real. Do not try to make this bigger than it needs to be. The goal is not to build a complete wallet product. The goal is to make enums carry real meaning and then handle them cleanly with `match`. ## What You Are Building Build a tiny wallet manager with three ideas: * an account can be in one of several states * a transaction can be in one of several states * wallet operations can succeed or fail for explicit reasons That is enough. You do not need NFTs, staking, network simulation, or recovery flows. Those would only distract from the real practice target. ## What This Practice Should Prove By the end, you should be able to explain: * why `AccountStatus` is better as an enum than as a struct of flags * why `TransactionStatus` should carry different data in different variants * when to use `match` instead of `if let` * how `Result` makes wallet operations honest about failure ## Step 1: Define The Core Enums Start here. Do not write methods yet. Just define the state model. ```rust #[derive(Debug, Clone, PartialEq)] enum AccountType { System, Token { mint: String }, } #[derive(Debug, Clone, PartialEq)] enum AccountStatus { Active, Frozen { reason: String }, Closed, } #[derive(Debug, Clone, PartialEq)] enum TransactionStatus { Pending, Confirmed { signature: String }, Failed { error: String }, } #[derive(Debug)] enum WalletError { AccountNotFound(String), AccountFrozen(String), AccountClosed(String), InsufficientBalance { requested: u64, available: u64 }, } ``` ### Why this is the right starting point Each enum represents a different kind of decision: * `AccountType` answers what kind of account this is * `AccountStatus` answers whether the account can still be used * `TransactionStatus` answers what happened to the transaction * `WalletError` answers why an operation failed That separation matters. Do not merge them into one vague status type. ## Step 2: Add The Main Structs Now define one account type and one transaction type. ```rust #[derive(Debug, Clone)] struct Account { address: String, balance: u64, account_type: AccountType, status: AccountStatus, } #[derive(Debug, Clone)] struct Transaction { from: String, to: String, amount: u64, status: TransactionStatus, } #[derive(Debug)] struct WalletManager { accounts: Vec, transactions: Vec, } ``` Keep the shape small. This is practice. You are not trying to model the entire Solana runtime. ## Step 3: Write The First Useful `match` Start with a method that only reads state. That keeps the first step simple. ```rust impl Account { fn status_label(&self) -> &str { match &self.status { AccountStatus::Active => "active", AccountStatus::Frozen { .. } => "frozen", AccountStatus::Closed => "closed", } } } ``` This method is small, but it proves the most important habit: * inspect the enum * handle every variant * keep each branch specific ## Step 4: Make State Affect Behavior Now write a method that decides whether an account can send funds. ```rust impl Account { fn can_send(&self, amount: u64) -> Result<(), WalletError> { match &self.status { AccountStatus::Frozen { reason } => { return Err(WalletError::AccountFrozen(reason.clone())); } AccountStatus::Closed => { return Err(WalletError::AccountClosed(self.address.clone())); } AccountStatus::Active => {} } if amount > self.balance { return Err(WalletError::InsufficientBalance { requested: amount, available: self.balance, }); } Ok(()) } } ``` ### What this step teaches * enums do not just label state * enums control legal behavior * `Result` makes failure reasons explicit That is the whole point of this exercise. ## Step 5: Model Transaction Outcomes Properly Now write a helper on `Transaction`. ```rust impl Transaction { fn summary(&self) -> String { match &self.status { TransactionStatus::Pending => { format!("{} -> {} | {} lamports | pending", self.from, self.to, self.amount) } TransactionStatus::Confirmed { signature } => { format!( "{} -> {} | {} lamports | confirmed: {}", self.from, self.to, self.amount, signature ) } TransactionStatus::Failed { error } => { format!( "{} -> {} | {} lamports | failed: {}", self.from, self.to, self.amount, error ) } } } } ``` This is where the earlier lesson should click. Each transaction state carries different data because each state means something different. If all states shared the same fields, the enum would not be needed. ## Step 6: Build One Real Wallet Operation Now implement one `transfer` method on `WalletManager`. This is the main practice target. ```rust impl WalletManager { fn transfer(&mut self, from: &str, to: &str, amount: u64) -> Result<(), WalletError> { let sender_index = self .accounts .iter() .position(|account| account.address == from) .ok_or_else(|| WalletError::AccountNotFound(from.to_string()))?; let receiver_index = self .accounts .iter() .position(|account| account.address == to) .ok_or_else(|| WalletError::AccountNotFound(to.to_string()))?; self.accounts[sender_index].can_send(amount)?; self.accounts[sender_index].balance -= amount; self.accounts[receiver_index].balance += amount; self.transactions.push(Transaction { from: from.to_string(), to: to.to_string(), amount, status: TransactionStatus::Confirmed { signature: String::from("demo-signature-123"), }, }); Ok(()) } } ``` ### Why this is enough You do not need minting, burning, staking, and freezing logic all at once. One good transfer method already tests: * account lookup * status validation * error propagation * transaction state modeling That is enough for this stage. ## Step 7: Add One Failure Path On Purpose A practice page is weak if everything only succeeds. Create one frozen account and prove the transfer fails for the correct reason. ```rust fn main() { let mut manager = WalletManager { accounts: vec![ Account { address: String::from("alice"), balance: 1_000, account_type: AccountType::System, status: AccountStatus::Frozen { reason: String::from("manual review"), }, }, Account { address: String::from("bob"), balance: 500, account_type: AccountType::System, status: AccountStatus::Active, }, ], transactions: Vec::new(), }; let result = manager.transfer("alice", "bob", 200); match result { Ok(()) => println!("unexpected success"), Err(error) => println!("expected failure: {:?}", error), } } ``` This matters more than adding more features. It proves your enum state is actually controlling program behavior. ## Suggested Build Order If you want to work cleanly, build in this exact order: 1. write the enums 2. write the structs 3. implement `status_label` 4. implement `can_send` 5. implement `summary` 6. implement `transfer` 7. test one success case 8. test one frozen-account failure 9. test one insufficient-balance failure Do not skip straight to the full manager and then debug everything at once. ## Common Mistakes ### Mistake 1: using booleans instead of enums This is weak: ```rust struct Account { is_active: bool, is_frozen: bool, is_closed: bool, } ``` It allows nonsense combinations. Use one enum instead. ### Mistake 2: storing error strings everywhere If a wallet operation can fail for known reasons, model those reasons with `WalletError`. Do not just return random strings from every branch. ### Mistake 3: using `if let` for every case If you need to handle all states, use `match`. That is what gives you complete coverage. ### Mistake 4: making the practice page too ambitious If your file turns into a fake wallet protocol with ten features, you are no longer practicing enums. You are hiding the lesson inside too much surface area. ## What To Hand Yourself At The End When you finish, you should have a program where: * one account is active * one account is frozen or closed * one transfer succeeds * one transfer fails * at least one transaction summary prints correctly If you can explain why each failure happened by pointing to enum variants, the practice worked. ## Optional Extension Only do this if the main version is already clean. Add one token account variant: ```rust AccountType::Token { mint: String } ``` Then update one summary method so token accounts print differently from system accounts. That is a good extension because it reinforces the same lesson instead of introducing a new one. ## What Comes Next The next lesson moves to collections and strings. That is the right next step because once your state model is clear, you need better tools for storing, grouping, and transforming more data. *** ## Quick Check # Enums and Pattern Matching in Rust URL: https://www.learnsol.site/learn/rust-foundations/enums-and-pattern-matching Markdown URL: https://www.learnsol.site/learn/rust-foundations/enums-and-pattern-matching.mdx Learn how Rust enums model mutually exclusive states and how match, if let, Option, and Result help you handle those states explicitly. People often describe enums as "a value with a few named options." That is technically true, but it is too weak to be useful. The better mental model is this: An enum lets one value be in exactly one valid state at a time, and pattern matching forces you to handle that state explicitly. That is why enums matter so much in Rust. They are not just cleaner constants. They are how Rust models situations where different states carry different data and require different behavior. Enums become much more useful once you stop thinking of them as labels and start thinking of them as state shapes. B --> E A --> C --> E A --> D --> E `} /> ## Why Enums Exist A struct is good when every instance has the same shape. For example, every `Wallet` might have: * an owner * a balance * a frozen flag That is one consistent layout. But some problems do not have one consistent layout. A transaction status might be: * pending with no extra data yet * confirmed with a slot and signature * failed with an error message Those states do not all need the same fields. If you try to force them into one struct, you usually end up with weak designs like: ```rust struct TransactionStatus { is_pending: bool, confirmed_slot: Option, signature: Option, error: Option, } ``` This compiles, but the shape is vague. It allows nonsense combinations like: * `is_pending = true` and `signature = Some(...)` * `confirmed_slot = Some(...)` and `error = Some(...)` The type does not protect the meaning. An enum does. ## Your First Useful Enum ```rust #[derive(Debug)] enum TransactionStatus { Pending, Confirmed { slot: u64, signature: String }, Failed { error: String }, } ``` This is stronger because a value can only be one of these states: * `Pending` * `Confirmed { ... }` * `Failed { ... }` Not two at once. Not half-filled. Not ambiguous. That is the main benefit. ## How Variant Data Works Enums can store different kinds of variants. ```rust #[derive(Debug)] enum WalletAction { Deposit(u64), Withdraw(u64), Freeze, Rename { new_owner: String }, } ``` This one enum contains four different shapes: * `Deposit(u64)` stores one amount * `Withdraw(u64)` stores one amount * `Freeze` stores no extra data * `Rename { new_owner: String }` stores named data Rust lets you do this because the enum value is always exactly one variant at runtime. ## Pattern Matching Is The Other Half Enums are only half the story. Pattern matching is what makes them useful. When you use `match`, you tell Rust: * inspect the current variant * unpack any data inside it * run the correct branch ```rust #[derive(Debug)] enum WalletAction { Deposit(u64), Withdraw(u64), Freeze, Rename { new_owner: String }, } fn describe_action(action: WalletAction) { match action { WalletAction::Deposit(amount) => { println!("deposit {} lamports", amount); } WalletAction::Withdraw(amount) => { println!("withdraw {} lamports", amount); } WalletAction::Freeze => { println!("freeze wallet"); } WalletAction::Rename { new_owner } => { println!("rename wallet to {}", new_owner); } } } ``` ## Why `match` Matters `match` is not just nicer syntax. It gives you exhaustiveness. That means Rust checks that you handled every variant. If you later add this variant: ```rust enum WalletAction { Deposit(u64), Withdraw(u64), Freeze, Rename { new_owner: String }, Close, } ``` your old `match` stops compiling until you handle `Close` too. That is one of the biggest reasons Rust code stays honest as it grows. The compiler forces your control flow to stay in sync with your data model. ## A Small End-To-End Example Now put the pieces together. ```rust #[derive(Debug)] enum TransactionStatus { Pending, Confirmed { slot: u64, signature: String }, Failed { error: String }, } fn print_status(status: &TransactionStatus) { match status { TransactionStatus::Pending => { println!("transaction is still pending"); } TransactionStatus::Confirmed { slot, signature } => { println!("confirmed in slot {} with signature {}", slot, signature); } TransactionStatus::Failed { error } => { println!("transaction failed: {}", error); } } } fn main() { let pending = TransactionStatus::Pending; let confirmed = TransactionStatus::Confirmed { slot: 245, signature: String::from("5abc...xyz"), }; let failed = TransactionStatus::Failed { error: String::from("insufficient funds"), }; print_status(&pending); print_status(&confirmed); print_status(&failed); } ``` ### What this code is proving * one type can represent multiple valid runtime states * different states can carry different data * `match` can both inspect and unpack that data * each branch can stay specific to the current state That is the core pattern you will keep using. ## `Option` Is Just An Enum People often learn `Option` as a special feature. It is not special. It is a normal enum from the standard library. ```rust enum Option { Some(T), None, } ``` That means every time you use `Option`, you are already using enums and pattern matching. ```rust fn find_balance(name: &str) -> Option { match name { "alice" => Some(1_500_000), "bob" => Some(800_000), _ => None, } } fn main() { let balance = find_balance("alice"); match balance { Some(amount) => println!("balance: {}", amount), None => println!("wallet not found"), } } ``` ### Why `Option` matters Without `Option`, many languages use: * `null` * sentinel values * vague booleans Rust makes absence explicit in the type. You cannot quietly ignore it. ## `Result` Is Also Just An Enum `Result` works the same way. ```rust enum Result { Ok(T), Err(E), } ``` Use it when an operation can: * succeed with a value * fail with an error ```rust fn withdraw(balance: u64, amount: u64) -> Result { if amount > balance { return Err(String::from("insufficient balance")); } Ok(balance - amount) } fn main() { let result = withdraw(500, 200); match result { Ok(new_balance) => println!("new balance: {}", new_balance), Err(message) => println!("withdraw failed: {}", message), } } ``` ### Why `Result` matters This is much stronger than returning: * a magic value like `-1` * a success boolean plus separate output state * a string that might or might not mean failure The type tells you the operation can fail. The caller has to deal with that honestly. ## When To Use `if let` Sometimes a full `match` is more than you need. If you only care about one pattern, `if let` is often cleaner. ```rust fn main() { let status = TransactionStatus::Confirmed { slot: 245, signature: String::from("5abc...xyz"), }; if let TransactionStatus::Confirmed { slot, signature } = status { println!("confirmed in slot {} with signature {}", slot, signature); } } ``` Use `if let` when: * you only care about one successful pattern * the other cases do not need detailed handling Use `match` when: * every variant matters * you want the compiler to enforce complete handling * each branch has meaningful logic ## A More Realistic Example This example looks closer to what you will build in practice pages and future Solana code. ```rust #[derive(Debug)] enum AccountType { System, Token { mint: String }, Program { executable: bool }, } fn describe_account(account_type: &AccountType) -> String { match account_type { AccountType::System => String::from("system account"), AccountType::Token { mint } => { format!("token account for mint {}", mint) } AccountType::Program { executable } => { if *executable { String::from("executable program account") } else { String::from("program-owned data account") } } } } ``` This is the real strength of enums. They let you model meaning directly instead of hiding it in scattered booleans and optional fields. ## Common Mistakes ### Mistake 1: Using enums like simple labels If all variants are just names and none of them ever carry meaningful data, ask whether the enum is actually modeling state or just replacing strings. Sometimes that is still fine. Often it means the model is still too shallow. ### Mistake 2: Avoiding `match` with weak shortcuts If every branch matters, do not reach for nested `if` statements just because they feel familiar. `match` is clearer and safer. ### Mistake 3: Fighting the type system with duplicated flags This is weak: ```rust struct Status { pending: bool, failed: bool, error: Option, } ``` This is stronger: ```rust enum Status { Pending, Confirmed, Failed { error: String }, } ``` The enum prevents invalid combinations. ### Mistake 4: Using `if let` when full handling matters `if let` is convenient, but it can hide ignored states. If the other variants matter to correctness, use `match`. *** ## Quick Check ## Practice Before moving on, make sure you can do these without guessing: 1. Write an enum where one variant stores no data, one stores a tuple value, and one stores named fields. 2. Write a `match` that handles every variant and prints different output for each one. 3. Explain why `Option` and `Result` are enums, not special exceptions to the rule. 4. Explain why an enum is often better than a struct full of booleans and `Option` fields. ## What You Should Know Now After this lesson, you should be able to say: * an enum models one of several valid states * each variant can carry its own data shape * `match` is how you inspect and unpack those states safely * `Option` and `Result` are standard-library enums built on the same idea That is the foundation you need for the next page. The practice lesson will push this further by making you model accounts, transaction states, and wallet operations with enums instead of vague structs. # Rust for Solana URL: https://www.learnsol.site/learn/rust-foundations Markdown URL: https://www.learnsol.site/learn/rust-foundations.mdx Rust Foundations teaches the Rust fundamentals you need before Solana programs stop looking intimidating: ownership, structs, enums, collections, and guided practice. If Solana Foundations taught you how Solana behaves from the outside, Rust Foundations teaches you the language that most Solana programs are written in. That language is Rust. And Rust has a reputation for being hard for a reason. You do not get far by memorizing syntax alone. You need the right mental models for ownership, borrowing, data modeling, and pattern matching. Without those, Solana program code looks hostile even when it is correct. That is what this module fixes. Follow this section in order. Rust concepts build on each other much more aggressively than many JavaScript or Python learners expect. *** ## What Rust Foundations Covers By the end of this module, you should be able to: * set up a clean Rust development environment * write basic Rust syntax without fighting the compiler constantly * explain ownership and borrowing in plain language * model data with structs and methods * use enums and pattern matching to represent state safely * work with vectors, maps, and strings without getting lost in types * apply those ideas in guided builds and practice lessons That is the Rust foundation you need before Solana program code starts feeling reasonable. *** ## Why This Module Exists A lot of Solana learners try to jump from JavaScript straight into Anchor or on-chain Rust. That usually leads to fake progress. You can copy program code and still have no idea: * why a value moved * why a borrow is invalid * why the compiler rejects two references * why a struct method needs `&self` instead of `self` * why enums are a better fit than stringly typed state This module exists to remove that confusion before you hit real Solana program code. *** ## Rust Foundations Roadmap B --> C --> D --> E --> F --> G --> H --> I --> J `} /> 1. ### Rust Setup and Core Syntax You install the toolchain, write your first programs, and get comfortable with variables, functions, types, and control flow. This removes the first layer of Rust friction. 2. ### Ownership and Borrowing You learn the rule that makes Rust feel different from almost every other language. This is the lesson that changes how you think about memory, references, and mutation. 3. ### Structs and Methods You start modeling real data with custom types and attach behavior to that data. This is where Rust starts to feel useful instead of merely strict. 4. ### Wallet Ledger Build You stop reading and start combining the first half of the module into one small working system. This is where the theory either becomes real or falls apart. 5. ### Enums and Pattern Matching You learn how Rust represents state variants safely. This matters for everything from program state machines to error handling. 6. ### Collections and Strings You learn how to organize and process real data using vectors, maps, and string types. This is the layer that makes practical Rust programs less awkward. *** ## How To Use This Module Well Use these rules while going through this section: * type the examples yourself instead of only reading them * let compiler errors teach you instead of treating them as noise * rewrite small examples in your own words after they compile * do the challenge pages right after the lesson pages, not days later * when confused, ask what Rust is protecting you from That last question is the important one. Rust feels strict because it is trying to make bugs impossible or harder to express. *** ## What Success Looks Like A strong outcome for this section sounds like this: * I understand what ownership means * I know when to borrow and when to move * I can design a struct and attach methods to it * I can use enums instead of vague string states * I can work with collections without panicking over every type annotation * I can read beginner-level Solana Rust code without feeling completely blind If you can say that honestly, this module did its job. *** ## Quick Check *** ## Summary This section is the Rust foundation layer for the rest of the Solana curriculum. It teaches the language patterns that real Solana development depends on: * ownership * borrowing * data modeling * safe state representation * practical data handling Get these right now, and the later Anchor and program lessons will stop feeling like a wall of unfamiliar syntax. # Ownership and Borrowing Practice URL: https://www.learnsol.site/learn/rust-foundations/ownership-and-borrowing-practice Markdown URL: https://www.learnsol.site/learn/rust-foundations/ownership-and-borrowing-practice.mdx Practice Rust ownership with three focused exercises that make you distinguish copying, moving, immutable borrowing, mutable borrowing, and when cloning is actually necessary. Ownership only becomes real when the compiler starts rejecting code you thought was fine. That is what this page is for. These exercises are designed to force one decision again and again: Should this value move, be borrowed, or be cloned? If you can answer that clearly, this lesson did its job. Do these in order. The first exercise is read-only borrowing. The second adds mutation. The third makes you debug ownership decisions directly. *** ## What You Are Practicing By the end of this page, you should be able to: * tell when assignment copies and when it moves * pass borrowed data into functions without losing the original owner * use `&mut` only when a function really needs to change data * prefer borrowing over cloning when borrowing is enough * read basic ownership compiler errors without panicking If any of those still feels fuzzy, that is the exact reason to do the exercises carefully. *** ## Exercise 1: Borrow A Transfer Note Without Moving It Start with the simplest useful ownership decision. You have a `String` containing a transfer note. You want to inspect it in a helper function, but you still need to use the original value afterward. That means this exercise is about borrowing, not moving. ### What to build Create a new Cargo project: ```bash cargo new borrow_note cd borrow_note ``` Replace `src/main.rs` with this starter: ```rust fn main() { let note = String::from("rent payment for validator account"); let word_count = count_words(¬e); let first_word = first_word(¬e); println!("Word count: {}", word_count); println!("First word: {}", first_word); // `note` should still be usable here. println!("Original note: {}", note); } fn count_words(note: &str) -> usize { // write this 0 } fn first_word(note: &str) -> &str { // write this "" } ``` ### Why this exercise matters It proves that you understand three important ideas: * a helper function does not need ownership if it only reads data * `&str` is a better read-only interface than `&String` * slices let you return part of the original string without allocating a new one ### Your job 1. implement `count_words` 2. implement `first_word` 3. run `cargo run` 4. confirm that `note` is still usable after both function calls 5. change the note text and predict the output before rerunning ### What success looks like You should be able to explain: * why `note` was borrowed instead of moved * why `first_word` returns `&str` instead of `String` * why returning a slice is cheaper than cloning a new string *** ## Exercise 2: Update Transaction State With A Mutable Borrow Now move from read-only borrowing to mutation. You have a transfer request that starts as `pending`. A helper function should change it to `confirmed`. That means the function needs temporary exclusive access. ### What to build Use this starter code: ```rust #[derive(Debug)] struct TransferRequest { from: String, to: String, amount: u64, status: String, } fn main() { let mut request = TransferRequest { from: String::from("alice"), to: String::from("bob"), amount: 250_000, status: String::from("pending"), }; print_summary(&request); mark_confirmed(&mut request); print_summary(&request); } fn print_summary(request: &TransferRequest) { println!( "{} -> {} | amount: {} | status: {}", request.from, request.to, request.amount, request.status ); } fn mark_confirmed(request: &mut TransferRequest) { // write this } ``` ### Why this exercise matters This is the ownership rule that beginners usually understand in theory but misuse in code. * `print_summary` only reads, so it should borrow immutably * `mark_confirmed` changes the struct, so it needs `&mut` * `request` itself must be declared with `mut` because the owned value is changing ### Your job 1. implement `mark_confirmed` 2. run the program and confirm the second print shows `confirmed` 3. add another helper like `mark_failed` or `mark_sent` 4. explain why Rust would reject this if an immutable borrow and mutable borrow overlapped incorrectly ### What success looks like You should be able to say this cleanly: * immutable borrows are for reading * mutable borrows are for changing * one mutable borrow needs exclusive access while it is active *** ## Exercise 3: Fix Move Errors Without Reaching For `clone()` First Now practice the part that actually trips people up. The code below is broken on purpose. Your job is not just to make it compile. Your job is to choose the right fix. ### Broken program ```rust fn main() { let wallet_name = String::from("treasury-wallet"); print_label(wallet_name); println!("Wallet still available: {}", wallet_name); } fn print_label(label: String) { println!("Label: {}", label); } ``` ### What is wrong `wallet_name` moves into `print_label` because the function takes ownership with `String`. That means the final `println!` tries to use a value that no longer belongs to `main`. ### Your job Fix this in the best way. The correct first fix is to change the function so it borrows the string: ```rust fn print_label(label: &str) { println!("Label: {}", label); } ``` And then call it like this: ```rust print_label(&wallet_name); ``` After that works, try two more experiments: 1. intentionally use `.clone()` instead and explain why it works but is not the best default 2. write a second function that really should take ownership, such as one that returns the string uppercased after consuming it ### What success looks like You should be able to explain the difference between these three choices: * borrow when the function only needs temporary read access * move when ownership transfer is the point * clone only when you truly need a second owned copy That decision process is the real exercise. *** ## How To Work Through These Exercises 1. ### Read the function signatures first Before changing any code, inspect the function parameter types. Ownership problems usually start there. 2. ### Ask what the function actually needs Does it only read the data? Then borrow it. Does it need to change the data? Then use `&mut`. Does it need to become the new owner? Then moving may be correct. 3. ### Treat `clone()` as a deliberate cost Do not use `.clone()` as your first reflex. Use it only after you can explain why borrowing is not enough. *** ## Common Failure Modes If you only need to point at part of existing string data, return `&str` instead of allocating a new `String`. For read-only string input, `&str` is usually the cleaner and more flexible function signature. A clone may compile, but that does not mean it was the right ownership choice. First ask whether borrowing already solves the problem. *** ## Quick Check *** ## Summary These exercises were built to make ownership concrete. If you worked through them honestly, you now have hands-on practice with: * immutable borrowing * mutable borrowing * slices * move errors * the difference between borrowing, moving, and cloning That is the practical core of this lesson. The next lesson moves into structs and methods, where these same ownership decisions start showing up inside real data models instead of tiny standalone examples. # Understanding Rust Ownership and Borrowing URL: https://www.learnsol.site/learn/rust-foundations/ownership-and-borrowing Markdown URL: https://www.learnsol.site/learn/rust-foundations/ownership-and-borrowing.mdx Learn the ownership rule that makes Rust feel different: one owner for a value, borrowing through references, and the compiler checks that prevent invalid memory access. Rust does not feel hard because the syntax is strange. Rust feels hard because it forces you to answer a question many other languages hide. That question is: who owns this data right now? If you cannot answer that clearly, Rust will stop the program from compiling. That is what this lesson is about. This is the lesson where Rust stops feeling like “new syntax” and starts feeling like a different model of programming. When Rust rejects your ownership code, the compiler is not being stubborn. It is usually preventing one of these bugs: * reading freed memory * freeing the same memory twice * mutating data while something else is reading it *** ## The Core Mental Model Rust organizes many memory-safety rules around one idea: A value has one owner. Other code can temporarily borrow that value, but borrowing is tightly controlled. If you keep that model in your head, most ownership errors stop looking random. They become questions like these: * did ownership move to a new variable? * am I trying to use the old variable after the move? * am I borrowing the value read-only or mutably? * am I trying to mix incompatible borrows at the same time? That is the real teaching spine for this lesson. B B --> C --> F B --> D B --> E `} /> *** ## Why Rust Needs This Rule Other languages solve memory management in different ways. Some languages make you manage memory manually. That gives you speed, but it also gives you bugs like: * memory leaks * dangling pointers * double free errors Other languages use a garbage collector. That avoids many memory bugs, but cleanup happens later at runtime, not at the exact moment ownership becomes irrelevant. Rust takes a different path. It tracks ownership at compile time. That gives you: * memory safety * predictable cleanup * no garbage collector pauses This matters in Solana too. On-chain programs and performance-sensitive clients do not benefit from vague memory behavior. *** ## Start With The Difference Between Copy And Move Not every assignment behaves the same way in Rust. That is the first thing to understand. ### Stack-friendly values are usually copied Simple fixed-size values like integers are cheap to copy. ```rust fn main() { let x = 5; let y = x; println!("x: {}, y: {}", x, y); } ``` Both `x` and `y` are valid. That is because `i32` is a `Copy` type. Rust can duplicate it cheaply, so assignment does not invalidate `x`. ### Heap-backed values usually move Now look at `String`. ```rust fn main() { let s1 = String::from("hello"); let s2 = s1; // println!("{}", s1); // this does not compile println!("{}", s2); } ``` This assignment moves ownership from `s1` to `s2`. After that move, `s1` is no longer valid. That is not arbitrary. `String` owns heap data. If both variables thought they owned the same allocation, Rust would have to guess who should free it later. That is exactly the kind of ambiguity Rust refuses. *** ## Stack And Heap Only Matter Because Ownership Behaves Differently There You do not need a deep computer architecture lecture here. You only need the practical distinction. * stack data is fixed-size and cheap to copy * heap data is dynamic and usually owned through a wrapper like `String` or `Vec` That is why integers often copy cleanly while `String` often moves. ```rust fn main() { let amount: u64 = 5000; let copied_amount = amount; let wallet_name = String::from("treasury"); let moved_name = wallet_name; println!("{}", copied_amount); println!("{}", moved_name); } ``` The `u64` copy is trivial. The `String` move transfers ownership of heap-backed data. *** ## The Three Ownership Rules You can compress most of this lesson into three rules. ### Rule 1: every value has one owner ```rust fn main() { let name = String::from("validator"); println!("{}", name); } ``` Here, `name` owns the string. ### Rule 2: ownership can move ```rust fn main() { let name = String::from("validator"); let next_name = name; println!("{}", next_name); } ``` After the assignment, `next_name` owns the string and `name` does not. ### Rule 3: when the owner goes out of scope, Rust drops the value ```rust fn main() { { let message = String::from("temporary"); println!("{}", message); } // message is gone here } ``` Rust frees the owned data when the scope ends. That is the cleanup model. No manual `free`. No garbage collector. Clear ownership and deterministic scope-based cleanup. *** ## Function Calls Also Move Ownership Many beginners understand assignment moves, then get confused when the same thing happens in a function call. A function parameter can also take ownership. ```rust fn main() { let signature = String::from("abc123"); consume_signature(signature); // println!("{}", signature); // this does not compile } fn consume_signature(value: String) { println!("Consumed: {}", value); } ``` `consume_signature` takes a `String`, not a reference. That means the function becomes the new owner. When the function ends, `value` goes out of scope and Rust drops it. If you still need the original value afterward, do not move it. Borrow it instead. *** ## Borrowing Lets You Use Data Without Taking Ownership Moving every `String` into every function would be annoying and wasteful. That is why Rust has references. A reference lets you borrow data without becoming its owner. ### Immutable borrowing ```rust fn main() { let wallet_label = String::from("treasury-wallet"); let length = label_length(&wallet_label); println!("{} has length {}", wallet_label, length); } fn label_length(label: &String) -> usize { label.len() } ``` `label_length` borrows the string with `&String`. That means: * the function can read the data * the function does not own the data * the original variable is still valid afterward This is your first important ownership escape hatch. *** ## Mutable Borrowing Means Temporary Exclusive Access If a function needs to change the borrowed value, immutable borrowing is not enough. Use a mutable reference. ```rust fn main() { let mut memo = String::from("pending"); append_status(&mut memo); println!("{}", memo); } fn append_status(value: &mut String) { value.push_str(" -> confirmed"); } ``` Two things changed here. * the owner, `memo`, is declared with `mut` * the borrow uses `&mut` That is Rust being explicit again. Mutation is allowed, but only when the code says so clearly. *** ## The Borrowing Rule That Trips Up Most People Rust allows either: * many immutable references * one mutable reference But not both at the same time. This is one of the most important rules in the language. ```rust fn main() { let mut state = String::from("ready"); let r1 = &state; let r2 = &state; println!("{} {}", r1, r2); let r3 = &mut state; r3.push_str(" -> sent"); println!("{}", r3); } ``` This works because the immutable borrows are finished before the mutable borrow begins. Now look at the broken version: ```rust fn main() { let mut state = String::from("ready"); let r1 = &state; let r2 = &mut state; println!("{} {}", r1, r2); } ``` Rust rejects that code because one part of the program wants read access while another part wants exclusive write access. That is exactly how data races start in less strict languages. Rust stops it before the program runs. *** ## Slices Are Borrowed Views Into Data A slice is not a new owned value. A slice is a borrowed view into part of existing data. ### String slices ```rust fn main() { let phrase = String::from("solana runtime"); let first = &phrase[0..6]; let second = &phrase[7..14]; println!("{}", first); println!("{}", second); } ``` Both `first` and `second` borrow part of `phrase`. They do not own new strings. That is why slices are efficient. ### `&str` matters more than `&String` When you only need read access to string data, prefer `&str` in function signatures. ```rust fn main() { let owned = String::from("rpc-node"); let literal = "validator"; println!("{}", describe(owned.as_str())); println!("{}", describe(literal)); } fn describe(value: &str) -> &str { value } ``` `&str` is more flexible than `&String`. It works with string literals and borrowed pieces of `String` values too. That is the interface you usually want for read-only string input. *** ## Clone Only When You Actually Need A Second Owned Copy A lot of beginners overuse `.clone()` because it feels like an easy way out. Sometimes it is correct. Often it is just hiding confusion. ```rust fn main() { let original = String::from("payer"); let copied = original.clone(); println!("{} {}", original, copied); } ``` This is legal because `.clone()` creates a real second owned string. But cloning allocates and copies data. So the default order of thinking should be: 1. can I borrow this? 2. if not, does moving ownership make sense? 3. only then: do I truly need to clone? That rule matters even more when data structures get bigger. *** ## One Small Ownership Example That Looks Like Real Work Now put the ideas together in a simple transaction-like example. ```rust #[derive(Debug)] struct TransferRequest { from: String, to: String, amount: u64, note: String, } fn main() { let mut request = TransferRequest { from: String::from("alice"), to: String::from("bob"), amount: 500_000, note: String::from("pending"), }; print_summary(&request); mark_confirmed(&mut request); print_summary(&request); } fn print_summary(request: &TransferRequest) { println!( "{} -> {} | amount: {} | note: {}", request.from, request.to, request.amount, request.note ); } fn mark_confirmed(request: &mut TransferRequest) { request.note = String::from("confirmed"); } ``` What is happening here: * `request` owns all its fields * `print_summary` borrows it immutably because it only reads * `mark_confirmed` borrows it mutably because it changes `note` * ownership never moves out of `main`, so `request` is still usable at the end This is the kind of reasoning you need before structs and methods start feeling natural. *** ## Common Ownership Mistakes If you assign a `String` to a new variable or pass it into a function that takes ownership, the original binding is no longer valid. Do not fight that rule. Decide whether you actually wanted a borrow instead. If one part of the code is reading a value, Rust will not let another part mutate it at the same time. That restriction prevents inconsistent state and data races. `.clone()` is not evil, but it is not the default solution either. If you only need temporary access, borrow the value. *** ## Quick Check *** ## Summary Ownership is the rule that makes Rust feel different. The short version is: * one owner controls a value * ownership can move * borrows let other code use the value temporarily * mutable borrows require exclusive access * slices are borrowed views, not owned copies If you can trace who owns the data and who is only borrowing it, Rust starts to make sense. That is the capability you should leave this lesson with. The next lesson builds on this by introducing structs and methods, where ownership stops being a toy example and starts shaping real program design. # Rust Setup and Core Syntax URL: https://www.learnsol.site/learn/rust-foundations/rust-setup-and-core-syntax Markdown URL: https://www.learnsol.site/learn/rust-foundations/rust-setup-and-core-syntax.mdx Install the Rust toolchain, create your first Cargo project, and learn the variables, functions, types, and control flow that later Solana lessons depend on. A lot of new Solana learners think Rust becomes hard only when ownership starts. That is not true. Many people get blocked earlier because their toolchain is half-set up, they do not know what `cargo` is doing, and basic Rust syntax still feels unfamiliar. This lesson fixes that. The goal is simple: get a working Rust environment, write code that compiles, and understand the small language rules that show up in every later Solana lesson. Treat the compiler as part of the lesson. If Rust rejects your code, that is not friction to ignore. It is the language telling you what rule you just violated. *** ## The Mental Model For This Lesson You do not need to learn all of Rust today. You need to learn the workflow: 1. write a small program 2. compile it with `cargo` 3. read what the compiler says 4. fix the code until the program does what you expect That loop is the foundation for everything else. By the end of this lesson, you should be comfortable with: * the Rust toolchain: `rustup`, `rustc`, and `cargo` * creating and running a project * variables and mutability * basic scalar types * functions and return values * `if`, `loop`, `while`, and `for` B --> C --> D --> E --> F --> G --> D `} /> *** ## Install The Rust Toolchain Rust development usually starts with three tools: * `rustup`: installs and manages Rust toolchains * `rustc`: the Rust compiler * `cargo`: the project manager and build tool You will use `cargo` most of the time. `rustc` is the compiler under the hood. ### Install Rust On macOS and Linux: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` On Windows, install `rustup-init.exe` from [rustup.rs](https://rustup.rs/). ### Verify the installation Open a new terminal and run: ```bash rustc --version cargo --version rustup show ``` If these commands work, your toolchain is installed correctly. If they fail, fix that now. Do not move deeper into Rust with a broken environment. ### Update to the latest stable toolchain ```bash rustup update rustup default stable ``` This makes sure your local setup matches the stable Rust ecosystem most tutorials assume. *** ## Set Up Your Editor Use VS Code with the official `rust-analyzer` extension. That gives you the minimum useful developer experience: * inline errors * type hints * jump to definitions * quick fixes You can add extras later. `rust-analyzer` is the only extension that really matters right now. *** ## Create Your First Cargo Project Do not start with a random `.rs` file. Start with Cargo. Cargo gives you the correct project structure, handles builds, and will later manage dependencies for real Solana projects. ```bash cargo new day1_rust cd day1_rust cargo run ``` When `cargo run` succeeds, three useful things happened: * Cargo created a project directory * Rust compiled your program into a binary * Cargo ran that binary for you You should see output similar to: ```text Compiling day1_rust v0.1.0 Finished dev profile Running `target/debug/day1_rust` Hello, world! ``` That output matters. It proves your local Rust workflow works end to end. *** ## Read The Smallest Useful Rust Program Open `src/main.rs`. You will see this: ```rust fn main() { println!("Hello, world!"); } ``` This tiny file already teaches you several Rust rules. * `fn main()` declares the program entry point * braces `{}` define the function body * `println!` is a macro, which is why it ends with `!` * the line ends with `;`, so it is a statement At this stage, you do not need macro theory. You just need to recognize that `println!` writes text to the terminal. *** ## Build One Small Program You Can Reason About Instead of throwing isolated syntax examples at you, we will build one small Rust program. The program will simulate a wallet balance check. That keeps the logic simple while staying close to Solana-style thinking. Replace `src/main.rs` with this: ```rust const NETWORK_FEE_LAMPORTS: u64 = 5_000; fn main() { // `u64` is common for balances because balances cannot be negative. let starting_balance: u64 = 2_000_000; let transfer_amount: u64 = 750_000; println!("Starting balance: {} lamports", starting_balance); println!("Transfer amount: {} lamports", transfer_amount); let final_balance = calculate_remaining_balance(starting_balance, transfer_amount); println!("Final balance: {} lamports", final_balance); } fn calculate_remaining_balance(balance: u64, amount: u64) -> u64 { balance - amount - NETWORK_FEE_LAMPORTS } ``` Run it: ```bash cargo run ``` ### What this teaches * `const` creates a compile-time constant * `u64` is an unsigned 64-bit integer * `let` creates an immutable variable * `fn ... -> u64` means the function returns a `u64` * the last line of a function can return a value without using `return` That last rule matters a lot in Rust. The final line in `calculate_remaining_balance` is an expression, not a statement. If you add a semicolon there, the function stops returning the number you expect. *** ## Variables Are Immutable Unless You Opt In This is one of the first Rust rules that surprises JavaScript learners. In Rust, variables do not change unless you say they can change. ```rust fn main() { let balance = 100; // balance = 200; // this does not compile } ``` If a value really needs to change, use `mut`. ```rust fn main() { let mut balance = 100; balance = 200; println!("{}", balance); } ``` This is not just syntax preference. Rust makes mutation explicit because hidden state changes are a common source of bugs. *** ## The Basic Types You Will Use First You do not need the whole type system today. You need the small set that shows up constantly. ```rust fn main() { let balance: u64 = 2_000_000; let retry_count: u8 = 3; let price: f64 = 1.25; let is_confirmed: bool = true; let grade: char = 'A'; println!("{} {} {} {} {}", balance, retry_count, price, is_confirmed, grade); } ``` Use this mental shortcut: * integers like `u64` for counts and balances * `f64` for decimal values * `bool` for true or false * `char` for a single Unicode character Rust can often infer types for you, but explicit types are useful when you want the code to be obvious. *** ## Functions Are Where Rust Starts To Feel Structured A function is just named behavior. Here is the same wallet example, expanded slightly: ```rust const NETWORK_FEE_LAMPORTS: u64 = 5_000; fn main() { let balance = 2_000_000; let amount = 750_000; if can_afford_transfer(balance, amount) { let final_balance = calculate_remaining_balance(balance, amount); println!("Transfer is valid. Final balance: {}", final_balance); } else { println!("Transfer would fail: not enough lamports."); } } fn can_afford_transfer(balance: u64, amount: u64) -> bool { balance >= amount + NETWORK_FEE_LAMPORTS } fn calculate_remaining_balance(balance: u64, amount: u64) -> u64 { balance - amount - NETWORK_FEE_LAMPORTS } ``` Two points matter here. First, function parameters need explicit types. Second, Rust makes return types explicit too. That is what `-> bool` and `-> u64` mean. That strictness helps later when Solana programs start passing around accounts, lamports, and custom structs. The compiler forces you to be precise. *** ## `if` Is Not Just Control Flow In Rust, `if` can decide execution, but it can also produce a value. That makes it more powerful than many beginners expect. ```rust fn main() { let priority_fee = true; let extra_fee = if priority_fee { 10_000 } else { 0 }; println!("Extra fee: {}", extra_fee); } ``` Both branches must return the same kind of value. That rule prevents messy code where one branch produces a number and the other produces something unrelated. *** ## Learn The Three Loop Forms Early Rust gives you three common ways to repeat work. ### `loop` Use `loop` when you want a manual exit with `break`. ```rust fn main() { let mut attempts = 0; let final_attempt = loop { attempts += 1; if attempts == 3 { break attempts; } }; println!("Stopped after {} attempts", final_attempt); } ``` ### `while` Use `while` when repetition depends on a condition. ```rust fn main() { let mut confirmations_left = 3; while confirmations_left > 0 { println!("Waiting... {}", confirmations_left); confirmations_left -= 1; } } ``` ### `for` Use `for` when iterating over a range or collection. ```rust fn main() { let transfers = [100_000, 250_000, 400_000]; for amount in transfers { println!("Observed transfer: {}", amount); } } ``` For beginner Rust, `for` is usually the cleanest loop to reach for first. *** ## Put The Pieces Together Now build one complete program that uses constants, variables, functions, `if`, and `for` together. Replace `src/main.rs` with this: ```rust const NETWORK_FEE_LAMPORTS: u64 = 5_000; fn main() { let starting_balance: u64 = 2_000_000; let outgoing_transfers: [u64; 3] = [150_000, 300_000, 900_000]; let mut current_balance = starting_balance; println!("Starting balance: {} lamports", current_balance); for amount in outgoing_transfers { if can_afford_transfer(current_balance, amount) { current_balance = calculate_remaining_balance(current_balance, amount); println!("Sent {} lamports", amount); println!("New balance: {} lamports", current_balance); } else { println!("Skipped {} lamports: insufficient balance", amount); } } println!("Finished with {} lamports", current_balance); } fn can_afford_transfer(balance: u64, amount: u64) -> bool { balance >= amount + NETWORK_FEE_LAMPORTS } fn calculate_remaining_balance(balance: u64, amount: u64) -> u64 { balance - amount - NETWORK_FEE_LAMPORTS } ``` Run it, then change the numbers yourself. Try these edits: * make one transfer too large and watch the `else` branch run * change the starting balance and predict the output before running * add a fourth transfer to the array and confirm the loop handles it That is the right way to learn basic Rust syntax: change the program and observe what breaks or what changes. *** ## Common Mistakes If you try to reassign an immutable variable, Rust will stop you. That error is correct. Add `mut` only when the value truly changes. This fails: ```rust fn add(a: i32, b: i32) -> i32 { a + b; } ``` The semicolon turns the expression into a statement. This works: ```rust fn add(a: i32, b: i32) -> i32 { a + b } ``` Rust does not silently smooth over type mismatches for you. If one value is `u8` and another is `u64`, be explicit about conversions. *** ## Quick Check *** ## Summary This lesson was about getting the Rust workflow under control. You now have the basics that later Solana Rust lessons rely on: * a working Rust toolchain * a usable Cargo workflow * immutable and mutable variables * core scalar types * functions with explicit return types * `if`, `loop`, `while`, and `for` That is enough to start reading and writing small Rust programs without guessing what the syntax is doing. The next lesson goes deeper into the rule that makes Rust feel fundamentally different: ownership and borrowing. Before you move on, do the practice page while this syntax is still fresh. # Rust Syntax Practice URL: https://www.learnsol.site/learn/rust-foundations/rust-syntax-practice Markdown URL: https://www.learnsol.site/learn/rust-foundations/rust-syntax-practice.mdx Practice core Rust syntax with three focused exercises that force you to use variables, functions, control flow, loops, and basic input handling without guessing. The fastest way to forget a syntax lesson is to read it once and move on. The fastest way to keep it is to write small programs that force you to use the syntax deliberately. That is what this page is for. These exercises are not random puzzles. Each one is here to prove that you can use the core building blocks without relying on copy-paste luck. Do these in order. Each exercise adds one more layer of Rust pressure: variables and functions first, then control flow, then simple input handling. *** ## What You Are Practicing By the end of this page, you should be able to: * create a Cargo project and edit `src/main.rs` * choose sensible numeric types * split logic into functions * use `if`, `while`, and `for` for the right reasons * mutate state only when it actually changes * parse simple terminal input without getting lost If any of those still feels shaky, that is the point of the exercises. *** ## Exercise 1: Transaction Fee Calculator Start with the easiest useful task. Write a program that takes a starting balance, a transfer amount, and a fixed network fee, then prints whether the transfer can happen and what the remaining balance would be. ### Why this exercise matters This forces you to practice: * constants * integer types * function parameters and return values * `if` branches * basic arithmetic ### What to build Create a new Cargo project: ```bash cargo new fee_calculator cd fee_calculator ``` Then replace `src/main.rs` with this starter: ```rust const NETWORK_FEE_LAMPORTS: u64 = 5_000; fn main() { let balance: u64 = 2_000_000; let amount: u64 = 750_000; if can_afford_transfer(balance, amount) { let remaining = remaining_balance(balance, amount); println!("Transfer approved. Remaining balance: {}", remaining); } else { println!("Transfer rejected. Not enough lamports."); } } fn can_afford_transfer(balance: u64, amount: u64) -> bool { // write this false } fn remaining_balance(balance: u64, amount: u64) -> u64 { // write this 0 } ``` ### Your job 1. implement `can_afford_transfer` 2. implement `remaining_balance` 3. run the program with `cargo run` 4. change the numbers and predict the result before running again ### What success looks like You should be able to explain: * why `u64` is a better fit than `i64` here * why `NETWORK_FEE_LAMPORTS` is a `const` * why one function returns `bool` and the other returns `u64` If you cannot explain those choices, you are still memorizing syntax instead of understanding it. *** ## Exercise 2: Confirmation Countdown Now practice loops and mutation. Write a program that simulates waiting for transaction confirmations. ### Why this exercise matters Rust beginners often understand variables in isolation but get sloppy once a value has to change repeatedly. This exercise forces you to manage state carefully. ### What to build Use this starter code: ```rust fn main() { let mut confirmations_left = 5; while confirmations_left > 0 { println!("Waiting for confirmation {}...", confirmations_left); confirmations_left -= 1; } println!("Transaction finalized."); } ``` After that works, extend it. Add a second example that loops over a fixed list of transfer amounts: ```rust fn main() { let transfers = [100_000_u64, 200_000, 350_000]; for amount in transfers { println!("Observed transfer: {} lamports", amount); } } ``` ### Your job 1. run the `while` version and confirm you understand why it needs `mut` 2. rewrite the countdown so it starts from a different number 3. add the `for` example below it 4. explain why `while` and `for` are solving different problems here ### What success looks like You should be able to say this in plain language: * `while` is good when repetition depends on a changing condition * `for` is good when I already have a range or collection to iterate over That distinction matters later when you start reading real Rust code. *** ## Exercise 3: Simple Transfer Checker With Input The first two exercises stayed fully hardcoded. Now add one layer of interactivity. ### Why this exercise matters You need to see how Rust handles terminal input, strings, parsing, and failure cases. This is still basic, but it is the first time your program depends on real user input instead of fixed values. ### What to build Create a program that: * asks the user for a starting balance * asks for a transfer amount * parses both as `u64` * prints whether the transfer can happen after a fixed fee Use this starter: ```rust use std::io; const NETWORK_FEE_LAMPORTS: u64 = 5_000; fn main() { println!("Enter your starting balance in lamports:"); let balance = read_u64(); println!("Enter the transfer amount in lamports:"); let amount = read_u64(); if balance >= amount + NETWORK_FEE_LAMPORTS { println!("Transfer approved."); println!( "Remaining balance: {}", balance - amount - NETWORK_FEE_LAMPORTS ); } else { println!("Transfer rejected. Not enough lamports."); } } fn read_u64() -> u64 { let mut input = String::new(); io::stdin() .read_line(&mut input) .expect("failed to read input"); input .trim() .parse::() .expect("please enter a valid unsigned integer") } ``` ### Read this code carefully Two details matter here. First, terminal input starts as a `String`, not as a number. Second, `.parse::()` can fail, which is why the code uses `.expect(...)`. That is your first real example of Rust making you face the possibility of invalid input. ### Your job 1. run the program with valid input 2. run it again with invalid input like `abc` 3. observe the failure message 4. change the code so the fee is shown separately in the output 5. move the approval check into a helper function if you want extra practice ### What success looks like You should understand this sequence clearly: * read text from stdin * trim whitespace * parse into a numeric type * handle the possibility that parsing fails That is a real programming workflow, not just Rust trivia. *** ## How To Work Through These Exercises 1. ### Type the code yourself Do not copy large blocks mindlessly. Typing forces you to notice punctuation, types, and function signatures. 2. ### Change one thing at a time After the program works, change one number, one type, or one branch condition and rerun it. That is how you build intuition instead of only collecting finished code. 3. ### Read compiler errors fully If Rust rejects the code, do not skip to random fixes. Read the full message and ask what rule the compiler is enforcing. *** ## Common Failure Modes If a value cannot be negative, defaulting to an unsigned integer like `u64` is usually the cleaner choice. That is why balances and lamports in these exercises use `u64`. `read_line` does not magically give you a number. It gives you a string. Parsing is a separate step. Use `mut` only when the value actually changes. If a value never changes, keep it immutable. *** ## Quick Check *** ## Summary These exercises were designed to make the lesson concrete. If you finished them honestly, you have now used: * constants * immutable and mutable variables * function signatures * boolean logic * loops * arrays * terminal input * numeric parsing That is enough practice to move into the next lesson without the basics being a blur. The next step is ownership and borrowing. That is where Rust stops feeling like just new syntax and starts feeling like a different way of thinking about memory. # Struct and Method Practice URL: https://www.learnsol.site/learn/rust-foundations/structs-and-methods-practice Markdown URL: https://www.learnsol.site/learn/rust-foundations/structs-and-methods-practice.mdx Practice Rust by designing small types, adding methods, and choosing the right receiver: `&self`, `&mut self`, or `self`. This is where Rust starts looking like real program design. You are no longer just pushing values through helper functions. Now you are deciding: * what data belongs together * what behavior belongs on that data * which methods only read * which methods mutate * which methods should consume the value entirely That is what these exercises are testing. Do these in order. The first exercise is about struct shape. The second adds methods. The third forces you to choose the correct receiver type. *** ## What You Are Practicing By the end of this page, you should be able to: * design a named-field struct that has a clear purpose * instantiate that struct cleanly * add methods with `impl` * choose between `&self`, `&mut self`, and `self` * use an associated function like `new` when it improves clarity If any of those still feels shaky, that is exactly what the exercises are for. *** ## Exercise 1: Model A Wallet Cleanly Start with the base skill. Take related values and turn them into one named type. ### What to build Create a new Cargo project: ```bash cargo new wallet_model cd wallet_model ``` Replace `src/main.rs` with this starter: ```rust #[derive(Debug)] struct Wallet { owner: String, balance: u64, frozen: bool, } fn main() { let wallet = Wallet { owner: String::from("alice"), balance: 1_500_000, frozen: false, }; println!("{:?}", wallet); } ``` ### Why this exercise matters This is the first real shift from scattered variables to deliberate data modeling. You are saying that `owner`, `balance`, and `frozen` are not separate facts. They are one wallet. ### Your job 1. type the struct and run it with `cargo run` 2. add one more field like `nonce: u32` or `label: String` 3. print a couple of fields directly with dot syntax 4. explain why this is better than storing the same data as unrelated variables ### What success looks like You should be able to say: * what this struct represents * why these fields belong together * what the type means in plain language If you cannot do that, the struct is probably not well designed yet. *** ## Exercise 2: Add Methods That Match The Data Now give the struct behavior. A wallet should not just exist. It should do things. ### What to build Extend the same `Wallet` type with methods. Use this starter: ```rust #[derive(Debug)] struct Wallet { owner: String, balance: u64, frozen: bool, } impl Wallet { fn deposit(&mut self, amount: u64) { // write this } fn can_withdraw(&self, amount: u64) -> bool { // write this false } fn freeze(&mut self) { // write this } } fn main() { let mut wallet = Wallet { owner: String::from("alice"), balance: 1_500_000, frozen: false, }; wallet.deposit(250_000); println!("Can withdraw 500000? {}", wallet.can_withdraw(500_000)); wallet.freeze(); println!("Can withdraw 500000 after freeze? {}", wallet.can_withdraw(500_000)); } ``` ### Why this exercise matters This is where method receiver choices stop being abstract. * `deposit` changes state, so it needs `&mut self` * `can_withdraw` only reads state, so it should use `&self` * `freeze` changes state too, so it also needs `&mut self` The method signature should match the behavior exactly. ### Your job 1. implement all three methods 2. run the program and check the outputs 3. add another read-only method like `summary(&self) -> String` 4. explain why `can_withdraw` should not take `&mut self` ### What success looks like You should be able to explain each method signature in one sentence. If the method reads, say that. If it mutates, say that. That clarity matters more than the syntax itself. *** ## Exercise 3: Choose Between `&self`, `&mut self`, and `self` This is the decision that separates “I saw the syntax” from “I understand the design.” ### What to build Take the same wallet and add: * an associated function `new` * a read-only method `summary` * a consuming method `close` Use this starter: ```rust #[derive(Debug)] struct Wallet { owner: String, balance: u64, frozen: bool, } impl Wallet { fn new(owner: String) -> Self { // write this Self { owner, balance: 0, frozen: false, } } fn summary(&self) -> String { // write this String::new() } fn close(self) -> String { // write this String::new() } } fn main() { let wallet = Wallet::new(String::from("alice")); println!("{}", wallet.summary()); let closing_message = wallet.close(); println!("{}", closing_message); // wallet should not be usable here anymore } ``` ### Why this exercise matters This exercise forces three different design choices. * `new` does not operate on an existing instance, so it takes no `self` * `summary` reads the wallet, so it uses `&self` * `close` consumes the wallet, so it takes `self` That is the cleanest possible way to understand receiver types. ### Your job 1. implement `new`, `summary`, and `close` 2. run the program 3. uncomment or add code after `wallet.close()` that tries to use `wallet` again 4. read the compiler error and explain why it happens ### What success looks like You should be able to explain exactly why these three signatures are different. That is the real point of the exercise. *** ## How To Work Through These Exercises 1. ### Start with the type before the methods First decide what fields belong together. Only after that should you add behavior. 2. ### Make method signatures tell the truth Do not choose `&mut self` just because it feels safer. Use the narrowest receiver that matches what the method actually does. 3. ### Let the compiler teach ownership again When you experiment with a consuming method like `close(self)`, try using the value afterward on purpose. The error message is part of the lesson. *** ## Common Failure Modes Not every function belongs on the struct. Put behavior in `impl` only when it clearly belongs to the type. A method that only reads data should not demand mutable access. That makes your API harder to use and hides the real behavior. If a method takes `self`, the caller loses that instance afterward. That is correct when you truly want ownership transfer. *** ## Quick Check *** ## Summary These exercises were designed to make the lesson practical. If you finished them honestly, you have now practiced: * designing a struct * grouping related data into one named type * adding methods with `impl` * choosing `&self`, `&mut self`, and `self` correctly * using an associated function to construct a value cleanly That is the core of this lesson. The next lessons can build on this only if this part is solid. # Structs and Methods in Rust URL: https://www.learnsol.site/learn/rust-foundations/structs-and-methods Markdown URL: https://www.learnsol.site/learn/rust-foundations/structs-and-methods.mdx Learn how Rust structs model related data and how methods attach behavior to that data, using small account-style examples that prepare you for Solana program code. After ownership, the next Rust question is simpler. How do you represent real data without scattering related values across loose variables? That is what structs solve. A struct lets you group related data into one named type. A method lets you attach behavior to that type. That combination is the start of real Rust program design. If ownership answers "who controls this data?", structs answer "what shape does this data have?" And methods answer "what can this data do?" *** ## The Core Mental Model Do not think of a struct as just “an object” because other languages have objects. Think of it more precisely. A struct is a custom data shape. It lets you say: * these fields belong together * this type has a clear meaning * this data should be passed around as one value instead of four unrelated variables Then methods let you say: * this behavior belongs with that data * reading this field should look one way * mutating this type should look another way That is the teaching spine for this lesson. B --> C C --> D C --> E C --> F `} /> *** ## Start With The Problem Structs Actually Solve Without a struct, related values drift apart quickly. ```rust fn main() { let address = String::from("alice"); let balance = 1_500_000_u64; let nonce = 3_u32; let is_active = true; println!("{} {} {} {}", address, balance, nonce, is_active); } ``` This compiles, but the data shape is weak. Nothing in the type system says these four values belong to one account. A struct fixes that. ```rust struct Account { address: String, balance: u64, nonce: u32, is_active: bool, } fn main() { let account = Account { address: String::from("alice"), balance: 1_500_000, nonce: 3, is_active: true, }; println!("{}", account.address); } ``` Now the data has a name and a shape. That is the first real benefit. *** ## Define One Useful Struct First Do not start by memorizing every struct variation. Start with the named-field struct because that is the one you will use most. ```rust #[derive(Debug)] struct TransferRequest { from: String, to: String, amount: u64, confirmed: bool, } fn main() { let request = TransferRequest { from: String::from("alice"), to: String::from("bob"), amount: 500_000, confirmed: false, }; println!("{:?}", request); } ``` ### What this teaches * `struct TransferRequest { ... }` defines a new type * each field has a name and a type * creating a value means filling in every required field * `#[derive(Debug)]` lets you print the value with `{:?}` That last part is practical, not cosmetic. When you are learning or debugging, `Debug` printing is one of the fastest ways to inspect your data. *** ## Field Access And Mutation Are Deliberate Field access is simple. ```rust println!("{}", request.amount); ``` Mutation still follows the Rust rules you already learned. If the struct value needs to change, the binding must be mutable. ```rust #[derive(Debug)] struct TransferRequest { from: String, to: String, amount: u64, confirmed: bool, } fn main() { let mut request = TransferRequest { from: String::from("alice"), to: String::from("bob"), amount: 500_000, confirmed: false, }; request.confirmed = true; println!("{:?}", request); } ``` This matters because a struct does not bypass ownership or mutability rules. It just groups values together. *** ## Methods Attach Behavior To The Struct A struct by itself only stores data. An `impl` block adds behavior. ```rust #[derive(Debug)] struct TransferRequest { from: String, to: String, amount: u64, confirmed: bool, } impl TransferRequest { fn mark_confirmed(&mut self) { self.confirmed = true; } fn summary(&self) -> String { format!( "{} -> {} | amount: {} | confirmed: {}", self.from, self.to, self.amount, self.confirmed ) } } fn main() { let mut request = TransferRequest { from: String::from("alice"), to: String::from("bob"), amount: 500_000, confirmed: false, }; println!("{}", request.summary()); request.mark_confirmed(); println!("{}", request.summary()); } ``` Now the code reads better because the behavior sits next to the data it operates on. That is why methods matter. *** ## `&self`, `&mut self`, and `self` Mean Three Different Things This is the part that beginners must get right before methods stop feeling magical. ### `&self` Use `&self` when the method only reads the struct. ```rust impl TransferRequest { fn summary(&self) -> String { format!("{} -> {}", self.from, self.to) } } ``` The method borrows the struct immutably. It can inspect fields, but it cannot change them. ### `&mut self` Use `&mut self` when the method changes the struct. ```rust impl TransferRequest { fn mark_confirmed(&mut self) { self.confirmed = true; } } ``` This needs exclusive mutable access. That is why the caller must have a mutable binding. ### `self` Use `self` when the method should consume the value. ```rust impl TransferRequest { fn into_log_entry(self) -> String { format!("LOG: {} sent {} to {}", self.from, self.amount, self.to) } } ``` This takes ownership of the entire struct. After calling that method, the original value is gone. That is sometimes correct, but it should be a deliberate choice. *** ## Associated Functions Usually Build Values Not every function in an `impl` block is a method. If it does not take `self`, it is an associated function. The most common use is constructing a value cleanly. ```rust #[derive(Debug)] struct TransferRequest { from: String, to: String, amount: u64, confirmed: bool, } impl TransferRequest { fn new(from: String, to: String, amount: u64) -> Self { Self { from, to, amount, confirmed: false, } } } fn main() { let request = TransferRequest::new( String::from("alice"), String::from("bob"), 500_000, ); println!("{:?}", request); } ``` `new` is not special syntax. It is just a naming convention Rust developers use for constructors. *** ## Field Init Shorthand Helps When Names Match Sometimes function parameters and struct fields use the same names. Rust lets you write this more cleanly. ```rust struct TransferRequest { from: String, to: String, amount: u64, confirmed: bool, } fn build_request(from: String, to: String, amount: u64) -> TransferRequest { TransferRequest { from, to, amount, confirmed: false, } } ``` That shorthand matters because it keeps constructors and factory functions readable. *** ## Struct Update Syntax Is Useful, But It Still Follows Ownership Rules Rust can build a new struct from an older one. ```rust #[derive(Debug)] struct RpcConfig { network: String, url: String, timeout_seconds: u32, } fn main() { let devnet = RpcConfig { network: String::from("devnet"), url: String::from("https://api.devnet.solana.com"), timeout_seconds: 30, }; let localnet = RpcConfig { network: String::from("localnet"), url: String::from("http://127.0.0.1:8899"), ..devnet }; println!("{:?}", localnet); } ``` This is convenient, but it is not free magic. If the reused fields own data, the original struct may be partially or fully moved. So struct update syntax is still part of the ownership world, not outside it. *** ## Tuple Structs And Unit Structs Exist, But They Are Secondary Today You should know they exist. You do not need to center your mental model around them yet. ### Tuple struct ```rust struct Lamports(u64); fn main() { let amount = Lamports(1_000_000); println!("{}", amount.0); } ``` Tuple structs are useful when you want a named type without named fields. ### Unit struct ```rust struct Devnet; fn main() { let _network = Devnet; } ``` Unit structs are useful when the type itself matters more than stored data. For beginner Solana-oriented Rust, named-field structs are still the priority. *** ## One Complete Example That Feels Like Real Program Design Now combine data and behavior in one small account model. ```rust #[derive(Debug)] struct Wallet { owner: String, balance: u64, frozen: bool, } impl Wallet { fn new(owner: String) -> Self { Self { owner, balance: 0, frozen: false, } } fn deposit(&mut self, amount: u64) { self.balance += amount; } fn can_withdraw(&self, amount: u64) -> bool { !self.frozen && self.balance >= amount } fn withdraw(&mut self, amount: u64) -> Result<(), String> { if self.frozen { return Err(String::from("wallet is frozen")); } if amount > self.balance { return Err(String::from("insufficient balance")); } self.balance -= amount; Ok(()) } fn freeze(&mut self) { self.frozen = true; } } fn main() { let mut wallet = Wallet::new(String::from("alice")); wallet.deposit(1_000_000); println!("Can withdraw 500000? {}", wallet.can_withdraw(500_000)); wallet.withdraw(500_000).unwrap(); wallet.freeze(); println!("{:?}", wallet); } ``` Why this example matters: * the struct keeps related wallet state together * methods make state transitions easier to read * `&self` and `&mut self` encode which operations read and which operations mutate * the API starts looking like real domain logic, not just loose helper functions That is the point of this lesson. *** ## Common Struct Mistakes That usually leads to code where the data shape and the logic drift apart. If behavior clearly belongs to the type, a method is usually the cleaner choice. If a method consumes the whole value, the caller loses it. Do that only when ownership transfer is intentional. A struct does not erase the ownership lesson that came before it. It just packages owned values together. *** ## Quick Check *** ## Summary This lesson teaches the next layer of Rust program design. You now have the core mental model: * a struct defines a named data shape * methods attach behavior to that data * `&self`, `&mut self`, and `self` describe different ownership relationships to the instance * associated functions usually construct values cleanly If you can read a struct and explain both its fields and its methods, you are ready for the next level of Rust code. The next step is practice: designing small types and attaching the right behavior to them without overcomplicating the API. # Wallet Ledger Build URL: https://www.learnsol.site/learn/rust-foundations/wallet-ledger-build Markdown URL: https://www.learnsol.site/learn/rust-foundations/wallet-ledger-build.mdx Combine ownership, structs, methods, and basic CLI input in one guided wallet ledger build before moving deeper into the curriculum. This page should not feel like a marketplace of ideas. It should feel like a focused build. By now, you have already covered: * basic Rust syntax * ownership and borrowing * structs and methods So the right next move is not choosing between five different app ideas. The right next move is building one small program that forces those ideas to work together. That is what this page does. The goal is not to build something impressive. The goal is to prove that you can model data, attach behavior to it, and keep ownership decisions clear while the program grows slightly beyond toy snippets. *** ## The Project Build a wallet ledger CLI. The program should let you: * create a wallet * deposit lamports * withdraw lamports * record a short history of actions * print a readable summary This is a good build because it forces you to use the exact skills from the first half of this section: * structs for `Wallet` and `LedgerEntry` * methods for wallet behavior * `String` ownership inside structs * `&self` and `&mut self` in the right places * simple terminal input with `std::io` *** ## What You Are Proving If you finish this honestly, you should be able to prove that you can: * model related data as named types * make methods match actual behavior * mutate state through clear APIs instead of random field writes everywhere * read and update a small Rust program without losing track of ownership That is the real outcome. B B --> C --> H B --> D --> H B --> E --> H B --> F F --> G `} /> *** ## Project Shape Use one file first. Do not over-engineer modules yet. Create a new Cargo project: ```bash cargo new wallet_ledger cd wallet_ledger ``` You will build everything in `src/main.rs`. That keeps your attention on the Rust concepts instead of file organization. *** ## Step 1: Define The Data Model Start by defining the two core types. ```rust #[derive(Debug, Clone)] struct LedgerEntry { action: String, amount: u64, } #[derive(Debug)] struct Wallet { owner: String, balance: u64, frozen: bool, history: Vec, } ``` ### Why this shape works * `Wallet` stores the long-lived state * `LedgerEntry` stores one event in the wallet history * `Vec` gives the wallet a growing transaction log This is already more realistic than loose variables. The types describe the program clearly. *** ## Step 2: Add A Constructor Now add an associated function that creates a clean wallet. ```rust impl Wallet { fn new(owner: String) -> Self { Self { owner, balance: 0, frozen: false, history: Vec::new(), } } } ``` This matters because it gives the type a stable way to start in a valid state. You do not want every caller manually remembering the default balance, frozen flag, and empty history. *** ## Step 3: Add Read-Only And Mutable Methods Now give the wallet behavior. ```rust impl Wallet { fn summary(&self) -> String { format!( "owner: {} | balance: {} | frozen: {} | entries: {}", self.owner, self.balance, self.frozen, self.history.len() ) } fn deposit(&mut self, amount: u64) { self.balance += amount; self.history.push(LedgerEntry { action: String::from("deposit"), amount, }); } fn withdraw(&mut self, amount: u64) -> Result<(), String> { if self.frozen { return Err(String::from("wallet is frozen")); } if amount > self.balance { return Err(String::from("insufficient balance")); } self.balance -= amount; self.history.push(LedgerEntry { action: String::from("withdraw"), amount, }); Ok(()) } fn freeze(&mut self) { self.frozen = true; } } ``` ### What this teaches * `summary(&self)` only reads state * `deposit(&mut self)` changes state * `withdraw(&mut self)` changes state and may fail * `freeze(&mut self)` changes state without returning anything This is where the earlier `&self` vs `&mut self` lesson becomes real. *** ## Step 4: Run A Simple Hardcoded Flow First Before adding user input, prove that the data model works. ```rust fn main() { let mut wallet = Wallet::new(String::from("alice")); println!("{}", wallet.summary()); wallet.deposit(1_000_000); println!("{}", wallet.summary()); wallet.withdraw(250_000).unwrap(); println!("{}", wallet.summary()); wallet.freeze(); println!("{}", wallet.summary()); } ``` Run it: ```bash cargo run ``` Do not skip this phase. If the hardcoded version is not solid, the interactive version will only hide the real problem. *** ## Step 5: Add Simple CLI Input Once the hardcoded flow works, add a small menu loop. Use this as the shape: ```rust use std::io::{self, Write}; fn main() { let mut wallet = Wallet::new(String::from("alice")); loop { println!("\n=== Wallet Ledger ==="); println!("1. Deposit"); println!("2. Withdraw"); println!("3. Freeze wallet"); println!("4. View summary"); println!("5. Exit"); print!("Choose an option: "); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); match input.trim() { "1" => { let amount = read_u64("Enter deposit amount: "); wallet.deposit(amount); } "2" => { let amount = read_u64("Enter withdrawal amount: "); match wallet.withdraw(amount) { Ok(()) => println!("Withdrawal recorded."), Err(err) => println!("Withdrawal failed: {}", err), } } "3" => { wallet.freeze(); println!("Wallet frozen."); } "4" => { println!("{}", wallet.summary()); print_history(&wallet); } "5" => break, _ => println!("Invalid option."), } } } ``` Then add two helpers: ```rust fn read_u64(prompt: &str) -> u64 { loop { print!("{}", prompt); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); match input.trim().parse::() { Ok(value) => return value, Err(_) => println!("Please enter a valid unsigned integer."), } } } fn print_history(wallet: &Wallet) { if wallet.history.is_empty() { println!("No ledger entries yet."); return; } for (index, entry) in wallet.history.iter().enumerate() { println!("{}. {} {}", index + 1, entry.action, entry.amount); } } ``` ### Why this is the right amount of CLI This adds enough complexity to feel real, but not so much that the build turns into a UI exercise. You are still practicing Rust fundamentals, not menu design. *** ## Step 6: Test The Failure Paths On Purpose Do not only test the happy path. Try these cases deliberately: 1. withdraw more than the current balance 2. freeze the wallet, then try to withdraw 3. enter invalid input like `abc` 4. view the summary after several actions and confirm history length is correct This is where your methods prove whether they actually encode the business rules clearly. *** ## What A Good Finished Version Should Feel Like A good result should have these properties: * the types are easy to name and understand * methods are short and clearly scoped * the wallet state changes in predictable places * `summary` and `print_history` do not mutate anything * deposits and withdrawals always record history consistently If your code feels tangled, it usually means one of three things: * the struct shape is weak * the methods are doing too much * mutation is happening in the wrong place *** ## Optional Extensions Only do these after the base version is solid. 1. add an `unfreeze` method 2. add a `can_withdraw(&self, amount: u64) -> bool` method 3. store a short text note in each `LedgerEntry` 4. add a consuming method like `close(self) -> String` That last one is especially useful because it forces you to think about whether a method should borrow or consume the whole wallet. *** ## Common Failure Modes If most of the business rules live in `main` instead of `impl Wallet`, the build is missing the point. The point is to let the type own its behavior. If you keep writing directly to `wallet.balance` or `wallet.history` from many places, the API is not doing enough work. Let the methods own the state transitions. You do not need modules, traits, generics, or multiple files yet. This build is about fundamentals being solid, not architecture being fancy. *** ## Quick Check *** ## Summary This build exists to prove that the first half of this section is working. If you can build this wallet ledger cleanly, you are no longer just reading Rust syntax. You are: * modeling data with structs * attaching behavior with methods * making ownership and mutability decisions deliberately * handling basic CLI input without losing control of the program That is the capability you should have before moving into enums, pattern matching, and richer data handling later in the week. # Working with Solana Accounts and Lamports URL: https://www.learnsol.site/learn/solana-foundations/accounts-lamports-exercise Markdown URL: https://www.learnsol.site/learn/solana-foundations/accounts-lamports-exercise.mdx Create two wallets, fund one, transfer SOL between them, inspect the receiving account, and understand what lamports and rent actually mean. A lot of beginner confusion comes from treating a wallet, an address, and an account like they are three different things. For this lesson, use a simpler model: * A **wallet keypair** controls an address * That **address** points to an on-chain account * That **account** can hold a balance in **lamports** When you send SOL, you are changing the balance stored in accounts on the network. That is the core idea this exercise is meant to lock in. This lesson assumes you already finished the devnet setup lesson. If something looks wrong, run `solana config get` first and make sure your CLI is still pointed at `devnet`. *** ## Key Terms A data structure on Solana that stores state (like balance) and is identified by a public key address. The smallest unit of SOL. 1 SOL = 1,000,000,000 lamports. Named after Leslie Lamport. The minimum balance an account must maintain to stay alive on-chain. Accounts below this threshold can be garbage collected. B --> C D --> E --> F C -- transfer lamports --> F `} /> *** ## What You Are Learning By the end of this exercise, you will be able to: * create two separate wallet keypairs * inspect the address behind each wallet * fund one wallet on devnet * transfer SOL to a second wallet * inspect the receiving account and read its important fields * understand what lamports and rent mean in practice *** ## The Mental Model You Need First Before you type commands, keep these two facts in your head: 1. **SOL is the user-facing unit** 2. **Lamports are the smallest unit underneath** The relationship is simple: ```txt 1 SOL = 1,000,000,000 lamports ``` So if you send `0.01` SOL, you are really moving `10,000,000` lamports. You do not need to memorize that number forever. You do need to understand that the CLI may show balances in either SOL or lamports depending on the command flags you use. *** ## Step-by-Step Exercise ### Step 1: Create Two Wallets Create two fresh keypairs so you can send funds between them. ```bash solana-keygen new --outfile wallet1.json solana-keygen new --outfile wallet2.json ``` These files are not just labels. They contain the private keys for two different wallets. In this exercise: * `wallet1.json` will be the sender * `wallet2.json` will be the receiver ### Step 2: Inspect the Addresses Behind Those Wallets A keypair file is local. The public key derived from it is the on-chain address. Print both addresses: ```bash solana address -k wallet1.json solana address -k wallet2.json ``` You should see two different base58 addresses. That matters because it proves you are working with two separate accounts, not two names for the same wallet. If you want, copy the second address somewhere temporary so you can recognize it in the next steps. ### Step 3: Check the Starting Balances New wallets on devnet usually start with no SOL. Confirm that before funding anything: ```bash solana balance -k wallet1.json solana balance -k wallet2.json ``` You should see `0 SOL` or something very close to it. This is your baseline. From here on, every command should change something you can observe. ### Step 4: Fund Wallet 1 Now give the sender some devnet SOL. ```bash solana airdrop 2 wallet1.json solana balance -k wallet1.json ``` After the airdrop, `wallet1` should show about `2 SOL`. If the airdrop fails, check these first: * you are on `devnet` * you are not being rate-limited by the faucet * you did not accidentally fund a different wallet ### Step 5: Transfer SOL to Wallet 2 Now send funds from the first wallet to the second. ```bash solana transfer wallet2.json 0.01 --from wallet1.json --allow-unfunded-recipient ``` This command is doing four important things: * `wallet2.json` tells the CLI who should receive the funds * `0.01` is the amount in **SOL** * `--from wallet1.json` says which wallet should sign and pay * `--allow-unfunded-recipient` allows the transfer even if the receiving account has not been funded before Why that last flag matters: If the recipient account does not exist on-chain yet, Solana has to create it as part of the transfer flow. ### Step 6: Confirm the Balances Changed Check both wallets again. ```bash solana balance -k wallet1.json solana balance -k wallet2.json solana balance wallet2.json --lamports ``` You should now see: * `wallet1` has a little less than before because it sent funds and paid a fee * `wallet2` now has about `0.01 SOL` * the lamport view shows about `10000000` lamports in `wallet2` That is the same balance shown in two different units. ### Step 7: Inspect the Receiving Account Now look at the account itself instead of just the balance. ```bash solana account wallet2.json ``` Pay attention to these fields in the output: * `lamports`: the raw balance stored in the account * `owner`: for a normal wallet, this should be the System Program * `executable`: this should be `false` for a wallet account * `dataLength`: for a plain system wallet, this is usually `0` This is the first time you are looking at account state directly instead of only looking at a wallet app or balance number. *** ## What Rent Means Here Solana accounts need enough balance to stay alive on-chain. That minimum is called the **rent-exempt threshold**. For an empty system account, you can inspect it with: ```bash solana rent 0 ``` Why this matters: * wallet accounts need enough lamports to exist and pay fees * program-owned accounts often need to be funded above a rent threshold based on their data size * later, when you create accounts for programs, rent becomes part of account design Right now, you do not need to calculate rent by hand. The important thing is to understand that account storage on Solana is not free, and balance is part of keeping account state alive. *** ## What You Just Proved This exercise was small, but it gave you four important facts: 1. A wallet keypair maps to a real on-chain account 2. Balances live in accounts, not in your CLI or your laptop 3. SOL and lamports are the same value shown in different units 4. A transfer is not abstract magic, it is a state change you can inspect before and after That is the foundation for understanding transactions in the next lesson. *** ## Common Mistakes When you work with multiple keypairs, always pass `-k wallet1.json` or `-k wallet2.json` explicitly. If you do not, the CLI may fall back to the default wallet in your global Solana config. For `solana transfer`, the amount is in SOL by default. If you send `0.01`, you are sending `0.01 SOL`, not `0.01 lamports`. After the transfer, the sender will not be reduced by exactly `0.01 SOL`. It will be reduced by the transfer amount plus a small network fee. A plain wallet account is usually just a system-owned account with a balance and no custom data. Program accounts are where richer state starts to appear. *** ## Summary You created two wallets, funded one, transferred SOL, inspected the receiving account, and checked the same balance in both SOL and lamports. That means you now understand the basic unit of state on Solana: * accounts hold value * keypairs control accounts * transfers update account balances * rent affects whether accounts can stay alive on-chain *** ## Quick Check *** Next, you can move into transactions with a much better mental model because you have already seen what a balance change looks like from the CLI. # Devnet Investigation Challenge URL: https://www.learnsol.site/learn/solana-foundations/basic-exploration-challenge Markdown URL: https://www.learnsol.site/learn/solana-foundations/basic-exploration-challenge.mdx Use your Solana foundations toolkit to inspect real accounts, send a real transfer on devnet, and write down what changed before and after the transaction. A bad beginner challenge asks you to reverse-engineer a random devnet program before you can even explain a normal transfer cleanly. That is not a good foundations test. A good foundations challenge asks a simpler question: Can you observe one real transaction, inspect the accounts around it, and explain what changed without hand-waving? That is what this challenge is for. Use the toolkit from the previous lesson. Do not build a new client from scratch here. The point is to use your tools to investigate the chain, not to start another project. *** ## What You Need To Prove By the end of this challenge, you should be able to show all of these clearly: * the sender account before the transfer * the recipient account before the transfer * the transaction signature that was produced * the sender account after the transfer * the recipient account after the transfer * a short explanation of who signed, who received funds, and why the balances changed the way they did If you can do that without guessing, this section is working. *** ## The Challenge Setup Use the same two wallets from the earlier lessons: * `wallet1.json` as the sender * `wallet2.json` as the recipient And use the toolkit commands you already built: * `balance.ts` * `inspect.ts` * `send.ts` If your toolkit lives in `kit-toolkit/`, all the examples below assume you are inside that directory. *** ## The Real Goal This challenge is not really about sending another `0.01 SOL` transfer. It is about learning to observe a state change properly. A lot of new developers can send a transaction. Far fewer can answer these basic questions cleanly afterward: * which account authorized the change * which account received value * why the sender lost slightly more than the recipient gained * what the account output looked like before and after * where the transaction can be verified independently That is the skill you are testing here. B --> C --> D --> E `} /> For this challenge, proof means recorded evidence, not memory. Before you move on, make sure you capture: * the before balances for both wallets * the before account inspection output for both wallets * the transaction signature * the after balances for both wallets * the after account inspection output for both wallets * the Explorer page that confirms the transfer and fee *** ## Step-by-Step Challenge ### Step 1: Record the Starting State Fetch the starting balances for both wallets. ```bash pnpm tsx scripts/balance.ts $(solana address -k wallet1.json) pnpm tsx scripts/balance.ts $(solana address -k wallet2.json) ``` Then inspect both accounts more deeply: ```bash pnpm tsx scripts/inspect.ts $(solana address -k wallet1.json) pnpm tsx scripts/inspect.ts $(solana address -k wallet2.json) ``` Write down what you see. You do not need every field. The important ones are: * address * lamports * owner * executable * data length If you skip this and only check the ending state, you lose the ability to prove what changed. ### Step 2: Send One Real Transfer Now send a small transfer from `wallet1` to `wallet2`. ```bash pnpm tsx scripts/send.ts wallet1.json $(solana address -k wallet2.json) 0.01 ``` Your script should print a transaction signature. Save that signature. It is the public receipt for what just happened on-chain. ### Step 3: Record the Ending State Run the same balance and inspect commands again. ```bash pnpm tsx scripts/balance.ts $(solana address -k wallet1.json) pnpm tsx scripts/balance.ts $(solana address -k wallet2.json) pnpm tsx scripts/inspect.ts $(solana address -k wallet1.json) pnpm tsx scripts/inspect.ts $(solana address -k wallet2.json) ``` Now compare before and after. You should be able to explain: * why `wallet2` increased * why `wallet1` decreased by the transfer amount plus a fee * why both accounts are still normal wallet accounts owned by the System Program Do not stop at “the numbers changed.” Write down the balance delta for each wallet in lamports or SOL. That makes the fee visible instead of leaving it as a vague footnote. ### Step 4: Verify the Transaction in an Explorer Take the signature from Step 2 and open it in Solana Explorer on devnet. ```txt https://explorer.solana.com/tx/?cluster=devnet ``` Check these details: * the transaction landed on `devnet` * the sender matches `wallet1` * the recipient matches `wallet2` * the System Program handled the instruction * the transaction shows a fee being charged This matters because your script output is not the only source of truth. A good investigation always checks an independent public view too. ### Step 5: Write a Short Report Create a short note in plain English with this structure: ```md # Devnet Investigation Report ## Accounts - Sender: - Recipient: ## Before - Sender balance: - Recipient balance: - Sender owner / executable / data length: - Recipient owner / executable / data length: ## Transaction - Signature: - Program used: - Who signed: - Who paid the fee: ## After - Sender balance: - Recipient balance: - Sender owner / executable / data length: - Recipient owner / executable / data length: ## What Changed - Explain the balance movement in 3-5 sentences. - Explain why the sender lost slightly more than the recipient gained. - Explain why the recipient did not need to sign to receive funds. ``` Keep it short. The point is clarity, not volume. If you want a sharper standard, write the report so another developer could verify every claim from your evidence without rerunning your commands. *** ## What a Good Answer Looks Like A strong report does not just say “the transaction worked.” It says something closer to this: * `wallet1` signed and paid the fee * `wallet2` received `0.01 SOL` * the transfer used the System Program * the sender balance dropped by the transfer amount plus a network fee * the recipient account remained a normal system-owned wallet account That is the level of precision you want. *** ## Optional Stretch Task If you finish the main challenge comfortably, add one small improvement to your toolkit: * print both the signature and Explorer URL directly from `send.ts` That is useful because it closes the loop between your script output and public verification. *** ## Common Mistakes A successful process exit is not the same thing as understanding the result. Always inspect balances and account output before and after. If you compare only the transfer amount, your math will look wrong. The sender pays a transaction fee in addition to the amount sent. Explorer is part of the investigation. It gives you an external confirmation that the signature, accounts, and program line up with what your script reported. If your report says “the blockchain updated” or “the transaction happened,” it is too vague. Name the signer, the recipient, the fee payer, and the observable balance changes. *** ## Summary This challenge tests whether you can do more than run commands. You used your toolkit to: * inspect real accounts * send a real devnet transaction * verify the result in more than one place * explain the state change in precise language That is a real beginner milestone. It means you are no longer just copying Solana commands. You are starting to reason about what the network actually did. *** ## Module Recap *** You now have the baseline Solana model in place. You can identify the active wallet and cluster, reason about accounts as state containers, explain what a transaction contains, inspect account state directly, and use a real client toolkit to submit and verify a state change on devnet. That is enough to move into the next layer of Solana development without guessing at the basics. # Solana Development Environment Setup URL: https://www.learnsol.site/learn/solana-foundations/devnet-setup Markdown URL: https://www.learnsol.site/learn/solana-foundations/devnet-setup.mdx Set up the Solana CLI the right way, create a wallet, switch to devnet, fund it, and verify your machine is ready for the rest of the course. Most setup guides teach Solana like it is just another SDK install. That is the wrong mental model. What you are really doing in this lesson is preparing three things: 1. A command-line tool that can talk to Solana 2. A keypair that acts as your wallet identity 3. A network target, usually devnet, where your commands will run If those three pieces are clear in your head, the rest of this section gets much easier. This lesson is not about memorizing commands. It is about understanding what each command changes on your machine and on the network. *** ## What You Are Setting Up By the end of this lesson, you will be able to: * Install the Solana CLI and verify that it works * Create a local wallet keypair and understand where it lives * Point your CLI at the correct cluster * Request devnet SOL and confirm your wallet is funded * Inspect your environment before you start building That may sound basic, but this is where a lot of beginners get lost. They install the CLI, run a few commands, and then have no idea: * which wallet they are using * which network they are connected to * where their private key is stored * why one command works and the next one fails We are going to fix that now. *** ## The Three Pieces You Need to Track Before you type anything, lock in this model: * The **CLI** is the tool * The **keypair** is the identity * The **cluster** is the environment If something breaks, the problem is usually one of those three. When Solana commands behave strangely, do not guess. Check your CLI version, your wallet address, and your current cluster first. D B --> E C --> F D --> G E --> G F --> G `} /> *** ## Step 1: Install the Solana CLI The Solana CLI is your terminal interface to the network. You will use it to: * inspect wallet state * request devnet funds * switch networks * deploy programs later * debug account and transaction behavior Install it with: ```bash sh -c "$(curl -sSfL https://release.solana.com/stable/install)" ``` If your shell does not pick up the binary automatically, add the Solana install directory to your `PATH`: ```bash export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" ``` Now verify the install: ```bash solana --version ``` If that command prints a version, the CLI is installed correctly. If it says `command not found`, the install may have succeeded but your shell still cannot find the binary. That is a `PATH` problem, not a Solana problem. If you are on Windows, use WSL2. Solana and Rust tooling are much more predictable there than in a native Windows shell. *** ## Step 2: Create Your Wallet Keypair Beginners often say “create a wallet” as if the wallet is a website account. It is not. On Solana, your wallet is a cryptographic keypair: * the **public key** is your address * the **private key** signs transactions Generate one with: ```bash solana-keygen new ``` This command writes a new keypair file to disk. By default, it usually ends up here: ```bash ~/.config/solana/id.json ``` That file matters. It is not a cache file. It is not disposable setup output. It is the private key material for the wallet you are about to use. ### Check the address for that wallet ```bash solana address ``` This prints the public key for the keypair the CLI is currently using. If you want to see the full CLI configuration, run: ```bash solana config get ``` You should see at least: * the RPC URL * the websocket URL * the keypair path That output tells you which machine identity and which cluster your CLI is actually using. Do not commit `id.json` to Git. If someone gets that file, they control that wallet. *** ## Step 3: Point the CLI at Devnet Solana has multiple clusters. A cluster is just a running network environment. For this course, the default target is usually **devnet** because it is public, free to use, and safe for learning. Set it explicitly: ```bash solana config set --url https://api.devnet.solana.com ``` Then verify it: ```bash solana config get ``` Do not skip that verification step. A lot of beginner confusion comes from assuming the CLI is on devnet when it is actually pointed somewhere else. ### What the main clusters are for ### Devnet The public learning and testing network. This is where you should spend most of your early time. ### Localnet A local validator running on your own machine. This is faster and more controlled, but it is a separate environment from devnet. ### Testnet Mostly used for validator and protocol testing. It is not the best place for beginner app development. ### Mainnet Beta The real network with real value. If you are still learning the CLI, you should not be here yet. *** ## Step 4: Fund the Wallet on Devnet Even on devnet, transactions are not free. You still need SOL to pay transaction fees. The difference is that on devnet, you can ask for test SOL from a faucet. Request an airdrop: ```bash solana airdrop 2 ``` Then confirm your balance: ```bash solana balance ``` If the airdrop fails, do not immediately reinstall everything. Check the obvious things first: * are you on `devnet`? * are you using the wallet you think you are using? * is the faucet rate-limiting requests? This is why the earlier `solana config get` step matters. *** ## Step 5: Learn the Minimum CLI Health Check Before you move to the next lesson, get comfortable with this tiny inspection workflow: ```bash solana --version solana config get solana address solana balance ``` Those four commands answer the four questions that matter most: * Is the CLI installed? * Which cluster am I talking to? * Which wallet am I using? * Does that wallet have SOL? If you build the habit of checking those early, you will debug much faster later. *** ## The Workflow You Are Building 1. ### Install the tool Install the CLI and make sure your shell can actually run `solana`. 2. ### Create the identity Generate a keypair and understand that the file on disk is your wallet. 3. ### Choose the environment Point the CLI at devnet so your commands hit the correct network. 4. ### Fund the wallet Request devnet SOL and confirm the balance before trying anything more advanced. 5. ### Inspect before debugging Use `solana config get`, `solana address`, and `solana balance` before assuming something is broken. That is the real setup flow. Not "install package, move on." *** ## Common Mistakes That Waste Time Your wallet is a keypair file on disk. If you change that file, or point the CLI to a different file, you are using a different identity. If you airdrop on devnet and later point the CLI to localnet or mainnet, your balance will appear to "disappear." It did not disappear. You changed environments. Do not rely on what you think your machine is using. Run `solana config get` and verify it. Mainnet is not a place to learn basic tooling. Learn on devnet, then move to localnet and production flows when your mental model is solid. *** ## What This Enables Next With the CLI working, a funded devnet wallet, and a clear cluster configuration, you are ready for the rest of this section. That means you can now start learning the parts that actually make Solana feel different: * how accounts hold state * why lamports matter * how transactions are structured * how programs interact with accounts Without this setup, those lessons feel abstract. With this setup, they become testable. *** ## Quick Check *** ## Summary Here is the core takeaway: * The CLI is your tool * The keypair is your identity * The cluster is your environment Once those are clear, Solana setup stops feeling random. You now have: * a working Solana CLI * a wallet keypair on your machine * a devnet configuration * test SOL for experiments * a repeatable way to verify your environment before building That is enough foundation for the next lesson. # Solana Fundamentals URL: https://www.learnsol.site/learn/solana-foundations Markdown URL: https://www.learnsol.site/learn/solana-foundations.mdx Solana Foundations teaches the core mental model for Solana: wallets, accounts, transactions, network architecture, and your first modern client toolkit with @solana/kit. Solana Foundations is where Solana stops looking like a list of commands and starts looking like a system. If this week works, you should leave with a clean answer to questions like these: * what a wallet really is * where balance and state actually live * what a transaction really sends to the network * why Solana can process transactions quickly * how to read and write chain state from a real client script That is the point of this module. Follow this section in order. The sequence matters. Each lesson removes one source of beginner confusion before the next lesson adds a new moving part. *** ## What Solana Foundations Covers By the end of this week, you should be able to: * use the Solana CLI without guessing which wallet or cluster you are using * explain the difference between a keypair, an address, and an account * send and inspect a real devnet transaction * understand the parts inside a Solana transaction * explain the high-level systems that make Solana fast * build a small devnet toolkit with `@solana/kit` * investigate a real transfer and explain the state change clearly That is a solid foundation. It is not everything about Solana, and it does not need to be. It is the part that makes later lessons stop feeling mysterious. *** ## Why This Order Exists A common mistake in Solana learning is jumping straight into programs, Anchor, or random SDK code without first understanding basic state and transaction flow. That creates fake progress. You can copy commands and still have no idea: * which wallet is signing * why a transaction failed * what an account owner means * why the sender balance changed by more than the transfer amount * what the network actually had to do after you clicked send This week fixes that in the right order. *** ## Solana Foundations Roadmap B --> C --> D --> E --> F `} /> 1. ### Development Environment Setup You set up the CLI, create a wallet, point it at devnet, and fund it. This lesson removes the first source of confusion: not knowing which tool, wallet, or cluster you are actually using. 2. ### Working with Accounts and Lamports You create two wallets, move SOL between them, and inspect the resulting accounts. This lesson makes the account model concrete. 3. ### Understanding Solana Transactions You learn that a transaction is not just a wallet action or a terminal command. It is a signed message with instructions, accounts, signatures, and a lifetime. This lesson fixes the transaction mental model. 4. ### How Solana Stays Fast You zoom out and look at validators, leaders, Proof of History, Sealevel, Gulf Stream, and Turbine. This lesson explains how the network handles ordering, execution, and propagation. 5. ### Build a Solana Devnet Toolkit with Kit You build your first real client toolkit with the modern SDK path: * read balances * inspect accounts * send SOL on devnet This lesson moves you from CLI-only interaction into real client code. 6. ### Devnet Investigation Challenge You use your toolkit to inspect a real transfer before and after it happens, verify it in Explorer, and explain the result precisely. This lesson proves you can reason about state changes instead of only copying commands. *** ## What You Will Build By The End By the end of this section, you will have built a small off-chain toolkit that can: * fetch account balances * inspect account facts * send a real transaction on devnet That may sound small, but it is exactly the right size for a first module. You do not need a giant project yet. You need a trustworthy mental model and one working toolchain. *** ## How To Use This Module Well Use these rules while going through the lessons: * run the commands yourself * inspect outputs before moving on * keep using the same two wallets across the exercises * do not skip the balance and account inspection steps * when something feels unclear, ask what changed on-chain, not just what command you typed That last rule matters the most. Solana gets easier once you stop thinking in terms of command trivia and start thinking in terms of state changes. *** ## What Success Looks Like A strong outcome for this section sounds like this: * I know which wallet I am using and which cluster I am on * I can explain where SOL balance lives * I understand what a transaction contains * I know why the sender pays a fee * I can inspect an account and interpret the important fields * I can send a real transfer from code using `@solana/kit` If you can say all of that honestly, you are ready to move beyond the basics. *** ## Quick Check *** ## Summary This section is the foundation layer. It teaches the minimum Solana model that everything else depends on: * wallets control accounts * accounts hold state * transactions express state changes * the network has to order, execute, verify, and propagate those changes * clients need to read and write that state cleanly That is what the rest of the course will build on. # Build a Solana Devnet Toolkit with Kit URL: https://www.learnsol.site/learn/solana-foundations/program-interaction-project Markdown URL: https://www.learnsol.site/learn/solana-foundations/program-interaction-project.mdx Build a small TypeScript CLI with @solana/kit to read balances, inspect accounts, and send SOL on devnet using the modern Solana client stack. A lot of older Solana tutorials still teach client code with `@solana/web3.js`. That is no longer the right default for new code. For new scripts and clients, `@solana/web3.js` 1.x is the legacy path. The modern low-level SDK is `@solana/kit`, which is the renamed 2.x direction for Solana JavaScript development. So this project will not teach the old client surface first and fix it later. We will build with `@solana/kit` from the start. This project does not ask you to write an on-chain program yet. The goal is to build a small off-chain toolkit that can talk to devnet, inspect state, and send a real transaction. *** ## What You Are Building By the end of this project, you will have a tiny CLI toolkit with three commands: * `balance`: read the balance of any address * `inspect`: fetch raw account facts and print them clearly * `send`: send SOL from one wallet to another on devnet That is enough for a strong foundations project because it teaches three real client responsibilities: 1. reading chain state 2. understanding account shape 3. building and sending a transaction correctly *** ## The Mental Model for This Project Do not think of this toolkit as “three random scripts.” Think of it as one small client with three jobs: * connect to an RPC endpoint * turn user input into typed Solana values * either read state or produce a signed transaction That is the same pattern you will keep using later in frontend code, backend services, and Anchor clients. *** ## What You Need Before You Start You should already have: * Node.js installed * `pnpm` available on your machine * the Solana CLI set to `devnet` * a funded wallet from the earlier lessons in this section, such as `wallet1.json` * a second wallet address to receive SOL, such as the address from `wallet2.json` If you are not sure those are ready, stop and re-run the earlier setup and account lessons first. *** ## The Final Project Shape We are going to build this exact file structure: ```txt kit-toolkit/ package.json tsconfig.json scripts/ lib/ solana.ts balance.ts inspect.ts send.ts ``` Keep it small. This is not the time to build a framework. This is the time to make the client model feel normal. B --> E A --> C --> F A --> D --> G `} /> *** ## Step 1: Scaffold the Toolkit Create a fresh project directory and install the dependencies. ```bash mkdir kit-toolkit cd kit-toolkit pnpm init pnpm add @solana/kit @solana-program/system pnpm add -D typescript tsx @types/node ``` Why these packages: * `@solana/kit` gives you the modern RPC, signer, and transaction-building APIs * `@solana-program/system` gives you the System Program instruction builder for SOL transfers * `tsx` lets you run TypeScript files directly without adding extra build friction Now create `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["scripts/**/*.ts"] } ``` This is a minimal config. It is enough for a small Node-based toolkit. *** ## Step 2: Create the Shared Solana Helper All three scripts need the same basic pieces: * a devnet RPC client * a devnet WebSocket subscriptions client for confirmation * a helper that loads a signer from a local keypair JSON file Create `scripts/lib/solana.ts`: ```ts import { readFile } from "node:fs/promises"; import { createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcSubscriptions, } from "@solana/kit"; export const DEVNET_RPC_URL = "https://api.devnet.solana.com"; export const DEVNET_WS_URL = "wss://api.devnet.solana.com"; // One HTTP RPC client for read requests and transaction-building data. export const rpc = createSolanaRpc(DEVNET_RPC_URL); // One subscriptions client for waiting on transaction confirmation. export const rpcSubscriptions = createSolanaRpcSubscriptions(DEVNET_WS_URL); export async function loadSignerFromFile(path: string) { // Solana CLI keypair files are JSON arrays of secret key bytes. const file = await readFile(path, "utf8"); const bytes = new Uint8Array(JSON.parse(file)); // Convert raw bytes into a Kit signer that can actually sign transactions. return createKeyPairSignerFromBytes(bytes); } ``` Why this file matters: It keeps the RPC and signer setup in one place. If you scatter that logic across every script, the project gets harder to reason about for no benefit. *** ## Step 3: Build the Balance Command Start with the easiest possible read. Create `scripts/balance.ts`: ```ts import { address } from "@solana/kit"; import { rpc } from "./lib/solana"; async function main() { const input = process.argv[2]; if (!input) { throw new Error("Usage: pnpm tsx scripts/balance.ts
"); } // Convert the raw string into a typed Solana address. const accountAddress = address(input); const { value: balanceInLamports } = await rpc.getBalance(accountAddress).send(); console.log({ address: input, lamports: balanceInLamports.toString(), sol: Number(balanceInLamports) / 1_000_000_000, }); } main().catch((error) => { console.error("balance failed", error); process.exit(1); }); ``` Run it against the wallet you funded earlier: ```bash pnpm tsx scripts/balance.ts $(solana address -k wallet1.json) ``` If you see `0` lamports, verify your wallet is funded on devnet with `solana balance -k wallet1.json`. If you get an RPC error, check that devnet is reachable with `solana cluster-version`. What this teaches: * how to create an RPC client with Kit * how to turn a string into an `Address` * how basic RPC reads work in the `request.send()` model *** ## Step 4: Build the Account Inspector Balance is only one fact. Now inspect the account itself. Create `scripts/inspect.ts`: ```ts import { address, fetchEncodedAccount } from "@solana/kit"; import { rpc } from "./lib/solana"; async function main() { const input = process.argv[2]; if (!input) { throw new Error("Usage: pnpm tsx scripts/inspect.ts
"); } const accountAddress = address(input); const account = await fetchEncodedAccount(rpc, accountAddress); if (!account.exists) { console.log({ exists: false, address: input }); return; } console.log({ exists: true, address: input, lamports: account.lamports.toString(), owner: account.owner.toString(), executable: account.executable, dataLength: account.data.length, }); } main().catch((error) => { console.error("inspect failed", error); process.exit(1); }); ``` Run it: ```bash pnpm tsx scripts/inspect.ts $(solana address -k wallet2.json) ``` What to notice in the output: * `lamports` is the raw balance stored in the account * `owner` tells you which program owns the account layout * `executable` is `false` for a normal wallet account * `dataLength` is usually `0` for a plain system wallet This is the same account model you already saw in the CLI lessons, now accessed through code. *** ## Step 5: Build the Send Command This is the most important part of the project. You are now moving from reads to writes. That means you need to: * load a signer * fetch a fresh blockhash * build a transaction message * append an instruction * sign the message * send it and wait for confirmation Create `scripts/send.ts`: ```ts import { address, appendTransactionMessageInstructions, createTransactionMessage, getSignatureFromTransaction, lamports, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit"; import { getTransferSolInstruction } from "@solana-program/system"; import { loadSignerFromFile, rpc, rpcSubscriptions } from "./lib/solana"; async function main() { const senderKeypairPath = process.argv[2]; const recipientAddressInput = process.argv[3]; const amountSolInput = process.argv[4]; if (!senderKeypairPath || !recipientAddressInput || !amountSolInput) { throw new Error( "Usage: pnpm tsx scripts/send.ts " ); } const sender = await loadSignerFromFile(senderKeypairPath); const recipientAddress = address(recipientAddressInput); const amountSol = Number(amountSolInput); if (!Number.isFinite(amountSol) || amountSol <= 0) { throw new Error("Amount must be a positive SOL value like 0.01"); } // Convert SOL into lamports so the on-chain amount is explicit. const amountLamports = BigInt(Math.round(amountSol * 1_000_000_000)); // Fetch a fresh blockhash before building the transaction message. const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); // Build the transfer instruction for the System Program. const transferInstruction = getTransferSolInstruction({ source: sender, destination: recipientAddress, amount: lamports(amountLamports), }); // Build the transaction in the correct order: // message -> fee payer -> lifetime -> instructions. const message = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(sender, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([transferInstruction], tx) ); // Sign after the message is fully built. const signedTransaction = await signTransactionMessageWithSigners(message); // Send the transaction bytes and wait for confirmation. const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); await sendAndConfirm(signedTransaction, { commitment: "confirmed" }); console.log({ signature: getSignatureFromTransaction(signedTransaction), from: sender.address, to: recipientAddress, amountSol, }); } main().catch((error) => { console.error("send failed", error); process.exit(1); }); ``` Now run it using the wallet from your earlier lessons: ```bash pnpm tsx scripts/send.ts wallet1.json $(solana address -k wallet2.json) 0.01 ``` Then confirm the state change: ```bash pnpm tsx scripts/balance.ts $(solana address -k wallet1.json) pnpm tsx scripts/balance.ts $(solana address -k wallet2.json) ``` If the send fails with "insufficient funds," remember you need enough SOL to cover both the transfer amount and the transaction fee (\~5000 lamports). Use `pnpm tsx scripts/balance.ts` to check your sender's balance first. If you see "blockhash not found" or "transaction expired," the blockhash became stale before confirmation. This happens if there is a long delay between fetching the blockhash and sending. Re-run the command to get a fresh blockhash. What this command teaches that the read scripts did not: * the fee payer must be explicit * transaction lifetime depends on a fresh blockhash * the message must be fully built before signing * sending and confirming are separate steps The order in `send.ts` is not stylistic. It is a correctness rule. You start with an empty transaction message. Then you attach the fee payer, because the network must know who is covering the fee. Then you attach the latest blockhash, because the transaction needs a freshness window. Then you append the transfer instruction, because the message now has the context it needs. Only after all of that do you sign. That sequence matters because a signature is over the fully constructed message. If you change the fee payer, change the lifetime, or add instructions after signing, the original signature no longer matches the message you are trying to send. *** ## The Client Pattern You Just Built This project should not feel like three unrelated scripts. It should feel like one small Solana client split into three commands. Each command reuses the same foundation: * one RPC setup * one signer-loading path * one way to turn raw strings into typed Solana values From there, the commands branch into two kinds of work: * read state with `balance.ts` and `inspect.ts` * write state with `send.ts` That is the pattern that keeps showing up later in real Solana work. A frontend does the same thing. A backend job does the same thing. An Anchor client does the same thing. The surface area gets bigger, but the client model does not fundamentally change. *** ## Common Mistakes For new low-level client code, prefer `@solana/kit`. If you keep learning through the legacy surface first, you will eventually have to unlearn the object-heavy API model anyway. Reading an account only needs a valid address and RPC access. Sending a transaction needs a signer, a fresh blockhash, a complete message, and a confirmation flow. Once the transaction message is signed, you should not mutate it. Build the fee payer, lifetime, and instructions first. Sign last. `wallet1.json` contains secret key material. Never commit it, publish it, or paste it into random tools. *** ## Summary You built a real Solana client toolkit with the modern SDK direction: * one helper for RPC and signer loading * one script for balance reads * one script for account inspection * one script for signed SOL transfers That means you can now do the three things every real Solana client must eventually do: * fetch state * interpret state * submit state changes That is a much stronger outcome for this section than just knowing a few commands. ## Sources * [https://github.com/anza-xyz/kit](https://github.com/anza-xyz/kit) * [https://solana.com/developers/cookbook/wallets/restore-keypair](https://solana.com/developers/cookbook/wallets/restore-keypair) * [https://solana.com/developers/cookbook/accounts/get-account-balance](https://solana.com/developers/cookbook/accounts/get-account-balance) * [https://www.solanakit.com/docs/getting-started/build-transaction](https://www.solanakit.com/docs/getting-started/build-transaction) * [https://www.solanakit.com/docs/getting-started/send-transaction](https://www.solanakit.com/docs/getting-started/send-transaction) *** ## Quick Check # How Solana Stays Fast URL: https://www.learnsol.site/learn/solana-foundations/solana-architecture Markdown URL: https://www.learnsol.site/learn/solana-foundations/solana-architecture.mdx Understand how Solana orders, executes, and propagates transactions through validators, leaders, Proof of History, Sealevel, Gulf Stream, and Turbine. By this point, you have already done the most important beginner work: * set up the CLI * funded accounts on devnet * sent a transaction * inspected the state change Now the question gets bigger. How does Solana take all of those transactions from all of those users and process them fast enough to feel responsive without turning the network into a black box? That is the architecture question. You do not need to memorize every subsystem in this lesson. The goal is to leave with a clean mental model of how Solana handles ordering, leadership, execution, and propagation. *** ## What You Are Learning By the end of this lesson, you will be able to: * explain the main problem Solana architecture is trying to solve * understand what validators, leaders, slots, and epochs do * describe why Proof of History exists * explain how Sealevel makes parallel execution possible * understand what Gulf Stream and Turbine contribute to performance *** ## Start with the Real Problem Every blockchain has to solve a hard coordination problem. People all over the world are sending transactions at nearly the same time. The network has to answer questions like these: * which transaction came first * who is allowed to produce the next block * which transactions can run safely at the same time * how the result gets distributed to everyone else If that process is slow, the chain feels slow. If that process is sloppy, the chain becomes unsafe. Solana is built around the idea that you should make ordering, scheduling, and execution more efficient instead of forcing every validator to pause and rediscover basic timing information from scratch. *** ## The Mental Model For this section, use this simple model: Solana architecture is doing four jobs at once: 1. **Choose who leads next** 2. **Give the network a shared sense of ordering** 3. **Execute transactions efficiently** 4. **Propagate the results fast enough to keep up** Most of the named Solana systems fit into one of those jobs. B B --> C D --> A C --> E E --> F `} /> *** ## The Main Moving Parts Before we follow a transaction through the network, lock in these pieces. ### Validators Validators are the machines participating in the network. They do not all do the exact same thing at the exact same moment, but they all help maintain the ledger by: * receiving transactions * verifying blocks * voting on results * storing and serving network state A validator is not just a passive observer. It is part of the machinery that keeps Solana consistent. ### Leaders At any given slot, one validator is the **leader**. The leader has the short-term job of collecting transactions, ordering them, producing a block, and broadcasting it to the rest of the network. This matters because Solana does not make every validator compete blindly for block production at every moment. It gives the network a known producer for each short time window. ### Slots and Epochs A **slot** is a small unit of time in which a leader gets a chance to produce a block. An **epoch** is a larger stretch of time that contains many slots and defines the broader validator schedule. You do not need the exact math yet. What matters is the structure: * epochs organize the schedule * slots are the short windows where leaders take turns That predictable schedule reduces coordination overhead. ### Proof of History Proof of History, or **PoH**, is Solana's cryptographic clock. This is the part people often misunderstand. PoH is not the same thing as consensus. PoH does not replace validator voting. What it does is give the network a verifiable ordering signal so participants do not have to argue from scratch about when things happened. The short version is: * a sequential hash process keeps producing outputs * those outputs are hard to fake and easy to verify * transactions can be anchored into that sequence * validators can use that sequence as a shared timing reference That is why PoH is best thought of as a clock, not as the whole security model. Proof of History uses a **sequential SHA-256 hash chain** where each output becomes the input for the next hash. This creates a verifiable sequence that proves time has passed. **The Core Mechanism:** 1. Start with an initial value 2. Hash it to get output₁ 3. Use output₁ as input to get output₂ 4. Repeat continuously ``` hash(initial) → output₁ hash(output₁) → output₂ hash(output₂) → output₃ ... ``` **Why This Proves Time:** * Each hash takes a minimum amount of real time to compute * You cannot skip ahead—you must compute each hash in sequence * But verification is fast: validators can check the chain in parallel **How Transactions Fit In:** Transactions are "mixed in" to the hash chain. When a transaction arrives, its hash becomes part of the next PoH output. This anchors the transaction to a specific point in the sequence. **What PoH Does NOT Do:** * It does not replace consensus voting * It does not guarantee transaction finality * It does not prevent malicious leaders PoH is a **coordination optimization**. It gives everyone a shared clock so validators spend less time arguing about ordering. ### Sealevel Sealevel is Solana's parallel execution runtime. This is where the account model starts to matter. Because Solana transactions declare which accounts they touch, the runtime can see when two transactions do not conflict. If they do not touch the same writable state, they can run in parallel. If they do conflict, they must wait. So Solana is not “parallel all the time.” It is parallel when the account access pattern allows it. ### Gulf Stream and Turbine These two systems help Solana move faster without changing the basic meaning of transactions. * **Gulf Stream** forwards transactions toward upcoming leaders instead of relying on a heavy mempool-style waiting room. * **Turbine** helps distribute blocks efficiently across the validator set by breaking the data into smaller pieces for propagation. One helps transactions get to the right producer quickly. The other helps finished blocks spread through the network quickly. *** ## Follow One Transaction Through the Architecture Use a simple SOL transfer as the anchor example. ```bash solana transfer wallet2.json 0.01 --from wallet1.json --allow-unfunded-recipient ``` You already understand the user-facing side of this transaction. Now look at what the network has to do behind the scenes. 1. ### Step 1: The Transaction Enters the Network Your client sends the signed transaction to an RPC node. From there, the network needs to get that transaction in front of the right validators quickly. This is where transaction forwarding matters. Solana tries to move transactions toward upcoming leaders instead of letting them sit around waiting without direction. 2. ### Step 2: The Current Leader Orders It The leader for the current slot collects transactions and places them into an ordered flow. Proof of History helps here by giving the network a shared timing reference. That does not mean every validator blindly trusts the leader. It means the ordering signal is much more structured and easier to verify. 3. ### Step 3: The Runtime Decides What Can Run Together When transactions are ready to execute, Sealevel checks their account access patterns. If two transactions touch different writable accounts, they can run at the same time. If they both need the same writable account, they cannot. That is one of the biggest reasons Solana can process more work without treating all transactions like one long queue. 4. ### Step 4: Validators Verify and Vote Other validators receive the produced block, verify what happened, and vote. This is why it is wrong to say PoH alone secures Solana. The network still depends on validators checking work and participating in consensus around the produced ledger state. 5. ### Step 5: The Block Gets Propagated Across the Network Once produced, block data has to spread fast enough that the rest of the network can keep up. Turbine helps with that propagation step. If block distribution were slow, fast execution would not be enough. The network would still bottleneck on communication. *** ## Why This Feels Different from Simpler Blockchain Models A simpler blockchain mental model is: * gather transactions * produce a block * everyone waits * repeat Solana is more structured than that. It uses: * scheduled leadership * a cryptographic clock for ordering * account-aware parallel execution * fast forwarding and propagation systems That is why the network can behave differently from systems where transactions mostly wait in a generic queue and execution is treated as broadly serial. *** ## A Small Example of Parallelism Imagine three transactions arrive close together: ```txt Tx1 writes Alice Tx2 writes Bob Tx3 writes Alice ``` Here is the important consequence: * `Tx1` and `Tx2` can run together because they do not conflict * `Tx3` cannot run at the same time as `Tx1` because both need writable access to Alice's state So the right mental model is not “Solana runs everything in parallel.” The right model is: Solana can parallelize work when the account graph allows it. That is much more precise, and it will help you later when you debug compute and account contention issues. *** ## Common Mistakes It is not. Proof of History provides a verifiable clock and ordering aid. Validators still verify blocks and vote as part of the network's broader consensus process. Solana is not fast because it skips verification. It is fast because the network is organized to reduce waste in ordering, scheduling, execution, and propagation. Parallelism depends on account access. If transactions contend on the same writable state, they still serialize. These concepts are not only for interviews. They explain real behavior you will see later: transaction delays, account contention, block production timing, and why some operations compose well while others do not. *** ## Summary Solana architecture is easiest to understand as a system for handling four jobs well: * choose leaders predictably * maintain a shared sense of ordering * execute non-conflicting work in parallel * propagate results across the network quickly That is where validators, slots, epochs, Proof of History, Sealevel, Gulf Stream, and Turbine fit. You do not need every internal detail yet. You do need the core model: when you send a transaction, Solana is not just “processing a request.” It is scheduling leadership, ordering messages, executing account-aware work, verifying the result, and spreading that result across a distributed network. With that model in place, the rest of the course will feel much less abstract. # Understanding Solana Transactions URL: https://www.learnsol.site/learn/solana-foundations/transaction-anatomy Markdown URL: https://www.learnsol.site/learn/solana-foundations/transaction-anatomy.mdx Use one simple SOL transfer to understand what a Solana transaction contains, why signers and recent blockhashes matter, and how a transaction moves through the network. Most beginners think a transaction is just “the thing that sends SOL.” That description is too shallow. A Solana transaction is a **signed message** that tells the network which instruction to run, which accounts are involved, and who has approved the action. That is the mental model you need for everything that comes later. If you understand transactions, program calls stop feeling magical. This lesson builds directly on the previous one. The `solana transfer` command you already ran is the concrete example we will keep returning to. *** ## What You Are Learning By the end of this lesson, you will be able to: * explain what a Solana transaction actually is * identify the main pieces inside a transaction * understand why some accounts sign and others do not * explain what a recent blockhash does * describe how a transaction goes from your machine to the network *** ## Start with One Real Transaction Use the transfer from the last lesson as the anchor example: ```bash solana transfer wallet2.json 0.01 --from wallet1.json --allow-unfunded-recipient ``` From the terminal, that looks like one line. Inside the network, it is more structured. This command causes the CLI to build a transaction that roughly means: ```txt Run a system transfer instruction. Move 0.01 SOL from wallet1 to wallet2. Use wallet1 as the signer and fee payer. Attach a recent blockhash so the network treats the message as fresh. ``` That is already enough to see the important shift in thinking: A transaction is not the same thing as a command. The command is what you type. The transaction is the signed message the CLI sends to Solana. *** ## The Four Pieces Inside a Transaction For this section, you can think of a transaction as having four important pieces: 1. **Instruction** 2. **Accounts** 3. **Signatures** 4. **Recent blockhash** Here is what each one means. ### 1. Instruction The instruction is the actual action you want the network to execute. In the example above, the instruction is a system transfer. Later in the course, instructions might also mean: * create an account * mint a token * call an Anchor program * update some on-chain state A transaction can contain one instruction or several. ### 2. Accounts Instructions do not run in a vacuum. They need accounts. For a simple SOL transfer, the important accounts are: * the sender account * the recipient account * the System Program The transaction has to name those accounts explicitly so the runtime knows what can be read, written, or executed. ### 3. Signatures A signature proves that the right private key approved the transaction. In the SOL transfer example, `wallet1` must sign because it is authorizing funds to leave that account. This is where beginners often get tripped up: * an account can be involved in a transaction without signing * an account can be writable without being a signer For example, `wallet2` receives funds, but it does not need to sign just to receive them. That distinction matters later when you start passing multiple accounts into programs. ### 4. Recent Blockhash A transaction also carries a recent blockhash. Do not think of it as a timestamp in the normal wall-clock sense. Think of it as a freshness marker. It tells the network, “this transaction was built recently enough to still be valid.” That helps prevent old signed transactions from being replayed later. Most of the time, the CLI or SDK fetches and inserts the blockhash for you. You still need to know why it is there. *** ## What the Transaction Is Really Saying If we rewrite the earlier transfer in plain English, the transaction is roughly saying: ```txt System Program, please move value from wallet1 to wallet2. wallet1 approves this action. Here is a recent blockhash to prove this request is still fresh. ``` That is a much better mental model than “I ran a transfer command.” The network is not reading your terminal command. It is validating and executing a structured, signed message. *** ## How a Transaction Moves Through the Network B B --> C B --> D B --> E C --> F D --> F E --> F F --> G --> H `} /> Now that you know what is inside the message, here is the lifecycle. 1. ### Step 1: The Client Builds the Transaction Your client can be the CLI, a frontend, or a backend script. It chooses: * which instruction to run * which accounts to include * which signer or fee payer is required * which recent blockhash to attach At this point, the transaction exists as data, but it is not authorized yet. 2. ### Step 2: Required Signers Approve It The transaction is signed with the private keys that must authorize it. For the transfer example, `wallet1.json` signs because it is the source of funds. If the required signature is missing or wrong, the network rejects the transaction. 3. ### Step 3: The Transaction Is Sent to Solana The signed transaction is submitted to an RPC node, which forwards it into the network. At this point, the network can inspect: * whether the signatures are valid * whether the blockhash is still fresh * whether the accounts and instruction are well formed 4. ### Step 4: A Leader Validator Executes It A leader validator includes the transaction in a block and executes the instruction. If execution succeeds, the account state changes. In the transfer example, that means: * the sender balance goes down * the recipient balance goes up * the fee payer also pays a transaction fee If execution fails, the state change does not partially apply. 5. ### Step 5: The Network Confirms the Result Other validators observe and vote on the block. From the developer point of view, this is when the transaction moves from “submitted” to “confirmed,” and later toward stronger finality. For now, the important beginner takeaway is simple: sending a transaction is not the same thing as knowing it succeeded. Confirmation is a separate part of the lifecycle. *** ## Why Signers Matter So Much Signers are the permission layer of Solana transactions. If the wrong account signs, or a required signer is missing, the instruction should fail. That is how Solana prevents random clients from spending someone else’s funds or mutating protected state. A useful rule for this section is: * if an account is paying, authorizing, or proving ownership, expect a signature to matter * if an account is only being referenced or receiving value, it may not need to sign This rule is not the full runtime model, but it will keep your intuition pointed in the right direction. *** ## Why the Recent Blockhash Matters Without a freshness check, an old signed transaction could be reused later. That would be dangerous. The recent blockhash reduces that risk by making transactions expire after they are no longer recent enough. This has a practical consequence for developers: if you build a transaction and wait too long before sending it, the transaction can expire and fail even if the signatures are correct. That is why transaction-building libraries fetch blockhashes close to send time. *** ## One More Useful Detail: Instructions Are Atomic A transaction can contain multiple instructions. When that happens, they run as one atomic unit. That means: * either the whole transaction succeeds * or the whole transaction fails You do not get half a successful transaction. That property becomes more important once you start creating accounts and then using them in the same transaction. *** ## Common Mistakes The CLI command is just how you ask for a transaction to be built. The network only sees the serialized transaction message and its signatures. That is not true. The sender in a transfer must sign. The recipient usually does not. You may not fetch it manually very often, but transaction freshness depends on it. If a transaction sits around too long, it can expire. Submitting a transaction and confirming a transaction are two different stages. Always care about the final result, not just the submission step. *** ## Summary A Solana transaction is a signed message, not just a wallet action. It carries: * an instruction that says what should happen * the accounts involved in that action * signatures that authorize it * a recent blockhash that proves the message is still fresh That is why the transfer from the previous lesson worked the way it did. The CLI built the message, `wallet1` signed it, the network checked it, and validators applied the state change. With that model in place, the next lessons on architecture and programs will make much more sense because you now know what is actually moving through the network when you interact with Solana. *** When you build a transaction, it gets serialized into a compact binary format before being sent to the network. Understanding this structure helps when debugging or working with raw transaction data. **Message Header** The header contains three bytes: * Number of required signatures * Number of read-only signed accounts * Number of read-only unsigned accounts **Account Keys Array** All accounts referenced by the transaction are listed in a specific order: 1. Writable signers (fee payer first) 2. Read-only signers 3. Writable non-signers 4. Read-only non-signers **Recent Blockhash** A 32-byte blockhash that anchors the transaction to a specific point in time. **Compact Instruction Array** Each instruction contains: * Program ID index (pointing to the account keys array) * Account indices (which accounts this instruction uses) * Instruction data (the actual payload for the program) **Signatures** An array of 64-byte Ed25519 signatures, one for each required signer, in the same order as signers appear in the account keys array. **Why This Matters** When you see errors like "Transaction too large" or "Invalid account index," understanding this binary layout helps you diagnose the issue. The maximum transaction size is 1232 bytes, which limits how many accounts and instructions you can pack into a single transaction. *** *** # Anchor Client Patterns with Kit URL: https://www.learnsol.site/learn/solana-kit-clients/anchor-client-with-kit Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/anchor-client-with-kit.mdx Build one real program action flow by keeping instruction building in a service layer and transport in the shared client pipeline. The main client-side mistake with Anchor is simple. People let React components assemble raw program calls directly. That feels fast on day one. It becomes a mess as soon as the app has two buttons, three account types, and one real failure path. The correct model is: * the service layer knows how to build program instructions * the shared client transport knows how to send them * the UI only gathers input, triggers the action, and renders status That split is what keeps a Solana app explainable. B[Service layer builds instruction] B --> C[Shared transport sends] C --> D[Wallet signs] D --> E[Signature and status return to UI] `} /> ## What This Lesson Owns This lesson is not about generating an SDK. It is about using one correctly. Assume your Anchor program already exposes a generated instruction builder for the `increment` instruction. That builder can come from your IDL generation step. The important point is where you use it. ## Step 1: Build The Program Action In One Place Create `lib/solana/programs/counterService.ts`: ```ts import { address, type Address } from "@solana/kit"; import { getIncrementInstruction } from "@/lib/generated/counter"; export type BuildIncrementCounterInput = { counterAddress: string; authorityAddress: string; }; export function buildIncrementCounterAction(input: BuildIncrementCounterInput) { const counter = address(input.counterAddress) as Address; const authority = address(input.authorityAddress) as Address; return { instructions: [ getIncrementInstruction({ counter, authority, }), ], }; } ``` What this file is doing: * converting loose UI strings into typed addresses * calling the generated instruction builder * returning a transport-ready instruction list What it is not doing: * rendering UI * opening wallet prompts * deciding button state That boundary matters. If instruction assembly lives inside the component, every future program action will duplicate logic badly. ## Step 2: Send Through The Shared Client Transport Now create `components/solana/CounterActionButton.tsx`: ```tsx "use client"; import React, { useState } from "react"; import { useSendTransaction, useWallet } from "@solana/react-hooks"; import { buildIncrementCounterAction } from "@/lib/solana/programs/counterService"; type CounterActionButtonProps = { counterAddress: string; }; export function CounterActionButton({ counterAddress }: CounterActionButtonProps) { const wallet = useWallet(); const { send, isSending, signature, error } = useSendTransaction(); const [message, setMessage] = useState(""); const onClick = async () => { setMessage(""); if (wallet.status !== "connected") { setMessage("Connect a wallet before submitting a program action."); return; } try { const action = buildIncrementCounterAction({ counterAddress, authorityAddress: wallet.session.account.address, }); await send({ instructions: action.instructions }); setMessage("Counter increment submitted successfully."); } catch (e: any) { setMessage(String(e?.message ?? e)); } }; return (
{message ?

{message}

: null} {signature ?

Signature: {signature}

: null} {error ?

{String(error)}

: null}
); } ``` Read the responsibility split carefully: * `buildIncrementCounterAction(...)` knows the program accounts and instruction shape * `useSendTransaction()` knows how to push those instructions through the wallet-aware client transport * the component knows when to disable itself and what to show the user That is the architecture you want. ## Why This Pattern Scales When the next instruction arrives, you do not rewrite the whole stack. You add another service function. Examples: * `buildCreateProfileAction(...)` * `buildCloseEscrowAction(...)` * `buildClaimRewardsAction(...)` The transport stays shared. The UI state pattern stays shared. Only the program-specific instruction builder changes. That is how client code stops collapsing into copy-paste logic. ## The Failure Mode To Avoid Do not do this inside a component: * parse addresses * build instruction accounts * compute PDA inputs * choose instruction builder * send transaction * map raw errors That is too much responsibility for one button. You will lose testability first. Then you will lose correctness. ## What You Can Do After This You can keep Anchor program specifics in one service layer while still using a kit-first transport and hook-based wallet flow. That is the bridge between on-chain program logic and a maintainable client. The next lesson builds on that by making transaction status and recovery behavior explicit in the UI. ## Quick Check ## Sources * [https://solana.com/docs/frontend/react-hooks](https://solana.com/docs/frontend/react-hooks) * [https://solana.com/docs/frontend/client](https://solana.com/docs/frontend/client) * [https://www.anchor-lang.com/docs/clients/typescript](https://www.anchor-lang.com/docs/clients/typescript) # Client Testing Strategy: Lite Unit and Integration URL: https://www.learnsol.site/learn/solana-kit-clients/client-testing-strategy-lite-unit-and-integration Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/client-testing-strategy-lite-unit-and-integration.mdx Test Solana client code at the right layers: pure logic first, UI behavior second, and one meaningful integration path on top of the shared runtime. Client testing gets expensive fast when the test layers are wrong. If you make every test talk to a live network, your feedback loop becomes slow and flaky. If you only test pure helpers, your UI and wallet flows break in ways the test suite never sees. The right model is layered. Test three things separately: * pure client logic * UI behavior around hooks and statuses * one real integration path that proves the stack works together That is enough for most serious frontend work. B[Component tests] B --> C[One integration path] A --> D[Fast feedback] B --> E[UI contract protection] C --> F[Runtime confidence] `} /> ## Start With Pure Logic Pure logic is the cheapest place to catch mistakes. Examples from this section: * `classifyTxError(...)` * `getRecoveryHint(...)` * token transfer input validation * address normalization helpers These functions do not need a wallet. They do not need RPC. They should be tested first. ## Step 1: Unit Test The Logic That Drives UX Decisions Install the test stack if needed: ```bash pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom ``` Now create `lib/solana/__tests__/uiTxState.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { classifyTxError, getRecoveryHint } from "../uiTxState"; describe("uiTxState", () => { it("maps user rejection", () => { expect(classifyTxError("User rejected request")).toBe("user-rejected"); }); it("maps blockhash expiry", () => { expect(classifyTxError("Blockhash not found")).toBe("expired"); }); it("returns a recovery hint for program errors", () => { expect(getRecoveryHint("program")).toMatch(/inputs/i); }); }); ``` This is the kind of test that should run constantly. It is fast. It is deterministic. And it directly protects the user-facing behavior of your app. ## Step 2: Test UI Behavior With Mocked Hook State The official React hooks docs explicitly call out two useful testing patterns: * mock hook return values directly * pass a mocked client into `SolanaProvider` For most UI components, mocking hook return values is the fastest path. Create `components/solana/__tests__/WalletPanel.test.tsx`: ```tsx import { describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { WalletPanel } from "../WalletPanel"; vi.mock("@solana/react-hooks", () => ({ useWalletConnection: () => ({ connectors: [{ id: "phantom", name: "Phantom" }], wallet: null, status: "disconnected", connect: vi.fn(), disconnect: vi.fn(), }), })); describe("WalletPanel", () => { it("renders available connectors when disconnected", () => { render(); expect(screen.getByText(/Choose wallet/i)).toBeTruthy(); expect(screen.getByText(/Connect Phantom/i)).toBeTruthy(); }); }); ``` This test is still cheap. But it protects a real UI contract. If the component stops showing available connectors correctly, the test catches it. ## Step 3: Keep One Real Integration Path After unit tests and UI tests, keep one integration path that proves your client stack still works together. That path should be small and meaningful. For this module, a good candidate is: * connect wallet in app state * build one SOL transfer instruction * send it through the shared transport * assert a real signature comes back That is enough. You do not need ten end-to-end tests to trust the client. You need one honest path that exercises the real runtime. ## A Practical Rule For This Section Use this split: * unit tests for logic helpers * component tests for status and rendering behavior * one integration flow for wallet + transport + signature outcome That gives you speed without losing realism. ## The Failure Modes To Avoid ### Testing only happy paths Solana clients fail in normal ways: * user rejection * disconnected wallet * expired lifetime * invalid program input If those are untested, the suite is weak. ### Making every test hit the network That turns the suite into a latency benchmark. Keep most tests local and deterministic. ### Letting UI tests depend on a real browser wallet Mock the hook state or pass a mocked client. Do not make simple component tests depend on wallet extensions. ## What You Can Do After This You can design a test suite that protects the real failure modes in a Solana client without making every test expensive or flaky. That is the correct testing posture for a modern kit-first frontend. ## Quick Check ## Sources * [https://solana.com/docs/frontend/react-hooks](https://solana.com/docs/frontend/react-hooks) * [https://solana.com/docs/frontend/client](https://solana.com/docs/frontend/client) * [https://vitest.dev/](https://vitest.dev/) # Frontend Transaction UX: Status, Errors, Recovery URL: https://www.learnsol.site/learn/solana-kit-clients/frontend-transaction-ux-status-errors-recovery Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/frontend-transaction-ux-status-errors-recovery.mdx Wrap a real kit-based send flow in a strict UI state machine so users always know what happened and what to do next. Transactions do not fail in only one way. Users reject signatures. RPC requests time out. Blockhashes expire. Programs reject invalid inputs. If your UI treats all of that as one generic red error banner, the product feels broken even when the chain is behaving normally. The right model is simple. A transaction UI is a state machine. It should always tell the user: * what phase they are in * whether they can act again * what the next safe action is B[preparing] B --> C[awaiting-signature] C --> D[sending] D --> E[confirmed] B --> F[failed] C --> F D --> F `} /> ## Step 1: Normalize Errors Before They Reach The UI Create `lib/solana/uiTxState.ts`: ```ts export type TxStatus = | "idle" | "preparing" | "awaiting-signature" | "sending" | "confirmed" | "failed"; export type TxErrorKind = "user-rejected" | "network" | "expired" | "program" | "unknown"; export function classifyTxError(err: string): TxErrorKind { const message = err.toLowerCase(); if (message.includes("reject")) return "user-rejected"; if (message.includes("blockhash") || message.includes("expired")) return "expired"; if (message.includes("timeout") || message.includes("network")) return "network"; if (message.includes("instruction") || message.includes("program")) return "program"; return "unknown"; } export function getRecoveryHint(kind: TxErrorKind) { switch (kind) { case "user-rejected": return "Review the transaction and sign again when you are ready."; case "expired": return "Rebuild the transaction with a fresh blockhash and retry."; case "network": return "Retry after checking RPC health and wallet connection."; case "program": return "Check the inputs and on-chain account state before retrying."; default: return "Inspect logs, then retry only after you understand the failure."; } } ``` This file does one job. It turns messy raw failures into a small set of UI decisions. That is what makes the component predictable. ## Step 2: Wrap A Real Send Flow In Explicit Status States Create `components/solana/TxActionCard.tsx`: ```tsx "use client"; import React, { useState } from "react"; import { address, lamports } from "@solana/kit"; import { getTransferSolInstruction } from "@solana-program/system"; import { useSendTransaction, useWallet } from "@solana/react-hooks"; import { classifyTxError, getRecoveryHint, type TxStatus } from "@/lib/solana/uiTxState"; type TxActionCardProps = { destination: string; }; export function TxActionCard({ destination }: TxActionCardProps) { const wallet = useWallet(); const { send, isSending, signature } = useSendTransaction(); const [status, setStatus] = useState("idle"); const [error, setError] = useState(""); const run = async () => { setError(""); if (wallet.status !== "connected") { setStatus("failed"); setError("Connect a wallet before sending a transaction."); return; } try { setStatus("preparing"); const instruction = getTransferSolInstruction({ source: wallet.session.account, destination: address(destination), amount: lamports(100_000n), }); setStatus("awaiting-signature"); setStatus("sending"); await send({ instructions: [instruction] }); setStatus("confirmed"); } catch (e: any) { const kind = classifyTxError(String(e?.message ?? e)); setStatus("failed"); setError(`Failed (${kind}). ${getRecoveryHint(kind)}`); } }; return (
Status: {status}
{signature ?
Signature: {signature}
: null} {error ?
{error}
: null}
); } ``` This is a real transaction path. The button is not faking a signature anymore. It builds a real system transfer instruction and sends it through the shared wallet-aware transport. ## Read The State Machine In Order ### `preparing` The UI is building the instruction and validating local prerequisites. This is where missing wallet state or bad destination input should fail early. ### `awaiting-signature` The wallet is about to ask the user for approval. This phase matters because rejection here is not a network outage. It is a user decision. ### `sending` The signed transaction is moving through the transport pipeline. At this point, the UI must stay locked. Double-submit bugs happen when the interface ignores this phase. ### `confirmed` The send flow completed successfully and you have a real signature to show. That signature is not decoration. It is how the user verifies the outcome in Explorer. ## The Failure Mode To Avoid Do not collapse all failures into one bucket like `Something went wrong`. That hides the next safe action. A user rejection should not look like a broken RPC. An expired blockhash should not look like a program bug. Your UI does not need perfect error parsing. It does need useful recovery behavior. ## What You Can Do After This You can build a transaction card that sends a real instruction, exposes real state, and gives the user a recovery path instead of a dead end. That is the baseline for every serious Solana frontend. The next lesson uses the same discipline inside the final capstone page. ## Quick Check ## Sources * [https://solana.com/docs/frontend/react-hooks](https://solana.com/docs/frontend/react-hooks) * [https://solana.com/docs/core/transactions](https://solana.com/docs/core/transactions) * [https://solana.com/docs/frontend/client](https://solana.com/docs/frontend/client) # Solana Kit Clients URL: https://www.learnsol.site/learn/solana-kit-clients Markdown URL: https://www.learnsol.site/learn/solana-kit-clients.mdx Learn the modern Solana client stack with @solana/client, @solana/react-hooks, and @solana/kit so your frontend reads state, manages wallet sessions, and sends transactions cleanly. `@solana/web3.js` is now the legacy path for new frontend work. That does not just change imports. It changes how the client should be structured. This section teaches the modern stack in the right order: * shared runtime first * wallet state second * account reads next * transaction building and delivery after that * program actions, UX, and testing only after the foundations are stable That order matters. If you start from scattered button handlers and ad hoc RPC helpers, the app will feel fine for one page and collapse as soon as it grows. B[Wallet session] B --> C[RPC reads] C --> D[Transaction building] D --> E[Send and confirm] E --> F[Program and token actions] F --> G[UX and testing] `} /> ## What This Section Is For This section teaches how to build a client that is explainable. That means you should be able to answer questions like: * where does wallet session state live? * where are transactions built? * where do retries belong? * where should Anchor instruction assembly live? * what should the UI know, and what should stay in the service layer? If the codebase cannot answer those questions clearly, it is not ready. ## The Stack We Use Here The section is built around three packages with different responsibilities: * `@solana/client` for the shared runtime * `@solana/react-hooks` for wallet and transaction hooks in React * `@solana/kit` for typed RPC, message, signer, and transaction building blocks That split is deliberate. The runtime owns shared state. The hooks expose it to React. Kit handles the low-level client mechanics. ## Learning Path Follow the section in this order: 1. `kit-foundations` 2. `wallet-standard-connection-flow` 3. `rpc-reads-and-account-decoding-with-kit` 4. `transaction-message-building-and-signers` 5. `send-confirm-retry-and-lifetime-management` 6. `spl-token-and-token-2022-client-flows` 7. `anchor-client-with-kit` 8. `frontend-transaction-ux-status-errors-recovery` 9. `client-testing-strategy-lite-unit-and-integration` 10. `mini-capstone-kit-client-for-anchor-program` This is not a menu. It is a dependency chain. For example: * transaction UX only makes sense after you understand sending and confirmation * Anchor client patterns only make sense after you understand runtime, signing, and transport * the capstone only matters if the earlier pieces are real and not mocked ## What You Will Be Able To Do After This Section By the end, you should be able to: * create one shared Solana client runtime for a Next.js app * connect Wallet Standard wallets honestly * read and normalize chain state safely * build and sign transaction messages correctly * separate submission from confirmation and retries * assemble token and Anchor program actions without leaking logic into the UI * test the client at the right layers That is enough to build a serious first Solana frontend. ## Prerequisites You should already be comfortable with: * TypeScript basics * the Solana account and transaction model * basic command-line workflow You should also have: * Node.js installed * a devnet wallet with test SOL ## Where To Start Start with `kit-foundations`. That lesson sets up the shared runtime model the rest of the section depends on. ## Quick Check # Kit Foundations URL: https://www.learnsol.site/learn/solana-kit-clients/kit-foundations Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/kit-foundations.mdx Set up one shared Solana client runtime so your app reads state, manages wallet sessions, and sends transactions without duplicating transport logic. `@solana/web3.js` is now the legacy path for new frontend work. The modern client stack is not just a package swap. It changes the architecture. The old mistake was letting every page create its own RPC connection, wallet glue, and transaction helpers. That leads to inconsistent cluster state, duplicated subscriptions, and UI bugs that are hard to explain. The correct model is simpler. Your app should create one shared Solana client runtime and let everything else sit on top of it. B[createClient] B --> C[SolanaProvider] C --> D[Wallet hooks] C --> E[RPC reads] C --> F[Transaction hooks] G[QueryClientProvider] --> C `} /> ## What The Shared Runtime Actually Owns A proper Solana client runtime owns these responsibilities: * RPC endpoint selection * WebSocket subscriptions * wallet connector registry * wallet session state * transaction helpers and watchers That is what `@solana/client` gives you. Then `@solana/react-hooks` exposes React hooks on top of the same runtime. That split is the key idea for this whole section. ## Install The Client Stack Once ```bash pnpm add @solana/client @solana/react-hooks @solana/kit @tanstack/react-query ``` You need: * `@solana/client` for the shared runtime * `@solana/react-hooks` for React integration * `@solana/kit` for typed transaction and RPC building blocks used elsewhere in this module * `@tanstack/react-query` if you want query state to stay predictable across the app ## Step 1: Centralize Network Configuration Create `lib/solana/config.ts`: ```ts export const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC ?? "https://api.devnet.solana.com"; export const SOLANA_WS = process.env.NEXT_PUBLIC_SOLANA_WS ?? "wss://api.devnet.solana.com"; ``` This file should stay boring. That is the point. If cluster configuration is spread across random files, you will eventually read from one cluster and send to another. ## Step 2: Create One Client Instance Now create `app/providers.tsx`: ```tsx "use client"; import React from "react"; import { autoDiscover, createClient } from "@solana/client"; import { SolanaProvider } from "@solana/react-hooks"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { SOLANA_RPC, SOLANA_WS } from "@/lib/solana/config"; const queryClient = new QueryClient(); const client = createClient({ endpoint: SOLANA_RPC, websocketEndpoint: SOLANA_WS, walletConnectors: autoDiscover(), }); export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Read this file in the right order: 1. `createClient(...)` builds the shared runtime 2. `autoDiscover()` registers Wallet Standard connectors available in the browser 3. `SolanaProvider` makes that runtime available to hooks 4. `QueryClientProvider` keeps async UI state stable around it That is the real foundation. Not the UI. The runtime. ## Step 3: Wrap The App Once In `app/layout.tsx`: ```tsx import { Providers } from "./providers"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Do this once. Do not recreate the client per page. Do not create a different provider subtree for every feature. One runtime is the clean default. ## Why This Architecture Matters The official docs describe one client instance exposing store, actions, watchers, and helpers. That matters because it gives your app one shared truth for: * current wallet session * current endpoint * account watchers * pending transaction state If each page rolls its own transport logic, those truths drift apart. That is when frontend bugs start to feel random. They are not random. They are architecture problems. ## The Failure Modes To Avoid ### Recreating the client inside components That resets transport and wallet-related state far more often than you think. Keep client creation at the app boundary. ### Hardcoding a different endpoint in one feature That produces split-brain behavior. One screen reads devnet while another flow sends somewhere else. ### Treating hooks as magic The hooks are just a React interface over the shared client runtime. If the runtime is wrong, the hooks will still fail correctly. ## What You Can Do After This You can explain where wallet sessions, RPC state, and transaction helpers actually live in a kit-first app. That is the baseline for every other client lesson. The next lesson moves one layer up and shows how wallet connection works on top of this shared runtime. ## Quick Check ## Sources * [https://solana.com/docs/frontend/client](https://solana.com/docs/frontend/client) * [https://solana.com/docs/frontend/react-hooks](https://solana.com/docs/frontend/react-hooks) * [https://solana.com/docs/frontend/nextjs-solana](https://solana.com/docs/frontend/nextjs-solana) # Mini Capstone: Kit Client for an Anchor Program URL: https://www.learnsol.site/learn/solana-kit-clients/mini-capstone-kit-client-for-anchor-program Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/mini-capstone-kit-client-for-anchor-program.mdx Ship one real client slice: connect a wallet, read state, send a real transaction, submit a real program action, and verify the outcome. This capstone should feel smaller than a full product and more real than a toy. That is the right target. You are not building ten pages. You are building one vertical slice that proves your client architecture is sound. A good vertical slice does four things cleanly: * acquires signer access * reads chain state * submits a real transaction * proves the result after the write path finishes If one of those is fake, the slice is incomplete. B[Read chain state] B --> C[Send SOL transfer] C --> D[Submit Anchor action] D --> E[Verify signatures and state] `} /> ## What You Are Building Build one page that composes the pieces from the earlier lessons: * wallet connection * account inspection * one real SOL transfer card * one real Anchor program action button That is enough. Do not add tabs, dashboards, or multi-program routing. The point is correctness, not surface area. ## Step 1: Prepare One Real Devnet Wallet And One Real Recipient This page only becomes runnable if the browser wallet and the local setup script both use the same authority. Use one dedicated devnet wallet for that. The cleanest path is to use a mnemonic and derive the first Solana account from it in both places. 1. create or import one dedicated devnet wallet in your browser extension 2. keep that wallet connected to devnet 3. store its mnemonic locally in `.env.local` ```bash CAPSTONE_AUTHORITY_MNEMONIC="word1 word2 word3 ... word12" ``` Now install the one extra dependency needed to derive the same wallet locally: ```bash pnpm add bip39 micro-ed25519-hdkey ``` Next, create one real recipient address you can inspect later: ```bash mkdir -p keys solana-keygen new --no-bip39-passphrase -o keys/kit-capstone-recipient.json solana address -k keys/kit-capstone-recipient.json ``` Copy the printed recipient address. You will use it for the SOL transfer card. ## Step 2: Create One Real Counter Account With The Same Authority Create `scripts/create-capstone-counter.ts`: ```ts import fs from "node:fs"; import * as bip39 from "bip39"; import { HDKey } from "micro-ed25519-hdkey"; import { airdropFactory, address, appendTransactionMessageInstructions, createKeyPairSignerFromBytes, createKeyPairSignerFromPrivateKeyBytes, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, generateKeyPairSigner, getSignatureFromTransaction, lamports, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit"; import { getInitializeInstruction } from "../lib/generated/counter"; const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL ?? "https://api.devnet.solana.com"; const RPC_WS_URL = process.env.NEXT_PUBLIC_RPC_WS_URL ?? "wss://api.devnet.solana.com"; const SYSTEM_PROGRAM_ADDRESS = address("11111111111111111111111111111111"); async function getAuthorityFromMnemonic(mnemonic: string) { const seed = bip39.mnemonicToSeedSync(mnemonic); const hd = HDKey.fromMasterSeed(seed.toString("hex")); const child = hd.derive("m/44'/501'/0'/0'"); return createKeyPairSignerFromPrivateKeyBytes(new Uint8Array(child.privateKey)); } async function loadRecipient() { const recipientBytes = Uint8Array.from( JSON.parse(fs.readFileSync("keys/kit-capstone-recipient.json", "utf8")), ); return createKeyPairSignerFromBytes(recipientBytes); } async function main() { const mnemonic = process.env.CAPSTONE_AUTHORITY_MNEMONIC; if (!mnemonic) { throw new Error("Missing CAPSTONE_AUTHORITY_MNEMONIC in .env.local"); } const rpc = createSolanaRpc(RPC_URL); const rpcSubscriptions = createSolanaRpcSubscriptions(RPC_WS_URL); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); const authority = await getAuthorityFromMnemonic(mnemonic); const recipient = await loadRecipient(); const counter = await generateKeyPairSigner(); await airdropFactory({ rpc, rpcSubscriptions })({ recipientAddress: authority.address, lamports: lamports(2_000_000_000n), commitment: "confirmed", }); const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const initializeInstruction = getInitializeInstruction({ authority: authority.address, counter, systemProgram: SYSTEM_PROGRAM_ADDRESS, }); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(authority, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([initializeInstruction], tx), ); const signedTransaction = await signTransactionMessageWithSigners( transactionMessage, ); await sendAndConfirmTransaction(signedTransaction, { commitment: "confirmed", }); console.log("AUTHORITY_ADDRESS=", authority.address); console.log("TRANSFER_DESTINATION=", recipient.address); console.log("COUNTER_ACCOUNT=", counter.address); console.log( "INITIALIZE_SIGNATURE=", getSignatureFromTransaction(signedTransaction), ); } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run it with: ```bash pnpm tsx scripts/create-capstone-counter.ts ``` This script does four important jobs: * derives the same authority that your browser wallet should be using * funds that authority on devnet * creates a brand new counter account * prints the exact addresses the capstone page needs If the authority address printed here does not match the connected browser wallet, stop and fix that first. Do not keep going with mismatched authorities. ## Step 3: Save The Printed Values In One Config File Create `lib/solana/capstoneConfig.ts`: ```ts export const CAPSTONE_TRANSFER_DESTINATION = "PASTE_TRANSFER_DESTINATION_HERE"; export const CAPSTONE_COUNTER_ACCOUNT = "PASTE_COUNTER_ACCOUNT_HERE"; ``` Paste the exact values printed by `create-capstone-counter.ts`. That makes the page deterministic. No fake placeholders. No guessing. ## Step 4: Put The Capstone On A Semantic Route Create `app/kit-client-capstone/page.tsx`: ```tsx "use client"; import React from "react"; import { WalletPanel } from "@/components/solana/WalletPanel"; import { AccountInspector } from "@/components/solana/AccountInspector"; import { TxActionCard } from "@/components/solana/TxActionCard"; import { CounterActionButton } from "@/components/solana/CounterActionButton"; import { CAPSTONE_COUNTER_ACCOUNT, CAPSTONE_TRANSFER_DESTINATION, } from "@/lib/solana/capstoneConfig"; export default function KitClientCapstonePage() { return (

Kit Client Capstone

Wallet

Read Chain State

Send A Real Transaction

Submit A Real Program Action

); } ``` This route name matters. The curriculum is not week-based anymore. Your app structure should stop pretending it is. ## Step 5: Verify The Slice End To End A successful capstone run should let you prove all of these: 1. the wallet panel shows a real connected session 2. the account inspector fetches a real account from the configured cluster 3. the transaction card returns a real signature after sending SOL 4. the program action button returns a real signature for the Anchor instruction 5. you can inspect both signatures in Explorer That is the real checkpoint. Not whether the page "looks complete". ## The Main Failure Modes ### The wallet connects but writes still fail That usually means one of these: * wrong cluster * wrong destination address * not enough devnet SOL * the connected wallet does not match the mnemonic used by the setup script ### The read panel works but the program action fails That usually means your generated instruction builder or account wiring is wrong. This is exactly why the service layer exists. The failure should be isolated to the program action path, not spread through the page. ### The signatures appear but you never verify them Then the capstone is incomplete. A real client flow ends with verification. Use Explorer or your own read-back path and confirm that the state change actually happened. ## What You Can Do After This You can build one real client page that combines wallet access, reads, writes, program actions, and verification without collapsing into ad hoc code. That is enough foundation to start a larger Solana frontend responsibly. ## Quick Check ## Sources * [https://solana.com/docs/frontend/client](https://solana.com/docs/frontend/client) * [https://solana.com/docs/frontend/react-hooks](https://solana.com/docs/frontend/react-hooks) * [https://solana.com/developers/cookbook/wallets/restore-from-mnemonic](https://solana.com/developers/cookbook/wallets/restore-from-mnemonic) * [https://solana.com/developers/cookbook/development/load-keypair-from-file](https://solana.com/developers/cookbook/development/load-keypair-from-file) * [https://solana.com/developers/cookbook/development/test-sol](https://solana.com/developers/cookbook/development/test-sol) * [https://www.anchor-lang.com/docs/clients/typescript](https://www.anchor-lang.com/docs/clients/typescript) # RPC Reads and Account Decoding with Kit URL: https://www.learnsol.site/learn/solana-kit-clients/rpc-reads-and-account-decoding-with-kit Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/rpc-reads-and-account-decoding-with-kit.mdx Fetch accounts safely with @solana/kit, normalize the result for UI code, and decode account data only after you know what you fetched. Reading Solana state looks simple until the client starts guessing. A string that is not a real address gets passed through the UI. A missing account is treated like a crash. Raw bytes get decoded before you even know which program owns them. That is how read paths become brittle. The correct model is: * fetch first * check existence * inspect ownership and shape * decode only when you know what you are looking at That order matters. B[fetchEncodedAccount] B --> C{exists?} C -->|no| D[Return missing account view] C -->|yes| E[Inspect owner and shape] E --> F[Normalize for UI] E --> G[Decode only with correct program codec] `} /> ## What A Read Path Actually Returns At the RPC layer, an account is just facts: * address * lamports * owner * executable flag * raw data bytes Those facts are useful on their own. Decoding is a second step. Do not merge them too early. ## Step 1: Fetch One Account In A Unified Shape The Kit docs recommend `fetchEncodedAccount(...)` when you want one consistent account shape instead of juggling raw RPC return variants. Create `lib/solana/rpc.ts`: ```ts import { address, assertAccountExists, createSolanaRpc, fetchEncodedAccount, type Address, } from "@solana/kit"; import { SOLANA_RPC } from "@/lib/solana/config"; const rpc = createSolanaRpc(SOLANA_RPC); export type AccountView = | { exists: false; address: string } | { exists: true; address: string; lamports: string; owner: string; executable: boolean; dataLength: number; }; export async function fetchAccountView(input: string): Promise { const accountAddress = address(input.trim()) as Address; const account = await fetchEncodedAccount(rpc, accountAddress); if (!account.exists) { return { exists: false, address: input.trim(), }; } return { exists: true, address: account.address, lamports: account.lamports.toString(), owner: account.owner, executable: account.executable, dataLength: account.data.length, }; } export async function fetchExistingEncodedAccount(input: string) { const accountAddress = address(input.trim()) as Address; const account = await fetchEncodedAccount(rpc, accountAddress); assertAccountExists(account); return account; } ``` This file teaches the main split: * `fetchAccountView(...)` is for UI-safe inspection * `fetchExistingEncodedAccount(...)` is for flows where missing data should be treated as a real error That is already a much stronger model than returning raw RPC blobs everywhere. ## Why `exists` Matters Kit returns a `MaybeEncodedAccount` shape on purpose. That is not ceremony. A missing account is normal on Solana. Examples: * wrong cluster * address copied incorrectly * account not created yet * PDA derived correctly but not initialized yet That should not explode your UI by default. ## Step 2: Show The Read Result Without Guessing Now create `components/solana/AccountInspector.tsx`: ```tsx "use client"; import React, { useState } from "react"; import { fetchAccountView, type AccountView } from "@/lib/solana/rpc"; export function AccountInspector() { const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(""); const onFetch = async () => { setLoading(true); setError(""); try { setResult(await fetchAccountView(input)); } catch (e: any) { setResult(null); setError(String(e?.message ?? e)); } finally { setLoading(false); } }; return (
setInput(e.target.value)} className="w-full px-3 py-2 rounded bg-zinc-900" placeholder="Account address" /> {error ?
{error}
: null} {result ? (
          {JSON.stringify(result, null, 2)}
        
) : null}
); } ``` This component is honest. It does not pretend every lookup succeeds. It does not decode program data blindly. It just shows the chain facts cleanly. That is the correct first read tool. ## Step 3: Decode Only After You Know The Program Once you know an account exists and belongs to the program you expect, then decoding makes sense. For program-specific accounts, the Kit docs and program clients make this easier. Example with the token program: ```ts import { decodeMint } from "@solana-program/token"; import { fetchExistingEncodedAccount } from "@/lib/solana/rpc"; export async function fetchMintSummary(input: string) { const encodedAccount = await fetchExistingEncodedAccount(input); const mintAccount = decodeMint(encodedAccount); return { address: mintAccount.address, lamports: mintAccount.lamports.toString(), decimals: mintAccount.data.decimals, supply: mintAccount.data.supply.toString(), }; } ``` Read that in order: 1. fetch the encoded account 2. assert it exists 3. decode with the program client helper 4. normalize again for the UI That is the safe decoding path. ## The Failure Modes To Avoid ### Decoding before checking ownership If you decode arbitrary bytes with the wrong codec, your client logic becomes nonsense quickly. The account may exist and still be the wrong thing. ### Throwing on every missing account That makes ordinary read states feel like fatal errors. Use missing-account behavior deliberately. ### Passing raw RPC data straight to components That leaks transport details into UI code. Normalize first. Always. ## What You Can Do After This You can fetch account data with a stable Kit helper, distinguish missing accounts from real failures, and decode program data only after the read path proves you are looking at the right account. That is the right base for the transaction-building lessons that come next. ## Quick Check ## Sources * [https://www.solanakit.com/docs/getting-started/fetch-account](https://www.solanakit.com/docs/getting-started/fetch-account) * [https://www.solanakit.com/api/functions/fetchEncodedAccount](https://www.solanakit.com/api/functions/fetchEncodedAccount) * [https://solana.com/docs/references/rpc-api](https://solana.com/docs/references/rpc-api) # Send, Confirm, Retry and Lifetime Management URL: https://www.learnsol.site/learn/solana-kit-clients/send-confirm-retry-and-lifetime-management Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/send-confirm-retry-and-lifetime-management.mdx Send a signed transaction the right way, separate sending from confirmation, and retry only when rebuilding is actually safe. The biggest transaction UX bug is simple. Apps act like `submitted` means `done`. It does not. A transaction can be signed, have a valid signature, and still fail to confirm. It can also expire before the network accepts it. That is why the send flow has to separate three different ideas: * the transaction already has a signature * the transaction has been sent to the network * the transaction has been confirmed at the commitment you care about If you collapse those into one step, your retry logic will be wrong. B[Extract signature] B --> C[Send transaction] C --> D{confirmed?} D -->|yes| E[Done] D -->|retryable lifetime/network failure| F[Rebuild with fresh blockhash] F --> A D -->|non-retryable failure| G[Surface real error] `} /> ## The Signature Exists Before Sending This matters more than most beginners expect. With Kit, the transaction signature is available as soon as the transaction is signed by the fee payer. You do not need to wait for the RPC node to return it. That means: * you can display the signature immediately * you can log it before send * you can still fail later during send or confirmation That is the right model. ## Step 1: Fund The Sender Before You Test Retries The old version of this lesson had a real bug. It generated a fresh sender and tried to transfer lamports immediately. That script could not succeed. This version fixes that first. Create `scripts/send-confirm.ts`: ```ts import { airdropFactory, appendTransactionMessageInstructions, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, generateKeyPairSigner, getSignatureFromTransaction, lamports, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit"; import { getTransferSolInstruction } from "@solana-program/system"; const RPC_URL = "http://127.0.0.1:8899"; const RPC_WS_URL = "ws://127.0.0.1:8900"; const LAMPORTS_PER_SOL = 1_000_000_000n; type GeneratedSigner = Awaited>; const rpc = createSolanaRpc(RPC_URL); const rpcSubscriptions = createSolanaRpcSubscriptions(RPC_WS_URL); async function prepareActors(): Promise<{ sender: GeneratedSigner; recipient: GeneratedSigner; }> { const sender = await generateKeyPairSigner(); const recipient = await generateKeyPairSigner(); await airdropFactory({ rpc, rpcSubscriptions })({ recipientAddress: sender.address, lamports: lamports(LAMPORTS_PER_SOL), commitment: "confirmed", }); return { sender, recipient }; } async function buildAndSign(sender: GeneratedSigner, recipient: GeneratedSigner) { const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transferInstruction = getTransferSolInstruction({ source: sender, destination: recipient.address, amount: lamports(LAMPORTS_PER_SOL / 100n), }); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(sender, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([transferInstruction], tx), ); return signTransactionMessageWithSigners(transactionMessage); } async function main() { const { sender, recipient } = await prepareActors(); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); for (let attempt = 1; attempt <= 2; attempt++) { try { const signedTransaction = await buildAndSign(sender, recipient); const signature = getSignatureFromTransaction(signedTransaction); console.log("submitted", signature); await sendAndConfirmTransaction(signedTransaction, { commitment: "confirmed", }); console.log("confirmed", signature); return; } catch (e: any) { const message = String(e?.message ?? e); const retryable = message.includes("Blockhash") || message.includes("timeout") || message.includes("expired"); if (!retryable || attempt === 2) { throw e; } console.log("retrying with a freshly rebuilt transaction..."); } } } main().catch((e) => { console.error("send/confirm failed", e); process.exit(1); }); ``` Run it with: ```bash pnpm tsx scripts/send-confirm.ts ``` `prepareActors()` funds the sender before any transfer is built, so the retry loop is testing send and confirmation behavior instead of failing early for insufficient funds. That distinction matters. If the example is going to teach retry policy, it has to survive the first send. ## Read The Flow In The Right Order ### First, rebuild before every retry This is the real rule. A retry is not `send the same bytes again forever`. If the blockhash lifetime is stale, the correct retry path is: 1. fetch a fresh blockhash 2. rebuild the message 3. sign again 4. send again That is why the helper above calls `buildAndSign(sender, recipient)` again inside the retry loop. ### Then separate submission from confirmation ```ts const signature = getSignatureFromTransaction(signedTransaction); console.log("submitted", signature); await sendAndConfirmTransaction(signedTransaction, { commitment: "confirmed", }); console.log("confirmed", signature); ``` That is the correct distinction. The signature exists before confirmation. Confirmation is about network outcome, not identity. ### Then keep retry policy explicit This line matters: ```ts const retryable = message.includes("Blockhash") || message.includes("timeout") || message.includes("expired"); ``` The point is not perfect string parsing. The point is policy. Some failures deserve rebuild-and-retry. Some failures do not. User rejection is not retryable. Program logic errors are not retryable until inputs or state change. ## The Failure Modes To Avoid ### Retrying stale signed bytes That is one of the most common client mistakes. If the lifetime expired, you need a new message. ### Reporting `confirmed` too early Users need honest state. If the app only knows the transaction was submitted, say that. Do not claim success before the confirmation path finishes. ### Treating every error as retryable That turns real bugs into noisy loops. Retry only when the failure mode actually supports retry. ## What You Can Do After This You can separate signature, submission, and confirmation correctly, and you can retry only by rebuilding the transaction when the lifetime requires it. That is the baseline for reliable client-side transaction delivery. The next lessons build on that for token flows and broader client testing. ## Quick Check ## Sources * [https://solana.com/developers/cookbook/development/test-sol](https://solana.com/developers/cookbook/development/test-sol) * [https://solana.com/developers/cookbook/transactions/send-sol](https://solana.com/developers/cookbook/transactions/send-sol) * [https://solana.com/docs/rpc/websocket](https://solana.com/docs/rpc/websocket) # SPL Token and Token-2022 Client Flows URL: https://www.learnsol.site/learn/solana-kit-clients/spl-token-and-token-2022-client-flows Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/spl-token-and-token-2022-client-flows.mdx Build token transfer plans with explicit program selection, real instruction builders, and validation that happens before the wallet prompt. Token flows go wrong when the client guesses. The app guesses the token program. It guesses the decimals. It assumes any token account pair is valid for the chosen mint. That is how users end up signing transfers that were malformed before the wallet popup even opened. The correct model is stricter. Before you ask the wallet to sign, the client should already know: * which token program you are using * which mint the transfer is for * which token accounts belong to that mint * which raw amount should be transferred That is the whole lesson. B[Read mint] B --> C[Derive associated token accounts] C --> D[Fetch token accounts] D --> E[Validate owners and mint] E --> F[Build transferChecked instruction] `} /> ## Classic SPL Token and Token-2022 Are Different Programs They are not just two names for the same thing. They are different program IDs with different extension capabilities. So the client must keep the token program choice explicit. Do not hide it inside a helper that silently assumes classic SPL Token. ## Step 1: Build A Transfer Plan With Real Preflight Validation Install the token clients if you have not already: ```bash pnpm add @solana-program/token @solana-program/token-2022 ``` Now create `lib/solana/tokenFlow.ts`: ```ts import { address, createSolanaRpc, type Address } from "@solana/kit"; import { TOKEN_PROGRAM_ADDRESS, fetchMint as fetchSplMint, fetchToken as fetchSplToken, findAssociatedTokenPda as findSplAssociatedTokenPda, getTransferCheckedInstruction as getSplTransferCheckedInstruction, } from "@solana-program/token"; import { TOKEN_2022_PROGRAM_ADDRESS, fetchMint as fetchToken2022Mint, fetchToken as fetchToken2022Token, findAssociatedTokenPda as findToken2022AssociatedTokenPda, getTransferCheckedInstruction as getToken2022TransferCheckedInstruction, } from "@solana-program/token-2022"; export type TokenProgramKind = "spl" | "token2022"; export type TokenTransferInput = { programKind: TokenProgramKind; mintAddress: string; sourceOwnerAddress: string; destinationOwnerAddress: string; authorityAddress: string; amountRaw: bigint; }; const rpc = createSolanaRpc( process.env.NEXT_PUBLIC_RPC_URL ?? "https://api.devnet.solana.com", ); function getTokenApi(programKind: TokenProgramKind) { if (programKind === "spl") { return { tokenProgramAddress: TOKEN_PROGRAM_ADDRESS, fetchMint: fetchSplMint, fetchToken: fetchSplToken, findAssociatedTokenPda: findSplAssociatedTokenPda, getTransferCheckedInstruction: getSplTransferCheckedInstruction, }; } return { tokenProgramAddress: TOKEN_2022_PROGRAM_ADDRESS, fetchMint: fetchToken2022Mint, fetchToken: fetchToken2022Token, findAssociatedTokenPda: findToken2022AssociatedTokenPda, getTransferCheckedInstruction: getToken2022TransferCheckedInstruction, }; } export function validateTokenTransferInput(input: TokenTransferInput) { if (!input.mintAddress) throw new Error("Missing mint address"); if (!input.sourceOwnerAddress) throw new Error("Missing source owner address"); if (!input.destinationOwnerAddress) { throw new Error("Missing destination owner address"); } if (!input.authorityAddress) throw new Error("Missing transfer authority"); if (input.amountRaw <= 0n) throw new Error("Amount must be positive"); } export async function buildTokenTransferPlan(input: TokenTransferInput) { validateTokenTransferInput(input); const mintAddress = address(input.mintAddress) as Address; const sourceOwnerAddress = address(input.sourceOwnerAddress) as Address; const destinationOwnerAddress = address(input.destinationOwnerAddress) as Address; const authorityAddress = address(input.authorityAddress) as Address; const tokenApi = getTokenApi(input.programKind); if (authorityAddress !== sourceOwnerAddress) { throw new Error("This example expects the source owner to sign the transfer."); } const mint = await tokenApi.fetchMint(rpc, mintAddress); const [sourceTokenAccount] = await tokenApi.findAssociatedTokenPda({ mint: mintAddress, owner: sourceOwnerAddress, tokenProgram: tokenApi.tokenProgramAddress, }); const [destinationTokenAccount] = await tokenApi.findAssociatedTokenPda({ mint: mintAddress, owner: destinationOwnerAddress, tokenProgram: tokenApi.tokenProgramAddress, }); const sourceToken = await tokenApi.fetchToken(rpc, sourceTokenAccount); const destinationToken = await tokenApi.fetchToken(rpc, destinationTokenAccount); if (sourceToken.data.mint !== mintAddress) { throw new Error("Source token account is not for the selected mint."); } if (destinationToken.data.mint !== mintAddress) { throw new Error("Destination token account is not for the selected mint."); } if (sourceToken.data.owner !== sourceOwnerAddress) { throw new Error("Source token account is not owned by the expected wallet."); } if (destinationToken.data.owner !== destinationOwnerAddress) { throw new Error( "Destination token account is not owned by the expected wallet.", ); } return { tokenProgramAddress: tokenApi.tokenProgramAddress, sourceTokenAccount, destinationTokenAccount, decimals: mint.data.decimals, instruction: tokenApi.getTransferCheckedInstruction({ source: sourceTokenAccount, mint: mintAddress, destination: destinationTokenAccount, authority: authorityAddress, amount: input.amountRaw, decimals: mint.data.decimals, }), }; } ``` This file now does the job the old version only claimed to do: * fetch the mint before trusting decimals * derive the token accounts instead of trusting pasted token-account strings * verify that both accounts belong to the selected mint * verify that the source and destination owners are the wallets you think they are * only then build the transfer instruction That is real client-side token validation. ## Why The Input Model Changed The old version asked for raw token-account addresses. That is too loose for a beginner client. It makes the UI responsible for knowing three things at once: * the wallet owner * the token account address * whether that token account was created for the chosen mint and token program This version is stricter. You give the client wallet owner addresses and the mint. The client derives the associated token accounts itself and verifies the actual on-chain data before it builds the transfer. ## Why `transferChecked` Still Matters `transferChecked` is still the right beginner default. But now the client is not guessing the decimals anymore. It reads `mint.data.decimals` from chain state and uses that value in the instruction. That is the correct relationship: * the mint defines precision * the client reads that precision * the transfer builder uses that exact precision ## Step 2: Preview The Transfer Before Sending Now create `components/solana/TokenTransferPreview.tsx`: ```tsx "use client"; import React, { useState } from "react"; import { buildTokenTransferPlan, type TokenProgramKind, } from "@/lib/solana/tokenFlow"; export function TokenTransferPreview() { const [programKind, setProgramKind] = useState("spl"); const [result, setResult] = useState(""); const [error, setError] = useState(""); const onPreview = async () => { setError(""); try { const plan = await buildTokenTransferPlan({ programKind, mintAddress: "ENTER_REAL_MINT_ADDRESS", sourceOwnerAddress: "ENTER_REAL_SOURCE_WALLET_ADDRESS", destinationOwnerAddress: "ENTER_REAL_DESTINATION_WALLET_ADDRESS", authorityAddress: "ENTER_REAL_SOURCE_WALLET_ADDRESS", amountRaw: 1_000_000n, }); setResult( JSON.stringify( { tokenProgram: plan.tokenProgramAddress, sourceTokenAccount: plan.sourceTokenAccount, destinationTokenAccount: plan.destinationTokenAccount, decimals: plan.decimals, }, null, 2, ), ); } catch (e: any) { setResult(""); setError(String(e?.message ?? e)); } }; return (
{error ?
{error}
: null} {result ?
{result}
: null}
); } ``` This preview still does not send the transfer. That is still correct. But now the preview is honest. It only succeeds after the client has validated the mint, derived the token accounts, and confirmed the chain data matches the requested transfer. ## The Failure Modes To Avoid ### Assuming any token account can be pasted into the form That is exactly how clients build invalid transfers. If your app already knows the wallet owner and the mint, derive the associated token account and verify it. ### Treating decimals as a display-only concern Decimals affect raw amount construction. They are part of transfer correctness. The client should read them from the mint instead of trusting a manual input field. ### Letting the wallet prompt become the first validation step By the time the wallet opens, the transfer plan should already be coherent. If the mint, owners, and token accounts do not line up, fail before the signature request. ## What You Can Do After This You can build token transfer plans with explicit program choice, derive and validate the actual token accounts involved, and use the mint's real decimals instead of guessing. That is the client-side baseline for both classic SPL Token and Token-2022 flows. ## Quick Check ## Sources * [https://solana.com/docs/tokens/basics/transfer-tokens](https://solana.com/docs/tokens/basics/transfer-tokens) * [https://solana.com/developers/cookbook/tokens/get-token-account](https://solana.com/developers/cookbook/tokens/get-token-account) * [https://solana.com/developers/cookbook/tokens/get-token-mint](https://solana.com/developers/cookbook/tokens/get-token-mint) # Transaction Message Building and Signers URL: https://www.learnsol.site/learn/solana-kit-clients/transaction-message-building-and-signers Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/transaction-message-building-and-signers.mdx Build one transaction message in the correct order, understand who really signs it, and see why message mutation after signing is invalid. Most client-side transaction bugs come from one bad assumption. People treat a transaction like a function call they can keep editing until the last second. That is not what it is. A Solana transaction message is a plan. It fixes: * which instructions will run * which accounts are involved * who pays fees * which recent blockhash defines the lifetime Then signers approve that exact plan. If the plan changes after signing, the signatures are no longer valid. That is the whole mental model for this lesson. B[Set fee payer] B --> C[Set recent blockhash lifetime] C --> D[Append instructions] D --> E[Sign final message] E --> F[Extract signature] `} /> ## The Order Is Not Arbitrary With Kit, the correct build order is: 1. create the message shell 2. set the fee payer signer 3. set the lifetime with a fresh blockhash 4. append instructions 5. sign the final message That order exists for a reason. The fee payer and lifetime are part of what gets signed. So they must exist before the signature step. ## Step 1: Build One Transfer Message Install the system program client if you have not already: ```bash pnpm add @solana-program/system ``` Now create `scripts/build-message.ts`: ```ts import { appendTransactionMessageInstructions, createSolanaRpc, createTransactionMessage, generateKeyPairSigner, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit"; import { getTransferSolInstruction } from "@solana-program/system"; const rpc = createSolanaRpc("http://127.0.0.1:8899"); async function main() { const sender = await generateKeyPairSigner(); const recipient = await generateKeyPairSigner(); const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transferInstruction = getTransferSolInstruction({ source: sender, destination: recipient.address, amount: lamports(1_000_000n), }); const message = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(sender, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([transferInstruction], tx), ); const signedTransaction = await signTransactionMessageWithSigners(message); console.log("signature", getSignatureFromTransaction(signedTransaction)); } main().catch(console.error); ``` Run it with: ```bash pnpm tsx scripts/build-message.ts ``` This script does not send the transaction. That is intentional. The lesson is about building and signing the message correctly first. ## Read The Script In The Right Order ### `createTransactionMessage({ version: 0 })` This creates the message shell. At this point, you do not have enough information to sign anything yet. You only have the basic container. ### `setTransactionMessageFeePayerSigner(...)` The fee payer is not just metadata. It is part of the signed message. In this example, the sender is also paying fees. That is common, but it is not required by the protocol. ### `setTransactionMessageLifetimeUsingBlockhash(...)` The recent blockhash defines how long the message stays valid. This is why blockhash freshness matters. A stale blockhash means the message may be structurally correct and still be rejected by the runtime. ### `appendTransactionMessageInstructions(...)` Only now do you attach the ordered program operations. Instruction order is part of the message. If you reorder instructions later, you did not make a small edit. You created a different message. ### `signTransactionMessageWithSigners(...)` This is the lock point. After this, the message contents must not change. If they change, the signatures no longer describe the transaction honestly. ## Who Actually Signs This is where beginners often get lost. In the transfer above: * `sender` signs * `recipient` does not sign Why: * the sender is authorizing SOL to leave their account * the recipient is only receiving funds Being involved in a transaction is not the same as being a signer. That distinction matters in every serious client flow. ## The Failure Modes To Avoid ### Setting lifetime after signing That invalidates the signatures. The message changed. ### Reusing an old blockhash blindly That makes retries fail for the wrong reason. If the message lifetime expired, rebuild the message first. ### Treating recipients like signers by default Most accounts in a transaction do not sign. Only accounts that must authorize the message do. ## What You Can Do After This You can build a transaction message in the right order, explain why the fee payer and blockhash belong inside the signed plan, and identify which accounts actually need to sign. That is the minimum foundation for sending transactions safely. The next lesson builds on this by covering confirmation, retries, and lifetime-aware resend behavior. ## Quick Check ## Sources * [https://www.solanakit.com/docs/getting-started/build-transaction](https://www.solanakit.com/docs/getting-started/build-transaction) * [https://solana.com/docs/core/transactions/transaction-structure](https://solana.com/docs/core/transactions/transaction-structure) # Wallet Standard Connection Flow URL: https://www.learnsol.site/learn/solana-kit-clients/wallet-standard-connection-flow Markdown URL: https://www.learnsol.site/learn/solana-kit-clients/wallet-standard-connection-flow.mdx Build a wallet connection panel that reflects the real session state, offers the right connectors, and never lies about connection status. Wallet connection looks simple until the UI lies. The usual bugs are not cryptographic. They are product bugs. A button says `Connected` before the wallet approves. A rejected request looks like a crash. A disconnected session still leaves send buttons enabled. The fix is not more styling. The fix is to treat wallet connection as explicit runtime state. B[User chooses wallet] B --> C[Runtime connection state changes] C --> D[Connected session exists] C --> E[Rejected or failed request] D --> F[Signer actions unlock] E --> G[UI stays usable] `} /> ## The Correct Mental Model Your app does not own the wallet. It asks the wallet connector for a session. That session can be: * absent * connecting * connected * failed Your UI should reflect that state exactly. Nothing more. Nothing less. ## Step 1: Read Connection State From One Hook The current React hooks docs expose `useWalletConnection()` as the main connection surface. It gives you the available connectors, the current wallet session, the runtime status, and the connect/disconnect actions. That is the right place to start. Create `components/solana/WalletPanel.tsx`: ```tsx "use client"; import React from "react"; import { useWalletConnection } from "@solana/react-hooks"; function shortAddress(address?: string) { if (!address) return ""; return `${address.slice(0, 4)}...${address.slice(-4)}`; } export function WalletPanel() { const { connectors, wallet, status, connect, disconnect } = useWalletConnection(); const onConnect = async (connectorId: string) => { try { await connect({ connectorId }); } catch (e) { console.error("connect failed", e); } }; const onDisconnect = async () => { try { await disconnect(); } catch (e) { console.error("disconnect failed", e); } }; return (
Status: {status}
{wallet ? ( <>
Connected: {shortAddress(wallet.address)}
) : connectors.length === 0 ? (
No wallet connector was found in this browser.
) : (
Choose wallet:
{connectors.map((connector) => ( ))}
)}
); } ``` This component is doing only three honest things: * reading the current session state * listing the available wallet connectors * triggering connect or disconnect when the user asks That is enough for a first wallet panel. ## Read The Panel In The Right Order ### `connectors` These are the Wallet Standard connectors the client runtime discovered in the browser. If there are no connectors, the right UI is not `Connected`. The right UI is: there is no wallet available to choose. ### `status` This is the connection truth. If the status is still changing, your UI should not pretend the wallet is ready. That is why the status label matters. ### `wallet` This is the connected session. If it exists, you can show the address and unlock signer-required actions. If it does not, write buttons elsewhere in the app should stay disabled. ## The Failure Modes To Avoid ### Treating user rejection like an app error A user can reject a wallet prompt. That is normal. Your UI should recover cleanly and stay usable. ### Unlocking actions before the session exists Do not assume a button click means a connected wallet. Wait for the runtime state to say so. ### Hiding which wallet the user is choosing If there are multiple connectors, the user should see exactly which wallet they are selecting. Ambiguity here creates avoidable trust problems. ## What You Can Do After This You can build a wallet panel that reflects real wallet-standard session state instead of guessing. That gives the rest of the client a stable starting point. The next lesson builds on that by reading chain state through the same shared runtime. ## Quick Check ## Sources * [https://solana.com/docs/frontend/react-hooks](https://solana.com/docs/frontend/react-hooks) * [https://solana.com/docs/frontend/nextjs-solana](https://solana.com/docs/frontend/nextjs-solana) # Challenge 1 - Constant Product Invariant URL: https://www.learnsol.site/learn/challenges/cpmm/1 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/1.mdx Implement the core constant-product invariant used by CPMMs and verify that reserves produce the expected k value. # Challenge 1: Constant Product Invariant A constant-product market maker tracks the relationship: ```txt x * y = k ``` Where: * `x` is token A reserve * `y` is token B reserve * `k` is the pool invariant ## Task Implement `constant_product`. ## Requirements * Accept two `u64` reserves. * Return a `u128`. * Cast before multiplying so large values do not overflow `u64` math. > [!NOTE] > On Solana, token math often starts with `u64` amounts but intermediate math must use wider types. ## Hint ```rust (x_reserve as u128) * (y_reserve as u128) ``` # Challenge 10 - Invariant Does Not Decrease URL: https://www.learnsol.site/learn/challenges/cpmm/10 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/10.mdx Verify that the post-swap reserve state does not decrease the pool invariant when fees are applied. # Challenge 10: Invariant Does Not Decrease A fee-charging CPMM should not end a valid swap with a smaller invariant. ## Task Implement `invariant_ok`. ## Rules * Do multiplication in `u128`. * Return `true` if the new invariant is greater than or equal to the old one. ## Example Compare: * old state: `(1000, 2000)` * new state: `(1099, 1819)` You should find that the new invariant is slightly larger because fees stayed inside the pool. # Challenge 11 - Initial LP Supply URL: https://www.learnsol.site/learn/challenges/cpmm/11 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/11.mdx Compute the initial LP token supply for a brand new pool using the integer square root of the reserve product. # Challenge 11: Initial LP Supply When a brand new pool is created, LP shares are often based on the geometric mean of the starting reserves. ## Task Implement `initial_lp_supply`. ## Rule Return: ```txt floor(sqrt(x_reserve * y_reserve)) ``` A helper `integer_sqrt` is already provided. ## Example For `400` and `900`: ```txt sqrt(400 * 900) = sqrt(360000) = 600 ``` # Challenge 12 - Proportional Deposit Quote URL: https://www.learnsol.site/learn/challenges/cpmm/12 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/12.mdx Given an X token deposit, compute how much Y must also be deposited to keep the pool price unchanged. # Challenge 12: Proportional Deposit Quote Liquidity should usually be added in the same ratio as the existing reserves. ## Task Implement `required_y_deposit`. ## Formula Use: ```txt x_deposit * y_reserve / x_reserve ``` ## Example For: * `x_reserve = 1000` * `y_reserve = 2000` * `x_deposit = 250` Then: ```txt 250 * 2000 / 1000 = 500 ``` # Challenge 13 - LP Shares Minted On Deposit URL: https://www.learnsol.site/learn/challenges/cpmm/13 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/13.mdx Mint LP shares for a proportional deposit based on the existing total LP supply and reserve balances. # Challenge 13: LP Shares Minted On Deposit If a liquidity provider adds assets in the correct ratio, their LP shares should match the fraction of the pool they added. ## Task Implement `lp_shares_to_mint`. ## Formula Use: ```txt x_deposit * total_lp_supply / x_reserve ``` ## Example If the pool has: * total LP supply = `1000` * X reserve = `1000` And the depositor adds `250` X proportionally, they should receive: ```txt 250 * 1000 / 1000 = 250 ``` # Challenge 14 - Withdraw Amounts From LP Burn URL: https://www.learnsol.site/learn/challenges/cpmm/14 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/14.mdx Given LP shares being burned, compute the proportional X and Y amounts the liquidity provider should receive. # Challenge 14: Withdraw Amounts From LP Burn Burning LP shares returns the same fraction of each reserve. ## Task Implement `withdraw_amounts`. ## Formula Use: ```txt x_out = lp_burn * x_reserve / total_lp_supply y_out = lp_burn * y_reserve / total_lp_supply ``` ## Example Burning `250` LP out of `1000` total supply means burning `25%` of the pool. So from reserves `(1200, 2400)` the provider receives: ```txt 300 600 ``` # Challenge 15 - Reject Unbalanced Deposit URL: https://www.learnsol.site/learn/challenges/cpmm/15 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/15.mdx Check whether a proposed liquidity deposit is proportional to the current pool reserves. # Challenge 15: Reject Unbalanced Deposit A deposit that changes the reserve ratio effectively moves the pool price. ## Task Implement `is_proportional_deposit`. ## Hint Instead of dividing, compare cross products: ```txt x_deposit * y_reserve == y_deposit * x_reserve ``` Use `u128` for the multiplication. ## Example For reserves `(1000, 2000)` and deposit `(250, 500)`, the ratio matches exactly, so return `true`. # Challenge 16 - Checked Product Helper URL: https://www.learnsol.site/learn/challenges/cpmm/16 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/16.mdx Build a safe multiplication helper that returns an error instead of silently overflowing. # Challenge 16: Checked Product Helper Unchecked arithmetic is dangerous in financial code. ## Task Implement `checked_product`. ## Rules * Use `checked_mul`. * Return `Ok(product)` when multiplication succeeds. * Return `Err("overflow")` otherwise. ## Why this matters Many Solana program bugs start with safe-looking arithmetic that fails at large values. # Challenge 17 - Conservative Quote Rounding URL: https://www.learnsol.site/learn/challenges/cpmm/17 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/17.mdx Round a quoted output amount down to the nearest whole token unit so the pool never pays out too much. # Challenge 17: Conservative Quote Rounding In AMM math, rounding direction matters. ## Task Implement `floor_output`. ## Requirement Always round down. That means: ```txt floor(2000 / 11) = 181 ``` ## Why this matters Rounding up user output can leak value from the pool over time. # Challenge 18 - Pause State Guard URL: https://www.learnsol.site/learn/challenges/cpmm/18 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/18.mdx Reject swaps when the pool is paused and allow them only when the pool is active. # Challenge 18: Pause State Guard A production pool often includes a pause switch for emergencies. ## Task Implement `can_swap`. ## Requirement * `Active` => `true` * `Paused` => `false` This is simple, but it forces you to model state explicitly instead of assuming every instruction should always run. # Challenge 19 - Authority Match Check URL: https://www.learnsol.site/learn/challenges/cpmm/19 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/19.mdx Validate that the caller matches the configured admin authority before a sensitive pool action is allowed. # Challenge 19: Authority Match Check Many serious Solana bugs are not math bugs. They are authorization bugs. ## Task Implement `is_authority`. ## Requirement Return `true` only when the provided authority matches exactly. ## Why it matters A CPMM usually has privileged actions like: * pausing the pool * changing fee config * collecting protocol fees Those should never run for the wrong signer. # Challenge 2 - Spot Price From Reserves URL: https://www.learnsol.site/learn/challenges/cpmm/2 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/2.mdx Compute the spot price implied by pool reserves using integer math scaled to basis points. # Challenge 2: Spot Price From Reserves A CPMM does not store a price directly. The current spot price is implied by the reserve ratio. ## Task Implement `spot_price_bps(x_reserve, y_reserve)`. ## Requirements * Return the price of 1 unit of X in units of Y. * Scale the result by `10_000` so you can stay in integer math. * For `x = 2_000` and `y = 3_000`, return `15_000`. That corresponds to: ```txt 3_000 / 2_000 = 1.5 1.5 * 10_000 = 15_000 ``` > [!WARNING] > Do multiplication before division when possible, or integer division will throw away precision too early. # Challenge 20 - Secure CPMM Swap Flow URL: https://www.learnsol.site/learn/challenges/cpmm/20 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/20.mdx Combine pause-state validation, fee-adjusted exact-in quoting, and slippage protection into one secure swap function. # Challenge 20: Secure CPMM Swap Flow This challenge combines the most important ideas in the track. ## Task Implement `secure_swap_output`. ## Required behavior ### 1. State guard If the pool is paused, return: ```txt Err("pool paused") ``` ### 2. Fee-adjusted input Use: ```txt effective_input = x_in * 9_970 / 10_000 ``` ### 3. Constant-product quote Use: ```txt k = x_reserve * y_reserve new_x = x_reserve + effective_input new_y = k / new_x output = y_reserve - new_y ``` ### 4. Slippage guard If `output < min_out`, return: ```txt Err("slippage exceeded") ``` ### 5. Success case Return: ```txt Ok(output) ``` ## Sample For `(Active, 1000, 2000, 100, 180)`, the correct result is: ```txt 181 ``` # Challenge 3 - Exact In Swap Quote URL: https://www.learnsol.site/learn/challenges/cpmm/3 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/3.mdx Implement the basic constant-product exact-in quote for a swap without fees. # Challenge 3: Exact In Swap Quote Now compute the output amount for an exact-in swap. ## Formula Starting from: ```txt x * y = k ``` After adding `x_in` to the X reserve: ```txt new_x = x + x_in new_y = k / new_x y_out = y - new_y ``` ## Task Implement `quote_exact_in`. ## Requirements * Do intermediate math in `u128`. * Return the final output as `u64`. * Round down naturally through integer division. For the sample: ```txt x = 1000 y = 2000 x_in = 100 k = 2_000_000 new_x = 1100 new_y = 1818 y_out = 182 ``` # Challenge 4 - Fee Adjusted Swap Input URL: https://www.learnsol.site/learn/challenges/cpmm/4 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/4.mdx Apply a 30 basis point trading fee to the input amount before quoting the output of a swap. # Challenge 4: Fee Adjusted Swap Input Real CPMMs usually apply a fee before the input amount touches the swap formula. ## Task Implement `fee_adjusted_input(x_in)`. ## Rules * Fee = `30` basis points. * `10_000` basis points = `100%`. * Effective input = `x_in * 9_970 / 10_000`. ## Example ```txt x_in = 1000 1000 * 9970 / 10000 = 997 ``` > [!NOTE] > This challenge isolates fee math so you can reuse it in later swap logic. # Challenge 5 - Slippage Protected Swap URL: https://www.learnsol.site/learn/challenges/cpmm/5 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/5.mdx Combine fee-adjusted input, constant-product quote math, and a minimum output check to protect the trader from excessive slippage. # Challenge 5: Slippage Protected Swap This is the first mini end-to-end CPMM exercise. ## Task Implement `protected_swap_output`. ## Flow 1. Apply the 30 bps fee. 2. Use the exact-in constant-product quote. 3. Compare the result to `min_out`. 4. If the quote is too small, reject the trade. ## Sample Walkthrough With: ```txt x = 1000 y = 2000 x_in = 100 min_out = 180 ``` You should: * fee-adjust `100` to `99` * compute output using `new_x = 1099` * get `181` * accept because `181 >= 180` ## Important Round down during division. Conservative rounding is safer for AMM math. # Challenge 6 - Mutating Swap Reserves URL: https://www.learnsol.site/learn/challenges/cpmm/6 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/6.mdx Update pool reserves after an exact-in swap with a 30 basis point fee and return the new reserve balances. # Challenge 6: Mutating Swap Reserves Earlier challenges quoted output. Now mutate pool state. ## Task Implement `apply_swap`. ## Rules * Fee = 30 bps. * Effective input = `x_in * 9_970 / 10_000`. * `new_x = x + effective_input` * `k = x * y` * `new_y = k / new_x` ## Example For `x = 1000`, `y = 2000`, `x_in = 100`: * effective input = `99` * `new_x = 1099` * `k = 2_000_000` * `new_y = 1_819` Return: ```txt 1099 1819 ``` # Challenge 7 - Trading Fee Collected URL: https://www.learnsol.site/learn/challenges/cpmm/7 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/7.mdx Compute how much fee the protocol collects from a swap input amount when the fee rate is 30 basis points. # Challenge 7: Trading Fee Collected Every swap has two related numbers: * the user input amount * the fee taken from that input ## Task Implement `trading_fee(x_in)`. ## Rules * Fee rate = `30` basis points. * `10_000` basis points = `100%`. * For `x_in = 1000`, fee = `1000 * 30 / 10_000 = 3`. ## Why it matters A CPMM often needs to track the fee separately from the fee-adjusted input used in the quote formula. # Challenge 8 - Effective Execution Price URL: https://www.learnsol.site/learn/challenges/cpmm/8 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/8.mdx Calculate the average execution price of a swap using integer math scaled by 10,000. # Challenge 8: Effective Execution Price Spot price is not the same as execution price. ## Task Implement `execution_price_bps(x_in, y_out)`. ## Formula If a trader spends `x_in` units of X and receives `y_out` units of Y, then the average price in Y-per-X is: ```txt y_out / x_in ``` Scale by `10_000` so the result stays in integer math: ```txt y_out * 10_000 / x_in ``` ## Example For `x_in = 100` and `y_out = 181`: ```txt 181 * 10_000 / 100 = 18_100 ``` # Challenge 9 - Slippage In Basis Points URL: https://www.learnsol.site/learn/challenges/cpmm/9 Markdown URL: https://www.learnsol.site/learn/challenges/cpmm/9.mdx Compare spot price and execution price to compute realized slippage in basis points. # Challenge 9: Slippage In Basis Points Slippage measures how far the executed average price moved from the pre-trade spot price. ## Task Implement `slippage_bps`. ## Formula Use: ```txt (spot_price_bps - execution_price_bps) * 10_000 / spot_price_bps ``` ## Example For: * spot price = `20_000` * execution price = `18_100` Then: ```txt (20_000 - 18_100) * 10_000 / 20_000 = 950 ``` # Challenge 1 - Valid Limit Order URL: https://www.learnsol.site/learn/challenges/orderbook/1 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/1.mdx Define the basic limit order model and reject orders with zero price or zero quantity. # Challenge 1: Valid Limit Order Every matching engine starts with a precise order model. ## Task Implement `new_order`. ## Rules * `price == 0` is invalid. * `quantity == 0` is invalid. * Otherwise construct and return the order. This locks in the fields every later challenge uses: side, price, quantity, and timestamp. # Challenge 10 - Price Time Priority URL: https://www.learnsol.site/learn/challenges/orderbook/10 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/10.mdx Sort resting orders by best price first and earlier timestamp first when prices are equal. # Challenge 10: Price Time Priority Orderbooks usually use price-time priority. ## Task Implement `sort_by_priority`. ## Rules For bids: 1. higher price first 2. earlier timestamp first at the same price For asks: 1. lower price first 2. earlier timestamp first at the same price This is what makes matching deterministic and fair among same-price orders. # Challenge 11 - Post Only Rejection URL: https://www.learnsol.site/learn/challenges/orderbook/11 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/11.mdx Reject post-only orders that would immediately cross the top of book and take liquidity. # Challenge 11: Post Only Rejection Post-only orders are intended to add maker liquidity, not immediately take liquidity. ## Task Implement `accepts_post_only`. ## Rules * Post-only bid is rejected when it crosses the best ask. * Post-only ask is rejected when it crosses the best bid. * If the opposite side is empty, the order can post. This introduces maker/taker behavior without adding fee complexity. # Challenge 12 - Immediate Or Cancel Buy URL: https://www.learnsol.site/learn/challenges/orderbook/12 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/12.mdx Execute as much of an IOC buy as possible against asks and cancel any unfilled remainder instead of posting it. # Challenge 12: Immediate Or Cancel Buy An IOC order executes immediately and cancels whatever cannot fill. ## Task Implement `ioc_buy`. ## Rules * Consume asks from lowest price to highest price. * Track total base filled. * Track total quote spent. * Return leftover quantity as canceled. * Do not insert leftover quantity into the book. ## Example Trying to buy `8` base units against: * `2` at `100` * `3` at `105` Only `5` units can fill: ```txt filled = 5 spent = 2*100 + 3*105 = 515 canceled = 3 ``` # Challenge 2 - Crossing Logic URL: https://www.learnsol.site/learn/challenges/orderbook/2 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/2.mdx Implement the core price-crossing rule that decides whether a taker order can match a maker order. # Challenge 2: Crossing Logic A match can happen only when prices cross. ## Task Implement `crosses`. ## Rules * Buy/taker bid crosses maker ask if `bid_price >= ask_price`. * Sell/taker ask crosses maker bid if `ask_price <= bid_price`. This is the central predicate used by limit orders, post-only orders, and IOC orders. # Challenge 3 - Best Bid And Best Ask URL: https://www.learnsol.site/learn/challenges/orderbook/3 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/3.mdx Find the top of book: highest bid price and lowest ask price from a list of orders. # Challenge 3: Best Bid And Best Ask The top of book is the most important summary of an orderbook. ## Task Implement `best_bid_ask`. ## Rules * Best bid = highest bid price. * Best ask = lowest ask price. * Return `None` when a side has no orders. Use iteration and comparisons; no matching is needed yet. # Challenge 4 - Single Fill Calculation URL: https://www.learnsol.site/learn/challenges/orderbook/4 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/4.mdx Compute the filled amount and remaining quantities when one taker order meets one maker order. # Challenge 4: Single Fill Calculation Before matching a full book, handle one maker order. ## Task Implement `fill_once`. ## Rule The filled quantity is the smaller of taker quantity and maker quantity. Return: ```txt (filled_qty, taker_remaining, maker_remaining) ``` For taker `7` and maker `5`, fill `5`, leaving taker `2` and maker `0`. # Challenge 5 - Market Buy Against Asks URL: https://www.learnsol.site/learn/challenges/orderbook/5 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/5.mdx Match a market buy against ask liquidity from cheapest ask upward and compute filled base and quote spent. # Challenge 5: Market Buy Against Asks A market buy consumes the cheapest available asks first. ## Task Implement `market_buy`. ## Rules * Assume asks are already sorted low-to-high. * Fill until the buy quantity is satisfied or asks run out. * Quote spent at a level is `filled_at_level * price`. ## Example Buying `6` base units: * take `3` at `100` = `300` * take `3` at `105` = `315` Return: ```txt 6 615 ``` # Challenge 6 - Market Sell Against Bids URL: https://www.learnsol.site/learn/challenges/orderbook/6 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/6.mdx Match a market sell against bid liquidity from highest bid downward and compute filled base and quote received. # Challenge 6: Market Sell Against Bids A market sell consumes the highest available bids first. ## Task Implement `market_sell`. ## Rules * Assume bids are already sorted high-to-low. * Fill until the sell quantity is satisfied or bids run out. * Quote received at a level is `filled_at_level * price`. ## Example Selling `6` base units: * take `2` at `99` = `198` * take `4` at `98` = `392` Return: ```txt 6 590 ``` # Challenge 7 - Average Fill Price URL: https://www.learnsol.site/learn/challenges/orderbook/7 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/7.mdx Compute average execution price from total quote and base filled using integer math scaled by 10,000. # Challenge 7: Average Fill Price After a market order fills across multiple prices, you need an average execution price. ## Task Implement `average_fill_price_bps`. ## Formula Use: ```txt total_quote * 10_000 / base_filled ``` ## Example If a market buy filled `6` base and spent `615` quote: ```txt 615 / 6 = 102.5 102.5 * 10_000 = 1_025_000 ``` Return `1025000`. # Challenge 8 - Insert Limit Order Sorted URL: https://www.learnsol.site/learn/challenges/orderbook/8 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/8.mdx Insert a limit order into one side of the book while preserving the side-specific price ordering. # Challenge 8: Insert Limit Order Sorted A book side must remain sorted after inserting a new limit order. ## Task Implement `insert_sorted`. ## Rules * Bid book: highest price first. * Ask book: lowest price first. This challenge focuses on maintaining book shape, not matching. # Challenge 9 - Cancel Order By ID URL: https://www.learnsol.site/learn/challenges/orderbook/9 Markdown URL: https://www.learnsol.site/learn/challenges/orderbook/9.mdx Remove an order from the book by ID and return whether a matching order was found. # Challenge 9: Cancel Order By ID Users must be able to cancel resting orders that have not fully filled. ## Task Implement `cancel_order`. ## Rules * Find the first order with matching `id`. * Remove it from the vector. * Return `true` if removed. * Return `false` if not found. # Day 1 - Hello Rust URL: https://www.learnsol.site/learn/challenges/rust/1 Markdown URL: https://www.learnsol.site/learn/challenges/rust/1.mdx Start your Rust journey with Day 1: Learn to declare variables, understand basic types, and use println! macro for output. Perfect for Solana development beginners. # Day 1: Hello Rust Welcome to your Rust journey! Today we'll write our first Rust program and learn the fundamentals. > [!NOTE] > Rust is a systems programming language focused on safety, speed, and concurrency. It's the language of choice for Solana programs! ## What You'll Learn * Declaring variables with `let` * Basic string types * The `println!` macro for output * String interpolation with `{}` ## The Task Create a variable called `name` with the value `"Rustacean"` and print a greeting message. ## Key Concepts ### Variables In Rust, variables are declared with `let`: ```rust let x = 5; // immutable by default let mut y = 10; // mutable with 'mut' ``` ### The println! Macro Use `println!` to print to the console: ```rust let name = "World"; println!("Hello, {}!", name); // Output: Hello, World! ``` ## Requirements * Create a variable `name` with the value `"Rustacean"` * Print exactly: `Hello, Rustacean! Welcome to Rust.` ## Hints > [!WARNING] > Make sure your output matches exactly - including punctuation and spacing! * Use `let` to declare the variable * Use `println!` with `{}` for interpolation * The string must be a `&str` literal (in quotes) # Day 10: Methods & impl URL: https://www.learnsol.site/learn/challenges/rust/10 Markdown URL: https://www.learnsol.site/learn/challenges/rust/10.mdx Add behavior to types with impl blocks in Day 10: methods, associated functions, and self parameter. Build reusable components for Solana programs. # Day 10: Methods & impl Methods are functions defined within the context of a struct (or enum/trait). ## Defining Methods ```rust struct Circle { radius: f64, } impl Circle { // Associated function (no self) - like a constructor fn new(radius: f64) -> Self { Circle { radius } } // Method (takes &self) fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } // Method that modifies self fn grow(&mut self, amount: f64) { self.radius += amount; } } ``` ## The self Parameter | Syntax | Meaning | | ----------- | ---------------- | | `self` | Takes ownership | | `&self` | Immutable borrow | | `&mut self` | Mutable borrow | > [!NOTE] > `Self` (capital S) is an alias for the type being implemented. `self` (lowercase) is the instance. ## Calling Methods ```rust let mut c = Circle::new(5.0); // associated function let a = c.area(); // method with &self c.grow(2.0); // method with &mut self ``` ## The Task Create a `Rectangle` with: 1. `new(width, height)` - associated function 2. `area(&self)` - returns width × height 3. `is_square(&self)` - returns true if width == height ## Requirements * `Rectangle::new(5, 5).area()` → `25` * `Rectangle::new(5, 5).is_square()` → `true` * `Rectangle::new(5, 3).is_square()` → `false` ## Hints ```rust impl Rectangle { fn new(width: u32, height: u32) -> Self { Self { width, height } } fn area(&self) -> u32 { self.width * self.height } } ``` # Day 11: Result & Error Handling URL: https://www.learnsol.site/learn/challenges/rust/11 Markdown URL: https://www.learnsol.site/learn/challenges/rust/11.mdx Handle errors gracefully in Day 11: Result type, the ? operator, and error propagation patterns. Master Solana's error handling conventions. # Day 11: Result & Error Handling Rust uses `Result` for operations that can fail. No exceptions! ## The Result Enum ```rust enum Result { Ok(T), // Success with value T Err(E), // Error with value E } ``` ## Handling Results ```rust let result: Result = "42".parse(); match result { Ok(num) => println!("Parsed: {}", num), Err(e) => println!("Error: {}", e), } ``` ## The ? Operator Propagate errors automatically: ```rust fn read_number() -> Result { let num = "42".parse().map_err(|_| String::from("parse error"))?; Ok(num * 2) } ``` > [!WARNING] > The `?` operator can only be used in functions that return `Result` or `Option`! ## Common Result Methods ```rust result.unwrap() // Panics on Err result.expect("msg") // Panics with message result.unwrap_or(0) // Default value on Err result.is_ok() // Returns bool result.is_err() // Returns bool ``` ## The Task Implement `parse_and_double(s)` that: 1. Parses the string to an `i32` 2. Returns `Ok(number * 2)` on success 3. Returns `Err("Invalid number")` on failure ## Requirements * `parse_and_double("21")` → `Ok(42)` * `parse_and_double("abc")` → `Err("Invalid number")` ## Hints ```rust fn parse_and_double(s: &str) -> Result { match s.parse::() { Ok(n) => Ok(n * 2), Err(_) => Err(String::from("Invalid number")), } } ``` # Day 12: Vectors URL: https://www.learnsol.site/learn/challenges/rust/12 Markdown URL: https://www.learnsol.site/learn/challenges/rust/12.mdx Master Vec in Day 12: Rust's growable, heap-allocated array. Learn push, pop, iteration, and memory management for dynamic Solana data. # Day 12: Vectors `Vec` is Rust's dynamic array - it can grow and shrink at runtime. ## Creating Vectors ```rust let v1: Vec = Vec::new(); // Empty vector let v2 = vec![1, 2, 3]; // Using vec! macro let v3 = Vec::with_capacity(10); // Pre-allocated ``` ## Modifying Vectors ```rust let mut v = vec![1, 2, 3]; v.push(4); // Add to end v.pop(); // Remove from end, returns Option v.insert(0, 10); // Insert at index v.remove(0); // Remove at index ``` ## Accessing Elements ```rust let v = vec![1, 2, 3, 4, 5]; // By index (panics if out of bounds) let third = v[2]; // Safe access with get (returns Option) match v.get(2) { Some(val) => println!("{}", val), None => println!("No element"), } ``` ## Iterating ```rust for item in &v { println!("{}", item); } // With mutable reference for item in &mut v { *item += 1; } ``` > [!NOTE] > Vectors are crucial for collecting iterator results with `.collect()`! ## The Task Implement `sum_and_max(numbers)` that returns `(sum, max)` of a slice. ## Requirements * Take `&[i32]` (slice, not Vec) * Return `(sum, max)` as a tuple * Input: `[3, 1, 4, 1, 5, 9, 2, 6]` * Output: `(31, 9)` ## Hints ```rust fn sum_and_max(numbers: &[i32]) -> (i32, i32) { let sum = numbers.iter().sum(); let max = *numbers.iter().max().unwrap(); (sum, max) } ``` Or manually: ```rust let mut sum = 0; let mut max = numbers[0]; for &n in numbers { sum += n; if n > max { max = n; } } ``` # Day 13: Strings URL: https://www.learnsol.site/learn/challenges/rust/13 Markdown URL: https://www.learnsol.site/learn/challenges/rust/13.mdx Understand String vs &str in Day 13: owned strings, string slices, and manipulation methods. Essential for handling text data in Solana programs. # Day 13: Strings Rust has two main string types, and understanding them is crucial! ## String vs \&str | Type | Ownership | Mutability | Location | | -------- | --------- | -------------- | -------- | | `String` | Owned | Can be mutable | Heap | | `&str` | Borrowed | Immutable | Anywhere | ```rust let s1: &str = "hello"; // String literal (in binary) let s2: String = String::from("hello"); // Owned, heap-allocated let s3: &str = &s2; // Borrow String as &str ``` ## String Operations ```rust let mut s = String::from("hello"); s.push(' '); // Append char s.push_str("world"); // Append &str s += "!"; // Concat with += let combined = format!("{} {}", "hello", "world"); ``` ## Converting Between Types ```rust // &str → String let owned = "hello".to_string(); let owned = String::from("hello"); // String → &str let borrowed: &str = &owned; ``` > [!WARNING] > Strings in Rust are UTF-8 encoded. Indexing by byte position can be dangerous! ## String Methods ```rust let s = "hello world"; s.len() // Byte length s.is_empty() // Check if empty s.contains("world") // Substring check s.split_whitespace() // Iterator over words s.chars() // Iterator over chars ``` ## The Task Implement `reverse_words(s)` that reverses the order of words. * Input: `"rust is awesome"` * Output: `"awesome is rust"` ## Requirements * Split by whitespace * Reverse the order * Join back with spaces ## Hints ```rust fn reverse_words(s: &str) -> String { s.split_whitespace() .rev() .collect::>() .join(" ") } ``` Or: ```rust let words: Vec<&str> = s.split(' ').collect(); words.into_iter().rev().collect::>().join(" ") ``` # Day 14: HashMaps URL: https://www.learnsol.site/learn/challenges/rust/14 Markdown URL: https://www.learnsol.site/learn/challenges/rust/14.mdx Store key-value pairs with HashMap in Day 14: insertion, lookup, iteration, and common patterns. Perfect for mapping accounts in Solana applications. # Day 14: HashMaps `HashMap` stores key-value pairs with O(1) average lookup time. ## Creating HashMaps ```rust use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert("Alice", 100); scores.insert("Bob", 80); ``` ## Accessing Values ```rust // Returns Option<&V> if let Some(score) = scores.get("Alice") { println!("Alice: {}", score); } // Or with default let score = scores.get("Charlie").unwrap_or(&0); ``` ## The Entry API Powerful pattern for conditional insertion: ```rust // Insert only if key doesn't exist scores.entry("Alice").or_insert(50); // Modify existing or insert let count = scores.entry("Alice").or_insert(0); *count += 1; ``` > [!NOTE] > The Entry API is perfect for counting frequencies - a common interview pattern! ## Iterating ```rust for (key, value) in &scores { println!("{}: {}", key, value); } ``` ## The Task Implement `char_frequency(s)` that counts character occurrences. * Only count alphabetic characters * Convert to lowercase * Return `HashMap` ## Requirements * `"Hello"` → `{h: 1, e: 1, l: 2, o: 1}` * Ignore non-alphabetic characters ## Hints ```rust use std::collections::HashMap; fn char_frequency(s: &str) -> HashMap { let mut freq = HashMap::new(); for c in s.chars() { if c.is_alphabetic() { let c = c.to_ascii_lowercase(); *freq.entry(c).or_insert(0) += 1; } } freq } ``` # Day 15: Generics URL: https://www.learnsol.site/learn/challenges/rust/15 Markdown URL: https://www.learnsol.site/learn/challenges/rust/15.mdx Write flexible code with generics in Day 15: generic functions, structs, and type parameters. Build reusable Solana program components. # Day 15: Generics Generics let you write code that works with multiple types while maintaining type safety. ## Generic Functions ```rust fn largest(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } ``` ## Generic Structs ```rust struct Point { x: T, y: T, } let integer_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.0, y: 4.0 }; ``` ## Multiple Generic Types ```rust struct Point { x: T, y: U, } let mixed = Point { x: 5, y: 4.0 }; ``` > [!NOTE] > Generics have zero runtime cost in Rust! The compiler generates specialized code for each concrete type (monomorphization). ## Trait Bounds Constrain what types can be used: ```rust fn print_debug(item: T) { println!("{:?}", item); } // Multiple bounds fn compare(a: T, b: T) -> T { if a > b { a } else { b } } ``` ## The Task Implement a generic `largest` function that finds the largest element in a slice. ## Requirements * Works with any type that implements `PartialOrd + Copy` * Returns the value (not a reference, since we have Copy) * `largest(&[34, 50, 25, 100, 65])` → `100` ## Hints ```rust fn largest(list: &[T]) -> T { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest } ``` # Day 16: Traits URL: https://www.learnsol.site/learn/challenges/rust/16 Markdown URL: https://www.learnsol.site/learn/challenges/rust/16.mdx Define shared behavior with traits in Day 16: Rust's approach to interfaces. Learn trait bounds and implementations for Solana program design. # Day 16: Traits Traits define shared behavior that types can implement - similar to interfaces in other languages. ## Defining a Trait ```rust trait Summary { fn summarize(&self) -> String; // Default implementation fn preview(&self) -> String { format!("Read more: {}", self.summarize()) } } ``` ## Implementing a Trait ```rust struct Article { title: String, content: String, } impl Summary for Article { fn summarize(&self) -> String { format!("{}: {}", self.title, &self.content[..50]) } } ``` ## Traits as Parameters ```rust // impl Trait syntax (simpler) fn notify(item: &impl Summary) { println!("Breaking: {}", item.summarize()); } // Trait bound syntax (more flexible) fn notify(item: &T) { println!("Breaking: {}", item.summarize()); } ``` > [!NOTE] > Common traits in Rust: `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`, `Default` ## The Task 1. Define a `Describable` trait with a `describe(&self) -> String` method 2. Implement it for `Dog` (return `"Dog: {name}"`) 3. Implement it for `Cat` (return `"Cat: {name}"`) ## Requirements * Both structs have a `name: String` field * Trait method returns a `String` * Test only uses `Dog { name: "Buddy" }` ## Hints ```rust trait Describable { fn describe(&self) -> String; } impl Describable for Dog { fn describe(&self) -> String { format!("Dog: {}", self.name) } } ``` # Day 17: Trait Objects URL: https://www.learnsol.site/learn/challenges/rust/17 Markdown URL: https://www.learnsol.site/learn/challenges/rust/17.mdx Use dyn Trait for runtime polymorphism in Day 17: trait objects and heterogeneous collections. Advanced patterns for flexible Solana architectures. # Day 17: Trait Objects Trait objects enable dynamic dispatch - deciding which method to call at runtime. ## Static vs Dynamic Dispatch ```rust // Static dispatch (monomorphization) fn notify(item: &impl Summary) { ... } // Dynamic dispatch (trait object) fn notify(item: &dyn Summary) { ... } ``` ## Creating Trait Objects ```rust trait Draw { fn draw(&self); } let shapes: Vec> = vec![ Box::new(Circle { radius: 5.0 }), Box::new(Rectangle { width: 10.0, height: 5.0 }), ]; ``` > [!WARNING] > Trait objects use `Box` because the size is unknown at compile time. The `Box` provides heap allocation with a known pointer size. ## When to Use Trait Objects | Use Case | Approach | | ------------------------ | -------------------------- | | Homogeneous collection | `Vec` with generics | | Heterogeneous collection | `Vec>` | | Plugin systems | Trait objects | | Maximum performance | Generics (static dispatch) | ## Object Safety Not all traits can be made into trait objects. A trait is object-safe if: * No `Self` in method return types * No generic type parameters in methods ## The Task 1. Implement the `Speak` trait for `Cat` (return `"Meow!"`) 2. Create a `Vec>` with a Dog and Cat 3. Iterate and print each animal's speech ## Requirements * `Dog.speak()` → `"Woof!"` * `Cat.speak()` → `"Meow!"` * Output two lines: `Woof!` then `Meow!` ## Hints ```rust impl Speak for Cat { fn speak(&self) -> String { String::from("Meow!") } } fn main() { let animals: Vec> = vec![ Box::new(Dog), Box::new(Cat), ]; for animal in &animals { println!("{}", animal.speak()); } } ``` # Day 18: Lifetimes URL: https://www.learnsol.site/learn/challenges/rust/18 Markdown URL: https://www.learnsol.site/learn/challenges/rust/18.mdx Master lifetime annotations in Day 18: Rust's way of ensuring reference validity. Critical for understanding Solana's account lifetime requirements. # Day 18: Lifetimes Lifetimes ensure references are valid for as long as they're used. Most are inferred, but sometimes you need explicit annotations. ## The Problem ```rust // This won't compile - which input does the return reference come from? fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } ``` The compiler doesn't know if the returned reference comes from `x` or `y`, so it can't verify the reference will be valid. ## Lifetime Annotations ```rust fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } ``` > [!NOTE] > Lifetime annotations don't change how long references live. They describe relationships between lifetimes so the compiler can check validity. ## Lifetime Syntax * `'a` - a lifetime parameter (convention: lowercase letters) * `&'a T` - a reference with lifetime 'a * `&'a mut T` - a mutable reference with lifetime 'a * `'static` - the entire program duration ## Lifetime Elision Rules The compiler applies these rules automatically: 1. Each input reference gets its own lifetime 2. If there's exactly one input lifetime, it applies to all outputs 3. If there's `&self` or `&mut self`, that lifetime applies to outputs ## Struct Lifetimes ```rust struct ImportantExcerpt<'a> { part: &'a str, // This reference must live at least as long as the struct } ``` ## The Task Add lifetime annotations to make `longest` compile. ## Requirements * Both inputs share the same lifetime * Output has the same lifetime as inputs * Return the longer of two strings ## Hints ```rust fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } ``` The `'a` says: "The returned reference will be valid for the shorter of the two input lifetimes." # Day 19: Iterators URL: https://www.learnsol.site/learn/challenges/rust/19 Markdown URL: https://www.learnsol.site/learn/challenges/rust/19.mdx Master Rust iterators in Day 19: functional data processing with map, filter, fold, and collect. Write efficient Solana data transformations. # Day 19: Iterators Iterators are one of Rust's most powerful features - zero-cost abstractions for processing sequences. ## The Iterator Trait ```rust trait Iterator { type Item; fn next(&mut self) -> Option; } ``` ## Creating Iterators ```rust let v = vec![1, 2, 3]; v.iter() // &T - immutable references v.iter_mut() // &mut T - mutable references v.into_iter() // T - takes ownership ``` ## Iterator Adapters Adapters transform iterators (lazy - don't execute until consumed): ```rust let v = vec![1, 2, 3, 4, 5]; v.iter() .filter(|&&x| x > 2) // Keep if predicate is true .map(|&x| x * 2) // Transform each element .take(2) // Take first n elements .skip(1) // Skip first n elements ``` > [!NOTE] > Iterators are lazy! Nothing happens until you call a consuming method like `collect()`, `sum()`, `for_each()`. ## Consuming Iterators ```rust let v = vec![1, 2, 3, 4, 5]; let sum: i32 = v.iter().sum(); let product: i32 = v.iter().product(); let collected: Vec<_> = v.iter().map(|x| x * 2).collect(); let count = v.iter().count(); let max = v.iter().max(); ``` ## Method Chaining ```rust let result: Vec = (1..=10) .filter(|x| x % 2 == 0) // 2, 4, 6, 8, 10 .map(|x| x * x) // 4, 16, 36, 64, 100 .collect(); ``` ## The Task Implement `process_numbers` that: 1. Filters to keep only even numbers 2. Squares each remaining number 3. Collects into a Vec ## Requirements * Input: `vec![1, 2, 3, 4, 5, 6]` * Output: `vec![4, 16, 36]` (2², 4², 6²) ## Hints ```rust fn process_numbers(numbers: Vec) -> Vec { numbers .into_iter() .filter(|&x| x % 2 == 0) .map(|x| x * x) .collect() } ``` # Day 2: Primitive Types URL: https://www.learnsol.site/learn/challenges/rust/2 Markdown URL: https://www.learnsol.site/learn/challenges/rust/2.mdx Master Rust's fundamental types in Day 2: integers, floats, tuples, and basic arithmetic operations. Build a strong foundation for Solana smart contract development. # Day 2: Primitive Types Today we explore Rust's primitive types - the building blocks of all data in Rust. ## What You'll Learn * Integer types (`i32`, `u64`, etc.) * Tuples and destructuring * Basic arithmetic operations * Return values from functions ## Rust's Integer Types | Signed | Unsigned | Size | | ------ | -------- | ------- | | `i8` | `u8` | 8-bit | | `i16` | `u16` | 16-bit | | `i32` | `u32` | 32-bit | | `i64` | `u64` | 64-bit | | `i128` | `u128` | 128-bit | > [!NOTE] > For Solana development, `u64` is commonly used for token amounts (lamports), and `u8` for single-byte flags. ## Tuples Tuples group multiple values of different types: ```rust let point = (10, 20); // type: (i32, i32) let (x, y) = point; // destructuring println!("x: {}, y: {}", x, y); ``` ## The Task Implement `calculate(a, b)` that returns a tuple of `(sum, product)`. ## Requirements * Take two `i32` parameters * Return `(a + b, a * b)` as a tuple * The test uses `(7, 6)` expecting `(13, 42)` ## Hints * Return a tuple with parentheses: `(value1, value2)` * No semicolon on the last expression = return value # Day 20: Closures URL: https://www.learnsol.site/learn/challenges/rust/20 Markdown URL: https://www.learnsol.site/learn/challenges/rust/20.mdx Capture environment with closures in Day 20: anonymous functions with superpowers. Learn Fn, FnMut, FnOnce traits for Solana callback patterns. # Day 20: Closures Closures are anonymous functions that can capture values from their environment. ## Closure Syntax ```rust // Full syntax let add = |a: i32, b: i32| -> i32 { a + b }; // Type inference (common) let add = |a, b| a + b; // No parameters let greet = || println!("Hello!"); ``` ## Capturing Environment ```rust let x = 4; let add_x = |n| n + x; // Captures x from environment println!("{}", add_x(5)); // 9 ``` ## Three Closure Traits | Trait | Captures | Example | | -------- | ------------------------------- | ----------------------- | | `Fn` | By reference (`&T`) | Read-only access | | `FnMut` | By mutable reference (`&mut T`) | Modify captured values | | `FnOnce` | By value (ownership) | Consume captured values | Use `move` to force ownership transfer: ```rust let s = String::from("hello"); let closure = move || println!("{}", s); // s is no longer valid here ``` ## Closures as Parameters ```rust fn apply(f: F) where F: Fn(i32) -> i32 { println!("{}", f(5)); } apply(|x| x * 2); // 10 ``` ## Returning Closures ```rust fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n } let add_5 = make_adder(5); println!("{}", add_5(10)); // 15 ``` ## The Task Implement `make_counter(start)` that returns a closure which: * Returns the current count * Increments the count for the next call ## Requirements * `make_counter(1)` starts at 1 * First call returns 1, second returns 2, third returns 3 ## Hints ```rust fn make_counter(start: i32) -> impl FnMut() -> i32 { let mut count = start; move || { let current = count; count += 1; current } } ``` # Day 21: Modules URL: https://www.learnsol.site/learn/challenges/rust/21 Markdown URL: https://www.learnsol.site/learn/challenges/rust/21.mdx Organize code with modules in Day 21: visibility, pub keyword, and clean API design. Structure your Solana programs professionally. # Day 21: Modules Modules help organize code into logical units with controlled visibility. ## Defining Modules ```rust mod garden { pub mod vegetables { pub fn plant() { println!("Planting vegetables!"); } } fn private_helper() { // Only accessible within garden module } } ``` ## Visibility Rules | Keyword | Visibility | | ------------ | ------------------------- | | (none) | Private to current module | | `pub` | Public | | `pub(crate)` | Public within crate | | `pub(super)` | Public to parent module | > [!WARNING] > In Rust, everything is private by default! You must explicitly mark items as `pub` to make them accessible. ## Using Items ```rust // Absolute path crate::garden::vegetables::plant(); // Relative path garden::vegetables::plant(); // With use statement use garden::vegetables::plant; plant(); ``` ## The super Keyword Access parent module: ```rust mod parent { pub fn helper() -> i32 { 42 } mod child { pub fn use_parent() -> i32 { super::helper() // Access parent's function } } } ``` ## File-Based Modules ``` src/ ├── main.rs ├── garden.rs // mod garden └── garden/ └── vegetables.rs // mod garden::vegetables ``` ## The Task Fix the module visibility: 1. Make `math::add` public 2. Make `math::advanced::multiply_and_add` public 3. Use `super::add` in `multiply_and_add` ## Requirements * `math::add(5, 3)` → `8` * `math::advanced::multiply_and_add(2, 3, 4)` → `10` (2\*3 + 4) ## Hints ```rust mod math { pub fn add(a: i32, b: i32) -> i32 { a + b } pub mod advanced { pub fn multiply_and_add(a: i32, b: i32, c: i32) -> i32 { super::add(a * b, c) } } } ``` # Day 22: Smart Pointers URL: https://www.learnsol.site/learn/challenges/rust/22 Markdown URL: https://www.learnsol.site/learn/challenges/rust/22.mdx Explore smart pointers in Day 22: Box, Rc, and RefCell for advanced memory management. Understand patterns used in complex Solana programs. # Day 22: Smart Pointers Smart pointers are data structures that act like pointers but have additional metadata and capabilities. ## Box\: Heap Allocation `Box` stores data on the heap instead of the stack: ```rust let b = Box::new(5); // 5 is stored on the heap println!("{}", b); // Deref coercion: prints 5 ``` ### When to Use Box * When you have a type whose size can't be known at compile time * When you have a large amount of data to transfer ownership without copying * When you want to own a value that implements a trait (trait objects) ## Recursive Types Without `Box`, recursive types would have infinite size: ```rust enum List { Cons(i32, Box), Nil, } ``` > [!NOTE] > This pattern is exactly how linked lists work! The `Box` gives us a known size (pointer size) at compile time. ## Other Smart Pointers | Type | Use Case | | ------------ | ------------------------------------ | | `Box` | Heap allocation, known size | | `Rc` | Multiple ownership (single-threaded) | | `Arc` | Multiple ownership (thread-safe) | | `RefCell` | Interior mutability | ## Rc\: Reference Counting ```rust use std::rc::Rc; let a = Rc::new(5); let b = Rc::clone(&a); // Both point to same data println!("count: {}", Rc::strong_count(&a)); // 2 ``` ## The Task Implement `sum_list` that recursively sums all values in a cons list. ## Requirements * `Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))))` → `6` * Handle both `Cons` and `Nil` cases ## Hints ```rust fn sum_list(list: &List) -> i32 { match list { Cons(value, next) => value + sum_list(next), Nil => 0, } } ``` # Day 23: Derive Macros URL: https://www.learnsol.site/learn/challenges/rust/23 Markdown URL: https://www.learnsol.site/learn/challenges/rust/23.mdx Auto-implement traits with #[derive] in Day 23: Debug, Clone, Copy, PartialEq, and more. Essential macros for Solana account serialization. # Day 23: Derive Macros Derive macros automatically implement traits for your types - a huge productivity boost! ## Common Derivable Traits ```rust #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] struct User { name: String, age: u32, } ``` | Trait | Purpose | | ------------ | ----------------------------------- | | `Debug` | Enables `{:?}` formatting | | `Clone` | Enables `.clone()` method | | `Copy` | Implicit copying (for simple types) | | `PartialEq` | Enables `==` comparison | | `Eq` | Marker for full equality | | `Hash` | Enables use as HashMap key | | `Default` | Enables `Type::default()` | | `PartialOrd` | Enables `<`, `>` comparison | | `Ord` | Full ordering | **Solana Relevance**: In Anchor, you'll frequently use: ```rust #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct TokenAccount { ... } ``` ## Debug Trait ```rust #[derive(Debug)] struct Point { x: i32, y: i32 } let p = Point { x: 1, y: 2 }; println!("{:?}", p); // Point { x: 1, y: 2 } println!("{:#?}", p); // Pretty-printed ``` ## Clone vs Copy ```rust #[derive(Clone, Copy)] // Copy requires Clone struct Point { x: i32, y: i32 } let p1 = Point { x: 1, y: 2 }; let p2 = p1; // Copy happens automatically // p1 is still valid! ``` > [!NOTE] > `Copy` can only be derived for types whose fields are all `Copy`. Types with heap data (like `String`) cannot be `Copy`. ## The Task Add the necessary derive macros to make the code compile: * `Debug` for `{:?}` printing * `Clone` for `.clone()` * `PartialEq` for `==` comparison ## Requirements * Print the token with `{:?}` * Compare two tokens with `==` * Clone works correctly ## Hints ```rust #[derive(Debug, Clone, PartialEq)] struct Token { symbol: String, amount: u64, } ``` # Day 24: Attributes URL: https://www.learnsol.site/learn/challenges/rust/24 Markdown URL: https://www.learnsol.site/learn/challenges/rust/24.mdx Control compilation with attributes in Day 24: #[repr], #[allow], and memory layout control. Crucial for Solana program optimization. # Day 24: Attributes Attributes are metadata applied to modules, crates, items, and expressions. ## Attribute Syntax ```rust #[attribute] // Outer attribute (applies to next item) #![attribute] // Inner attribute (applies to enclosing item) ``` ## Common Attributes ### Conditional Compilation ```rust #[cfg(target_os = "linux")] fn linux_only() { } #[cfg(test)] mod tests { } #[cfg(feature = "advanced")] fn advanced_feature() { } ``` ### Linting ```rust #[allow(dead_code)] fn unused_function() { } #[warn(unused_variables)] fn strict_function() { } ``` ## Memory Layout: #\[repr] > [!WARNING] > **Critical for Solana!** Account data must have predictable memory layouts for serialization. ```rust #[repr(C)] // C-compatible layout struct AccountData { is_initialized: bool, // 1 byte bump: u8, // 1 byte authority: [u8; 32], // 32 bytes balance: u64, // 8 bytes } ``` | Repr | Meaning | | ---------------------- | --------------------------------- | | `#[repr(C)]` | C-compatible layout (predictable) | | `#[repr(packed)]` | No padding between fields | | `#[repr(transparent)]` | Same layout as single field | | `#[repr(u8)]` | Enum discriminant is u8 | ## Why #\[repr(C)] Matters Without `#[repr(C)]`, Rust may reorder fields or add padding: ```rust // Without repr(C) - Rust may optimize layout struct Bad { a: bool, // Could be anywhere b: u64, // Could be anywhere } // With repr(C) - Predictable order #[repr(C)] struct Good { a: bool, // Offset 0 b: u64, // Offset 8 (after padding) } ``` ## The Task Add `#[repr(C)]` to make `AccountHeader` have exactly 34 bytes: * `is_initialized: bool` (1 byte) * `account_type: u8` (1 byte) * `authority: [u8; 32]` (32 bytes) ## Hints ```rust #[repr(C)] struct AccountHeader { is_initialized: bool, account_type: u8, authority: [u8; 32], } ``` # Day 25: Byte Serialization URL: https://www.learnsol.site/learn/challenges/rust/25 Markdown URL: https://www.learnsol.site/learn/challenges/rust/25.mdx Manually serialize data to bytes in Day 25: the foundation for understanding Borsh serialization used throughout Solana development. # Day 25: Byte Serialization Serialization converts data structures to bytes. This is fundamental for Solana where all account data is stored as bytes! ## Why This Matters for Solana > [!WARNING] > Solana accounts store data as raw bytes (`&[u8]`). You need to serialize (write) and deserialize (read) your data structures. ## Endianness Byte order matters when converting multi-byte values: ```rust let num: u32 = 0x12345678; // Little-endian (Solana uses this) // [0x78, 0x56, 0x34, 0x12] // Big-endian // [0x12, 0x34, 0x56, 0x78] ``` ## Converting Integers to Bytes ```rust let n: u64 = 1000; // To bytes (little-endian) let bytes: [u8; 8] = n.to_le_bytes(); // From bytes let restored = u64::from_le_bytes(bytes); ``` ## Manual Serialization ```rust struct Transfer { amount: u64, recipient: [u8; 4], } impl Transfer { fn to_bytes(&self) -> [u8; 12] { let mut bytes = [0u8; 12]; // Copy amount (8 bytes, little-endian) bytes[0..8].copy_from_slice(&self.amount.to_le_bytes()); // Copy recipient (4 bytes) bytes[8..12].copy_from_slice(&self.recipient); bytes } fn from_bytes(data: &[u8; 12]) -> Self { let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); let mut recipient = [0u8; 4]; recipient.copy_from_slice(&data[8..12]); Self { amount, recipient } } } ``` ## The Task Implement `to_bytes()` and `from_bytes()` for the Transfer struct. ## Requirements * Amount is serialized as little-endian u64 (8 bytes) * Recipient is copied directly (4 bytes) * Total: 12 bytes * Round-trip must preserve data ## Hints ```rust fn to_bytes(&self) -> [u8; 12] { let mut bytes = [0u8; 12]; bytes[0..8].copy_from_slice(&self.amount.to_le_bytes()); bytes[8..12].copy_from_slice(&self.recipient); bytes } fn from_bytes(data: &[u8; 12]) -> Self { let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); let mut recipient = [0u8; 4]; recipient.copy_from_slice(&data[8..12]); Self { amount, recipient } } ``` # Day 26: Working with Bytes URL: https://www.learnsol.site/learn/challenges/rust/26 Markdown URL: https://www.learnsol.site/learn/challenges/rust/26.mdx Build a Pubkey-like struct in Day 26: implement a 32-byte public key type similar to Solana's. Essential for Solana address handling. # Day 26: Working with Bytes In Solana, public keys are 32-byte arrays. Let's build a simplified `Pubkey` type! ## Solana's Pubkey ```rust // Actual Solana SDK pub struct Pubkey([u8; 32]); ``` > [!NOTE] > Every account, program, and wallet in Solana is identified by a 32-byte public key. Understanding byte manipulation is essential! ## Fixed-Size Byte Arrays ```rust // Creating arrays let zeros = [0u8; 32]; // All zeros let ones = [1u8; 32]; // All ones let mixed = [1, 2, 3, 0, 0, ...]; // Specific values // Comparing arrays zeros == [0u8; 32] // true zeros != ones // true ``` ## Newtype Pattern Wrap a type for type safety: ```rust #[derive(Debug, Clone, Copy, PartialEq)] struct Pubkey([u8; 32]); impl Pubkey { const fn new(bytes: [u8; 32]) -> Self { Pubkey(bytes) } } ``` ## Common Operations ```rust impl Pubkey { // Check if all zeros fn is_zero(&self) -> bool { self.0 == [0u8; 32] } // Get as slice fn as_bytes(&self) -> &[u8] { &self.0 } // Get first n bytes fn first_bytes(&self) -> [u8; N] { self.0[..N].try_into().unwrap() } } ``` ## The "System Program" Pattern ```rust impl Pubkey { // Constant pubkey (like system program) const SYSTEM_PROGRAM: Pubkey = Pubkey([0u8; 32]); fn is_system_program(&self) -> bool { *self == Self::SYSTEM_PROGRAM } } ``` ## The Task Implement for `Pubkey`: 1. `default()` - returns a pubkey with all zeros 2. `is_zero()` - checks if all bytes are zero 3. `as_bytes()` - returns the bytes as a slice ## Requirements * `Pubkey::default().is_zero()` → `true` * `Pubkey::new([1u8; 32]).is_zero()` → `false` * `Pubkey::new([1u8; 32]).as_bytes()[0]` → `1` ## Hints ```rust fn default() -> Self { Pubkey([0u8; 32]) } fn is_zero(&self) -> bool { self.0 == [0u8; 32] } fn as_bytes(&self) -> &[u8] { &self.0 } ``` # Day 27: The From Trait URL: https://www.learnsol.site/learn/challenges/rust/27 Markdown URL: https://www.learnsol.site/learn/challenges/rust/27.mdx Implement type conversions in Day 27: From/Into traits used everywhere in Solana for seamless type transformations. # Day 27: The From Trait The `From` trait enables type conversions. Implementing `From` automatically gives you `Into`! ## From and Into ```rust // Implementing From impl From for MyType { fn from(value: i32) -> Self { MyType(value) } } // You get Into for free! let x: MyType = 42.into(); let y: MyType = MyType::from(42); ``` > [!NOTE] > **Convention**: Implement `From`, not `Into`. The blanket implementation handles `Into` automatically. ## Common Patterns ```rust // String conversions impl From<&str> for MyString { fn from(s: &str) -> Self { MyString(s.to_string()) } } // Error conversions impl From for MyError { fn from(e: std::io::Error) -> Self { MyError::Io(e) } } ``` ## Solana: Lamports and SOL ```rust // 1 SOL = 1,000,000,000 lamports const LAMPORTS_PER_SOL: u64 = 1_000_000_000; struct Lamports(u64); struct Sol(f64); impl From for Lamports { fn from(sol: Sol) -> Self { Lamports((sol.0 * LAMPORTS_PER_SOL as f64) as u64) } } impl From for Sol { fn from(lamports: Lamports) -> Self { Sol(lamports.0 as f64 / LAMPORTS_PER_SOL as f64) } } ``` ## TryFrom for Fallible Conversions ```rust impl TryFrom for Lamports { type Error = &'static str; fn try_from(value: i64) -> Result { if value < 0 { Err("Lamports cannot be negative") } else { Ok(Lamports(value as u64)) } } } ``` ## The Task Implement `From` conversions between `Sol` and `Lamports`: * 1 SOL = 1,000,000,000 lamports * Converting Sol to Lamports multiplies by 1B * Converting Lamports to Sol divides by 1B ## Requirements * `Sol(1.5).into()` → `Lamports(1_500_000_000)` * `Lamports(1_500_000_000).into()` → `Sol(1.5)` ## Hints ```rust impl From for Lamports { fn from(sol: Sol) -> Self { Lamports((sol.0 * 1_000_000_000.0) as u64) } } impl From for Sol { fn from(lamports: Lamports) -> Self { Sol(lamports.0 as f64 / 1_000_000_000.0) } } ``` # Day 28: Error Enums URL: https://www.learnsol.site/learn/challenges/rust/28 Markdown URL: https://www.learnsol.site/learn/challenges/rust/28.mdx Create Solana-style error enums in Day 28: numeric error codes, error messages, and the patterns used in production Solana programs. # Day 28: Error Enums Solana programs use error enums with numeric codes for efficient error handling. ## Solana Error Pattern ```rust #[derive(Debug, Clone, Copy)] #[repr(u32)] // Each variant has a u32 code pub enum ProgramError { NotInitialized = 0, AlreadyInitialized = 1, InvalidAuthority = 2, InsufficientFunds = 3, } ``` > [!WARNING] > In Solana, error codes are returned as u32 values in transaction logs. Having explicit codes makes debugging easier! ## Why Numeric Codes? 1. **Efficient transmission**: Only a u32 crosses the runtime boundary 2. **Clear logging**: Error codes appear in transaction logs 3. **Client parsing**: SDKs can decode error codes 4. **Versioning**: New errors get new codes ## Implementing Error Methods ```rust impl ProgramError { fn code(&self) -> u32 { *self as u32 } fn from_code(code: u32) -> Option { match code { 0 => Some(Self::NotInitialized), 1 => Some(Self::AlreadyInitialized), 2 => Some(Self::InvalidAuthority), 3 => Some(Self::InsufficientFunds), _ => None, } } } ``` ## Anchor's Error Pattern ```rust #[error_code] pub enum MyError { #[msg("Account not initialized")] NotInitialized, // Code: 6000 #[msg("Invalid authority")] InvalidAuthority, // Code: 6001 } ``` ## Error Conversion with From ```rust impl From for u32 { fn from(e: ProgramError) -> u32 { e as u32 } } ``` ## The Task Implement for `ProgramError`: 1. `code()` - returns the numeric error code 2. `from_code(code)` - converts code back to error (if valid) ## Requirements * `ProgramError::InsufficientFunds.code()` → `3` * `ProgramError::from_code(3)` → `Some(InsufficientFunds)` * `ProgramError::from_code(99)` → `None` ## Hints ```rust fn code(&self) -> u32 { *self as u32 } fn from_code(code: u32) -> Option { match code { 0 => Some(Self::NotInitialized), 1 => Some(Self::AlreadyInitialized), 2 => Some(Self::InvalidAuthority), 3 => Some(Self::InsufficientFunds), _ => None, } } ``` # Day 29: Account Validation URL: https://www.learnsol.site/learn/challenges/rust/29 Markdown URL: https://www.learnsol.site/learn/challenges/rust/29.mdx Implement account validation in Day 29: discriminators, type checking, and the security patterns that protect Solana programs. # Day 29: Account Validation Solana accounts need validation before use. Let's implement the patterns you'll use every day! ## The Discriminator Pattern A discriminator is a unique byte (or bytes) that identifies account type: ```rust const TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 1; const METADATA_ACCOUNT_DISCRIMINATOR: u8 = 2; #[repr(C)] struct TokenAccount { discriminator: u8, // What type of account is this? is_initialized: bool, // Has it been set up? owner: [u8; 32], // Who owns it? balance: u64, // The data } ``` > [!NOTE] > Anchor uses 8-byte discriminators derived from the account name hash. Native programs often use 1-byte discriminators. ## Why Validate? 1. **Type Safety**: Ensure account is the correct type 2. **Initialization**: Prevent using uninitialized accounts 3. **Authority**: Verify caller has permission 4. **State**: Check account is in valid state ## Validation Pattern ```rust impl TokenAccount { fn validate(&self) -> Result<(), &'static str> { if self.discriminator != TOKEN_ACCOUNT_DISCRIMINATOR { return Err("Invalid discriminator"); } if !self.is_initialized { return Err("Account not initialized"); } Ok(()) } fn is_owner(&self, pubkey: &[u8; 32]) -> bool { self.owner == *pubkey } } ``` ## Account Creation ```rust impl TokenAccount { fn new(owner: [u8; 32], balance: u64) -> Self { Self { discriminator: TOKEN_ACCOUNT_DISCRIMINATOR, is_initialized: true, owner, balance, } } } ``` ## Common Validation Checks | Check | Purpose | | --------------- | ----------------------------- | | Discriminator | Correct account type | | is\_initialized | Account has been set up | | Owner match | Caller has authority | | Signer | Transaction was signed by key | ## The Task Implement for `TokenAccount`: 1. `new(owner, balance)` - creates initialized account with discriminator 2. `validate()` - checks discriminator and is\_initialized 3. `is_owner(pubkey)` - checks if pubkey matches owner ## Requirements * New accounts have `discriminator = 1` and `is_initialized = true` * `validate()` returns `Ok(())` for valid accounts * `is_owner()` compares byte arrays ## Hints ```rust fn new(owner: [u8; 32], balance: u64) -> Self { Self { discriminator: ACCOUNT_DISCRIMINATOR, is_initialized: true, owner, balance, } } fn validate(&self) -> Result<(), &'static str> { if self.discriminator != ACCOUNT_DISCRIMINATOR { return Err("Invalid discriminator"); } if !self.is_initialized { return Err("Not initialized"); } Ok(()) } fn is_owner(&self, pubkey: &[u8; 32]) -> bool { self.owner == *pubkey } ``` # Day 3: Functions URL: https://www.learnsol.site/learn/challenges/rust/3 Markdown URL: https://www.learnsol.site/learn/challenges/rust/3.mdx Learn Rust functions in Day 3: parameters, return types, expressions vs statements, and function composition. Essential skills for writing Solana programs. # Day 3: Functions Functions are the building blocks of Rust programs. Let's master them! ## What You'll Learn * Function syntax and parameters * Return types with `->` * Expressions vs statements * Implicit returns ## Function Syntax ```rust fn function_name(param1: Type1, param2: Type2) -> ReturnType { // function body } ``` ## Expressions vs Statements > [!WARNING] > This is crucial! In Rust: > > * **Expressions** return a value (no semicolon) > * **Statements** perform an action (with semicolon) ```rust fn add(a: i32, b: i32) -> i32 { a + b // expression - no semicolon, this is returned } fn add_with_return(a: i32, b: i32) -> i32 { return a + b; // explicit return (less idiomatic) } ``` ## The Task Convert Celsius to Fahrenheit using the formula: $F = C \times \frac{9}{5} + 32$ ## Requirements * Take a `f64` (floating point) parameter * Return the Fahrenheit value as `f64` * `100.0°C` should return `212.0°F` ## Hints * Be careful with integer vs float division * `9.0 / 5.0` for float division * Don't add a semicolon to your result expression! # Day 30: Capstone - State Machine URL: https://www.learnsol.site/learn/challenges/rust/30 Markdown URL: https://www.learnsol.site/learn/challenges/rust/30.mdx Build a complete escrow state machine in Day 30: your capstone project combining all Rust concepts for a production-ready Solana pattern. # Day 30: Capstone - Solana-Style State Machine 🎉 **Congratulations on making it to Day 30!** Today we combine everything you've learned into a complete Solana-style state machine. ## What You've Learned * ✅ Ownership, borrowing, lifetimes * ✅ Structs, enums, traits * ✅ Error handling with Result * ✅ Generics and trait bounds * ✅ Byte serialization * ✅ Account validation patterns * ✅ Derive macros and attributes ## The Escrow Pattern Escrows are fundamental in DeFi. Here's the state machine: ``` Uninitialized → Initialized → Deposited → Completed ↓ ↑ └──────→ Cancelled ←───────┘ ``` > [!NOTE] > This pattern is used in token swaps, NFT marketplaces, governance proposals, and more! ## State Transitions | Current State | Action | Next State | Who Can Do It | | --------------------- | -------- | ---------- | -------------- | | Initialized | deposit | Deposited | Maker only | | Deposited | complete | Completed | Anyone (taker) | | Initialized/Deposited | cancel | Cancelled | Maker only | ## The Implementation ```rust #[derive(Debug, Clone, Copy, PartialEq)] #[repr(u8)] enum EscrowState { Uninitialized = 0, Initialized = 1, Deposited = 2, Completed = 3, Cancelled = 4, } #[derive(Debug)] enum EscrowError { InvalidState, InvalidAmount, Unauthorized, } #[repr(C)] struct Escrow { state: EscrowState, maker: [u8; 32], taker: [u8; 32], amount: u64, } ``` ## Implementing State Transitions Each transition should: 1. Check current state 2. Verify authorization 3. Update state and data ```rust fn deposit(&mut self, caller: &[u8; 32], amount: u64) -> Result<(), EscrowError> { // 1. Check state if self.state != EscrowState::Initialized { return Err(EscrowError::InvalidState); } // 2. Check authorization if *caller != self.maker { return Err(EscrowError::Unauthorized); } // 3. Validate input if amount == 0 { return Err(EscrowError::InvalidAmount); } // 4. Update self.amount = amount; self.state = EscrowState::Deposited; Ok(()) } ``` ## The Task Implement the escrow state machine: 1. `deposit(caller, amount)` - maker deposits funds 2. `complete(taker)` - someone completes the escrow 3. `cancel(caller)` - maker cancels (if not completed) ## Requirements * Each method validates state and authorization * Returns appropriate `EscrowError` on failure * Test flow: new → deposit → complete ## Hints ```rust fn deposit(&mut self, caller: &[u8; 32], amount: u64) -> Result<(), EscrowError> { if self.state != EscrowState::Initialized { return Err(EscrowError::InvalidState); } if *caller != self.maker { return Err(EscrowError::Unauthorized); } self.amount = amount; self.state = EscrowState::Deposited; Ok(()) } fn complete(&mut self, taker: [u8; 32]) -> Result<(), EscrowError> { if self.state != EscrowState::Deposited { return Err(EscrowError::InvalidState); } self.taker = taker; self.state = EscrowState::Completed; Ok(()) } fn cancel(&mut self, caller: &[u8; 32]) -> Result<(), EscrowError> { if self.state == EscrowState::Completed { return Err(EscrowError::InvalidState); } if *caller != self.maker { return Err(EscrowError::Unauthorized); } self.state = EscrowState::Cancelled; Ok(()) } ``` *** ## 🎓 What's Next? You're now ready for **Solana development**! Your next steps: 1. **Anchor Framework** - The standard framework for Solana programs 2. **SPL Tokens** - Token program and associated accounts 3. **PDAs** - Program Derived Addresses 4. **CPIs** - Cross-Program Invocations **Congratulations on completing 30 Days of Rust!** 🦀🚀 # Day 4: Control Flow URL: https://www.learnsol.site/learn/challenges/rust/4 Markdown URL: https://www.learnsol.site/learn/challenges/rust/4.mdx Master Rust control flow in Day 4: if/else expressions, match patterns, and loops (loop, while, for). Critical for Solana program logic implementation. # Day 4: Control Flow Control flow determines the order of execution. Rust has powerful pattern matching! ## What You'll Learn * `if`/`else` expressions * The `match` expression * Loops: `loop`, `while`, `for` ## If/Else as Expressions In Rust, `if` is an expression that returns a value: ```rust let number = if condition { 5 } else { 10 }; ``` ## Match Expressions `match` is Rust's powerful pattern matching: ```rust match value { 1 => println!("one"), 2 | 3 => println!("two or three"), 4..=10 => println!("four to ten"), _ => println!("something else"), // catch-all } ``` > [!NOTE] > `match` must be exhaustive - you must handle all possible cases! ## The Task: FizzBuzz Implement the classic FizzBuzz for a single number: * Return `"FizzBuzz"` if divisible by both 3 and 5 * Return `"Fizz"` if divisible by 3 only * Return `"Buzz"` if divisible by 5 only * Return the number as a string otherwise ## Requirements * Check divisibility with the modulo operator `%` * Return a `String` * Test case: `fizzbuzz(15)` → `"FizzBuzz"` ## Hints * Use `n % 3 == 0` to check divisibility * Check both 3 AND 5 first! * Use `n.to_string()` to convert number to String * Use `String::from("...")` for string literals # Day 5: Ownership Basics URL: https://www.learnsol.site/learn/challenges/rust/5 Markdown URL: https://www.learnsol.site/learn/challenges/rust/5.mdx Understand Rust's ownership system in Day 5: the core concept that makes Rust memory-safe without garbage collection. Fundamental for Solana account management. # Day 5: Ownership Basics Ownership is Rust's most unique feature and enables memory safety without garbage collection. > [!WARNING] > This concept is **crucial** for Solana development! Account data ownership and borrowing follow similar principles. ## The Three Rules of Ownership 1. Each value has an **owner** 2. There can only be **one owner** at a time 3. When the owner goes out of scope, the value is **dropped** ## Move Semantics When you assign a `String` to another variable, ownership **moves**: ```rust let s1 = String::from("hello"); let s2 = s1; // s1 is now invalid! // println!("{}", s1); // ERROR: s1 was moved ``` ## Clone: Deep Copy Use `.clone()` to create a deep copy: ```rust let s1 = String::from("hello"); let s2 = s1.clone(); // Both s1 and s2 are valid ``` ## The Task The provided code has an ownership error. The `take_ownership` function takes ownership of the String, so the second call fails. Fix it so both calls work and the output is `18` (9 + 9). ## Requirements * Don't change the `take_ownership` function signature * Make both function calls work * Output should be `18` ## Hints * You need to clone the string before the first call * Or restructure to create two separate strings * `message.clone()` creates a copy # Day 6: References & Borrowing URL: https://www.learnsol.site/learn/challenges/rust/6 Markdown URL: https://www.learnsol.site/learn/challenges/rust/6.mdx Learn Rust borrowing and references in Day 6: immutable and mutable borrows, reference rules, and avoiding ownership transfer. Key for Solana account references. # Day 6: References & Borrowing References allow you to refer to a value without taking ownership - this is called **borrowing**. ## Two Types of References | Type | Syntax | Can Modify? | How Many? | | --------- | -------- | ----------- | --------- | | Immutable | `&T` | No | Unlimited | | Mutable | `&mut T` | Yes | Only one | ## Immutable References ```rust fn calculate_length(s: &String) -> usize { s.len() } // s goes out of scope, but doesn't drop the String let s = String::from("hello"); let len = calculate_length(&s); // s is still valid! ``` ## Mutable References ```rust fn add_world(s: &mut String) { s.push_str(" world"); } let mut s = String::from("hello"); add_world(&mut s); // s is now "hello world" ``` > [!WARNING] > You can have either ONE mutable reference OR any number of immutable references - never both at once! ## The Task Implement two functions: 1. `add_suffix` - takes `&mut String` and appends `"_modified"` 2. `get_length` - takes `&String` and returns the length ## Requirements * `add_suffix` should modify the string in place * `get_length` should not take ownership * Final output: `rust_modified 13` ## Hints * Use `s.push_str("_modified")` to append * Use `s.len()` to get length * Remember: `&mut` for mutable, `&` for immutable # Day 7: Slices URL: https://www.learnsol.site/learn/challenges/rust/7 Markdown URL: https://www.learnsol.site/learn/challenges/rust/7.mdx Master Rust slices in Day 7: string slices, array slices, and working with contiguous sequences. Essential for handling Solana account data efficiently. # Day 7: Slices Slices are references to a contiguous sequence of elements - they don't own the data. ## String Slices: `&str` ```rust let s = String::from("hello world"); let hello = &s[0..5]; // "hello" let world = &s[6..11]; // "world" // Shorthand: let hello = &s[..5]; // from start let world = &s[6..]; // to end let full = &s[..]; // entire string ``` > [!NOTE] > String literals are already slices: `let s: &str = "hello";` ## Array Slices: `&[T]` ```rust let arr = [1, 2, 3, 4, 5]; let slice: &[i32] = &arr[1..4]; // [2, 3, 4] ``` ## Why Slices Matter for Solana Account data in Solana is accessed as byte slices (`&[u8]`). Understanding slices is essential! ## The Task Implement `first_word` that returns the first word in a string (up to the first space). ## Requirements * Return a `&str` slice of the input * If no space found, return the entire string * `"hello world"` → `"hello"` ## Hints ```rust // Iterate with index for (i, c) in s.chars().enumerate() { if c == ' ' { return &s[..i]; } } ``` * Use `.chars()` to iterate over characters * Use `.enumerate()` to get indices * Use `&s[..index]` to slice from start * Use `&s[..]` or just `s` to return the whole string # Day 8: Structs URL: https://www.learnsol.site/learn/challenges/rust/8 Markdown URL: https://www.learnsol.site/learn/challenges/rust/8.mdx Create custom data types with Rust structs in Day 8: define fields, implement methods, and model complex data. Foundation for Solana account structures. # Day 8: Structs Structs let you create custom types by grouping related data together. ## Defining a Struct ```rust struct User { username: String, email: String, active: bool, sign_in_count: u64, } ``` ## Creating Instances ```rust let user = User { username: String::from("alice"), email: String::from("alice@example.com"), active: true, sign_in_count: 1, }; ``` ## Field Init Shorthand When variable names match field names: ```rust fn create_user(username: String, email: String) -> User { User { username, // same as username: username email, // same as email: email active: true, sign_in_count: 1, } } ``` > [!NOTE] > In Solana, program accounts are modeled as structs with careful memory layouts! ## The Task 1. Define a `User` struct with `name: String` and `age: u32` 2. Implement `create_user` to create a User instance 3. Implement `greet` to return a greeting string ## Requirements * Struct must have fields `name` and `age` * `greet` returns: `"Hi, I'm {name} and I'm {age} years old"` * Test creates `User("Alice", 30)` ## Hints * Use `String::from(name)` or `name.to_string()` to convert `&str` to `String` * Use `format!()` macro to build the greeting string: ```rust format!("Hi, I'm {} and I'm {} years old", user.name, user.age) ``` # Day 9: Enums & Option URL: https://www.learnsol.site/learn/challenges/rust/9 Markdown URL: https://www.learnsol.site/learn/challenges/rust/9.mdx Master Rust enums and Option type in Day 9: define variants, pattern match, and handle null-safety elegantly. Critical for Solana error handling patterns. # Day 9: Enums & Option Enums define a type by enumerating its possible values. Combined with pattern matching, they're incredibly powerful! ## Defining Enums ```rust enum Direction { North, South, East, West, } enum Message { Quit, // no data Move { x: i32, y: i32 }, // named fields Write(String), // single value Color(u8, u8, u8), // multiple values } ``` ## The Option Enum Rust has no `null`! Instead, it uses `Option`: ```rust enum Option { Some(T), None, } ``` > [!WARNING] > Unlike null in other languages, you MUST handle both cases with Option. The compiler enforces this! ## Pattern Matching with match ```rust fn handle_option(opt: Option) { match opt { Some(value) => println!("Got: {}", value), None => println!("Nothing"), } } ``` ## The Task Implement `safe_divide(a, b)` that: * Returns `None` if `b` is 0 * Returns `Some(a / b)` otherwise ## Requirements * Return type is `Option` * Handle division by zero gracefully * `safe_divide(10, 2)` → `Some(5)` * `safe_divide(10, 0)` → `None` ## Hints ```rust fn safe_divide(a: i32, b: i32) -> Option { if b == 0 { None } else { Some(a / b) } } ```