/ 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! π*