@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
npm install @opchan/core
Quick Start
1. Initialize Client
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
// 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)
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
// 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
// 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
- Outbound: User action → Sign with delegated key → Store in LocalDatabase → Send via Waku
- 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.
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:
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.
async delegate(
address: `0x${string}`,
duration: '7days' | '30days',
signFunction: (message: string) => Promise<string>
): Promise<boolean>
Example:
const success = await client.delegation.delegate(
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
'7days',
async (msg) => await wallet.signMessage(msg)
);
delegateAnonymous(duration)
Create an anonymous delegation (no wallet required).
async delegateAnonymous(
duration: '7days' | '30days' = '7days'
): Promise<string>
Returns a session ID for the anonymous user.
signMessage(message)
Sign a message with the delegated browser key.
async signMessage(
message: UnsignedMessage
): Promise<OpchanMessage | null>
verify(message)
Verify a signed message's authenticity.
async verify(message: OpchanMessage): Promise<boolean>
getStatus(currentAddress?)
Get current delegation status.
async getStatus(
currentAddress?: string
): Promise<DelegationFullStatus>
Returns:
interface DelegationFullStatus {
hasDelegation: boolean;
isValid: boolean;
timeRemaining?: number;
publicKey?: string;
address?: `0x${string}`;
proof?: DelegationProof;
}
clear()
Clear stored delegation.
async clear(): Promise<void>
LocalDatabase
IndexedDB-backed local storage with in-memory caching.
Properties
cache - In-memory cache of all content
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.
async open(): Promise<void>
applyMessage(message)
Apply an incoming message to the database.
async applyMessage(message: unknown): Promise<boolean>
Returns true if message was newly processed and stored.
storeUser(user) / loadUser() / clearUser()
Persist user authentication state.
async storeUser(user: User): Promise<void>
async loadUser(): Promise<User | null>
async clearUser(): Promise<void>
storeDelegation(delegation) / loadDelegation() / clearDelegation()
Persist delegation information.
async storeDelegation(delegation: DelegationInfo): Promise<void>
async loadDelegation(): Promise<DelegationInfo | null>
async clearDelegation(): Promise<void>
markPending(id) / clearPending(id) / isPending(id)
Track pending message synchronization.
markPending(id: string): void
clearPending(id: string): void
isPending(id: string): boolean
onPendingChange(listener: () => void): () => void
addBookmark(bookmark) / removeBookmark(id)
Manage bookmarks.
async addBookmark(bookmark: Bookmark): Promise<void>
async removeBookmark(bookmarkId: string): Promise<void>
async getUserBookmarks(userId: string): Promise<Bookmark[]>
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).
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.
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.
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.
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).
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.
async getIdentity(
address: string,
opts?: { fresh?: boolean }
): Promise<UserIdentity | null>
Returns:
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).
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.
getDisplayName({
address: string,
ensName?: string | null,
displayPreference?: EDisplayPreference
}): string
subscribe(listener)
Subscribe to identity changes.
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.
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.
async sendMessage(
message: OpchanMessage,
statusCallback?: (status: MessageStatus) => void
): Promise<void>
onMessageReceived(callback)
Subscribe to incoming messages.
onMessageReceived(
callback: (message: OpchanMessage) => void
): () => void
onHealthChange(callback)
Monitor network health.
onHealthChange(
callback: (isHealthy: boolean) => void
): () => void
onSyncStatus(callback)
Monitor synchronization status.
onSyncStatus(
callback: (status: SyncStatus) => void
): () => void
Type Definitions
Core Types
// 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
// 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
// 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
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 => `<div>${post.title}</div>`)
.join('');
}
Pattern 2: Identity Resolution
// 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
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
// 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
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:
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:
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:
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:
const client = new OpChanClient({
wakuConfig: {
contentTopic: '/my-app/1/messages/proto',
reliableChannelId: 'my-app-messages',
// Additional Waku options...
}
});
Best Practices
- Always open database before use: Call
client.database.open()on initialization - Listen for messages: Set up
onMessageReceivedlistener to stay synchronized - Handle delegation expiry: Check delegation status and refresh when needed
- Validate user permissions: Use
ForumActionsvalidation or implement custom checks - Use transformers for UI: Transform raw messages to enhanced types for rendering
- Monitor network health: Subscribe to health changes for connection indicators
- Implement optimistic UI: Mark items as pending during network operations
- Cache identity lookups: Use
UserIdentityServicefor efficient identity resolution
Troubleshooting
Messages not appearing
- Check delegation: Ensure delegation is valid with
delegation.getStatus() - Verify network: Monitor
onHealthChangefor 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 🌐