/ NETWORK_TOGGLE_PLAN.md
NETWORK_TOGGLE_PLAN.md
   1  # Network Toggle Implementation Plan (Mainnet ↔ Testnet)
   2  
   3  ## 🎯 Objective
   4  
   5  **Current Problem**: 
   6  - ComposableScan is **mainnet-only** (https://query.main.net.espresso.network)
   7  - Caff Node is **testnet-only** (https://rari.caff.testnet.espresso.network)
   8  - **Cannot test Caff Node integration** without testnet support
   9  
  10  **Goal**: Add dynamic network switching with instant toggle (NO page reload)
  11  
  12  ---
  13  
  14  ## πŸ“Š Current Architecture Analysis
  15  
  16  ### Current State (Mainnet Only)
  17  
  18  ```typescript
  19  // src/lib/config.ts - CURRENT (Hardcoded)
  20  const MAINNET_CONFIG: NetworkConfig = {
  21    name: 'Mainnet',
  22    apiBaseUrl: 'https://query.main.net.espresso.network',
  23    apiVersion: 'v0',
  24    wsBaseUrl: 'wss://query.main.net.espresso.network',
  25    scanBaseUrl: 'https://explorer.main.net.espresso.network',
  26    webWorkerUrl: 'https://explorer.main.net.espresso.network/assets/...'
  27  }
  28  
  29  export const getCurrentNetworkConfig = (): NetworkConfig => {
  30    return MAINNET_CONFIG; // ❌ Always mainnet
  31  }
  32  ```
  33  
  34  **Problems**:
  35  1. ❌ Config is static, decided at build time
  36  2. ❌ No testnet configuration exists
  37  3. ❌ No React state for network switching
  38  4. ❌ WebSocket connections not network-aware
  39  5. ❌ All API calls hardcoded to mainnet
  40  
  41  ---
  42  
  43  ## πŸ—οΈ Proposed Architecture
  44  
  45  ### New Multi-Network Architecture
  46  
  47  ```
  48  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  49  β”‚                     User Interface                           β”‚
  50  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
  51  β”‚  β”‚  Header with Toggle Switch                             β”‚ β”‚
  52  β”‚  β”‚  [ Mainnet ] ←→ [ Testnet ]                            β”‚ β”‚
  53  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
  54  β”‚                          ↓                                   β”‚
  55  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
  56  β”‚  β”‚  NetworkContext (React Context)                        β”‚ β”‚
  57  β”‚  β”‚  β€’ currentNetwork: 'mainnet' | 'testnet'               β”‚ β”‚
  58  β”‚  β”‚  β€’ switchNetwork(network)                              β”‚ β”‚
  59  β”‚  β”‚  β€’ getConfig() β†’ NetworkConfig                         β”‚ β”‚
  60  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
  61  β”‚                          ↓                                   β”‚
  62  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
  63  β”‚  β”‚  Network-Aware Services                                β”‚ β”‚
  64  β”‚  β”‚  β€’ API Client (uses context)                           β”‚ β”‚
  65  β”‚  β”‚  β€’ WebSocket (reconnects on switch)                    β”‚ β”‚
  66  β”‚  β”‚  β€’ Caff Node (testnet only)                            β”‚ β”‚
  67  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
  68  β”‚                          ↓                                   β”‚
  69  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
  70  β”‚  β”‚   Mainnet Endpoints β”‚  Testnet Endpoints               β”‚ β”‚
  71  β”‚  β”‚  query.main.net... β”‚  query.testnet.espresso.network  β”‚ β”‚
  72  β”‚  β”‚  wss://query.main..β”‚  wss://query.testnet...          β”‚ β”‚
  73  β”‚  β”‚  rari.caff.main... β”‚  rari.caff.testnet... βœ…         β”‚ β”‚
  74  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
  75  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  76  ```
  77  
  78  ---
  79  
  80  ## πŸ”§ Implementation Plan
  81  
  82  ### Phase 1: Multi-Network Configuration (Day 1-2)
  83  
  84  #### Step 1.1: Update `config.ts` with Both Networks
  85  
  86  ```typescript
  87  // src/lib/config.ts - NEW VERSION
  88  
  89  export type NetworkType = 'mainnet' | 'testnet';
  90  
  91  interface NetworkConfig {
  92    name: string;
  93    apiBaseUrl: string;
  94    apiVersion: string;
  95    wsBaseUrl: string;
  96    scanBaseUrl: string;
  97    webWorkerUrl: string;
  98    caffNodeUrl: string;  // NEW: Caff Node URL
  99  }
 100  
 101  // Validate environment variables
 102  if (typeof window === 'undefined') {
 103    const required = [
 104      // Mainnet
 105      'NEXT_PUBLIC_MAINNET_API_BASE_URL',
 106      'NEXT_PUBLIC_MAINNET_API_VERSION', 
 107      'NEXT_PUBLIC_MAINNET_WS_BASE_URL',
 108      'NEXT_PUBLIC_MAINNET_SCAN_BASE_URL',
 109      'NEXT_PUBLIC_MAINNET_WEB_WORKER_URL',
 110      'NEXT_PUBLIC_MAINNET_CAFF_NODE_URL',
 111      // Testnet
 112      'NEXT_PUBLIC_TESTNET_API_BASE_URL',
 113      'NEXT_PUBLIC_TESTNET_API_VERSION',
 114      'NEXT_PUBLIC_TESTNET_WS_BASE_URL',
 115      'NEXT_PUBLIC_TESTNET_SCAN_BASE_URL',
 116      'NEXT_PUBLIC_TESTNET_WEB_WORKER_URL',
 117      'NEXT_PUBLIC_TESTNET_CAFF_NODE_URL'
 118    ];
 119    
 120    const missing = required.filter(key => !process.env[key]);
 121    if (missing.length > 0) {
 122      console.warn('Missing environment variables:', missing);
 123    }
 124  }
 125  
 126  // Configuration object
 127  const config = {
 128    // Mainnet
 129    MAINNET_API_BASE_URL: process.env.NEXT_PUBLIC_MAINNET_API_BASE_URL!,
 130    MAINNET_API_VERSION: process.env.NEXT_PUBLIC_MAINNET_API_VERSION!,
 131    MAINNET_WS_BASE_URL: process.env.NEXT_PUBLIC_MAINNET_WS_BASE_URL!,
 132    MAINNET_SCAN_BASE_URL: process.env.NEXT_PUBLIC_MAINNET_SCAN_BASE_URL!,
 133    MAINNET_WEB_WORKER_URL: process.env.NEXT_PUBLIC_MAINNET_WEB_WORKER_URL!,
 134    MAINNET_CAFF_NODE_URL: process.env.NEXT_PUBLIC_MAINNET_CAFF_NODE_URL || 'https://rari.caff.mainnet.espresso.network',
 135    
 136    // Testnet
 137    TESTNET_API_BASE_URL: process.env.NEXT_PUBLIC_TESTNET_API_BASE_URL!,
 138    TESTNET_API_VERSION: process.env.NEXT_PUBLIC_TESTNET_API_VERSION!,
 139    TESTNET_WS_BASE_URL: process.env.NEXT_PUBLIC_TESTNET_WS_BASE_URL!,
 140    TESTNET_SCAN_BASE_URL: process.env.NEXT_PUBLIC_TESTNET_SCAN_BASE_URL!,
 141    TESTNET_WEB_WORKER_URL: process.env.NEXT_PUBLIC_TESTNET_WEB_WORKER_URL!,
 142    TESTNET_CAFF_NODE_URL: process.env.NEXT_PUBLIC_TESTNET_CAFF_NODE_URL || 'https://rari.caff.testnet.espresso.network',
 143    
 144    // Default network
 145    DEFAULT_NETWORK: (process.env.NEXT_PUBLIC_DEFAULT_NETWORK as NetworkType) || 'mainnet',
 146    
 147    // Other settings
 148    NETWORK_STATS_REFRESH_MS: parseInt(process.env.NEXT_PUBLIC_NETWORK_STATS_REFRESH_MS || '30000')
 149  };
 150  
 151  // Network configurations
 152  const MAINNET_CONFIG: NetworkConfig = {
 153    name: 'Mainnet',
 154    apiBaseUrl: config.MAINNET_API_BASE_URL,
 155    apiVersion: config.MAINNET_API_VERSION,
 156    wsBaseUrl: config.MAINNET_WS_BASE_URL,
 157    scanBaseUrl: config.MAINNET_SCAN_BASE_URL,
 158    webWorkerUrl: config.MAINNET_WEB_WORKER_URL,
 159    caffNodeUrl: config.MAINNET_CAFF_NODE_URL
 160  };
 161  
 162  const TESTNET_CONFIG: NetworkConfig = {
 163    name: 'Testnet',
 164    apiBaseUrl: config.TESTNET_API_BASE_URL,
 165    apiVersion: config.TESTNET_API_VERSION,
 166    wsBaseUrl: config.TESTNET_WS_BASE_URL,
 167    scanBaseUrl: config.TESTNET_SCAN_BASE_URL,
 168    webWorkerUrl: config.TESTNET_WEB_WORKER_URL,
 169    caffNodeUrl: config.TESTNET_CAFF_NODE_URL
 170  };
 171  
 172  // Network registry
 173  const NETWORK_CONFIGS: Record<NetworkType, NetworkConfig> = {
 174    mainnet: MAINNET_CONFIG,
 175    testnet: TESTNET_CONFIG
 176  };
 177  
 178  /**
 179   * Get network configuration by network type
 180   */
 181  export const getNetworkConfig = (network: NetworkType): NetworkConfig => {
 182    return NETWORK_CONFIGS[network];
 183  };
 184  
 185  /**
 186   * Get default network from environment
 187   */
 188  export const getDefaultNetwork = (): NetworkType => {
 189    return config.DEFAULT_NETWORK;
 190  };
 191  
 192  /**
 193   * Helper to build API URL for specific network
 194   */
 195  export const getApiUrl = (network: NetworkType, endpoint: string): string => {
 196    const networkConfig = getNetworkConfig(network);
 197    return `${networkConfig.apiBaseUrl}/${networkConfig.apiVersion}${endpoint}`;
 198  };
 199  
 200  /**
 201   * Helper to build WebSocket URL for specific network
 202   */
 203  export const getWebSocketUrl = (network: NetworkType, endpoint: string): string => {
 204    const networkConfig = getNetworkConfig(network);
 205    return `${networkConfig.wsBaseUrl}/${networkConfig.apiVersion}${endpoint}`;
 206  };
 207  
 208  /**
 209   * Helper to get Caff Node URL for specific network
 210   */
 211  export const getCaffNodeUrl = (network: NetworkType): string => {
 212    const networkConfig = getNetworkConfig(network);
 213    return networkConfig.caffNodeUrl;
 214  };
 215  
 216  export { config, type NetworkConfig, type NetworkType };
 217  
 218  // DEPRECATED: Keep for backward compatibility, will be removed
 219  export const getCurrentNetworkConfig = (): NetworkConfig => {
 220    console.warn('getCurrentNetworkConfig is deprecated, use NetworkContext instead');
 221    return MAINNET_CONFIG;
 222  };
 223  ```
 224  
 225  #### Step 1.2: Update `env.example`
 226  
 227  ```bash
 228  # Espresso Network Mainnet Configuration
 229  NEXT_PUBLIC_MAINNET_API_BASE_URL=https://query.main.net.espresso.network
 230  NEXT_PUBLIC_MAINNET_API_VERSION=v0
 231  NEXT_PUBLIC_MAINNET_WS_BASE_URL=wss://query.main.net.espresso.network
 232  NEXT_PUBLIC_MAINNET_SCAN_BASE_URL=https://explorer.main.net.espresso.network
 233  NEXT_PUBLIC_MAINNET_WEB_WORKER_URL=https://explorer.main.net.espresso.network/assets/node_validator_web_worker_api.js-bT9djMJi.js
 234  NEXT_PUBLIC_MAINNET_CAFF_NODE_URL=https://rari.caff.mainnet.espresso.network
 235  
 236  # Espresso Network Testnet Configuration
 237  NEXT_PUBLIC_TESTNET_API_BASE_URL=https://query.testnet.espresso.network
 238  NEXT_PUBLIC_TESTNET_API_VERSION=v0
 239  NEXT_PUBLIC_TESTNET_WS_BASE_URL=wss://query.testnet.espresso.network
 240  NEXT_PUBLIC_TESTNET_SCAN_BASE_URL=https://explorer.testnet.espresso.network
 241  NEXT_PUBLIC_TESTNET_WEB_WORKER_URL=https://explorer.testnet.espresso.network/assets/node_validator_web_worker_api.js-TESTNET.js
 242  NEXT_PUBLIC_TESTNET_CAFF_NODE_URL=https://rari.caff.testnet.espresso.network
 243  
 244  # Default network (mainnet or testnet)
 245  NEXT_PUBLIC_DEFAULT_NETWORK=mainnet
 246  
 247  # Network statistics refresh interval (milliseconds)
 248  NEXT_PUBLIC_NETWORK_STATS_REFRESH_MS=30000
 249  ```
 250  
 251  ---
 252  
 253  ### Phase 2: React Context for Network State (Day 2-3)
 254  
 255  #### Step 2.1: Create Enhanced NetworkContext
 256  
 257  ```typescript
 258  // src/contexts/NetworkContext.tsx - REPLACE EXISTING
 259  
 260  import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
 261  import { NetworkType, getNetworkConfig, getDefaultNetwork, type NetworkConfig } from '@/lib/config';
 262  
 263  interface NetworkContextType {
 264    currentNetwork: NetworkType;
 265    networkConfig: NetworkConfig;
 266    switchNetwork: (network: NetworkType) => void;
 267    isMainnet: boolean;
 268    isTestnet: boolean;
 269  }
 270  
 271  const NetworkContext = createContext<NetworkContextType | undefined>(undefined);
 272  
 273  interface NetworkProviderProps {
 274    children: ReactNode;
 275  }
 276  
 277  export function NetworkProvider({ children }: NetworkProviderProps) {
 278    // Initialize from localStorage or default
 279    const [currentNetwork, setCurrentNetwork] = useState<NetworkType>(() => {
 280      if (typeof window !== 'undefined') {
 281        const saved = localStorage.getItem('espresso-network');
 282        if (saved === 'mainnet' || saved === 'testnet') {
 283          return saved as NetworkType;
 284        }
 285      }
 286      return getDefaultNetwork();
 287    });
 288  
 289    // Get current network config
 290    const networkConfig = getNetworkConfig(currentNetwork);
 291  
 292    // Helper flags
 293    const isMainnet = currentNetwork === 'mainnet';
 294    const isTestnet = currentNetwork === 'testnet';
 295  
 296    // Switch network function
 297    const switchNetwork = (network: NetworkType) => {
 298      console.log(`Switching network from ${currentNetwork} to ${network}`);
 299      setCurrentNetwork(network);
 300      
 301      // Persist to localStorage
 302      if (typeof window !== 'undefined') {
 303        localStorage.setItem('espresso-network', network);
 304      }
 305      
 306      // Emit custom event for services to react
 307      if (typeof window !== 'undefined') {
 308        window.dispatchEvent(new CustomEvent('network-changed', { 
 309          detail: { network, config: getNetworkConfig(network) } 
 310        }));
 311      }
 312    };
 313  
 314    // Log network changes
 315    useEffect(() => {
 316      console.log('Current network:', currentNetwork, networkConfig);
 317    }, [currentNetwork]);
 318  
 319    const value: NetworkContextType = {
 320      currentNetwork,
 321      networkConfig,
 322      switchNetwork,
 323      isMainnet,
 324      isTestnet
 325    };
 326  
 327    return (
 328      <NetworkContext.Provider value={value}>
 329        {children}
 330      </NetworkContext.Provider>
 331    );
 332  }
 333  
 334  /**
 335   * Hook to access network context
 336   */
 337  export function useNetwork(): NetworkContextType {
 338    const context = useContext(NetworkContext);
 339    if (!context) {
 340      throw new Error('useNetwork must be used within NetworkProvider');
 341    }
 342    return context;
 343  }
 344  
 345  // Re-export types
 346  export type { NetworkType, NetworkConfig };
 347  ```
 348  
 349  #### Step 2.2: Update Layout to Include NetworkProvider
 350  
 351  ```typescript
 352  // src/app/layout.tsx - MODIFY
 353  
 354  import { NetworkProvider } from '@/contexts/NetworkContext';
 355  
 356  export default function RootLayout({
 357    children,
 358  }: {
 359    children: React.ReactNode;
 360  }) {
 361    return (
 362      <html lang="en">
 363        <body>
 364          <NetworkProvider>
 365            {children}
 366          </NetworkProvider>
 367        </body>
 368      </html>
 369    );
 370  }
 371  ```
 372  
 373  ---
 374  
 375  ### Phase 3: Network Toggle UI Component (Day 3-4)
 376  
 377  #### Step 3.1: Create NetworkToggle Component
 378  
 379  ```typescript
 380  // src/components/ui/NetworkToggle.tsx - NEW FILE
 381  
 382  'use client';
 383  
 384  import React from 'react';
 385  import { useNetwork } from '@/contexts/NetworkContext';
 386  import { motion } from 'framer-motion';
 387  
 388  export function NetworkToggle() {
 389    const { currentNetwork, switchNetwork, isMainnet, isTestnet } = useNetwork();
 390  
 391    const handleToggle = () => {
 392      const newNetwork = isMainnet ? 'testnet' : 'mainnet';
 393      switchNetwork(newNetwork);
 394    };
 395  
 396    return (
 397      <div className="flex items-center gap-3">
 398        {/* Network Label */}
 399        <span className="text-sm font-medium text-gray-700">
 400          Network:
 401        </span>
 402  
 403        {/* Toggle Switch */}
 404        <button
 405          onClick={handleToggle}
 406          className="relative inline-flex h-8 w-32 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
 407          style={{
 408            backgroundColor: isMainnet ? '#10b981' : '#f59e0b'
 409          }}
 410          aria-label={`Switch to ${isMainnet ? 'testnet' : 'mainnet'}`}
 411        >
 412          {/* Background Labels */}
 413          <span className="absolute left-2 text-xs font-semibold text-white">
 414            {isMainnet ? 'MAIN' : ''}
 415          </span>
 416          <span className="absolute right-2 text-xs font-semibold text-white">
 417            {isTestnet ? 'TEST' : ''}
 418          </span>
 419  
 420          {/* Animated Slider */}
 421          <motion.span
 422            className="inline-block h-6 w-14 transform rounded-full bg-white shadow-lg"
 423            layout
 424            animate={{
 425              x: isMainnet ? 2 : 62
 426            }}
 427            transition={{
 428              type: "spring",
 429              stiffness: 500,
 430              damping: 30
 431            }}
 432          />
 433        </button>
 434  
 435        {/* Network Badge */}
 436        <span className={`
 437          px-2 py-1 text-xs font-semibold rounded-full
 438          ${isMainnet 
 439            ? 'bg-green-100 text-green-800' 
 440            : 'bg-orange-100 text-orange-800'
 441          }
 442        `}>
 443          {currentNetwork.toUpperCase()}
 444        </span>
 445      </div>
 446    );
 447  }
 448  
 449  /**
 450   * Compact version for mobile/small spaces
 451   */
 452  export function NetworkToggleCompact() {
 453    const { currentNetwork, switchNetwork, isMainnet } = useNetwork();
 454  
 455    return (
 456      <button
 457        onClick={() => switchNetwork(isMainnet ? 'testnet' : 'mainnet')}
 458        className={`
 459          px-3 py-1 text-xs font-bold rounded-full transition-colors
 460          ${isMainnet 
 461            ? 'bg-green-500 hover:bg-green-600 text-white' 
 462            : 'bg-orange-500 hover:bg-orange-600 text-white'
 463          }
 464        `}
 465      >
 466        {currentNetwork.toUpperCase()}
 467      </button>
 468    );
 469  }
 470  ```
 471  
 472  #### Step 3.2: Add Toggle to Main Layout
 473  
 474  ```typescript
 475  // src/app/page.tsx - MODIFY
 476  
 477  import { NetworkToggle } from '@/components/ui/NetworkToggle';
 478  import SearchInterface from "@/components/search/interface";
 479  
 480  export default function Home() {
 481    return (
 482      <div className="min-h-screen bg-white">
 483        {/* Header with Network Toggle */}
 484        <header className="border-b border-gray-200 bg-white sticky top-0 z-50">
 485          <div className="max-w-7xl mx-auto px-4 py-3 flex justify-between items-center">
 486            <h1 className="text-xl font-bold text-gray-900">
 487              ComposableScan
 488            </h1>
 489            <NetworkToggle />
 490          </div>
 491        </header>
 492  
 493        {/* Main Content */}
 494        <div className="flex items-center justify-center p-4">
 495          <div className="w-full max-w-2xl">
 496            <SearchInterface />
 497          </div>
 498        </div>
 499      </div>
 500    );
 501  }
 502  ```
 503  
 504  ---
 505  
 506  ### Phase 4: Update Services to Use Network Context (Day 4-5)
 507  
 508  #### Step 4.1: Update API Services
 509  
 510  ```typescript
 511  // src/services/api/main.ts - MODIFY
 512  
 513  import { useNetwork } from '@/contexts/NetworkContext';
 514  import { getApiUrl as buildApiUrl } from '@/lib/config';
 515  
 516  // For client components, use hook
 517  export function useApiService() {
 518    const { currentNetwork } = useNetwork();
 519    
 520    const getApiUrl = (endpoint: string) => {
 521      return buildApiUrl(currentNetwork, endpoint);
 522    };
 523    
 524    const getBlock = async (height: number) => {
 525      const url = getApiUrl(`/availability/block/${height}`);
 526      const response = await fetch(url);
 527      return response.json();
 528    };
 529    
 530    // ... other methods
 531    
 532    return {
 533      getBlock,
 534      // ... other methods
 535    };
 536  }
 537  
 538  // For server-side or non-React code
 539  export async function getBlockStatic(height: number, network: NetworkType) {
 540    const url = buildApiUrl(network, `/availability/block/${height}`);
 541    const response = await fetch(url);
 542    return response.json();
 543  }
 544  ```
 545  
 546  #### Step 4.2: Update WebSocket Stream to Reconnect on Network Change
 547  
 548  ```typescript
 549  // src/services/ws/stream.ts - MODIFY
 550  
 551  import { getWebSocketUrl } from '@/lib/config';
 552  import type { NetworkType } from '@/lib/config';
 553  
 554  export class EspressoBlockStream {
 555    private ws: WebSocket | null = null;
 556    private currentNetwork: NetworkType;
 557    // ... other properties
 558  
 559    constructor(network: NetworkType = 'mainnet') {
 560      this.currentNetwork = network;
 561      
 562      // Listen for network changes
 563      if (typeof window !== 'undefined') {
 564        window.addEventListener('network-changed', this.handleNetworkChange.bind(this));
 565      }
 566    }
 567  
 568    private handleNetworkChange(event: CustomEvent) {
 569      const { network } = event.detail;
 570      console.log('Network changed in WebSocket, reconnecting...', network);
 571      
 572      // Disconnect current connection
 573      this.disconnect();
 574      
 575      // Update network
 576      this.currentNetwork = network;
 577      
 578      // Reconnect to new network
 579      this.connect();
 580    }
 581  
 582    async connect() {
 583      // ... existing code ...
 584      
 585      // Build WebSocket URL with current network
 586      const wsUrl = getWebSocketUrl(
 587        this.currentNetwork,
 588        `/availability/stream/blocks/${latestHeight}`
 589      );
 590  
 591      this.ws = new WebSocket(wsUrl);
 592      
 593      // ... rest of existing code ...
 594    }
 595  
 596    disconnect() {
 597      if (this.ws) {
 598        // Remove listeners
 599        this.ws.onopen = null;
 600        this.ws.onmessage = null;
 601        this.ws.onerror = null;
 602        this.ws.onclose = null;
 603        
 604        // Close connection
 605        this.ws.close();
 606        this.ws = null;
 607      }
 608  
 609      this.reconnectAttempts = this.maxReconnectAttempts;
 610    }
 611    
 612    // Add cleanup on destroy
 613    destroy() {
 614      if (typeof window !== 'undefined') {
 615        window.removeEventListener('network-changed', this.handleNetworkChange.bind(this));
 616      }
 617      this.disconnect();
 618    }
 619  }
 620  ```
 621  
 622  #### Step 4.3: Create Network-Aware Hook for API Calls
 623  
 624  ```typescript
 625  // src/hooks/useNetworkApi.ts - NEW FILE
 626  
 627  import { useNetwork } from '@/contexts/NetworkContext';
 628  import { getApiUrl, getCaffNodeUrl } from '@/lib/config';
 629  import { useCallback } from 'react';
 630  
 631  export function useNetworkApi() {
 632    const { currentNetwork, networkConfig } = useNetwork();
 633  
 634    const buildApiUrl = useCallback((endpoint: string) => {
 635      return getApiUrl(currentNetwork, endpoint);
 636    }, [currentNetwork]);
 637  
 638    const apiCall = useCallback(async (endpoint: string, options?: RequestInit) => {
 639      const url = buildApiUrl(endpoint);
 640      const response = await fetch(url, options);
 641      if (!response.ok) {
 642        throw new Error(`API call failed: ${response.statusText}`);
 643      }
 644      return response.json();
 645    }, [buildApiUrl]);
 646  
 647    const caffNodeCall = useCallback(async (method: string, params: any[]) => {
 648      const url = getCaffNodeUrl(currentNetwork);
 649      const response = await fetch(url, {
 650        method: 'POST',
 651        headers: { 'Content-Type': 'application/json' },
 652        body: JSON.stringify({
 653          jsonrpc: '2.0',
 654          method,
 655          params,
 656          id: 1
 657        })
 658      });
 659      
 660      if (!response.ok) {
 661        throw new Error(`Caff Node call failed: ${response.statusText}`);
 662      }
 663      
 664      const data = await response.json();
 665      if (data.error) {
 666        throw new Error(`Caff Node error: ${data.error.message}`);
 667      }
 668      
 669      return data.result;
 670    }, [currentNetwork]);
 671  
 672    return {
 673      currentNetwork,
 674      networkConfig,
 675      buildApiUrl,
 676      apiCall,
 677      caffNodeCall
 678    };
 679  }
 680  ```
 681  
 682  ---
 683  
 684  ### Phase 5: Testing & Validation (Day 5-6)
 685  
 686  #### Step 5.1: Create Test Suite
 687  
 688  ```typescript
 689  // src/__tests__/network-toggle.test.tsx - NEW FILE
 690  
 691  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 692  import { NetworkProvider, useNetwork } from '@/contexts/NetworkContext';
 693  import { NetworkToggle } from '@/components/ui/NetworkToggle';
 694  
 695  describe('Network Toggle', () => {
 696    test('should default to mainnet', () => {
 697      const TestComponent = () => {
 698        const { currentNetwork } = useNetwork();
 699        return <div>{currentNetwork}</div>;
 700      };
 701  
 702      render(
 703        <NetworkProvider>
 704          <TestComponent />
 705        </NetworkProvider>
 706      );
 707  
 708      expect(screen.getByText('mainnet')).toBeInTheDocument();
 709    });
 710  
 711    test('should toggle between mainnet and testnet', async () => {
 712      render(
 713        <NetworkProvider>
 714          <NetworkToggle />
 715        </NetworkProvider>
 716      );
 717  
 718      const toggle = screen.getByRole('button');
 719      
 720      // Should start on mainnet
 721      expect(screen.getByText('MAINNET')).toBeInTheDocument();
 722      
 723      // Click to switch to testnet
 724      fireEvent.click(toggle);
 725      
 726      await waitFor(() => {
 727        expect(screen.getByText('TESTNET')).toBeInTheDocument();
 728      });
 729      
 730      // Click again to switch back to mainnet
 731      fireEvent.click(toggle);
 732      
 733      await waitFor(() => {
 734        expect(screen.getByText('MAINNET')).toBeInTheDocument();
 735      });
 736    });
 737  
 738    test('should persist network selection', () => {
 739      const TestComponent = () => {
 740        const { switchNetwork } = useNetwork();
 741        return <button onClick={() => switchNetwork('testnet')}>Switch</button>;
 742      };
 743  
 744      render(
 745        <NetworkProvider>
 746          <TestComponent />
 747        </NetworkProvider>
 748      );
 749  
 750      fireEvent.click(screen.getByText('Switch'));
 751      
 752      // Check localStorage
 753      expect(localStorage.getItem('espresso-network')).toBe('testnet');
 754    });
 755  
 756    test('should emit network-changed event', (done) => {
 757      const handleNetworkChange = (event: CustomEvent) => {
 758        expect(event.detail.network).toBe('testnet');
 759        done();
 760      };
 761  
 762      window.addEventListener('network-changed', handleNetworkChange as EventListener);
 763  
 764      const TestComponent = () => {
 765        const { switchNetwork } = useNetwork();
 766        return <button onClick={() => switchNetwork('testnet')}>Switch</button>;
 767      };
 768  
 769      render(
 770        <NetworkProvider>
 771          <TestComponent />
 772        </NetworkProvider>
 773      );
 774  
 775      fireEvent.click(screen.getByText('Switch'));
 776    });
 777  });
 778  ```
 779  
 780  #### Step 5.2: Manual Testing Checklist
 781  
 782  **Test Scenarios**:
 783  
 784  1. **Initial Load**:
 785     - [ ] App loads with default network (mainnet)
 786     - [ ] Toggle displays correct state
 787     - [ ] API calls go to mainnet endpoints
 788  
 789  2. **Switch to Testnet**:
 790     - [ ] Click toggle
 791     - [ ] Smooth animation
 792     - [ ] Badge updates immediately
 793     - [ ] WebSocket reconnects
 794     - [ ] API calls now go to testnet
 795     - [ ] Caff Node works (testnet only)
 796  
 797  3. **Switch Back to Mainnet**:
 798     - [ ] Click toggle again
 799     - [ ] Everything switches back
 800     - [ ] No errors in console
 801  
 802  4. **Persistence**:
 803     - [ ] Refresh page
 804     - [ ] Network selection persists
 805  
 806  5. **Search Functionality**:
 807     - [ ] Search works on mainnet
 808     - [ ] Switch to testnet
 809     - [ ] Search works on testnet
 810     - [ ] Results are network-specific
 811  
 812  6. **WebSocket**:
 813     - [ ] Connection active on mainnet
 814     - [ ] Switch to testnet
 815     - [ ] Old connection closes
 816     - [ ] New connection opens
 817     - [ ] Live updates work
 818  
 819  ---
 820  
 821  ## 🎨 UI/UX Considerations
 822  
 823  ### Visual Design
 824  
 825  **Toggle States**:
 826  ```
 827  Mainnet:  [● MAIN |  test ] (green)
 828  Testnet:  [ main | TEST ●] (orange)
 829  ```
 830  
 831  **Colors**:
 832  - Mainnet: Green (#10b981) - Production
 833  - Testnet: Orange (#f59e0b) - Development
 834  
 835  **Placement Options**:
 836  
 837  **Option A**: Header Right (Recommended)
 838  ```
 839  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 840  β”‚ ComposableScan          [ MAIN | test ] 🟒    β”‚
 841  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 842  ```
 843  
 844  **Option B**: Above Search
 845  ```
 846  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 847  β”‚                ComposableScan                  β”‚
 848  β”‚          Network: [ MAIN | test ] 🟒          β”‚
 849  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
 850  β”‚  β”‚ Search...                                β”‚ β”‚
 851  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
 852  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 853  ```
 854  
 855  **Option C**: Floating (Mobile-Friendly)
 856  ```
 857  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 858  β”‚                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
 859  β”‚ ComposableScan                     β”‚MAIN/testβ”‚ β”‚
 860  β”‚                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
 861  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 862  ```
 863  
 864  ### User Feedback
 865  
 866  **On Network Switch**:
 867  1. **Visual**: Toggle animates smoothly
 868  2. **Toast Notification**: "Switched to Testnet" (3s)
 869  3. **Loading State**: Brief spinner during reconnection
 870  4. **Status Indicator**: Badge color changes
 871  
 872  **Error Handling**:
 873  - If testnet is down: Show warning badge
 874  - If WebSocket fails: Retry with backoff
 875  - If API fails: Graceful fallback message
 876  
 877  ---
 878  
 879  ## ⚑ Performance Optimization
 880  
 881  ### Instant Switching (No Page Reload)
 882  
 883  **Key Techniques**:
 884  
 885  1. **React State**: All components re-render with new config
 886  2. **WebSocket Management**: Auto-reconnect on network change
 887  3. **localStorage**: Persist choice
 888  4. **Custom Events**: Notify non-React services
 889  
 890  **Performance Goals**:
 891  - Switch time: < 500ms
 892  - WebSocket reconnect: < 2s
 893  - No flash of wrong content
 894  - Smooth animations
 895  
 896  ### Caching Strategy
 897  
 898  ```typescript
 899  // Separate cache per network
 900  const cacheKey = `${network}:${endpoint}`;
 901  const cached = cache.get(cacheKey);
 902  ```
 903  
 904  ---
 905  
 906  ## πŸ§ͺ Testing Strategy
 907  
 908  ### Unit Tests
 909  - [ ] NetworkContext initialization
 910  - [ ] switchNetwork function
 911  - [ ] Persistence (localStorage)
 912  - [ ] Event emission
 913  
 914  ### Integration Tests
 915  - [ ] Toggle component
 916  - [ ] API service integration
 917  - [ ] WebSocket reconnection
 918  - [ ] Search with network switch
 919  
 920  ### E2E Tests
 921  - [ ] Full user flow: mainnet β†’ testnet β†’ search
 922  - [ ] Persistence across page reload
 923  - [ ] Multiple rapid switches
 924  - [ ] Network error scenarios
 925  
 926  ---
 927  
 928  ## πŸ“Š Migration Strategy
 929  
 930  ### Backward Compatibility
 931  
 932  **For Existing Code**:
 933  ```typescript
 934  // OLD (still works, but deprecated)
 935  import { getCurrentNetworkConfig } from '@/lib/config';
 936  const config = getCurrentNetworkConfig(); // Always mainnet
 937  
 938  // NEW (recommended)
 939  import { useNetwork } from '@/contexts/NetworkContext';
 940  const { networkConfig } = useNetwork();
 941  ```
 942  
 943  **Migration Path**:
 944  1. Add new config + context (no breaking changes)
 945  2. Update components gradually
 946  3. Deprecate old functions with warnings
 947  4. Remove deprecated code in v2.0
 948  
 949  ---
 950  
 951  ## πŸš€ Deployment Plan
 952  
 953  ### Phase 1: Add Configuration (No UI)
 954  - Add testnet config to env
 955  - Add NetworkContext
 956  - No visible changes
 957  - Test internally
 958  
 959  ### Phase 2: Add Toggle UI
 960  - Show toggle in header
 961  - Enable switching
 962  - Monitor for issues
 963  - Collect feedback
 964  
 965  ### Phase 3: Enable Caff Node
 966  - Add Caff Node integration
 967  - Test on testnet
 968  - Gradual rollout
 969  
 970  ---
 971  
 972  ## πŸ“ Summary
 973  
 974  ### Changes Required
 975  
 976  **New Files** (3):
 977  1. `src/components/ui/NetworkToggle.tsx` - Toggle component
 978  2. `src/hooks/useNetworkApi.ts` - Network-aware API hook
 979  3. `src/__tests__/network-toggle.test.tsx` - Tests
 980  
 981  **Modified Files** (4):
 982  1. `src/lib/config.ts` - Add testnet config
 983  2. `src/contexts/NetworkContext.tsx` - Enhance with network switching
 984  3. `src/services/ws/stream.ts` - Add network change handling
 985  4. `src/app/page.tsx` - Add toggle to UI
 986  
 987  **Environment Variables** (6 new):
 988  ```bash
 989  NEXT_PUBLIC_TESTNET_API_BASE_URL
 990  NEXT_PUBLIC_TESTNET_API_VERSION
 991  NEXT_PUBLIC_TESTNET_WS_BASE_URL
 992  NEXT_PUBLIC_TESTNET_SCAN_BASE_URL
 993  NEXT_PUBLIC_TESTNET_WEB_WORKER_URL
 994  NEXT_PUBLIC_TESTNET_CAFF_NODE_URL
 995  ```
 996  
 997  ### Benefits
 998  
 999  βœ… **Instant switching** (no page reload)
1000  βœ… **Testnet support** for Caff Node testing
1001  βœ… **Persistent selection** (localStorage)
1002  βœ… **Backward compatible** (existing code still works)
1003  βœ… **Clean architecture** (React Context pattern)
1004  βœ… **Smooth UX** (animated toggle)
1005  
1006  ### Timeline
1007  
1008  - **Day 1-2**: Configuration + Context
1009  - **Day 3-4**: UI Component
1010  - **Day 4-5**: Service Updates
1011  - **Day 5-6**: Testing
1012  
1013  **Total**: 6 days
1014  
1015  ---
1016  
1017  ## ❓ Discussion Points
1018  
1019  ### 1. Default Network
1020  **Question**: Should default be mainnet or testnet?
1021  
1022  **Options**:
1023  - A. Mainnet (production data, more stable)
1024  - B. Testnet (easier to test Caff Node)
1025  - C. Remember last selection (localStorage)
1026  
1027  **Recommendation**: Option C (Remember last, default mainnet)
1028  
1029  ### 2. Toggle Visibility
1030  **Question**: Should toggle always be visible or only for developers?
1031  
1032  **Options**:
1033  - A. Always visible (everyone can switch)
1034  - B. Only in dev mode (process.env.NODE_ENV === 'development')
1035  - C. With URL parameter (e.g., ?showNetworkToggle=true)
1036  
1037  **Recommendation**: Option A (Always visible, builds confidence)
1038  
1039  ### 3. Testnet Availability
1040  **Question**: What if testnet is down?
1041  
1042  **Options**:
1043  - A. Disable toggle
1044  - B. Show warning but allow switch
1045  - C. Auto-fallback to mainnet
1046  
1047  **Recommendation**: Option B (Show warning, let user decide)
1048  
1049  ### 4. Data Isolation
1050  **Question**: Should we show warning when switching with cached data?
1051  
1052  **Options**:
1053  - A. Clear all data on switch
1054  - B. Keep separate caches per network
1055  - C. Show warning + confirmation modal
1056  
1057  **Recommendation**: Option B (Separate caches, seamless UX)
1058  
1059  ---
1060  
1061  *Network Toggle Implementation Plan v1.0*
1062  *Ready for discussion and approval! πŸš€*