# @opchan/core Core browser library for building decentralized forum applications with cryptographic identity management and peer-to-peer messaging. ## Overview `@opchan/core` provides the foundational infrastructure for the OpChan protocol, featuring: - 🔐 **Cryptographic Identity** - Ed25519 key delegation with wallet signatures - 📡 **Waku Messaging** - Peer-to-peer communication via Waku network - 💾 **Local-First Storage** - IndexedDB persistence with in-memory caching - ⚖️ **Content Management** - Cells, posts, comments, and voting system - 🎯 **Relevance Scoring** - Multi-factor content ranking algorithm - 🛡️ **Message Validation** - Cryptographic verification of all content - 👤 **Identity Resolution** - ENS integration and user profiles - 🔖 **Bookmarks** - Client-side bookmark management ## Installation ```bash npm install @opchan/core ``` ## Quick Start ### 1. Initialize Client ```typescript import { OpChanClient } from '@opchan/core'; const client = new OpChanClient({ wakuConfig: { contentTopic: '/opchan/1/messages/proto', reliableChannelId: 'opchan-messages' }, reownProjectId: 'your-reown-project-id' // Optional, for WalletConnect }); // Open database await client.database.open(); ``` ### 2. Set Up Message Listening ```typescript // Listen for incoming messages client.messageManager.onMessageReceived(async (message) => { // Apply message to local database await client.database.applyMessage(message); console.log('Received message:', message.type); }); // Monitor network health client.messageManager.onHealthChange((isHealthy) => { console.log('Network health:', isHealthy ? 'Connected' : 'Disconnected'); }); ``` ### 3. Delegate Signing Keys (Wallet Users) ```typescript import { signMessage } from 'viem/accounts'; // Generate delegation for 7 days const success = await client.delegation.delegate( walletAddress, '7days', async (message) => { return await signMessage({ message, account: walletAddress }); } ); if (success) { console.log('Delegation created successfully'); } ``` ### 4. Create Content ```typescript // Create a cell (requires ENS verification) const cellResult = await client.forumActions.createCell( { name: 'Tech Discussion', description: 'A place for tech talk', icon: '🚀', currentUser: user, isAuthenticated: true }, () => refreshUI() ); // Create a post const postResult = await client.forumActions.createPost( { cellId: 'cell-id', title: 'Hello World', content: 'My first post!', currentUser: user, isAuthenticated: true }, () => refreshUI() ); // Add a comment const commentResult = await client.forumActions.createComment( { postId: 'post-id', content: 'Great post!', currentUser: user, isAuthenticated: true }, () => refreshUI() ); // Vote on content await client.forumActions.vote( { targetId: 'post-id', isUpvote: true, currentUser: user, isAuthenticated: true }, () => refreshUI() ); ``` ### 5. Access Cached Data ```typescript // Get all cells const cells = Object.values(client.database.cache.cells); // Get all posts const posts = Object.values(client.database.cache.posts); // Get posts for a specific cell const cellPosts = posts.filter(post => post.cellId === 'cell-id'); // Get comments for a post const comments = Object.values(client.database.cache.comments) .filter(comment => comment.postId === 'post-id'); // Get votes for a post const votes = Object.values(client.database.cache.votes) .filter(vote => vote.targetId === 'post-id'); ``` ## Core Concepts ### Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ OpChanClient │ │ ┌─────────────────────────────────────────────────┐ │ │ │ MessageManager (Waku Network) │ │ │ │ - WakuNodeManager: Connection handling │ │ │ │ - ReliableMessaging: Message delivery │ │ │ │ - MessageService: Send/receive orchestration │ │ │ └─────────────────────────────────────────────────┘ │ │ ↕ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ LocalDatabase (IndexedDB + Cache) │ │ │ │ - In-memory cache for fast reads │ │ │ │ - IndexedDB for persistence │ │ │ │ - Message validation and deduplication │ │ │ └─────────────────────────────────────────────────┘ │ │ ↕ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Services Layer │ │ │ │ - ForumActions: Content creation │ │ │ │ - UserIdentityService: Identity resolution │ │ │ │ - DelegationManager: Key management │ │ │ │ - RelevanceCalculator: Content scoring │ │ │ │ - BookmarkService: Bookmark management │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### Message Flow 1. **Outbound**: User action → Sign with delegated key → Store in LocalDatabase → Send via Waku 2. **Inbound**: Waku receives → Validate signature → Validate delegation proof → Store in LocalDatabase → Notify listeners ### Key Delegation OpChan uses a two-tier signing system: - **Wallet Key**: Used once to authorize a browser key - **Browser Key**: Used for all subsequent messages Benefits: - Reduces wallet prompts from dozens to one - Messages still cryptographically verifiable - Works with anonymous sessions (no wallet required) ## API Reference ### OpChanClient Main client class that orchestrates all services. ```typescript class OpChanClient { readonly config: OpChanClientConfig; readonly messageManager: DefaultMessageManager; readonly database: LocalDatabase; readonly forumActions: ForumActions; readonly relevance: RelevanceCalculator; readonly messageService: MessageService; readonly userIdentityService: UserIdentityService; readonly delegation: DelegationManager; } ``` **Configuration:** ```typescript interface OpChanClientConfig { wakuConfig: WakuConfig; reownProjectId?: string; } interface WakuConfig { contentTopic: string; reliableChannelId: string; } ``` --- ### DelegationManager Manages cryptographic key delegation and message signing. #### Methods **`delegate(address, duration, signFunction)`** Create a wallet-signed delegation for browser keys. ```typescript async delegate( address: `0x${string}`, duration: '7days' | '30days', signFunction: (message: string) => Promise ): Promise ``` **Example:** ```typescript const success = await client.delegation.delegate( '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', '7days', async (msg) => await wallet.signMessage(msg) ); ``` --- **`delegateAnonymous(duration)`** Create an anonymous delegation (no wallet required). ```typescript async delegateAnonymous( duration: '7days' | '30days' = '7days' ): Promise ``` Returns a session ID for the anonymous user. --- **`signMessage(message)`** Sign a message with the delegated browser key. ```typescript async signMessage( message: UnsignedMessage ): Promise ``` --- **`verify(message)`** Verify a signed message's authenticity. ```typescript async verify(message: OpchanMessage): Promise ``` --- **`getStatus(currentAddress?)`** Get current delegation status. ```typescript async getStatus( currentAddress?: string ): Promise ``` **Returns:** ```typescript interface DelegationFullStatus { hasDelegation: boolean; isValid: boolean; timeRemaining?: number; publicKey?: string; address?: `0x${string}`; proof?: DelegationProof; } ``` --- **`clear()`** Clear stored delegation. ```typescript async clear(): Promise ``` --- ### LocalDatabase IndexedDB-backed local storage with in-memory caching. #### Properties **`cache`** - In-memory cache of all content ```typescript interface LocalDatabaseCache { cells: { [id: string]: CellMessage }; posts: { [id: string]: PostMessage }; comments: { [id: string]: CommentMessage }; votes: { [key: string]: VoteMessage }; moderations: { [key: string]: ModerateMessage }; userIdentities: { [address: string]: UserIdentityCache }; bookmarks: { [id: string]: Bookmark }; } ``` #### Methods **`open()`** Open database and hydrate cache from IndexedDB. ```typescript async open(): Promise ``` --- **`applyMessage(message)`** Apply an incoming message to the database. ```typescript async applyMessage(message: unknown): Promise ``` Returns `true` if message was newly processed and stored. --- **`storeUser(user)` / `loadUser()` / `clearUser()`** Persist user authentication state. ```typescript async storeUser(user: User): Promise async loadUser(): Promise async clearUser(): Promise ``` --- **`storeDelegation(delegation)` / `loadDelegation()` / `clearDelegation()`** Persist delegation information. ```typescript async storeDelegation(delegation: DelegationInfo): Promise async loadDelegation(): Promise async clearDelegation(): Promise ``` --- **`markPending(id)` / `clearPending(id)` / `isPending(id)`** Track pending message synchronization. ```typescript markPending(id: string): void clearPending(id: string): void isPending(id: string): boolean onPendingChange(listener: () => void): () => void ``` --- **`addBookmark(bookmark)` / `removeBookmark(id)`** Manage bookmarks. ```typescript async addBookmark(bookmark: Bookmark): Promise async removeBookmark(bookmarkId: string): Promise async getUserBookmarks(userId: string): Promise isBookmarked(userId: string, type: 'post' | 'comment', targetId: string): boolean ``` --- ### ForumActions High-level actions for content creation and moderation. #### Content Creation **`createCell(params, updateCallback)`** Create a new cell (requires ENS verification). ```typescript async createCell( params: { name: string; description: string; icon?: string; currentUser: User | null; isAuthenticated: boolean; }, updateCallback: () => void ): Promise<{ success: boolean; data?: Cell; error?: string }> ``` --- **`createPost(params, updateCallback)`** Create a new post. ```typescript async createPost( params: { cellId: string; title: string; content: string; currentUser: User | null; isAuthenticated: boolean; }, updateCallback: () => void ): Promise<{ success: boolean; data?: Post; error?: string }> ``` --- **`createComment(params, updateCallback)`** Create a new comment. ```typescript async createComment( params: { postId: string; content: string; currentUser: User | null; isAuthenticated: boolean; }, updateCallback: () => void ): Promise<{ success: boolean; data?: Comment; error?: string }> ``` --- #### Voting **`vote(params, updateCallback)`** Vote on a post or comment. ```typescript async vote( params: { targetId: string; isUpvote: boolean; currentUser: User | null; isAuthenticated: boolean; }, updateCallback: () => void ): Promise<{ success: boolean; data?: boolean; error?: string }> ``` --- #### Moderation **`moderatePost(params, updateCallback)`** Moderate a post (cell owner only). ```typescript async moderatePost( params: { cellId: string; postId: string; reason?: string; currentUser: User | null; isAuthenticated: boolean; cellOwner: string; }, updateCallback: () => void ): Promise<{ success: boolean; data?: boolean; error?: string }> ``` Similar methods exist for: `unmoderatePost`, `moderateComment`, `unmoderateComment`, `moderateUser`, `unmoderateUser` --- ### UserIdentityService Manages user identity resolution and profiles. #### Methods **`getIdentity(address, opts?)`** Get user identity with optional fresh resolution. ```typescript async getIdentity( address: string, opts?: { fresh?: boolean } ): Promise ``` **Returns:** ```typescript interface UserIdentity { address: `0x${string}`; ensName?: string; ensAvatar?: string; callSign?: string; displayPreference: EDisplayPreference; displayName: string; lastUpdated: number; verificationStatus: EVerificationStatus; } ``` --- **`updateProfile(address, updates)`** Update user profile (call sign and display preference). ```typescript async updateProfile( address: string, updates: { callSign?: string; displayPreference?: EDisplayPreference; } ): Promise<{ ok: true; identity: UserIdentity } | { ok: false; error: Error }> ``` --- **`getDisplayName(params)`** Get display name for a user based on their preferences. ```typescript getDisplayName({ address: string, ensName?: string | null, displayPreference?: EDisplayPreference }): string ``` --- **`subscribe(listener)`** Subscribe to identity changes. ```typescript subscribe( listener: (address: string, identity: UserIdentity | null) => void ): () => void ``` --- ### RelevanceCalculator Calculates content relevance scores based on multiple factors. #### Method **`calculatePostScore(post, votes, comments, userVerificationStatus, moderatedPosts)`** Calculate relevance score for a post. ```typescript calculatePostScore( post: PostMessage, votes: VoteMessage[], comments: CommentMessage[], userVerificationStatus: UserVerificationStatus, moderatedPosts: { [postId: string]: ModerateMessage } ): RelevanceScoreDetails ``` **Score Factors:** - **Base Score**: 100 points for all content - **Engagement**: 10 points per upvote, 3 points per comment - **Verification Bonuses**: - Author ENS verified: +20 points - Each verified upvote: +5 points - Each verified commenter: +10 points - **Time Decay**: Exponential decay over 7 days (half-life) - **Moderation Penalty**: -50% for moderated content --- ### MessageManager Manages Waku network connectivity and message transmission. #### Methods **`sendMessage(message, statusCallback?)`** Send a message via Waku network. ```typescript async sendMessage( message: OpchanMessage, statusCallback?: (status: MessageStatus) => void ): Promise ``` --- **`onMessageReceived(callback)`** Subscribe to incoming messages. ```typescript onMessageReceived( callback: (message: OpchanMessage) => void ): () => void ``` --- **`onHealthChange(callback)`** Monitor network health. ```typescript onHealthChange( callback: (isHealthy: boolean) => void ): () => void ``` --- **`onSyncStatus(callback)`** Monitor synchronization status. ```typescript onSyncStatus( callback: (status: SyncStatus) => void ): () => void ``` --- ## Type Definitions ### Core Types ```typescript // User verification levels enum EVerificationStatus { ANONYMOUS = 'anonymous', WALLET_UNCONNECTED = 'wallet-unconnected', WALLET_CONNECTED = 'wallet-connected', ENS_VERIFIED = 'ens-verified', } // User display preferences enum EDisplayPreference { CALL_SIGN = 'call-sign', WALLET_ADDRESS = 'wallet-address', } // User object interface User { address: string; // 0x${string} for wallet, UUID for anonymous ensName?: string; ensAvatar?: string; callSign?: string; displayPreference: EDisplayPreference; displayName: string; verificationStatus: EVerificationStatus; lastChecked?: number; } ``` ### Message Types ```typescript // All messages include signature fields interface SignedMessage { signature: string; browserPubKey: string; delegationProof?: DelegationProof; // Present for wallet users only } // Message types enum MessageType { CELL = 'cell', POST = 'post', COMMENT = 'comment', VOTE = 'vote', MODERATE = 'moderate', USER_PROFILE_UPDATE = 'user_profile_update', } // Cell message interface CellMessage { type: MessageType.CELL; id: string; name: string; description: string; icon?: string; timestamp: number; author: string; } // Post message interface PostMessage { type: MessageType.POST; id: string; cellId: string; title: string; content: string; timestamp: number; author: string; } // Comment message interface CommentMessage { type: MessageType.COMMENT; id: string; postId: string; content: string; timestamp: number; author: string; } // Vote message interface VoteMessage { type: MessageType.VOTE; id: string; targetId: string; // Post or comment ID value: 1 | -1; // Upvote or downvote timestamp: number; author: string; } ``` ### Extended Forum Types ```typescript // Extended cell with computed fields interface Cell extends CellMessage { relevanceScore?: number; activeMemberCount?: number; recentActivity?: number; postCount?: number; } // Extended post with computed fields interface Post extends PostMessage { authorAddress: string; upvotes: VoteMessage[]; downvotes: VoteMessage[]; moderated?: boolean; moderatedBy?: string; moderationReason?: string; relevanceScore?: number; verifiedUpvotes?: number; verifiedCommenters?: string[]; voteScore?: number; } // Extended comment with computed fields interface Comment extends CommentMessage { authorAddress: string; upvotes: VoteMessage[]; downvotes: VoteMessage[]; moderated?: boolean; moderatedBy?: string; moderationReason?: string; relevanceScore?: number; voteScore?: number; } ``` ## Usage Patterns ### Pattern 1: Complete Vanilla JS Application ```typescript import { OpChanClient, EVerificationStatus } from '@opchan/core'; // Initialize const client = new OpChanClient({ wakuConfig: { contentTopic: '/opchan/1/messages/proto', reliableChannelId: 'opchan-messages' } }); await client.database.open(); // Set up message listener client.messageManager.onMessageReceived(async (message) => { await client.database.applyMessage(message); renderUI(); }); // Create delegation const sessionId = await client.delegation.delegateAnonymous('7days'); const user = { address: sessionId, displayName: 'Anonymous', displayPreference: EDisplayPreference.WALLET_ADDRESS, verificationStatus: EVerificationStatus.ANONYMOUS }; // Create content const result = await client.forumActions.createPost( { cellId: 'general', title: 'Hello', content: 'First post!', currentUser: user, isAuthenticated: true }, () => renderUI() ); // Render UI function renderUI() { const posts = Object.values(client.database.cache.posts); document.getElementById('posts').innerHTML = posts .map(post => `
${post.title}
`) .join(''); } ``` --- ### Pattern 2: Identity Resolution ```typescript // Resolve user identity const identity = await client.userIdentityService.getIdentity( '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', { fresh: true } ); console.log('Display name:', identity?.displayName); console.log('ENS name:', identity?.ensName); console.log('Verification:', identity?.verificationStatus); // Update profile const result = await client.userIdentityService.updateProfile( userAddress, { callSign: 'alice', displayPreference: EDisplayPreference.CALL_SIGN } ); if (result.ok) { console.log('Profile updated:', result.identity); } // Subscribe to changes const unsubscribe = client.userIdentityService.subscribe( (address, identity) => { console.log('Identity updated:', address, identity); } ); ``` --- ### Pattern 3: Content Scoring ```typescript import { transformPost } from '@opchan/core'; // Transform raw post message to enhanced post const post = await transformPost(postMessage); // Calculate relevance const score = client.relevance.calculatePostScore( postMessage, votes, comments, userVerificationStatus, moderatedPosts ); console.log('Relevance score:', score.finalScore); console.log('Score breakdown:', { base: score.baseScore, engagement: score.engagementScore, verificationBonus: score.authorVerificationBonus + score.verifiedUpvoteBonus + score.verifiedCommenterBonus, timeDecay: score.timeDecayMultiplier, moderation: score.moderationPenalty }); ``` --- ### Pattern 4: Moderation ```typescript // Check if user can moderate const cellOwner = cell.author; const canModerate = currentUser.address === cellOwner; if (canModerate) { // Moderate a post await client.forumActions.moderatePost( { cellId: cell.id, postId: post.id, reason: 'Spam', currentUser, isAuthenticated: true, cellOwner }, () => refreshUI() ); // Unmoderate a post await client.forumActions.unmoderatePost( { cellId: cell.id, postId: post.id, currentUser, isAuthenticated: true, cellOwner }, () => refreshUI() ); } ``` --- ### Pattern 5: Bookmarks ```typescript import { BookmarkService } from '@opchan/core'; const bookmarkService = new BookmarkService(); // Add post bookmark await bookmarkService.addPostBookmark( post, userId, cellId ); // Add comment bookmark await bookmarkService.addCommentBookmark( comment, userId, postId ); // Get user bookmarks const bookmarks = await client.database.getUserBookmarks(userId); // Check if bookmarked const isBookmarked = client.database.isBookmarked( userId, 'post', postId ); // Remove bookmark await bookmarkService.removeBookmark(bookmarkId); ``` --- ## Complete Example Here's a complete vanilla JavaScript application using `@opchan/core`: ```typescript import { OpChanClient, EVerificationStatus, EDisplayPreference, transformPost, transformComment } from '@opchan/core'; class OpChanApp { private client: OpChanClient; private currentUser: any = null; async initialize() { // Create client this.client = new OpChanClient({ wakuConfig: { contentTopic: '/opchan/1/messages/proto', reliableChannelId: 'opchan-messages' } }); // Open database await this.client.database.open(); // Set up listeners this.setupListeners(); // Create anonymous session await this.startAnonymousSession(); // Initial render this.render(); } private setupListeners() { // Message listener this.client.messageManager.onMessageReceived(async (message) => { await this.client.database.applyMessage(message); this.render(); }); // Health listener this.client.messageManager.onHealthChange((isHealthy) => { console.log('Network:', isHealthy ? 'Connected' : 'Disconnected'); this.render(); }); } private async startAnonymousSession() { const sessionId = await this.client.delegation.delegateAnonymous('7days'); this.currentUser = { address: sessionId, displayName: 'Anonymous', displayPreference: EDisplayPreference.WALLET_ADDRESS, verificationStatus: EVerificationStatus.ANONYMOUS }; } private async createPost(cellId: string, title: string, content: string) { const result = await this.client.forumActions.createPost( { cellId, title, content, currentUser: this.currentUser, isAuthenticated: true }, () => this.render() ); if (result.success) { console.log('Post created:', result.data); } else { console.error('Failed to create post:', result.error); } } private async vote(targetId: string, isUpvote: boolean) { await this.client.forumActions.vote( { targetId, isUpvote, currentUser: this.currentUser, isAuthenticated: true }, () => this.render() ); } private render() { const cells = Object.values(this.client.database.cache.cells); const posts = Object.values(this.client.database.cache.posts); const comments = Object.values(this.client.database.cache.comments); console.log('Cells:', cells.length); console.log('Posts:', posts.length); console.log('Comments:', comments.length); // Update DOM here... } } // Start app const app = new OpChanApp(); app.initialize(); ``` ## Advanced Topics ### Message Validation All messages are validated before storage: ```typescript import { MessageValidator } from '@opchan/core'; const validator = new MessageValidator(); // Validate message const isValid = await validator.isValidMessage(message); // Get detailed validation report const report = await validator.getValidationReport(message); console.log('Validation report:', { isValid: report.isValid, missingFields: report.missingFields, invalidFields: report.invalidFields, hasValidSignature: report.hasValidSignature, errors: report.errors, warnings: report.warnings }); ``` ### Custom Transformers Transform raw messages into enhanced domain objects: ```typescript import { transformPost, transformComment, transformCell } from '@opchan/core'; // Transform post with votes and comments const enhancedPost = await transformPost(postMessage); // Transform comment with votes const enhancedComment = await transformComment(commentMessage); // Transform cell with stats const enhancedCell = await transformCell(cellMessage); ``` ### Network Configuration Configure Waku network parameters: ```typescript const client = new OpChanClient({ wakuConfig: { contentTopic: '/my-app/1/messages/proto', reliableChannelId: 'my-app-messages', // Additional Waku options... } }); ``` ## Best Practices 1. **Always open database before use**: Call `client.database.open()` on initialization 2. **Listen for messages**: Set up `onMessageReceived` listener to stay synchronized 3. **Handle delegation expiry**: Check delegation status and refresh when needed 4. **Validate user permissions**: Use `ForumActions` validation or implement custom checks 5. **Use transformers for UI**: Transform raw messages to enhanced types for rendering 6. **Monitor network health**: Subscribe to health changes for connection indicators 7. **Implement optimistic UI**: Mark items as pending during network operations 8. **Cache identity lookups**: Use `UserIdentityService` for efficient identity resolution ## Troubleshooting ### Messages not appearing - **Check delegation**: Ensure delegation is valid with `delegation.getStatus()` - **Verify network**: Monitor `onHealthChange` for connectivity issues - **Check validation**: Messages may be rejected if signature is invalid ### Database not persisting - **Call open()**: Ensure `database.open()` was called before use - **Check browser support**: IndexedDB must be available - **Clear cache**: Try `database.clearAll()` to reset ### Identity not resolving - **Set public client**: Call `userIdentityService.setPublicClient(publicClient)` for ENS - **Force refresh**: Use `getIdentity(address, { fresh: true })` to bypass cache - **Check network**: ENS resolution requires network connectivity ## License MIT --- **Built for decentralized communities** 🌐