# @opchan/core Sample Application Complete, production-ready forum application demonstrating all features of the OpChan Core SDK. --- ## Complete Forum Application A full-featured, production-ready decentralized forum demonstrating all features of the OpChan Core SDK including authentication, content management, moderation, identity resolution, bookmarks, and real-time updates. ### Features Demonstrated - ✅ **Client Initialization** - Setup and configuration - ✅ **Anonymous & Wallet Authentication** - Dual authentication modes - ✅ **Session Persistence** - Restore sessions across page loads - ✅ **Content Creation** - Posts, comments, and cells - ✅ **Voting System** - Upvote/downvote functionality - ✅ **Identity Resolution** - ENS lookup and call signs - ✅ **Real-Time Updates** - Live message synchronization - ✅ **Relevance Scoring** - Content ranking algorithm - ✅ **Moderation** - Cell owner moderation tools - ✅ **Bookmarks** - Save and manage favorite content - ✅ **Network Monitoring** - Connection health tracking - ✅ **Pending States** - Optimistic UI with sync indicators ### Application Structure ```typescript import { OpChanClient, EVerificationStatus, EDisplayPreference, BookmarkService, transformPost, type User, type Post, type Comment, type Cell } from '@opchan/core'; import { createPublicClient, http } from 'viem'; import { mainnet } from 'viem/chains'; import { signMessage } from 'viem/accounts'; class CompleteForum { // Core components private client: OpChanClient; private bookmarkService: BookmarkService; private currentUser: User | null = null; // Event handlers private unsubscribers: (() => void)[] = []; constructor() { this.client = new OpChanClient({ wakuConfig: { contentTopic: '/opchan/1/messages/proto', reliableChannelId: 'opchan-messages' }, reownProjectId: process.env.REOWN_PROJECT_ID }); this.bookmarkService = new BookmarkService(); } // ============================================================================ // INITIALIZATION // ============================================================================ async initialize() { console.log('🚀 Initializing OpChan Forum...\n'); // 1. Open database (hydrates from IndexedDB) await this.client.database.open(); console.log('✅ Database opened'); // 2. Set up ENS resolution const publicClient = createPublicClient({ chain: mainnet, transport: http() }); this.client.userIdentityService.setPublicClient(publicClient); console.log('✅ ENS resolution configured'); // 3. Set up event listeners this.setupListeners(); console.log('✅ Event listeners configured'); // 4. Restore or create session await this.restoreSession(); // 5. Initial render await this.render(); console.log('\n✅ Forum initialized successfully!\n'); } private setupListeners() { // Message listener - handles all incoming messages const msgUnsub = this.client.messageManager.onMessageReceived( async (message) => { const wasNew = await this.client.database.applyMessage(message); if (wasNew) { console.log(`📨 New ${message.type} received`); await this.render(); } } ); this.unsubscribers.push(msgUnsub); // Network health listener const healthUnsub = this.client.messageManager.onHealthChange( (isHealthy) => { console.log(isHealthy ? '🟢 Network: Connected' : '🔴 Network: Offline'); } ); this.unsubscribers.push(healthUnsub); // Identity updates listener const identityUnsub = this.client.userIdentityService.subscribe( (address, identity) => { if (identity) { console.log(`👤 Identity updated: ${identity.displayName}`); } } ); this.unsubscribers.push(identityUnsub); // Pending state listener const pendingUnsub = this.client.database.onPendingChange(() => { this.updatePendingIndicators(); }); this.unsubscribers.push(pendingUnsub); } // ============================================================================ // AUTHENTICATION // ============================================================================ private async restoreSession() { // Try to load stored user const storedUser = await this.client.database.loadUser(); if (storedUser) { // Validate delegation const status = await this.client.delegation.getStatus(storedUser.address); if (status.isValid) { this.currentUser = storedUser; console.log(`👤 Restored session: ${storedUser.displayName}`); } else { console.log('⚠️ Delegation expired, starting new session'); await this.startAnonymousSession(); } } else { await this.startAnonymousSession(); } } async startAnonymousSession() { const sessionId = await this.client.delegation.delegateAnonymous('7days'); this.currentUser = { address: sessionId, displayName: 'Anonymous', displayPreference: EDisplayPreference.WALLET_ADDRESS, verificationStatus: EVerificationStatus.ANONYMOUS }; await this.client.database.storeUser(this.currentUser); console.log('👤 Started anonymous session'); } async connectWallet(walletAddress: `0x${string}`) { console.log('\n🔐 Connecting wallet...'); // Create delegation const success = await this.client.delegation.delegate( walletAddress, '7days', async (message) => { // Sign with wallet (this would use your wallet provider) console.log('📝 Please sign the authorization message...'); return await signMessage({ message, account: walletAddress }); } ); if (!success) { console.error('❌ Failed to create delegation'); return; } // Get identity const identity = await this.client.userIdentityService.getIdentity( walletAddress, { fresh: true } ); if (!identity) { console.error('❌ Failed to resolve identity'); return; } this.currentUser = { address: walletAddress, displayName: identity.displayName, displayPreference: identity.displayPreference, verificationStatus: identity.verificationStatus, ensName: identity.ensName, ensAvatar: identity.ensAvatar, callSign: identity.callSign }; await this.client.database.storeUser(this.currentUser); console.log(`✅ Wallet connected: ${identity.displayName}`); console.log(` Verification: ${identity.verificationStatus}\n`); } async disconnect() { await this.client.delegation.clear(); await this.client.database.clearUser(); this.currentUser = null; console.log('👋 Disconnected'); } async setCallSign(callSign: string) { if (!this.currentUser) return; const result = await this.client.userIdentityService.updateProfile( this.currentUser.address, { callSign, displayPreference: EDisplayPreference.CALL_SIGN } ); if (result.ok) { this.currentUser.callSign = callSign; this.currentUser.displayName = callSign; this.currentUser.displayPreference = EDisplayPreference.CALL_SIGN; await this.client.database.storeUser(this.currentUser); console.log(`✅ Call sign set: ${callSign}`); } else { console.error('❌ Failed to set call sign:', result.error); } } // ============================================================================ // CONTENT CREATION // ============================================================================ async createCell(name: string, description: string, icon?: string) { if (!this.currentUser) { console.error('❌ Not authenticated'); return null; } const result = await this.client.forumActions.createCell( { name, description, icon, currentUser: this.currentUser, isAuthenticated: true }, () => this.render() ); if (result.success) { console.log(`✅ Cell created: ${name}`); this.client.database.markPending(result.data!.id); return result.data; } else { console.error(`❌ Failed to create cell: ${result.error}`); return null; } } async createPost(cellId: string, title: string, content: string) { if (!this.currentUser) { console.error('❌ Not authenticated'); return null; } const result = await this.client.forumActions.createPost( { cellId, title, content, currentUser: this.currentUser, isAuthenticated: true }, () => this.render() ); if (result.success) { console.log(`✅ Post created: ${title}`); this.client.database.markPending(result.data!.id); return result.data; } else { console.error(`❌ Failed to create post: ${result.error}`); return null; } } async createComment(postId: string, content: string) { if (!this.currentUser) { console.error('❌ Not authenticated'); return null; } const result = await this.client.forumActions.createComment( { postId, content, currentUser: this.currentUser, isAuthenticated: true }, () => this.render() ); if (result.success) { console.log('✅ Comment created'); this.client.database.markPending(result.data!.id); return result.data; } else { console.error(`❌ Failed to create comment: ${result.error}`); return null; } } // ============================================================================ // VOTING // ============================================================================ async vote(targetId: string, isUpvote: boolean) { if (!this.currentUser) { console.error('❌ Not authenticated'); return; } const result = await this.client.forumActions.vote( { targetId, isUpvote, currentUser: this.currentUser, isAuthenticated: true }, () => this.render() ); if (result.success) { console.log(`✅ ${isUpvote ? 'Upvoted' : 'Downvoted'}`); } else { console.error(`❌ Failed to vote: ${result.error}`); } } // ============================================================================ // MODERATION // ============================================================================ canModerate(cellId: string): boolean { if (!this.currentUser) return false; const cell = this.client.database.cache.cells[cellId]; return cell?.author === this.currentUser.address; } async moderatePost(cellId: string, postId: string, reason: string) { if (!this.currentUser || !this.canModerate(cellId)) { console.error('❌ Not authorized to moderate'); return; } const cell = this.client.database.cache.cells[cellId]; const result = await this.client.forumActions.moderatePost( { cellId, postId, reason, currentUser: this.currentUser, isAuthenticated: true, cellOwner: cell.author }, () => this.render() ); if (result.success) { console.log('✅ Post moderated'); } else { console.error(`❌ Failed to moderate: ${result.error}`); } } async unmoderatePost(cellId: string, postId: string) { if (!this.currentUser || !this.canModerate(cellId)) { console.error('❌ Not authorized'); return; } const cell = this.client.database.cache.cells[cellId]; const result = await this.client.forumActions.unmoderatePost( { cellId, postId, currentUser: this.currentUser, isAuthenticated: true, cellOwner: cell.author }, () => this.render() ); if (result.success) { console.log('✅ Post unmoderated'); } else { console.error(`❌ Failed to unmoderate: ${result.error}`); } } // ============================================================================ // BOOKMARKS // ============================================================================ async bookmarkPost(postId: string) { if (!this.currentUser) return; const post = this.client.database.cache.posts[postId]; if (!post) { console.error('❌ Post not found'); return; } await this.bookmarkService.addPostBookmark( post, this.currentUser.address, post.cellId ); console.log(`✅ Bookmarked: ${post.title}`); } async removeBookmark(bookmarkId: string) { await this.bookmarkService.removeBookmark(bookmarkId); console.log('✅ Bookmark removed'); } isBookmarked(type: 'post' | 'comment', targetId: string): boolean { if (!this.currentUser) return false; return this.client.database.isBookmarked( this.currentUser.address, type, targetId ); } // ============================================================================ // DATA ACCESS // ============================================================================ async getSortedPosts(cellId?: string): Promise { let posts = Object.values(this.client.database.cache.posts); if (cellId) { posts = posts.filter(p => p.cellId === cellId); } // Transform and score posts const transformedPosts = await Promise.all( posts.map(async p => { const transformed = await transformPost(p); if (!transformed) return null; // Calculate relevance const votes = Object.values(this.client.database.cache.votes) .filter(v => v.targetId === p.id); const comments = Object.values(this.client.database.cache.comments) .filter(c => c.postId === p.id); const userVerificationStatus = {}; for (const [addr, identity] of Object.entries( this.client.database.cache.userIdentities )) { userVerificationStatus[addr] = { isVerified: identity.verificationStatus === 'ens-verified', hasENS: !!identity.ensName, ensName: identity.ensName }; } const scoreDetails = this.client.relevance.calculatePostScore( p, votes, comments, userVerificationStatus, this.client.database.cache.moderations ); transformed.relevanceScore = scoreDetails.finalScore; transformed.relevanceDetails = scoreDetails; return transformed; }) ); return transformedPosts .filter((p): p is Post => p !== null) .sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0)); } getCells(): Cell[] { return Object.values(this.client.database.cache.cells); } getComments(postId: string): Comment[] { return Object.values(this.client.database.cache.comments) .filter(c => c.postId === postId) .sort((a, b) => a.timestamp - b.timestamp); } async getMyBookmarks() { if (!this.currentUser) return []; return await this.client.database.getUserBookmarks(this.currentUser.address); } // ============================================================================ // UI RENDERING // ============================================================================ private async render() { console.clear(); console.log('═══════════════════════════════════════════════════════════'); console.log(' OPCHAN FORUM '); console.log('═══════════════════════════════════════════════════════════\n'); // Network status const isHealthy = this.client.messageManager.isReady; console.log(`Network: ${isHealthy ? '🟢 Connected' : '🔴 Offline'}`); // User status if (this.currentUser) { console.log(`User: ${this.currentUser.displayName} (${this.currentUser.verificationStatus})`); } else { console.log('User: Not authenticated'); } console.log('\n───────────────────────────────────────────────────────────\n'); // Cells const cells = this.getCells(); console.log(`📁 CELLS (${cells.length})\n`); cells.forEach(cell => { const posts = Object.values(this.client.database.cache.posts) .filter(p => p.cellId === cell.id); console.log(` ${cell.icon || '📁'} ${cell.name} (${posts.length} posts)`); console.log(` ${cell.description}`); if (this.canModerate(cell.id)) { console.log(` 👮 You can moderate this cell`); } console.log(); }); // Posts (top 10 by relevance) const posts = await this.getSortedPosts(); console.log(`\n📝 TOP POSTS (${posts.length} total)\n`); posts.slice(0, 10).forEach((post, index) => { const isPending = this.client.database.isPending(post.id); const score = post.relevanceScore?.toFixed(0) || '0'; const upvotes = post.upvotes?.length || 0; const downvotes = post.downvotes?.length || 0; const comments = this.getComments(post.id).length; const isBookmarked = this.isBookmarked('post', post.id); console.log(`${index + 1}. [Score: ${score}] ${post.title}${isPending ? ' ⏳' : ''}`); console.log(` by ${post.author.slice(0, 8)}... | Cell: ${post.cellId}`); console.log(` ⬆️ ${upvotes} ⬇️ ${downvotes} 💬 ${comments}${isBookmarked ? ' 🔖' : ''}`); if (post.relevanceDetails) { const details = post.relevanceDetails; console.log(` 📊 Base: ${details.baseScore} + Engagement: ${details.engagementScore.toFixed(0)} × Decay: ${(details.timeDecayMultiplier * 100).toFixed(0)}%`); } console.log(` ${post.content.slice(0, 80)}${post.content.length > 80 ? '...' : ''}`); console.log(); }); // Bookmarks if (this.currentUser) { const bookmarks = await this.getMyBookmarks(); if (bookmarks.length > 0) { console.log(`\n🔖 MY BOOKMARKS (${bookmarks.length})\n`); bookmarks.slice(0, 5).forEach(b => { console.log(` ${b.title || b.id}`); }); console.log(); } } console.log('═══════════════════════════════════════════════════════════\n'); } private updatePendingIndicators() { // In a real UI, this would update visual indicators // For console, we just re-render this.render(); } // ============================================================================ // CLEANUP // ============================================================================ cleanup() { this.unsubscribers.forEach(unsub => unsub()); console.log('👋 Forum cleaned up'); } } // ============================================================================ // USAGE EXAMPLE // ============================================================================ async function main() { const forum = new CompleteForum(); try { // Initialize await forum.initialize(); // Wait a moment for content to load await new Promise(resolve => setTimeout(resolve, 2000)); // Example: Set a call sign for anonymous user await forum.setCallSign('alice'); // Example: Create a post await forum.createPost( 'general', 'Hello OpChan!', 'This is a comprehensive example of the OpChan forum application.' ); // Example: Vote on a post (would need actual post ID) // await forum.vote('post-id', true); // Example: Bookmark a post // await forum.bookmarkPost('post-id'); // Keep running and listening for updates console.log('\n✅ Forum is running. Press Ctrl+C to exit.\n'); // In a real application, this would be kept alive by your UI framework // For this example, we'll just wait await new Promise(() => {}); // Wait forever } catch (error) { console.error('❌ Error:', error); } finally { forum.cleanup(); } } // Run the application main(); ``` --- ## Key Features Explained ### 1. **Robust Initialization** The application properly initializes all components in order: - Opens database and hydrates from IndexedDB - Configures ENS resolution - Sets up event listeners - Restores or creates user session ### 2. **Dual Authentication** Supports both anonymous and wallet-connected users: - Anonymous users get a UUID session ID - Wallet users connect with ENS verification - Sessions persist across page reloads - Delegation expiry is checked on restoration ### 3. **Real-Time Synchronization** All events are properly wired: - Incoming messages trigger UI updates - Network health changes are logged - Identity updates propagate automatically - Pending states tracked for optimistic UI ### 4. **Complete Content Management** Full CRUD operations: - Create cells (ENS-verified users only) - Create posts and comments (all authenticated users) - Vote on content - Moderation tools for cell owners ### 5. **Advanced Features** Production-ready functionality: - Relevance scoring with detailed breakdown - Bookmark management - Identity resolution with ENS - Call sign support - Pending state indicators ### 6. **Proper Error Handling** All operations include error handling: - Permission checks before actions - Validation of delegation status - User-friendly error messages - Graceful degradation ### 7. **Clean Architecture** Well-organized code structure: - Logical section separation - Event-driven design - Proper cleanup on exit - Reusable methods --- ## Running the Application ### Prerequisites ```bash npm install @opchan/core viem ``` ### Environment Variables ```bash export REOWN_PROJECT_ID="your-project-id" ``` ### Run ```typescript // Save as forum.ts // Run with: npx tsx forum.ts ``` ### Integration with UI This application can be easily adapted for: - **React**: Convert methods to hooks, use state management - **Vue**: Use reactive refs and computed properties - **Svelte**: Use stores and reactive statements - **Web Components**: Create custom elements - **CLI**: Interactive command-line interface --- ## Extending the Application ### Add Wallet Support ```typescript import { useWalletClient } from 'wagmi'; async connectWithWagmi() { const { data: walletClient } = useWalletClient(); if (!walletClient) return; await this.connectWallet(walletClient.account.address); } ``` ### Add Search Functionality ```typescript searchPosts(query: string): Post[] { const posts = Object.values(this.client.database.cache.posts); return posts.filter(p => p.title.toLowerCase().includes(query.toLowerCase()) || p.content.toLowerCase().includes(query.toLowerCase()) ); } ``` ### Add Notifications ```typescript private setupListeners() { this.client.messageManager.onMessageReceived(async (message) => { const wasNew = await this.client.database.applyMessage(message); if (wasNew && message.type === 'comment') { this.notify(`New comment on your post!`); } }); } ``` --- ## Production Considerations ### Performance - Cache is in-memory for fast reads - IndexedDB persistence for reliability - Lazy loading of identities - Debounced identity lookups ### Security - All messages cryptographically signed - Delegation proofs verified - Permission checks enforced - Wallet signatures required for delegation ### Scalability - Event-driven architecture - Efficient data structures - Minimal re-renders - Optimistic UI updates ### Reliability - Session persistence - Delegation expiry handling - Network disconnect handling - Error recovery --- **This is a production-ready template demonstrating all features of the OpChan Core SDK. Use it as a foundation for building your decentralized forum application.**