DeFi App

Build a decentralized exchange (DEX) with token swaps and liquidity pools. This project brings together smart contract development, DeFi concepts, and frontend integration.

Project Overview

Text
┌─────────────────────────────────────────────────────────────────┐
│                        DeFi Exchange                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │ [Swap] [Pool] [Portfolio]               [Connect Wallet]  │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  SWAP TAB:                                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │                                                           │ │
│  │  From: ┌────────────────────────────────────────┐        │ │
│  │        │ 100.00                          [SOL]│        │ │
│  │        └────────────────────────────────────────┘        │ │
│  │                        ↓↑                                 │ │
│  │  To:   ┌────────────────────────────────────────┐        │ │
│  │        │ 1,985.00                       [USDC]│        │ │
│  │        └────────────────────────────────────────┘        │ │
│  │                                                           │ │
│  │  Rate: 1 SOL = 19.85 USDC                                │ │
│  │  Price Impact: 0.15%                                      │ │
│  │  Minimum Received: 1,975.08 USDC                         │ │
│  │                                                           │ │
│  │              [ Swap Tokens ]                              │ │
│  │                                                           │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  POOL TAB:                                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  Your Positions                                           │ │
│  │  ┌─────────────────────────────────────────────────────┐ │ │
│  │  │ SOL/USDC          $5,420.00        [Manage]         │ │ │
│  │  │ 12.5 LP Tokens    Share: 0.05%                      │ │ │
│  │  └─────────────────────────────────────────────────────┘ │ │
│  │                                                           │ │
│  │  [ Add Liquidity ]  [ Create Pool ]                      │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Architecture

Text
┌─────────────────────────────────────────────────────────────────┐
│                      DeFi App Architecture                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Frontend (Next.js)                                             │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ • Swap InterfacePool Management                │   │
│  │ • Token SelectionTransaction History            │   │
│  │ • Price ChartsPortfolio View                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                           │                                     │
│                           ▼                                     │
│  On-Chain Programs                                              │
│  ┌──────────────────┐  ┌──────────────────┐                    │
│  │   AMM Program    │  │  Token Vaults    │                    │
│  │                  │  │                  │                    │
│  │ • create_pool    │  │ • deposit        │                    │
│  │ • swap           │  │ • withdraw       │                    │
│  │ • add_liquidity  │  │ • LP tokens      │                    │
│  │ • remove_liquid. │  │                  │                    │
│  └──────────────────┘  └──────────────────┘                    │
│                           │                                     │
│                           ▼                                     │
│  Token Accounts (SPL)                                           │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ Pool Reserve APool Reserve BLP MintUser Tokens  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Project Setup

Bash
# Create Anchor project
anchor init dex
cd dex

# Get program ID and update
anchor build
solana address -k target/deploy/dex-keypair.json
# Update declare_id! in lib.rs
anchor build

Anchor Program

Rust
// programs/dex/src/lib.rs
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};

declare_id!("YOUR_PROGRAM_ID");

#[program]
pub mod dex {
    use super::*;

    /// Initialize a new liquidity pool
    pub fn initialize_pool(
        ctx: Context<InitializePool>,
        fee_numerator: u64,
        fee_denominator: u64,
    ) -> Result<()> {
        require!(
            fee_numerator <= fee_denominator,
            DexError::InvalidFee
        );

        let pool = &mut ctx.accounts.pool;
        pool.token_a_mint = ctx.accounts.token_a_mint.key();
        pool.token_b_mint = ctx.accounts.token_b_mint.key();
        pool.token_a_vault = ctx.accounts.token_a_vault.key();
        pool.token_b_vault = ctx.accounts.token_b_vault.key();
        pool.lp_mint = ctx.accounts.lp_mint.key();
        pool.fee_numerator = fee_numerator;
        pool.fee_denominator = fee_denominator;
        pool.authority = ctx.accounts.authority.key();
        pool.bump = ctx.bumps.pool;

        msg!("Pool initialized: {} / {}",
            ctx.accounts.token_a_mint.key(),
            ctx.accounts.token_b_mint.key()
        );
        Ok(())
    }

    /// Add liquidity to the pool
    pub fn add_liquidity(
        ctx: Context<AddLiquidity>,
        amount_a: u64,
        amount_b: u64,
        min_lp_tokens: u64,
    ) -> Result<()> {
        let pool = &ctx.accounts.pool;

        let reserve_a = ctx.accounts.token_a_vault.amount;
        let reserve_b = ctx.accounts.token_b_vault.amount;
        let lp_supply = ctx.accounts.lp_mint.supply;

        // Calculate LP tokens to mint
        let lp_tokens = if lp_supply == 0 {
            // Initial liquidity: sqrt(amount_a * amount_b)
            (amount_a as u128)
                .checked_mul(amount_b as u128)
                .ok_or(DexError::MathOverflow)?
                .integer_sqrt() as u64
        } else {
            // Proportional to existing reserves
            let lp_a = (amount_a as u128)
                .checked_mul(lp_supply as u128)
                .ok_or(DexError::MathOverflow)?
                .checked_div(reserve_a as u128)
                .ok_or(DexError::MathOverflow)? as u64;

            let lp_b = (amount_b as u128)
                .checked_mul(lp_supply as u128)
                .ok_or(DexError::MathOverflow)?
                .checked_div(reserve_b as u128)
                .ok_or(DexError::MathOverflow)? as u64;

            std::cmp::min(lp_a, lp_b)
        };

        require!(lp_tokens >= min_lp_tokens, DexError::SlippageExceeded);

        // Transfer tokens to vault
        token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.user_token_a.to_account_info(),
                    to: ctx.accounts.token_a_vault.to_account_info(),
                    authority: ctx.accounts.user.to_account_info(),
                },
            ),
            amount_a,
        )?;

        token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.user_token_b.to_account_info(),
                    to: ctx.accounts.token_b_vault.to_account_info(),
                    authority: ctx.accounts.user.to_account_info(),
                },
            ),
            amount_b,
        )?;

        // Mint LP tokens
        let seeds = &[
            b"pool",
            pool.token_a_mint.as_ref(),
            pool.token_b_mint.as_ref(),
            &[pool.bump],
        ];
        let signer = &[&seeds[..]];

        token::mint_to(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                token::MintTo {
                    mint: ctx.accounts.lp_mint.to_account_info(),
                    to: ctx.accounts.user_lp.to_account_info(),
                    authority: ctx.accounts.pool.to_account_info(),
                },
                signer,
            ),
            lp_tokens,
        )?;

        msg!("Added liquidity: {} A, {} B -> {} LP", amount_a, amount_b, lp_tokens);
        Ok(())
    }

    /// Remove liquidity from the pool
    pub fn remove_liquidity(
        ctx: Context<RemoveLiquidity>,
        lp_amount: u64,
        min_amount_a: u64,
        min_amount_b: u64,
    ) -> Result<()> {
        let pool = &ctx.accounts.pool;

        let reserve_a = ctx.accounts.token_a_vault.amount;
        let reserve_b = ctx.accounts.token_b_vault.amount;
        let lp_supply = ctx.accounts.lp_mint.supply;

        // Calculate tokens to return
        let amount_a = (lp_amount as u128)
            .checked_mul(reserve_a as u128)
            .ok_or(DexError::MathOverflow)?
            .checked_div(lp_supply as u128)
            .ok_or(DexError::MathOverflow)? as u64;

        let amount_b = (lp_amount as u128)
            .checked_mul(reserve_b as u128)
            .ok_or(DexError::MathOverflow)?
            .checked_div(lp_supply as u128)
            .ok_or(DexError::MathOverflow)? as u64;

        require!(amount_a >= min_amount_a, DexError::SlippageExceeded);
        require!(amount_b >= min_amount_b, DexError::SlippageExceeded);

        // Burn LP tokens
        token::burn(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                token::Burn {
                    mint: ctx.accounts.lp_mint.to_account_info(),
                    from: ctx.accounts.user_lp.to_account_info(),
                    authority: ctx.accounts.user.to_account_info(),
                },
            ),
            lp_amount,
        )?;

        // Transfer tokens from vault
        let seeds = &[
            b"pool",
            pool.token_a_mint.as_ref(),
            pool.token_b_mint.as_ref(),
            &[pool.bump],
        ];
        let signer = &[&seeds[..]];

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.token_a_vault.to_account_info(),
                    to: ctx.accounts.user_token_a.to_account_info(),
                    authority: ctx.accounts.pool.to_account_info(),
                },
                signer,
            ),
            amount_a,
        )?;

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.token_b_vault.to_account_info(),
                    to: ctx.accounts.user_token_b.to_account_info(),
                    authority: ctx.accounts.pool.to_account_info(),
                },
                signer,
            ),
            amount_b,
        )?;

        msg!("Removed liquidity: {} LP -> {} A, {} B", lp_amount, amount_a, amount_b);
        Ok(())
    }

    /// Swap token A for token B
    pub fn swap(
        ctx: Context<Swap>,
        amount_in: u64,
        minimum_amount_out: u64,
    ) -> Result<()> {
        let pool = &ctx.accounts.pool;

        // Determine swap direction
        let (
            reserve_in,
            reserve_out,
            vault_in,
            vault_out,
            user_in,
            user_out,
        ) = if ctx.accounts.user_token_in.mint == pool.token_a_mint {
            (
                ctx.accounts.token_a_vault.amount,
                ctx.accounts.token_b_vault.amount,
                ctx.accounts.token_a_vault.to_account_info(),
                ctx.accounts.token_b_vault.to_account_info(),
                ctx.accounts.user_token_in.to_account_info(),
                ctx.accounts.user_token_out.to_account_info(),
            )
        } else {
            (
                ctx.accounts.token_b_vault.amount,
                ctx.accounts.token_a_vault.amount,
                ctx.accounts.token_b_vault.to_account_info(),
                ctx.accounts.token_a_vault.to_account_info(),
                ctx.accounts.user_token_in.to_account_info(),
                ctx.accounts.user_token_out.to_account_info(),
            )
        };

        // Calculate output with fee
        // amount_out = reserve_out * amount_in * (1 - fee) / (reserve_in + amount_in * (1 - fee))
        let fee_amount = (amount_in as u128)
            .checked_mul(pool.fee_numerator as u128)
            .ok_or(DexError::MathOverflow)?
            .checked_div(pool.fee_denominator as u128)
            .ok_or(DexError::MathOverflow)? as u64;

        let amount_in_after_fee = amount_in.checked_sub(fee_amount)
            .ok_or(DexError::MathOverflow)?;

        let amount_out = (reserve_out as u128)
            .checked_mul(amount_in_after_fee as u128)
            .ok_or(DexError::MathOverflow)?
            .checked_div(
                (reserve_in as u128)
                    .checked_add(amount_in_after_fee as u128)
                    .ok_or(DexError::MathOverflow)?
            )
            .ok_or(DexError::MathOverflow)? as u64;

        require!(amount_out >= minimum_amount_out, DexError::SlippageExceeded);

        // Transfer tokens
        token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: user_in,
                    to: vault_in,
                    authority: ctx.accounts.user.to_account_info(),
                },
            ),
            amount_in,
        )?;

        let seeds = &[
            b"pool",
            pool.token_a_mint.as_ref(),
            pool.token_b_mint.as_ref(),
            &[pool.bump],
        ];
        let signer = &[&seeds[..]];

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: vault_out,
                    to: user_out,
                    authority: ctx.accounts.pool.to_account_info(),
                },
                signer,
            ),
            amount_out,
        )?;

        msg!("Swapped {} -> {} (fee: {})", amount_in, amount_out, fee_amount);
        Ok(())
    }
}

// ========== Accounts ==========

#[derive(Accounts)]
pub struct InitializePool<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        init,
        payer = authority,
        space = Pool::SIZE,
        seeds = [b"pool", token_a_mint.key().as_ref(), token_b_mint.key().as_ref()],
        bump
    )]
    pub pool: Account<'info, Pool>,

    pub token_a_mint: Account<'info, Mint>,
    pub token_b_mint: Account<'info, Mint>,

    #[account(
        init,
        payer = authority,
        token::mint = token_a_mint,
        token::authority = pool
    )]
    pub token_a_vault: Account<'info, TokenAccount>,

    #[account(
        init,
        payer = authority,
        token::mint = token_b_mint,
        token::authority = pool
    )]
    pub token_b_vault: Account<'info, TokenAccount>,

    #[account(
        init,
        payer = authority,
        mint::decimals = 9,
        mint::authority = pool
    )]
    pub lp_mint: Account<'info, Mint>,

    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct AddLiquidity<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    #[account(
        mut,
        seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
        bump = pool.bump
    )]
    pub pool: Account<'info, Pool>,

    #[account(mut, constraint = token_a_vault.key() == pool.token_a_vault)]
    pub token_a_vault: Account<'info, TokenAccount>,

    #[account(mut, constraint = token_b_vault.key() == pool.token_b_vault)]
    pub token_b_vault: Account<'info, TokenAccount>,

    #[account(mut, constraint = lp_mint.key() == pool.lp_mint)]
    pub lp_mint: Account<'info, Mint>,

    #[account(mut, constraint = user_token_a.mint == pool.token_a_mint)]
    pub user_token_a: Account<'info, TokenAccount>,

    #[account(mut, constraint = user_token_b.mint == pool.token_b_mint)]
    pub user_token_b: Account<'info, TokenAccount>,

    #[account(mut, constraint = user_lp.mint == pool.lp_mint)]
    pub user_lp: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
}

#[derive(Accounts)]
pub struct RemoveLiquidity<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    #[account(
        mut,
        seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
        bump = pool.bump
    )]
    pub pool: Account<'info, Pool>,

    #[account(mut)]
    pub token_a_vault: Account<'info, TokenAccount>,

    #[account(mut)]
    pub token_b_vault: Account<'info, TokenAccount>,

    #[account(mut)]
    pub lp_mint: Account<'info, Mint>,

    #[account(mut)]
    pub user_token_a: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_token_b: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_lp: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
}

#[derive(Accounts)]
pub struct Swap<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    #[account(
        seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
        bump = pool.bump
    )]
    pub pool: Account<'info, Pool>,

    #[account(mut)]
    pub token_a_vault: Account<'info, TokenAccount>,

    #[account(mut)]
    pub token_b_vault: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_token_in: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_token_out: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
}

// ========== State ==========

#[account]
pub struct Pool {
    pub token_a_mint: Pubkey,
    pub token_b_mint: Pubkey,
    pub token_a_vault: Pubkey,
    pub token_b_vault: Pubkey,
    pub lp_mint: Pubkey,
    pub fee_numerator: u64,
    pub fee_denominator: u64,
    pub authority: Pubkey,
    pub bump: u8,
}

impl Pool {
    pub const SIZE: usize = 8 + // discriminator
        32 * 5 +  // pubkeys
        8 * 2 +   // fee
        32 +      // authority
        1;        // bump
}

// ========== Errors ==========

#[error_code]
pub enum DexError {
    #[msg("Invalid fee configuration")]
    InvalidFee,
    #[msg("Math overflow")]
    MathOverflow,
    #[msg("Slippage tolerance exceeded")]
    SlippageExceeded,
}

// ========== Helpers ==========

trait IntegerSqrt {
    fn integer_sqrt(self) -> Self;
}

impl IntegerSqrt for u128 {
    fn integer_sqrt(self) -> Self {
        if self == 0 {
            return 0;
        }
        let mut x = self;
        let mut y = (x + 1) / 2;
        while y < x {
            x = y;
            y = (x + self / x) / 2;
        }
        x
    }
}

Frontend Swap Component

TypeScript
// components/SwapInterface.tsx
"use client";

import { useState, useMemo, useEffect } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import { ArrowDownUp, Settings } from "lucide-react";
import { useDex } from "@/hooks/useDex";

const TOKENS = [
  { symbol: "SOL", mint: "So11111111111111111111111111111111111111112", decimals: 9 },
  { symbol: "USDC", mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
  // Add more tokens
];

export function SwapInterface() {
  const { connected } = useWallet();
  const { pools, swap, isSwapping, getQuote } = useDex();

  const [fromToken, setFromToken] = useState(TOKENS[0]);
  const [toToken, setToToken] = useState(TOKENS[1]);
  const [amountIn, setAmountIn] = useState("");
  const [slippage, setSlippage] = useState(0.5);

  const quote = useMemo(() => {
    const amount = parseFloat(amountIn);
    if (isNaN(amount) || amount <= 0) return null;
    return getQuote(fromToken.mint, toToken.mint, amount);
  }, [fromToken, toToken, amountIn, getQuote]);

  const handleSwap = async () => {
    if (!quote) return;

    const minOut = quote.amountOut * (1 - slippage / 100);

    await swap({
      tokenIn: fromToken.mint,
      tokenOut: toToken.mint,
      amountIn: parseFloat(amountIn),
      minimumAmountOut: minOut,
    });

    setAmountIn("");
  };

  const toggleTokens = () => {
    setFromToken(toToken);
    setToToken(fromToken);
    setAmountIn("");
  };

  return (
    <div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg max-w-md mx-auto">
      <div className="flex justify-between items-center mb-6">
        <h2 className="text-xl font-bold">Swap</h2>
        <button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
          <Settings className="w-5 h-5" />
        </button>
      </div>

      {/* From Token */}
      <div className="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 mb-2">
        <div className="flex justify-between mb-2">
          <span className="text-sm text-gray-500">From</span>
          <span className="text-sm text-gray-500">
            Balance: 0.00
          </span>
        </div>
        <div className="flex items-center gap-4">
          <input
            type="number"
            value={amountIn}
            onChange={(e) => setAmountIn(e.target.value)}
            placeholder="0.00"
            className="flex-1 bg-transparent text-2xl font-medium outline-none"
          />
          <select
            value={fromToken.symbol}
            onChange={(e) => {
              const token = TOKENS.find((t) => t.symbol === e.target.value);
              if (token) setFromToken(token);
            }}
            className="px-3 py-2 bg-white dark:bg-gray-700 rounded-lg font-medium border dark:border-gray-600"
          >
            {TOKENS.map((t) => (
              <option key={t.mint} value={t.symbol}>
                {t.symbol}
              </option>
            ))}
          </select>
        </div>
      </div>

      {/* Swap Button */}
      <div className="flex justify-center -my-4 z-10 relative">
        <button
          onClick={toggleTokens}
          className="p-3 bg-gray-100 dark:bg-gray-800 rounded-xl border-4 border-white dark:border-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"
        >
          <ArrowDownUp className="w-5 h-5" />
        </button>
      </div>

      {/* To Token */}
      <div className="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 mt-2">
        <div className="flex justify-between mb-2">
          <span className="text-sm text-gray-500">To (estimated)</span>
        </div>
        <div className="flex items-center gap-4">
          <input
            type="text"
            value={quote ? quote.amountOut.toFixed(6) : "0.00"}
            readOnly
            className="flex-1 bg-transparent text-2xl font-medium outline-none"
          />
          <select
            value={toToken.symbol}
            onChange={(e) => {
              const token = TOKENS.find((t) => t.symbol === e.target.value);
              if (token) setToToken(token);
            }}
            className="px-3 py-2 bg-white dark:bg-gray-700 rounded-lg font-medium border dark:border-gray-600"
          >
            {TOKENS.map((t) => (
              <option key={t.mint} value={t.symbol}>
                {t.symbol}
              </option>
            ))}
          </select>
        </div>
      </div>

      {/* Swap Details */}
      {quote && (
        <div className="mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl space-y-2 text-sm">
          <div className="flex justify-between">
            <span className="text-gray-500">Rate</span>
            <span>
              1 {fromToken.symbol} = {quote.rate.toFixed(4)} {toToken.symbol}
            </span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">Price Impact</span>
            <span className={quote.priceImpact > 3 ? "text-red-500" : "text-green-500"}>
              {quote.priceImpact.toFixed(2)}%
            </span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">Min. Received</span>
            <span>
              {(quote.amountOut * (1 - slippage / 100)).toFixed(4)} {toToken.symbol}
            </span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">Fee</span>
            <span>{quote.fee.toFixed(4)} {fromToken.symbol}</span>
          </div>
        </div>
      )}

      {/* Slippage Selector */}
      <div className="mt-4 flex items-center gap-2">
        <span className="text-sm text-gray-500">Slippage:</span>
        {[0.1, 0.5, 1.0].map((s) => (
          <button
            key={s}
            onClick={() => setSlippage(s)}
            className={`px-3 py-1 rounded-lg text-sm ${
              slippage === s
                ? "bg-blue-600 text-white"
                : "bg-gray-100 dark:bg-gray-800"
            }`}
          >
            {s}%
          </button>
        ))}
      </div>

      {/* Swap Button */}
      <button
        onClick={handleSwap}
        disabled={!connected || !quote || isSwapping}
        className="w-full mt-6 py-4 bg-blue-600 text-white rounded-xl font-medium text-lg
                   hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed
                   transition-colors"
      >
        {!connected
          ? "Connect Wallet"
          : !amountIn
          ? "Enter Amount"
          : isSwapping
          ? "Swapping..."
          : "Swap"}
      </button>
    </div>
  );
}

Liquidity Component

TypeScript
// components/LiquidityPanel.tsx
"use client";

import { useState } from "react";
import { useDex } from "@/hooks/useDex";
import { useWallet } from "@solana/wallet-adapter-react";

export function LiquidityPanel() {
  const { connected } = useWallet();
  const { pools, positions, addLiquidity, removeLiquidity, isAdding, isRemoving } = useDex();

  const [selectedPool, setSelectedPool] = useState<string | null>(null);
  const [amountA, setAmountA] = useState("");
  const [amountB, setAmountB] = useState("");
  const [lpAmount, setLpAmount] = useState("");
  const [mode, setMode] = useState<"add" | "remove">("add");

  const handleAddLiquidity = async () => {
    if (!selectedPool) return;

    await addLiquidity({
      pool: selectedPool,
      amountA: parseFloat(amountA),
      amountB: parseFloat(amountB),
      minLpTokens: 0, // Calculate based on slippage
    });

    setAmountA("");
    setAmountB("");
  };

  const handleRemoveLiquidity = async () => {
    if (!selectedPool) return;

    await removeLiquidity({
      pool: selectedPool,
      lpAmount: parseFloat(lpAmount),
      minAmountA: 0,
      minAmountB: 0,
    });

    setLpAmount("");
  };

  return (
    <div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg">
      <h2 className="text-xl font-bold mb-6">Liquidity</h2>

      {/* Mode Toggle */}
      <div className="flex gap-2 mb-6">
        <button
          onClick={() => setMode("add")}
          className={`flex-1 py-2 rounded-lg font-medium ${
            mode === "add"
              ? "bg-green-600 text-white"
              : "bg-gray-100 dark:bg-gray-800"
          }`}
        >
          Add
        </button>
        <button
          onClick={() => setMode("remove")}
          className={`flex-1 py-2 rounded-lg font-medium ${
            mode === "remove"
              ? "bg-red-600 text-white"
              : "bg-gray-100 dark:bg-gray-800"
          }`}
        >
          Remove
        </button>
      </div>

      {/* Pool Selection */}
      <select
        value={selectedPool || ""}
        onChange={(e) => setSelectedPool(e.target.value || null)}
        className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg mb-4"
      >
        <option value="">Select Pool</option>
        {pools.map((pool) => (
          <option key={pool.address} value={pool.address}>
            {pool.tokenA.symbol} / {pool.tokenB.symbol}
          </option>
        ))}
      </select>

      {mode === "add" ? (
        /* Add Liquidity Form */
        <div className="space-y-4">
          <div>
            <label className="text-sm text-gray-500 block mb-1">Token A</label>
            <input
              type="number"
              value={amountA}
              onChange={(e) => setAmountA(e.target.value)}
              placeholder="0.00"
              className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
            />
          </div>
          <div>
            <label className="text-sm text-gray-500 block mb-1">Token B</label>
            <input
              type="number"
              value={amountB}
              onChange={(e) => setAmountB(e.target.value)}
              placeholder="0.00"
              className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
            />
          </div>
          <button
            onClick={handleAddLiquidity}
            disabled={!connected || !selectedPool || isAdding}
            className="w-full py-3 bg-green-600 text-white rounded-lg font-medium
                       hover:bg-green-700 disabled:bg-gray-400"
          >
            {isAdding ? "Adding..." : "Add Liquidity"}
          </button>
        </div>
      ) : (
        /* Remove Liquidity Form */
        <div className="space-y-4">
          <div>
            <label className="text-sm text-gray-500 block mb-1">LP Tokens</label>
            <input
              type="number"
              value={lpAmount}
              onChange={(e) => setLpAmount(e.target.value)}
              placeholder="0.00"
              className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
            />
          </div>
          <button
            onClick={handleRemoveLiquidity}
            disabled={!connected || !selectedPool || isRemoving}
            className="w-full py-3 bg-red-600 text-white rounded-lg font-medium
                       hover:bg-red-700 disabled:bg-gray-400"
          >
            {isRemoving ? "Removing..." : "Remove Liquidity"}
          </button>
        </div>
      )}

      {/* User Positions */}
      {positions.length > 0 && (
        <div className="mt-8">
          <h3 className="font-semibold mb-4">Your Positions</h3>
          <div className="space-y-2">
            {positions.map((pos) => (
              <div
                key={pos.pool}
                className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"
              >
                <div className="flex justify-between">
                  <span className="font-medium">{pos.name}</span>
                  <span>${pos.valueUsd.toFixed(2)}</span>
                </div>
                <div className="text-sm text-gray-500">
                  {pos.lpTokens.toFixed(4)} LP{pos.share.toFixed(2)}% share
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Key Learning Outcomes

SkillDescription
AMM MathConstant product formula, fees, slippage
Token VaultsSecure token storage with PDAs
LP TokensMint/burn for liquidity tracking
CPICross-program invocations for SPL Token
FrontendReal-time quotes, transaction building

Security Considerations

Text
┌─────────────────────────────────────────────────────────────────┐
│                   Security Checklist                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ☐ Verify all account constraints                              │
│  ☐ Check for integer overflow/underflow                        │
│  ☐ Validate slippage limits                                    │
│  ☐ Prevent price manipulation attacks                          │
│  ☐ Secure PDA derivation                                       │
│  ☐ Rate limiting for swaps                                     │
│  ☐ Emergency pause mechanism                                   │
│  ☐ Admin key management                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Next: DAO Governance - Build a decentralized governance system.