AMM Simulator

Build an interactive constant product AMM (Automated Market Maker) simulator to understand how decentralized exchanges like Raydium and Orca work.

Project Overview

Text
┌─────────────────────────────────────────────────────────────────┐
│                     AMM Simulator Interface                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    Liquidity Pool                       │   │
│  │                                                         │   │
│  │     SOL: 1000.00          USDC: 20000.00               │   │
│  │     ════════════          ══════════════                │   │
│  │                                                         │   │
│  │     Constant K = 20,000,000                            │   │
│  │     Price: 1 SOL = 20.00 USDC                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  ┌───────────────────────┐  ┌───────────────────────────┐      │
│  │        SWAP           │  │     ADD LIQUIDITY         │      │
│  │                       │  │                           │      │
│  │  [SOL][USDC]  │  │  SOL: [____]              │      │
│  │                       │  │  USDC: [____]             │      │
│  │  Amount: [100___]     │  │                           │      │
│  │                       │  │  LP Tokens: ~XXX          │      │
│  │  You receive: 1818.18 │  │                           │      │
│  │  Price impact: 9.09%  │  │  [ Add Liquidity ]        │      │
│  │  Fee: 0.30%           │  │                           │      │
│  │                       │  └───────────────────────────┘      │
│  │  [ Execute Swap ]     │                                     │
│  └───────────────────────┘                                     │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Price Curve Visualization                              │   │
│  │  ●                                                      │   │
│  │   ╲                                                     │   │
│  │    ●                                                    │   │
│  │     ╲                                                   │   │
│  │      ●●●●●●●●●●●●●●●●●●●●●                             │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Project Setup

Bash
bunx create-next-app@latest amm-simulator --typescript --tailwind --app
cd amm-simulator
bun add recharts lucide-react
bun dev

AMM Core Logic

TypeScript
// lib/amm.ts

export interface Pool {
  reserveA: number; // Token A reserve
  reserveB: number; // Token B reserve
  totalLpTokens: number; // Total LP tokens issued
  feePercent: number; // Swap fee (e.g., 0.3%)
}

export interface SwapResult {
  amountOut: number;
  priceImpact: number;
  fee: number;
  newReserveA: number;
  newReserveB: number;
  effectivePrice: number;
  spotPriceBefore: number;
  spotPriceAfter: number;
}

export interface LiquidityResult {
  lpTokens: number;
  actualAmountA: number;
  actualAmountB: number;
  shareOfPool: number;
}

/**
 * Constant Product AMM
 *
 * The core invariant: x * y = k
 *
 * Where:
 * - x = reserve of token A
 * - y = reserve of token B
 * - k = constant product (invariant)
 */
export class ConstantProductAMM {
  private pool: Pool;

  constructor(
    initialReserveA: number,
    initialReserveB: number,
    feePercent: number = 0.3,
  ) {
    this.pool = {
      reserveA: initialReserveA,
      reserveB: initialReserveB,
      totalLpTokens: Math.sqrt(initialReserveA * initialReserveB),
      feePercent,
    };
  }

  /**
   * Get the constant product (k)
   */
  get constantK(): number {
    return this.pool.reserveA * this.pool.reserveB;
  }

  /**
   * Get current spot price (A in terms of B)
   */
  get spotPrice(): number {
    return this.pool.reserveB / this.pool.reserveA;
  }

  /**
   * Get pool state
   */
  get state(): Pool {
    return { ...this.pool };
  }

  /**
   * Calculate swap output using constant product formula
   *
   * Formula: (x + Δx) * (y - Δy) = k
   * Solving for Δy: Δy = y - k / (x + Δx)
   *                 Δy = y * Δx / (x + Δx)
   */
  calculateSwapAToB(amountIn: number): SwapResult {
    const { reserveA, reserveB, feePercent } = this.pool;

    // Apply fee to input
    const fee = amountIn * (feePercent / 100);
    const amountInAfterFee = amountIn - fee;

    // Calculate output using constant product formula
    // Δy = y * Δx / (x + Δx)
    const amountOut =
      (reserveB * amountInAfterFee) / (reserveA + amountInAfterFee);

    // New reserves after swap
    const newReserveA = reserveA + amountIn;
    const newReserveB = reserveB - amountOut;

    // Price calculations
    const spotPriceBefore = reserveB / reserveA;
    const spotPriceAfter = newReserveB / newReserveA;
    const effectivePrice = amountOut / amountIn;

    // Price impact = (spot_before - effective) / spot_before
    const priceImpact =
      ((spotPriceBefore - effectivePrice) / spotPriceBefore) * 100;

    return {
      amountOut,
      priceImpact,
      fee,
      newReserveA,
      newReserveB,
      effectivePrice,
      spotPriceBefore,
      spotPriceAfter,
    };
  }

  /**
   * Calculate swap from B to A
   */
  calculateSwapBToA(amountIn: number): SwapResult {
    const { reserveA, reserveB, feePercent } = this.pool;

    const fee = amountIn * (feePercent / 100);
    const amountInAfterFee = amountIn - fee;

    const amountOut =
      (reserveA * amountInAfterFee) / (reserveB + amountInAfterFee);

    const newReserveA = reserveA - amountOut;
    const newReserveB = reserveB + amountIn;

    const spotPriceBefore = reserveA / reserveB;
    const spotPriceAfter = newReserveA / newReserveB;
    const effectivePrice = amountOut / amountIn;

    const priceImpact =
      ((spotPriceBefore - effectivePrice) / spotPriceBefore) * 100;

    return {
      amountOut,
      priceImpact,
      fee,
      newReserveA,
      newReserveB,
      effectivePrice,
      spotPriceBefore,
      spotPriceAfter,
    };
  }

  /**
   * Execute a swap and update pool state
   */
  executeSwap(amountIn: number, direction: "AtoB" | "BtoA"): SwapResult {
    const result =
      direction === "AtoB"
        ? this.calculateSwapAToB(amountIn)
        : this.calculateSwapBToA(amountIn);

    this.pool.reserveA = result.newReserveA;
    this.pool.reserveB = result.newReserveB;

    return result;
  }

  /**
   * Calculate LP tokens for adding liquidity
   *
   * Must add proportional amounts to maintain price
   * LP tokens = totalLP * min(amountA/reserveA, amountB/reserveB)
   */
  calculateAddLiquidity(amountA: number, amountB: number): LiquidityResult {
    const { reserveA, reserveB, totalLpTokens } = this.pool;

    // Calculate the ratio
    const ratioA = amountA / reserveA;
    const ratioB = amountB / reserveB;

    // Use the smaller ratio to maintain price
    const ratio = Math.min(ratioA, ratioB);

    // Calculate actual amounts used (proportional to reserves)
    const actualAmountA = ratio === ratioA ? amountA : reserveA * ratio;
    const actualAmountB = ratio === ratioB ? amountB : reserveB * ratio;

    // LP tokens minted
    const lpTokens = totalLpTokens * ratio;

    // Share of pool
    const shareOfPool = (lpTokens / (totalLpTokens + lpTokens)) * 100;

    return {
      lpTokens,
      actualAmountA,
      actualAmountB,
      shareOfPool,
    };
  }

  /**
   * Add liquidity to the pool
   */
  addLiquidity(amountA: number, amountB: number): LiquidityResult {
    const result = this.calculateAddLiquidity(amountA, amountB);

    this.pool.reserveA += result.actualAmountA;
    this.pool.reserveB += result.actualAmountB;
    this.pool.totalLpTokens += result.lpTokens;

    return result;
  }

  /**
   * Calculate tokens received for removing liquidity
   */
  calculateRemoveLiquidity(lpTokens: number): {
    amountA: number;
    amountB: number;
  } {
    const { reserveA, reserveB, totalLpTokens } = this.pool;

    const share = lpTokens / totalLpTokens;

    return {
      amountA: reserveA * share,
      amountB: reserveB * share,
    };
  }

  /**
   * Remove liquidity from the pool
   */
  removeLiquidity(lpTokens: number): { amountA: number; amountB: number } {
    const result = this.calculateRemoveLiquidity(lpTokens);

    this.pool.reserveA -= result.amountA;
    this.pool.reserveB -= result.amountB;
    this.pool.totalLpTokens -= lpTokens;

    return result;
  }

  /**
   * Generate price curve data for visualization
   */
  generatePriceCurve(
    points: number = 50,
  ): { reserveA: number; reserveB: number; price: number }[] {
    const k = this.constantK;
    const minA = this.pool.reserveA * 0.1;
    const maxA = this.pool.reserveA * 3;
    const step = (maxA - minA) / points;

    const data: { reserveA: number; reserveB: number; price: number }[] = [];

    for (let a = minA; a <= maxA; a += step) {
      const b = k / a;
      data.push({
        reserveA: a,
        reserveB: b,
        price: b / a,
      });
    }

    return data;
  }
}

Pool Display Component

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

interface PoolDisplayProps {
  reserveA: number;
  reserveB: number;
  tokenA: string;
  tokenB: string;
  constantK: number;
  spotPrice: number;
}

export function PoolDisplay({
  reserveA,
  reserveB,
  tokenA,
  tokenB,
  constantK,
  spotPrice,
}: PoolDisplayProps) {
  return (
    <div className="bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl p-6 text-white">
      <h2 className="text-lg font-semibold mb-4">Liquidity Pool</h2>

      <div className="grid grid-cols-2 gap-8">
        <div>
          <p className="text-blue-200 text-sm">{tokenA}</p>
          <p className="text-3xl font-bold">{reserveA.toLocaleString(undefined, { maximumFractionDigits: 2 })}</p>
        </div>
        <div>
          <p className="text-purple-200 text-sm">{tokenB}</p>
          <p className="text-3xl font-bold">{reserveB.toLocaleString(undefined, { maximumFractionDigits: 2 })}</p>
        </div>
      </div>

      <div className="mt-6 pt-4 border-t border-white/20 grid grid-cols-2 gap-4 text-sm">
        <div>
          <p className="text-blue-200">Constant K</p>
          <p className="font-mono">{constantK.toLocaleString(undefined, { maximumFractionDigits: 0 })}</p>
        </div>
        <div>
          <p className="text-purple-200">Spot Price</p>
          <p className="font-mono">1 {tokenA} = {spotPrice.toFixed(4)} {tokenB}</p>
        </div>
      </div>
    </div>
  );
}

Swap Interface

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

import { useState, useMemo } from "react";
import { ArrowDownUp } from "lucide-react";
import { ConstantProductAMM, SwapResult } from "@/lib/amm";

interface SwapInterfaceProps {
  amm: ConstantProductAMM;
  tokenA: string;
  tokenB: string;
  onSwap: (result: SwapResult, direction: "AtoB" | "BtoA") => void;
}

export function SwapInterface({ amm, tokenA, tokenB, onSwap }: SwapInterfaceProps) {
  const [amountIn, setAmountIn] = useState("");
  const [direction, setDirection] = useState<"AtoB" | "BtoA">("AtoB");

  const fromToken = direction === "AtoB" ? tokenA : tokenB;
  const toToken = direction === "AtoB" ? tokenB : tokenA;

  const swapResult = useMemo(() => {
    const amount = parseFloat(amountIn);
    if (isNaN(amount) || amount <= 0) return null;

    return direction === "AtoB"
      ? amm.calculateSwapAToB(amount)
      : amm.calculateSwapBToA(amount);
  }, [amm, amountIn, direction]);

  const handleSwap = () => {
    if (!swapResult) return;
    const amount = parseFloat(amountIn);
    const result = amm.executeSwap(amount, direction);
    onSwap(result, direction);
    setAmountIn("");
  };

  const toggleDirection = () => {
    setDirection(d => d === "AtoB" ? "BtoA" : "AtoB");
    setAmountIn("");
  };

  return (
    <div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-lg">
      <h3 className="text-lg font-semibold mb-4">Swap</h3>

      {/* From Token */}
      <div className="bg-gray-50 dark:bg-gray-800 rounded-lg 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: {direction === "AtoB"
              ? amm.state.reserveA.toFixed(2)
              : amm.state.reserveB.toFixed(2)} (pool)
          </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"
          />
          <span className="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded-lg font-medium">
            {fromToken}
          </span>
        </div>
      </div>

      {/* Swap Direction Toggle */}
      <div className="flex justify-center -my-2 relative z-10">
        <button
          onClick={toggleDirection}
          className="p-2 bg-gray-100 dark:bg-gray-800 rounded-full border-4 border-white dark:border-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
        >
          <ArrowDownUp className="w-5 h-5" />
        </button>
      </div>

      {/* To Token */}
      <div className="bg-gray-50 dark:bg-gray-800 rounded-lg 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={swapResult ? swapResult.amountOut.toFixed(4) : "0.00"}
            readOnly
            className="flex-1 bg-transparent text-2xl font-medium outline-none"
          />
          <span className="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded-lg font-medium">
            {toToken}
          </span>
        </div>
      </div>

      {/* Swap Details */}
      {swapResult && (
        <div className="mt-4 space-y-2 text-sm">
          <div className="flex justify-between">
            <span className="text-gray-500">Rate</span>
            <span>1 {fromToken} = {swapResult.effectivePrice.toFixed(4)} {toToken}</span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">Price Impact</span>
            <span className={swapResult.priceImpact > 5 ? "text-red-500" : "text-green-500"}>
              {swapResult.priceImpact.toFixed(2)}%
            </span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">Fee ({amm.state.feePercent}%)</span>
            <span>{swapResult.fee.toFixed(4)} {fromToken}</span>
          </div>
        </div>
      )}

      {/* Swap Button */}
      <button
        onClick={handleSwap}
        disabled={!swapResult || swapResult.amountOut <= 0}
        className="w-full mt-4 py-3 px-4 bg-blue-600 text-white rounded-lg font-medium
                   hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed
                   transition-colors"
      >
        {!amountIn ? "Enter amount" : "Swap"}
      </button>
    </div>
  );
}

Price Curve Chart

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

import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
  ReferenceDot,
} from "recharts";
import { ConstantProductAMM } from "@/lib/amm";

interface PriceCurveProps {
  amm: ConstantProductAMM;
  tokenA: string;
  tokenB: string;
}

export function PriceCurve({ amm, tokenA, tokenB }: PriceCurveProps) {
  const curveData = amm.generatePriceCurve(100);
  const currentState = amm.state;

  return (
    <div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-lg">
      <h3 className="text-lg font-semibold mb-4">Price Curve (x * y = k)</h3>

      <div className="h-64">
        <ResponsiveContainer width="100%" height="100%">
          <LineChart data={curveData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
            <CartesianGrid strokeDasharray="3 3" opacity={0.3} />
            <XAxis
              dataKey="reserveA"
              label={{ value: tokenA, position: "bottom" }}
              tickFormatter={(v) => v.toFixed(0)}
            />
            <YAxis
              dataKey="reserveB"
              label={{ value: tokenB, angle: -90, position: "left" }}
              tickFormatter={(v) => v.toFixed(0)}
            />
            <Tooltip
              formatter={(value: number) => value.toFixed(2)}
              labelFormatter={(label) => `${tokenA}: ${Number(label).toFixed(2)}`}
            />
            <Line
              type="monotone"
              dataKey="reserveB"
              stroke="#3b82f6"
              strokeWidth={2}
              dot={false}
            />
            {/* Current position marker */}
            <ReferenceDot
              x={currentState.reserveA}
              y={currentState.reserveB}
              r={8}
              fill="#ef4444"
              stroke="#fff"
              strokeWidth={2}
            />
          </LineChart>
        </ResponsiveContainer>
      </div>

      <div className="mt-4 text-center text-sm text-gray-500">
        <span className="inline-flex items-center gap-2">
          <span className="w-3 h-3 rounded-full bg-red-500" />
          Current Position
        </span>
      </div>
    </div>
  );
}

Liquidity Interface

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

import { useState, useMemo } from "react";
import { ConstantProductAMM, LiquidityResult } from "@/lib/amm";

interface LiquidityInterfaceProps {
  amm: ConstantProductAMM;
  tokenA: string;
  tokenB: string;
  onAddLiquidity: (result: LiquidityResult) => void;
}

export function LiquidityInterface({
  amm,
  tokenA,
  tokenB,
  onAddLiquidity,
}: LiquidityInterfaceProps) {
  const [amountA, setAmountA] = useState("");
  const [amountB, setAmountB] = useState("");

  const liquidityResult = useMemo(() => {
    const a = parseFloat(amountA) || 0;
    const b = parseFloat(amountB) || 0;
    if (a <= 0 && b <= 0) return null;

    // Auto-calculate proportional amount
    const state = amm.state;
    const ratio = state.reserveB / state.reserveA;

    if (a > 0 && !amountB) {
      return amm.calculateAddLiquidity(a, a * ratio);
    }
    if (b > 0 && !amountA) {
      return amm.calculateAddLiquidity(b / ratio, b);
    }

    return amm.calculateAddLiquidity(a, b);
  }, [amm, amountA, amountB]);

  // Auto-fill proportional amount
  const handleAmountAChange = (value: string) => {
    setAmountA(value);
    const a = parseFloat(value);
    if (!isNaN(a) && a > 0) {
      const state = amm.state;
      const ratio = state.reserveB / state.reserveA;
      setAmountB((a * ratio).toFixed(4));
    }
  };

  const handleAmountBChange = (value: string) => {
    setAmountB(value);
    const b = parseFloat(value);
    if (!isNaN(b) && b > 0) {
      const state = amm.state;
      const ratio = state.reserveA / state.reserveB;
      setAmountA((b * ratio).toFixed(4));
    }
  };

  const handleAddLiquidity = () => {
    if (!liquidityResult) return;
    const result = amm.addLiquidity(
      parseFloat(amountA) || 0,
      parseFloat(amountB) || 0
    );
    onAddLiquidity(result);
    setAmountA("");
    setAmountB("");
  };

  return (
    <div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-lg">
      <h3 className="text-lg font-semibold mb-4">Add Liquidity</h3>

      <div className="space-y-4">
        <div>
          <label className="text-sm text-gray-500 mb-1 block">{tokenA}</label>
          <input
            type="number"
            value={amountA}
            onChange={(e) => handleAmountAChange(e.target.value)}
            placeholder="0.00"
            className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 rounded-lg
                       text-lg font-medium outline-none focus:ring-2 focus:ring-blue-500"
          />
        </div>

        <div className="text-center text-gray-400">+</div>

        <div>
          <label className="text-sm text-gray-500 mb-1 block">{tokenB}</label>
          <input
            type="number"
            value={amountB}
            onChange={(e) => handleAmountBChange(e.target.value)}
            placeholder="0.00"
            className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 rounded-lg
                       text-lg font-medium outline-none focus:ring-2 focus:ring-blue-500"
          />
        </div>
      </div>

      {liquidityResult && (
        <div className="mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg space-y-2 text-sm">
          <div className="flex justify-between">
            <span className="text-gray-500">LP Tokens</span>
            <span className="font-medium">{liquidityResult.lpTokens.toFixed(4)}</span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">Share of Pool</span>
            <span className="font-medium">{liquidityResult.shareOfPool.toFixed(2)}%</span>
          </div>
        </div>
      )}

      <button
        onClick={handleAddLiquidity}
        disabled={!liquidityResult}
        className="w-full mt-4 py-3 px-4 bg-green-600 text-white rounded-lg font-medium
                   hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed
                   transition-colors"
      >
        Add Liquidity
      </button>
    </div>
  );
}

Main Page

TypeScript
// app/page.tsx
"use client";

import { useState, useMemo, useCallback } from "react";
import { ConstantProductAMM, SwapResult, LiquidityResult } from "@/lib/amm";
import { PoolDisplay } from "@/components/PoolDisplay";
import { SwapInterface } from "@/components/SwapInterface";
import { PriceCurve } from "@/components/PriceCurve";
import { LiquidityInterface } from "@/components/LiquidityInterface";

const TOKEN_A = "SOL";
const TOKEN_B = "USDC";
const INITIAL_RESERVE_A = 1000;  // 1000 SOL
const INITIAL_RESERVE_B = 20000; // 20000 USDC (price = $20/SOL)

export default function AMMSimulator() {
  const [amm, setAmm] = useState(() =>
    new ConstantProductAMM(INITIAL_RESERVE_A, INITIAL_RESERVE_B, 0.3)
  );
  const [history, setHistory] = useState<string[]>([]);

  const poolState = amm.state;

  const handleSwap = useCallback((result: SwapResult, direction: "AtoB" | "BtoA") => {
    const from = direction === "AtoB" ? TOKEN_A : TOKEN_B;
    const to = direction === "AtoB" ? TOKEN_B : TOKEN_A;

    setHistory(prev => [
      `Swapped ${result.amountOut.toFixed(4)} ${from} → ${to} (impact: ${result.priceImpact.toFixed(2)}%)`,
      ...prev.slice(0, 9),
    ]);

    // Force re-render
    setAmm(new ConstantProductAMM(
      amm.state.reserveA,
      amm.state.reserveB,
      amm.state.feePercent
    ));
  }, [amm]);

  const handleAddLiquidity = useCallback((result: LiquidityResult) => {
    setHistory(prev => [
      `Added liquidity: ${result.actualAmountA.toFixed(2)} ${TOKEN_A} + ${result.actualAmountB.toFixed(2)} ${TOKEN_B} → ${result.lpTokens.toFixed(4)} LP`,
      ...prev.slice(0, 9),
    ]);

    setAmm(new ConstantProductAMM(
      amm.state.reserveA,
      amm.state.reserveB,
      amm.state.feePercent
    ));
  }, [amm]);

  const handleReset = () => {
    setAmm(new ConstantProductAMM(INITIAL_RESERVE_A, INITIAL_RESERVE_B, 0.3));
    setHistory([]);
  };

  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-950">
      <header className="border-b dark:border-gray-800 bg-white dark:bg-gray-900">
        <div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
          <h1 className="text-xl font-bold">📊 AMM Simulator</h1>
          <button
            onClick={handleReset}
            className="px-4 py-2 text-sm bg-gray-100 dark:bg-gray-800 rounded-lg
                       hover:bg-gray-200 dark:hover:bg-gray-700"
          >
            Reset Pool
          </button>
        </div>
      </header>

      <main className="max-w-6xl mx-auto px-4 py-8">
        {/* Pool Display */}
        <PoolDisplay
          reserveA={poolState.reserveA}
          reserveB={poolState.reserveB}
          tokenA={TOKEN_A}
          tokenB={TOKEN_B}
          constantK={amm.constantK}
          spotPrice={amm.spotPrice}
        />

        {/* Main Grid */}
        <div className="grid lg:grid-cols-2 gap-6 mt-8">
          <SwapInterface
            amm={amm}
            tokenA={TOKEN_A}
            tokenB={TOKEN_B}
            onSwap={handleSwap}
          />
          <LiquidityInterface
            amm={amm}
            tokenA={TOKEN_A}
            tokenB={TOKEN_B}
            onAddLiquidity={handleAddLiquidity}
          />
        </div>

        {/* Price Curve */}
        <div className="mt-8">
          <PriceCurve amm={amm} tokenA={TOKEN_A} tokenB={TOKEN_B} />
        </div>

        {/* History */}
        {history.length > 0 && (
          <div className="mt-8 bg-white dark:bg-gray-900 rounded-xl p-6 shadow-lg">
            <h3 className="text-lg font-semibold mb-4">Transaction History</h3>
            <div className="space-y-2 text-sm">
              {history.map((item, i) => (
                <div key={i} className="py-2 border-b dark:border-gray-800 last:border-0">
                  {item}
                </div>
              ))}
            </div>
          </div>
        )}

        {/* Educational Notes */}
        <div className="mt-8 bg-blue-50 dark:bg-blue-900/20 rounded-xl p-6">
          <h3 className="text-lg font-semibold mb-4">💡 How AMMs Work</h3>
          <div className="grid md:grid-cols-2 gap-6 text-sm">
            <div>
              <h4 className="font-medium mb-2">Constant Product Formula</h4>
              <p className="text-gray-600 dark:text-gray-400">
                x × y = k — The product of reserves stays constant.
                When you buy token A, you add token B to the pool,
                maintaining the invariant.
              </p>
            </div>
            <div>
              <h4 className="font-medium mb-2">Price Impact</h4>
              <p className="text-gray-600 dark:text-gray-400">
                Large trades move the price significantly because
                they change the ratio of reserves. This is called
                "slippage" or price impact.
              </p>
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

What You'll Learn

ConceptImplementation
Constant Product Formulax * y = k invariant
Swap CalculationOutput = y * Δx / (x + Δx)
Price ImpactDifference between spot and effective price
LP TokensProportional share of pool
VisualizationInteractive price curve chart

Mini Projects Complete! Next: Fullstack dApp - Build a complete decentralized application.