chore(core): docs

This commit is contained in:
Danish Arora 2025-12-12 15:08:52 -05:00
parent 0bd7702395
commit aeba9823a7
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
9 changed files with 6408 additions and 45 deletions

88
package-lock.json generated
View File

@ -8014,17 +8014,17 @@
}
},
"node_modules/@waku/core": {
"version": "0.0.41-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.41-339e26e.0.tgz",
"integrity": "sha512-SgD/ne6F9ib8P71xUsohSdHAIAcU6HyQGvyzPKJXu60szLbh6dBXb/nouln9druEzxjULOYosBYMSP9cv5feFA==",
"version": "0.0.41-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.41-3e3c511.0.tgz",
"integrity": "sha512-b4F5CfTzMhLhO6QfwtSamtutoivGIG2b3qoBOoDQt5j2fUXZYm/lTmPP+2hvXNagnkgHG+8zs7HdWiiW72GMbg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@libp2p/ping": "2.0.35",
"@noble/hashes": "^1.3.2",
"@waku/enr": "0.0.34-339e26e.0",
"@waku/interfaces": "0.0.35-339e26e.0",
"@waku/proto": "0.0.16-339e26e.0",
"@waku/utils": "0.0.28-339e26e.0",
"@waku/enr": "0.0.34-3e3c511.0",
"@waku/interfaces": "0.0.35-3e3c511.0",
"@waku/proto": "0.0.16-3e3c511.0",
"@waku/utils": "0.0.28-3e3c511.0",
"debug": "^4.3.4",
"it-all": "^3.0.4",
"it-length-prefixed": "^9.0.4",
@ -8079,16 +8079,16 @@
}
},
"node_modules/@waku/discovery": {
"version": "0.0.14-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/discovery/-/discovery-0.0.14-339e26e.0.tgz",
"integrity": "sha512-MKknncK6gs/Y+g58tFxkifbOITLvy+RvLHn/eLnXQXLX0r+hs6j820wTd3HivsqlZ9xXjbxI5h/Mvus9Ycefrw==",
"version": "0.0.14-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/discovery/-/discovery-0.0.14-3e3c511.0.tgz",
"integrity": "sha512-tCVDjoxZdvJH0fMkmXBY5BlKTStc3NeyklR3H4JaKeUtFJyKqH7yz1V1zRVUhvPL9oXNVNqmXusXd/yRlVmfuQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@waku/core": "0.0.41-339e26e.0",
"@waku/enr": "0.0.34-339e26e.0",
"@waku/interfaces": "0.0.35-339e26e.0",
"@waku/proto": "0.0.16-339e26e.0",
"@waku/utils": "0.0.28-339e26e.0",
"@waku/core": "0.0.41-3e3c511.0",
"@waku/enr": "0.0.34-3e3c511.0",
"@waku/interfaces": "0.0.35-3e3c511.0",
"@waku/proto": "0.0.16-3e3c511.0",
"@waku/utils": "0.0.28-3e3c511.0",
"debug": "^4.3.4",
"dns-over-http-resolver": "^3.0.8",
"hi-base32": "^0.5.1",
@ -8099,9 +8099,9 @@
}
},
"node_modules/@waku/enr": {
"version": "0.0.34-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.34-339e26e.0.tgz",
"integrity": "sha512-AVo2Eg30mid1tb+ba+0ivrVLfZpuQvkDrR0gtKJhCCpDQ+8Dg7RFlqBZvD3FQyU7nj5jtwXVP+p0rMf94F2wwg==",
"version": "0.0.34-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.34-3e3c511.0.tgz",
"integrity": "sha512-8hMssbHuZAropSzD1C14AEYxWPLgI/g6cbypSXQER6exg6DA6JCbt2B9THZb30uHm9ReYs9RCl2AfxYFkLRupA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@ethersproject/rlp": "^5.7.0",
@ -8109,7 +8109,7 @@
"@libp2p/peer-id": "5.1.7",
"@multiformats/multiaddr": "^12.0.0",
"@noble/secp256k1": "^1.7.1",
"@waku/utils": "0.0.28-339e26e.0",
"@waku/utils": "0.0.28-3e3c511.0",
"debug": "^4.3.4",
"js-sha3": "^0.9.2"
},
@ -8153,18 +8153,18 @@
}
},
"node_modules/@waku/interfaces": {
"version": "0.0.35-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.35-339e26e.0.tgz",
"integrity": "sha512-7if75dK/RFF13ey9v/1gnrvR/WHZ3JogCmhWGtFp3q34cA1cyfHu7l66eGarVVHbwdSgBSVSH6fM8YFMsoacDA==",
"version": "0.0.35-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.35-3e3c511.0.tgz",
"integrity": "sha512-MIOozCY/sk2yg5yukLBTZyubC9uS0S7qd5fnP5CS/l5c/jd52AMMCNySOZj7XPB9hngmxv2ykboQuX4W29OX/g==",
"license": "MIT OR Apache-2.0",
"engines": {
"node": ">=22"
}
},
"node_modules/@waku/proto": {
"version": "0.0.16-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.16-339e26e.0.tgz",
"integrity": "sha512-jPHKCBt1HkBHenXO2kbRADkwqYbgVDdGalTxkHwNrWFFVK8pRTgG5VAqtSmw6yiba87P5ErstuKrDnO8ycFRjA==",
"version": "0.0.16-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.16-3e3c511.0.tgz",
"integrity": "sha512-AWqAI73wCHO6Z9RL0MjB2Kz7+FxFTjSLOg+OH/h6Yj3YRlsEixvdCH8rn+B1Kn+Fbqd0JiJtn59BZzRPVop3xw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"protons-runtime": "^5.4.0"
@ -8174,9 +8174,9 @@
}
},
"node_modules/@waku/sdk": {
"version": "0.0.37-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/sdk/-/sdk-0.0.37-339e26e.0.tgz",
"integrity": "sha512-rIoJjoFYinY8u4OHl4jdoCD9mAhVLklLm9WxWELp8nry9mWCoST68MJ4wFvV6ZMJPXwALf/Fx5g3Pz2DiweK4g==",
"version": "0.0.37-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/sdk/-/sdk-0.0.37-3e3c511.0.tgz",
"integrity": "sha512-If5KsDiatpf0Od0Utxsr+7TXFcnbagRuIcevLylZ5h1whIPIiXmB+zHK3ib8o4YDTY0jfD6oMBem/YUite0Dsw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@chainsafe/libp2p-noise": "16.1.3",
@ -8187,12 +8187,12 @@
"@libp2p/websockets": "9.2.16",
"@noble/hashes": "^1.3.3",
"@types/lodash.debounce": "^4.0.9",
"@waku/core": "0.0.41-339e26e.0",
"@waku/discovery": "0.0.14-339e26e.0",
"@waku/interfaces": "0.0.35-339e26e.0",
"@waku/proto": "0.0.16-339e26e.0",
"@waku/sds": "0.0.9-339e26e.0",
"@waku/utils": "0.0.28-339e26e.0",
"@waku/core": "0.0.41-3e3c511.0",
"@waku/discovery": "0.0.14-3e3c511.0",
"@waku/interfaces": "0.0.35-3e3c511.0",
"@waku/proto": "0.0.16-3e3c511.0",
"@waku/sds": "0.0.9-3e3c511.0",
"@waku/utils": "0.0.28-3e3c511.0",
"libp2p": "2.8.11",
"lodash.debounce": "^4.0.8"
},
@ -8201,15 +8201,15 @@
}
},
"node_modules/@waku/sds": {
"version": "0.0.9-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/sds/-/sds-0.0.9-339e26e.0.tgz",
"integrity": "sha512-eEd8Co++8ayCid6XEQ2ex55SoMxiRJ6ZvWLz0GjhUKGhApjXZlU6cnQ8nnRpPnVCCuRvS2EU1oo8JCOHBduZHQ==",
"version": "0.0.9-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/sds/-/sds-0.0.9-3e3c511.0.tgz",
"integrity": "sha512-+urgHIQxViPcOBHNEpGWHo8a1GFNRXGIV5W2Eev3hnMFL05xQ/LrrEUOyBqW3kVnBMjhN7t/7P9HR//KAJNJiw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@libp2p/interface": "2.10.4",
"@noble/hashes": "^1.7.1",
"@waku/proto": "0.0.16-339e26e.0",
"@waku/utils": "0.0.28-339e26e.0",
"@waku/proto": "0.0.16-3e3c511.0",
"@waku/utils": "0.0.28-3e3c511.0",
"chai": "^5.1.2",
"lodash": "^4.17.21"
},
@ -8233,13 +8233,13 @@
}
},
"node_modules/@waku/utils": {
"version": "0.0.28-339e26e.0",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.28-339e26e.0.tgz",
"integrity": "sha512-lzFcCN8xj3IN6JwbUdH3zc9FLwS6UQu775zP+RM8PnR5bMNHED8dlKR1fovZXfoggCIU+KnFwgITH+HhBEcV9w==",
"version": "0.0.28-3e3c511.0",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.28-3e3c511.0.tgz",
"integrity": "sha512-UcccO+L2zbbjL+kzCqIKfeoMkIpHSTipRBlEn8Tnl/MPK6XW+T+LIGE82fscQ/Vj+VEobGKU9AephSuOIZ+ZvQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@noble/hashes": "^1.3.2",
"@waku/interfaces": "0.0.35-339e26e.0",
"@waku/interfaces": "0.0.35-3e3c511.0",
"chai": "^4.3.10",
"debug": "^4.3.4",
"uint8arrays": "^5.0.1"
@ -19470,7 +19470,7 @@
"dependencies": {
"@noble/ed25519": "^2.2.3",
"@noble/hashes": "^1.8.0",
"@waku/sdk": "0.0.37-339e26e.0",
"@waku/sdk": "0.0.37-3e3c511.0",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.2",
"uuid": "^11.1.0",

View File

@ -0,0 +1,263 @@
# OpChan Core - Quick Start
Get up and running with OpChan in 5 minutes.
---
## Installation
```bash
npm install @opchan/core
```
---
## Minimal Working Example
```typescript
import { OpChanClient, EVerificationStatus, EDisplayPreference } from '@opchan/core';
// 1. Create client
const client = new OpChanClient({
wakuConfig: {
contentTopic: '/opchan/1/messages/proto',
reliableChannelId: 'opchan-messages'
}
});
// 2. Open database
await client.database.open();
// 3. Listen for messages
client.messageManager.onMessageReceived(async (message) => {
await client.database.applyMessage(message);
console.log('Received:', message.type);
});
// 4. Start anonymous session
const sessionId = await client.delegation.delegateAnonymous('7days');
const user = {
address: sessionId,
displayName: 'Anonymous',
displayPreference: EDisplayPreference.WALLET_ADDRESS,
verificationStatus: EVerificationStatus.ANONYMOUS
};
// 5. Create a post
const result = await client.forumActions.createPost(
{
cellId: 'general',
title: 'Hello World',
content: 'My first post!',
currentUser: user,
isAuthenticated: true
},
() => console.log('Post created!')
);
// 6. Read posts
const posts = Object.values(client.database.cache.posts);
console.log('Posts:', posts.length);
```
---
## Essential Operations
### Create Content
```typescript
// Post
await client.forumActions.createPost({
cellId: 'general',
title: 'Title',
content: 'Content',
currentUser: user,
isAuthenticated: true
}, () => {});
// Comment
await client.forumActions.createComment({
postId: 'post-id',
content: 'Great post!',
currentUser: user,
isAuthenticated: true
}, () => {});
// Vote
await client.forumActions.vote({
targetId: 'post-id',
isUpvote: true,
currentUser: user,
isAuthenticated: true
}, () => {});
```
### Read Content
```typescript
// All cells
const cells = Object.values(client.database.cache.cells);
// All posts
const posts = Object.values(client.database.cache.posts);
// Posts in a cell
const cellPosts = posts.filter(p => p.cellId === 'cell-id');
// Comments on a post
const comments = Object.values(client.database.cache.comments)
.filter(c => c.postId === 'post-id');
// Votes on a post
const votes = Object.values(client.database.cache.votes)
.filter(v => v.targetId === 'post-id');
```
### Network Status
```typescript
// Check connection
const isReady = client.messageManager.isReady;
// Listen for changes
client.messageManager.onHealthChange((isHealthy) => {
console.log(isHealthy ? 'Connected' : 'Offline');
});
```
---
## Complete Starter Template
```typescript
import { OpChanClient, EVerificationStatus, EDisplayPreference, type User } from '@opchan/core';
class MyForumApp {
private client: OpChanClient;
private currentUser: User | null = null;
async init() {
// Initialize
this.client = new OpChanClient({
wakuConfig: {
contentTopic: '/opchan/1/messages/proto',
reliableChannelId: 'opchan-messages'
}
});
await this.client.database.open();
// Set up listeners
client.messageManager.onMessageReceived(async (message) => {
await client.database.applyMessage(message);
this.onNewMessage(message);
});
// Start session
const sessionId = await client.delegation.delegateAnonymous('7days');
this.currentUser = {
address: sessionId,
displayName: 'Anonymous',
displayPreference: EDisplayPreference.WALLET_ADDRESS,
verificationStatus: EVerificationStatus.ANONYMOUS
};
this.render();
}
async createPost(cellId: string, title: string, content: string) {
if (!this.currentUser) return;
const result = await this.client.forumActions.createPost(
{ cellId, title, content, currentUser: this.currentUser, isAuthenticated: true },
() => this.render()
);
if (!result.success) {
console.error('Failed:', result.error);
}
}
private onNewMessage(message: any) {
console.log('New message:', message.type);
this.render();
}
private render() {
const posts = Object.values(this.client.database.cache.posts);
console.log('Posts:', posts.length);
// Update your UI here
}
}
// Start
const app = new MyForumApp();
await app.init();
```
---
## Next Steps
- **Full Documentation**: See [README.md](./README.md)
- **Getting Started Guide**: See [docs/getting-started.md](./docs/getting-started.md)
- **API Reference**: See [docs/api-reference.md](./docs/api-reference.md)
- **Architecture**: See [docs/architecture.md](./docs/architecture.md)
- **Sample Apps**: See [docs/sample-apps.md](./docs/sample-apps.md)
---
## Common Patterns
### Wallet Connection
```typescript
import { signMessage } from 'viem/accounts';
await client.delegation.delegate(
walletAddress,
'7days',
async (msg) => await signMessage({ message: msg, account: walletAddress })
);
```
### ENS Resolution
```typescript
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
const publicClient = createPublicClient({
chain: mainnet,
transport: http()
});
client.userIdentityService.setPublicClient(publicClient);
const identity = await client.userIdentityService.getIdentity(address);
console.log('ENS name:', identity?.ensName);
```
### Relevance Scoring
```typescript
import { transformPost } from '@opchan/core';
const post = await transformPost(postMessage);
console.log('Relevance score:', post?.relevanceScore);
```
### Bookmarks
```typescript
import { BookmarkService } from '@opchan/core';
const bookmarkService = new BookmarkService();
await bookmarkService.addPostBookmark(post, userId, cellId);
```
---
**Ready to build!** 🚀

1230
packages/core/README.md Normal file

File diff suppressed because it is too large Load Diff

327
packages/core/docs/INDEX.md Normal file
View File

@ -0,0 +1,327 @@
# OpChan Core Documentation Index
Complete documentation for `@opchan/core` - the foundational SDK for building decentralized forums.
---
## Quick Links
- **[Quick Start](../QUICK_START.md)** - Get started in 5 minutes
- **[README](../README.md)** - Package overview and installation
- **[Getting Started Guide](./getting-started.md)** - Comprehensive tutorial
- **[API Reference](./api-reference.md)** - Complete API documentation
- **[Architecture Guide](./architecture.md)** - System design and internals
- **[Sample Applications](./sample-apps.md)** - Full working examples
---
## Documentation Overview
### For Beginners
Start here if you're new to OpChan:
1. **[Quick Start](../QUICK_START.md)** (5 min)
- Minimal working example
- Essential operations
- Complete starter template
2. **[README](../README.md)** (15 min)
- Package overview
- Core concepts
- Quick start guide
- Usage patterns
3. **[Getting Started Guide](./getting-started.md)** (30 min)
- Step-by-step tutorial
- All features explained
- Best practices
- Complete application skeleton
### For Developers
Deep dive into building applications:
4. **[API Reference](./api-reference.md)** (Reference)
- Complete API documentation
- All classes and methods
- Type definitions
- Usage examples
5. **[Sample Applications](./sample-apps.md)** (Templates)
- Minimal forum
- Anonymous-first forum
- Moderation dashboard
- Identity explorer
- Bookmark manager
- Real-time feed
- CLI tool
### For Advanced Users
Understanding the system:
6. **[Architecture Guide](./architecture.md)** (Deep Dive)
- System overview
- Core architecture
- Data flow
- Key subsystems
- Cryptographic design
- Storage strategy
- Network layer
- Design patterns
- Performance considerations
- Security model
---
## By Topic
### Getting Started
- [Installation](../README.md#installation)
- [Quick Start](../QUICK_START.md)
- [Basic Setup](./getting-started.md#1-install-and-basic-setup)
- [First Application](../README.md#quick-start)
### Client Setup
- [OpChanClient Configuration](./api-reference.md#opchanclient)
- [Opening Database](./getting-started.md#1-install-and-basic-setup)
- [Message Synchronization](./getting-started.md#2-message-synchronization)
- [Network Monitoring](./getting-started.md#19-network-state-management)
### Authentication & Identity
- [Key Delegation (Wallet)](./getting-started.md#3-key-delegation--wallet-users)
- [Key Delegation (Anonymous)](./getting-started.md#4-key-delegation--anonymous-users)
- [Identity Resolution](./getting-started.md#11-identity-resolution)
- [User Profiles](./getting-started.md#12-user-profiles)
- [ENS Integration](./api-reference.md#useridentityservice)
### Content Management
- [Creating Cells](./getting-started.md#5-creating-content--cells)
- [Creating Posts](./getting-started.md#6-creating-content--posts)
- [Creating Comments](./getting-started.md#7-creating-content--comments)
- [Voting](./getting-started.md#8-voting)
- [Reading Content](./getting-started.md#10-reading-cached-data)
### Moderation
- [Moderating Posts](./getting-started.md#9-moderation-cell-owner-only)
- [Moderating Comments](./getting-started.md#9-moderation-cell-owner-only)
- [Moderating Users](./getting-started.md#9-moderation-cell-owner-only)
- [Moderation Dashboard Example](./sample-apps.md#moderation-dashboard)
### Advanced Features
- [Relevance Scoring](./getting-started.md#14-relevance-scoring)
- [Bookmarks](./getting-started.md#15-bookmarks)
- [Pending State Management](./getting-started.md#16-pending-state-management)
- [Message Validation](./getting-started.md#18-message-validation)
### Architecture & Internals
- [System Overview](./architecture.md#system-overview)
- [Core Architecture](./architecture.md#core-architecture)
- [Data Flow](./architecture.md#data-flow)
- [Delegation Subsystem](./architecture.md#1-delegation-subsystem)
- [Storage Subsystem](./architecture.md#2-storage-subsystem)
- [Identity Subsystem](./architecture.md#3-identity-subsystem)
- [Relevance Subsystem](./architecture.md#4-relevance-subsystem)
- [Cryptographic Design](./architecture.md#cryptographic-design)
- [Network Layer](./architecture.md#network-layer)
- [Security Model](./architecture.md#security-model)
---
## API Documentation
### Core Classes
- **[OpChanClient](./api-reference.md#opchanclient)** - Main entry point
- **[DelegationManager](./api-reference.md#delegationmanager)** - Key delegation
- **[LocalDatabase](./api-reference.md#localdatabase)** - Storage and caching
- **[ForumActions](./api-reference.md#forumactions)** - Content operations
- **[UserIdentityService](./api-reference.md#useridentityservice)** - Identity resolution
- **[RelevanceCalculator](./api-reference.md#relevancecalculator)** - Content scoring
- **[MessageManager](./api-reference.md#messagemanager)** - Network layer
- **[BookmarkService](./api-reference.md#bookmarkservice)** - Bookmarks
- **[MessageValidator](./api-reference.md#messagevalidator)** - Validation
### Type Definitions
- [Core Types](./api-reference.md#type-definitions)
- [User Types](./api-reference.md#user)
- [Message Types](./api-reference.md#message-types)
- [Forum Types](./api-reference.md#extended-forum-types)
- [Delegation Types](./api-reference.md#delegationproof)
---
## Sample Application
### Complete Production-Ready Template
**[Complete Forum Application](./sample-apps.md)**
A comprehensive, production-ready forum demonstrating all features:
- ✅ Client initialization and configuration
- ✅ Anonymous & wallet authentication
- ✅ Session persistence across reloads
- ✅ Content creation (cells, posts, comments)
- ✅ Voting system (upvote/downvote)
- ✅ Identity resolution (ENS, call signs)
- ✅ Real-time message synchronization
- ✅ Relevance scoring with breakdown
- ✅ Moderation tools (cell owners)
- ✅ Bookmark management
- ✅ Network health monitoring
- ✅ Optimistic UI with pending states
- ✅ Proper error handling
- ✅ Clean architecture
**Use this as a foundation for building your decentralized forum application.**
---
## Usage Patterns
### Common Workflows
- [Anonymous User Flow](../README.md#anonymous-user-flow)
- [Wallet User Flow](../README.md#wallet-user-flow)
- [Creating Content](../README.md#pattern-1-complete-vanilla-js-application)
- [Identity Resolution](../README.md#pattern-2-identity-resolution)
- [Content Scoring](../README.md#pattern-3-content-scoring)
- [Moderation](../README.md#pattern-4-moderation)
- [Bookmarks](../README.md#pattern-5-bookmarks)
### Best Practices
- [Database Management](./getting-started.md#21-best-practices)
- [Message Handling](./getting-started.md#21-best-practices)
- [Network Monitoring](./getting-started.md#21-best-practices)
- [Error Handling](./getting-started.md#22-error-handling)
- [Performance Optimization](./architecture.md#performance-considerations)
- [Security Considerations](./architecture.md#security-model)
---
## Additional Resources
### External Links
- **GitHub Repository**: [opchan](https://github.com/your-org/opchan)
- **React Package**: [@opchan/react](../../react/README.md)
- **Waku Protocol**: [Waku Docs](https://docs.waku.org/)
- **Viem**: [Viem Docs](https://viem.sh/)
### Related Documentation
- [React Package Docs](../../react/README.md)
- [React Getting Started](../../react/docs/getting-started.md)
- [Project README](../../../README.md)
---
## Quick Reference
### Essential Imports
```typescript
// Core client
import { OpChanClient } from '@opchan/core';
// Types
import {
EVerificationStatus,
EDisplayPreference,
type User,
type Cell,
type Post,
type Comment,
type Bookmark
} from '@opchan/core';
// Utilities
import {
transformPost,
transformComment,
transformCell,
BookmarkService,
MessageValidator
} from '@opchan/core';
// Services
import {
DelegationManager,
LocalDatabase,
ForumActions,
UserIdentityService,
RelevanceCalculator
} from '@opchan/core';
```
### Most Common Operations
```typescript
// Initialize
const client = new OpChanClient({ wakuConfig });
await client.database.open();
// Anonymous session
const sessionId = await client.delegation.delegateAnonymous('7days');
// Create post
await client.forumActions.createPost(params, callback);
// Read posts
const posts = Object.values(client.database.cache.posts);
// Get identity
const identity = await client.userIdentityService.getIdentity(address);
```
---
## Troubleshooting
### Common Issues
- [Messages not appearing](../README.md#messages-not-appearing)
- [Database not persisting](../README.md#database-not-persisting)
- [Identity not resolving](../README.md#identity-not-resolving)
- [Delegation expired](./getting-started.md#22-error-handling)
- [Network errors](./getting-started.md#22-error-handling)
### Debug Tips
1. Check delegation status: `await client.delegation.getStatus()`
2. Monitor network health: `client.messageManager.onHealthChange()`
3. Validate messages: `await validator.isValidMessage()`
4. Check database: `await client.database.open()`
5. Review logs: Enable verbose logging in browser console
---
## Contributing
Documentation improvements are welcome! Please see the main project README for contribution guidelines.
---
## License
MIT License - See [LICENSE](../../../LICENSE) for details.
---
**Last Updated**: December 2024
**Version**: 1.0.3
---

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,967 @@
# @opchan/core Architecture Guide
Deep dive into the architecture, design patterns, and implementation details of the OpChan Core SDK.
---
## Table of Contents
1. [System Overview](#system-overview)
2. [Core Architecture](#core-architecture)
3. [Data Flow](#data-flow)
4. [Key Subsystems](#key-subsystems)
5. [Cryptographic Design](#cryptographic-design)
6. [Storage Strategy](#storage-strategy)
7. [Network Layer](#network-layer)
8. [Design Patterns](#design-patterns)
9. [Performance Considerations](#performance-considerations)
10. [Security Model](#security-model)
---
## System Overview
OpChan Core is a decentralized forum infrastructure built on three pillars:
1. **Cryptographic Identity** - Ed25519 key delegation with wallet authorization
2. **P2P Messaging** - Waku protocol for decentralized communication
3. **Local-First Storage** - IndexedDB with in-memory caching
### Design Philosophy
- **Local-First**: All data persisted locally, network as synchronization mechanism
- **Optimistic UI**: Immediate feedback, eventual consistency
- **Privacy-Preserving**: No centralized servers, peer-to-peer architecture
- **Framework-Agnostic**: Pure TypeScript library, no UI framework dependencies
---
## Core Architecture
```
┌───────────────────────────────────────────────────────────────────┐
│ OpChanClient │
│ Entry point orchestrating all subsystems │
└───────────────────────────────────────────────────────────────────┘
┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ MessageManager │ │ LocalDatabase │ │ DelegationManager│
│ │ │ │ │ │
│ - WakuNode │ │ - IndexedDB │ │ - Key Generation │
│ - Reliable │ │ - In-memory │ │ - Signing │
│ Messaging │ │ Cache │ │ - Verification │
│ - Health Monitor │ │ - Validation │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Service Layer │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ForumActions │ │ Identity │ │ Relevance │ │
│ │ │ │ Service │ │ Calculator │ │
│ │ - Create │ │ │ │ │ │
│ │ - Moderate │ │ - ENS Lookup │ │ - Scoring │ │
│ │ - Vote │ │ - Profiles │ │ - Time Decay │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
### Component Responsibilities
#### OpChanClient
- **Role**: Facade pattern - single entry point for all operations
- **Responsibilities**:
- Instantiate and wire all services
- Configure environment
- Initialize message manager with Waku config
- Provide unified API surface
#### MessageManager
- **Role**: Network abstraction layer
- **Responsibilities**:
- Manage Waku node lifecycle
- Handle message sending/receiving
- Monitor network health
- Implement reliable messaging (retries, acknowledgments)
- Manage subscriptions to content topics
#### LocalDatabase
- **Role**: Persistence and caching layer
- **Responsibilities**:
- IndexedDB operations (async)
- In-memory cache (sync)
- Message validation
- Deduplication
- State management (pending, syncing)
#### DelegationManager
- **Role**: Cryptographic signing system
- **Responsibilities**:
- Generate Ed25519 keypairs
- Create wallet-authorized delegations
- Sign messages with browser keys
- Verify message signatures
- Verify delegation proofs
---
## Data Flow
### Outbound Message Flow (Creating Content)
```
1. User Action
└─> ForumActions.createPost()
├─> Validate permissions (wallet or anonymous)
├─> Create unsigned message with UUID
├─> DelegationManager.signMessage()
│ ├─> Load cached delegation
│ ├─> Check expiry
│ ├─> Sign with browser private key (Ed25519)
│ └─> Attach signature + browserPubKey + delegationProof
├─> LocalDatabase.applyMessage()
│ ├─> Validate signature
│ ├─> Check for duplicates
│ ├─> Store in cache
│ └─> Persist to IndexedDB
├─> LocalDatabase.markPending()
├─> Call updateCallback() [UI refresh]
└─> MessageManager.sendMessage()
├─> Encode with protobuf
├─> Send via Waku
└─> Wait for acknowledgment
└─> LocalDatabase.clearPending()
```
### Inbound Message Flow (Receiving Content)
```
1. Waku Network
└─> WakuNodeManager receives message
├─> Decode protobuf
├─> ReliableMessaging handles deduplication
└─> MessageService.onMessageReceived() callback
└─> LocalDatabase.applyMessage()
├─> MessageValidator.isValidMessage()
│ ├─> Check required fields
│ ├─> Verify signature (Ed25519)
│ ├─> If delegationProof:
│ │ ├─> Verify auth message format
│ │ ├─> Verify wallet signature (viem)
│ │ └─> Check expiry (optional)
│ └─> If anonymous:
│ └─> Verify session ID format (UUID)
├─> Check duplicate (type:id:timestamp key)
├─> Store in appropriate cache collection
│ ├─> cells[id]
│ ├─> posts[id]
│ ├─> comments[id]
│ ├─> votes[targetId:author]
│ ├─> moderations[key]
│ └─> userIdentities[address]
├─> Persist to IndexedDB
├─> Update lastSync timestamp
└─> Return true (new message)
```
---
## Key Subsystems
### 1. Delegation Subsystem
**Purpose**: Reduce wallet signature prompts while maintaining cryptographic security.
**Components**:
- **DelegationManager** - Core delegation logic
- **DelegationStorage** - IndexedDB persistence
- **DelegationCrypto** - Ed25519 signing/verification
**Delegation Process (Wallet)**:
```typescript
// 1. Generate browser keypair
const keypair = DelegationCrypto.generateKeypair();
// { publicKey: string, privateKey: string }
// 2. Create authorization message
const authMessage = `
Authorize browser key:
Public Key: ${keypair.publicKey}
Wallet: ${walletAddress}
Expires: ${new Date(expiryTimestamp).toISOString()}
Nonce: ${nonce}
`;
// 3. Sign with wallet (happens once)
const walletSignature = await signFunction(authMessage);
// 4. Store delegation
const delegation: WalletDelegationInfo = {
authMessage,
walletSignature,
expiryTimestamp,
walletAddress,
browserPublicKey: keypair.publicKey,
browserPrivateKey: keypair.privateKey,
nonce
};
await DelegationStorage.store(delegation);
// 5. Subsequent messages signed with browser key
const signature = DelegationCrypto.signRaw(messageJson, privateKey);
```
**Delegation Process (Anonymous)**:
```typescript
// 1. Generate browser keypair
const keypair = DelegationCrypto.generateKeypair();
// 2. Generate session ID (UUID)
const sessionId = crypto.randomUUID();
// 3. Store anonymous delegation
const delegation: AnonymousDelegationInfo = {
sessionId,
browserPublicKey: keypair.publicKey,
browserPrivateKey: keypair.privateKey,
expiryTimestamp,
nonce
};
// 4. No wallet signature required
// User address = sessionId
```
**Verification Process**:
```typescript
// 1. Verify message signature
const messagePayload = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined,
delegationProof: undefined
});
const signatureValid = DelegationCrypto.verifyRaw(
messagePayload,
message.signature,
message.browserPubKey
);
if (!signatureValid) return false;
// 2. Verify delegation authorization
if (message.delegationProof) {
// Wallet user - verify delegation proof
const proofValid = await DelegationCrypto.verifyWalletSignature(
message.delegationProof.authMessage,
message.delegationProof.walletSignature,
message.delegationProof.walletAddress
);
// Check auth message contains browser key, wallet address, expiry
const authMessageValid =
message.delegationProof.authMessage.includes(message.browserPubKey) &&
message.delegationProof.authMessage.includes(message.author) &&
message.delegationProof.authMessage.includes(
message.delegationProof.expiryTimestamp.toString()
);
return proofValid && authMessageValid;
} else {
// Anonymous user - verify session ID format
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
message.author
);
}
```
---
### 2. Storage Subsystem
**Purpose**: Fast local access with persistent storage.
**Two-Tier Architecture**:
1. **In-Memory Cache** (Tier 1)
- Synchronous access
- Fast reads for UI rendering
- Volatile (resets on page reload)
2. **IndexedDB** (Tier 2)
- Asynchronous access
- Persistent across sessions
- Hydrates cache on startup
**IndexedDB Schema**:
```typescript
const schema = {
// Content stores (keyPath = 'id')
cells: { keyPath: 'id' },
posts: { keyPath: 'id' },
comments: { keyPath: 'id' },
// Votes (keyPath = 'key', composite: targetId:author)
votes: { keyPath: 'key' },
// Moderations (keyPath = 'key', composite varies by type)
moderations: { keyPath: 'key' },
// User identities (keyPath = 'address')
userIdentities: {
keyPath: 'address',
indexes: []
},
// Bookmarks (keyPath = 'id')
bookmarks: {
keyPath: 'id',
indexes: [{ name: 'by_userId', keyPath: 'userId' }]
},
// Auth/state stores (keyPath = 'key')
userAuth: { keyPath: 'key' },
delegation: { keyPath: 'key' },
uiState: { keyPath: 'key' },
meta: { keyPath: 'key' }
};
```
**Cache Synchronization**:
```
┌─────────────────────────────────────────────────────┐
│ Write Operation │
│ │
│ 1. Update in-memory cache (immediate) │
│ 2. Write to IndexedDB (async, fire-and-forget) │
│ 3. Notify listeners (for UI update) │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ Read Operation │
│ │
│ 1. Check in-memory cache (fast path) │
│ 2. If not found, return null/empty │
│ (IndexedDB only used for hydration on startup) │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ Startup Hydration │
│ │
│ 1. Open IndexedDB connection │
│ 2. Load all stores in parallel │
│ 3. Populate in-memory cache │
│ 4. Ready for use │
└─────────────────────────────────────────────────────┘
```
**Deduplication**:
Messages are deduplicated using a composite key:
```typescript
const messageKey = `${message.type}:${message.id}:${message.timestamp}`;
```
This allows the same logical message to be received multiple times without creating duplicates.
---
### 3. Identity Subsystem
**Purpose**: Resolve and cache user identities with ENS integration.
**Resolution Strategy**:
```
┌────────────────────────────────────────────────────┐
│ Identity Resolution Flow │
│ │
│ 1. Check LocalDatabase cache │
│ └─> If found and fresh (< 5 min): return
│ │
│ 2. Check if Ethereum address │
│ └─> If not (anonymous/UUID): return null │
│ │
│ 3. Resolve ENS via PublicClient │
│ ├─> Get ENS name │
│ ├─> Get ENS avatar │
│ └─> Cache result for 5 minutes │
│ │
│ 4. Build UserIdentity object │
│ ├─> address │
│ ├─> ensName │
│ ├─> ensAvatar │
│ ├─> callSign (from profile messages) │
│ ├─> displayPreference │
│ ├─> displayName (computed) │
│ ├─> verificationStatus (computed) │
│ └─> lastUpdated │
│ │
│ 5. Store in LocalDatabase │
│ │
│ 6. Return identity │
└────────────────────────────────────────────────────┘
```
**Display Name Resolution**:
```typescript
function getDisplayName(identity: UserIdentity): string {
// Priority 1: Call sign (if preference is CALL_SIGN)
if (
identity.callSign &&
identity.displayPreference === EDisplayPreference.CALL_SIGN
) {
return identity.callSign;
}
// Priority 2: ENS name
if (identity.ensName) {
return identity.ensName;
}
// Priority 3: Shortened address
return `${identity.address.slice(0, 6)}...${identity.address.slice(-4)}`;
}
```
**Profile Updates**:
User profile updates are broadcast as USER_PROFILE_UPDATE messages:
```typescript
{
type: MessageType.USER_PROFILE_UPDATE,
id: uuid(),
timestamp: Date.now(),
author: userAddress,
callSign: 'alice', // optional
displayPreference: EDisplayPreference.CALL_SIGN,
signature: '...',
browserPubKey: '...',
delegationProof: { ... }
}
```
These messages update the `userIdentities` cache, propagating changes across the network.
---
### 4. Relevance Subsystem
**Purpose**: Score content based on engagement, verification, time, and moderation.
**Scoring Algorithm**:
```typescript
interface RelevanceFactors {
base: 100;
engagement: {
upvoteWeight: 10;
commentWeight: 3;
};
verification: {
authorBonus: 20; // ENS verified author
upvoteBonus: 5; // Per ENS verified upvoter
commenterBonus: 10; // Per ENS verified commenter
};
timeDecay: {
halfLifeDays: 7;
formula: 'exponential'; // exp(-0.693 * days / halfLife)
};
moderation: {
penalty: 0.5; // 50% reduction if moderated
};
}
function calculateScore(
post: Post,
votes: Vote[],
comments: Comment[],
verifications: Map<address, boolean>,
moderations: Map<postId, Moderation>
): number {
// Base score
let score = 100;
// Engagement
const upvotes = votes.filter(v => v.value === 1).length;
const downvotes = votes.filter(v => v.value === -1).length;
score += (upvotes * 10) + (comments.length * 3);
// Verification bonuses
if (verifications.get(post.author)) {
score += 20; // Author ENS verified
}
const verifiedUpvoters = votes
.filter(v => v.value === 1 && verifications.get(v.author))
.length;
score += verifiedUpvoters * 5;
const verifiedCommenters = new Set(
comments
.map(c => c.author)
.filter(author => verifications.get(author))
).size;
score += verifiedCommenters * 10;
// Time decay (exponential)
const daysOld = (Date.now() - post.timestamp) / (1000 * 60 * 60 * 24);
const decay = Math.exp(-0.693 * daysOld / 7); // Half-life 7 days
score *= decay;
// Moderation penalty
if (moderations.has(post.id)) {
score *= 0.5;
}
return Math.max(0, score);
}
```
**Score Components Breakdown**:
```typescript
interface RelevanceScoreDetails {
baseScore: 100,
engagementScore: (upvotes * 10) + (comments * 3),
authorVerificationBonus: isENS ? 20 : 0,
verifiedUpvoteBonus: verifiedUpvoters * 5,
verifiedCommenterBonus: verifiedCommenters * 10,
timeDecayMultiplier: exp(-0.693 * daysOld / 7),
moderationPenalty: isModerated ? 0.5 : 1.0,
finalScore: (base + engagement + bonuses) * decay * modPenalty
}
```
---
## Cryptographic Design
### Key Hierarchy
```
Wallet Private Key (User's wallet, never exposed)
├─> Signs authorization message
│ └─> Stored in delegationProof.walletSignature
└─> Authorizes ─────────────────────────┐
Browser Private Key (Generated, stored locally)
│ │
├─> Signs all messages │
│ └─> Stored in message.signature │
│ │
└─> Public key: message.browserPubKey ───┘
```
### Cryptographic Primitives
**Ed25519** (via @noble/ed25519):
- Browser key generation
- Message signing
- Signature verification
**ECDSA** (via viem):
- Wallet signature verification
- ENS resolution
### Message Signature Structure
```typescript
// Unsigned message
const unsignedMessage = {
type: 'post',
id: 'abc123',
cellId: 'xyz789',
title: 'Hello',
content: 'World',
timestamp: 1234567890,
author: '0x...'
};
// Message to sign (excludes signature fields)
const messageToSign = JSON.stringify({
...unsignedMessage,
signature: undefined,
browserPubKey: undefined,
delegationProof: undefined
});
// Sign with browser private key
const signature = await ed25519.sign(
sha512(messageToSign),
browserPrivateKey
);
// Signed message
const signedMessage = {
...unsignedMessage,
signature: bytesToHex(signature),
browserPubKey: bytesToHex(browserPublicKey),
delegationProof: {
authMessage: '...',
walletSignature: '...',
expiryTimestamp: 1234567890,
walletAddress: '0x...'
}
};
```
### Verification Logic
```typescript
async function verifyMessage(message: OpchanMessage): Promise<boolean> {
// 1. Verify message signature with browser public key
const messagePayload = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined,
delegationProof: undefined
});
const signatureValid = await ed25519.verify(
hexToBytes(message.signature),
sha512(messagePayload),
hexToBytes(message.browserPubKey)
);
if (!signatureValid) return false;
// 2. Verify delegation authorization
if (message.delegationProof) {
// Wallet user - verify wallet signature on auth message
const { authMessage, walletSignature, walletAddress } = message.delegationProof;
const walletSigValid = await verifyMessage({
account: walletAddress,
message: authMessage,
signature: walletSignature
});
if (!walletSigValid) return false;
// Verify auth message contains browser key and expiry
if (!authMessage.includes(message.browserPubKey)) return false;
if (!authMessage.includes(walletAddress)) return false;
if (!authMessage.includes(message.delegationProof.expiryTimestamp.toString())) {
return false;
}
return true;
} else {
// Anonymous user - verify session ID is valid UUID
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
message.author
);
}
}
```
---
## Network Layer
### Waku Protocol Integration
**Architecture**:
```
┌─────────────────────────────────────────────────────┐
│ MessageManager │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ WakuNodeManager │ │
│ │ - Create Waku node │ │
│ │ - Connect to bootstrap peers │ │
│ │ - Monitor health │ │
│ │ - Emit health events │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ ReliableMessaging │ │
│ │ - Store & Forward protocol │ │
│ │ - Message deduplication │ │
│ │ - Acknowledgments │ │
│ │ - Retries │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ MessageService │ │
│ │ - Send messages │ │
│ │ - Receive subscriptions │ │
│ │ - Codec management (protobuf) │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
**Content Topics**:
Messages are published/subscribed via content topics:
```typescript
const contentTopic = '/opchan/1/messages/proto';
// All message types share the same content topic
// Filtering happens at application layer based on message.type
```
**Message Encoding**:
Messages are encoded with protobuf before transmission (handled by Waku SDK).
**Reliable Messaging**:
The `ReliableMessaging` layer provides:
1. **Store-and-Forward**: Messages cached for offline peers
2. **Deduplication**: Filter out duplicate receives
3. **Acknowledgments**: Confirm message delivery
4. **Retries**: Resend on failure
---
## Design Patterns
### 1. Singleton Pattern
**DelegationManager**, **LocalDatabase**, **MessageManager** are exported as singletons:
```typescript
// Singleton instance
export const delegationManager = new DelegationManager();
export const localDatabase = new LocalDatabase();
export default new DefaultMessageManager();
```
### 2. Facade Pattern
**OpChanClient** acts as a facade, providing a simplified interface:
```typescript
class OpChanClient {
// Aggregates all services
readonly delegation: DelegationManager;
readonly database: LocalDatabase;
readonly messageManager: DefaultMessageManager;
readonly forumActions: ForumActions;
// ...
}
```
### 3. Observer Pattern
Event subscriptions throughout:
```typescript
// MessageManager
messageManager.onMessageReceived(callback);
messageManager.onHealthChange(callback);
messageManager.onSyncStatus(callback);
// UserIdentityService
userIdentityService.subscribe(callback);
// LocalDatabase
database.onPendingChange(callback);
```
### 4. Strategy Pattern
**MessageValidator** validates different message types:
```typescript
class MessageValidator {
async isValidMessage(message: unknown): Promise<boolean> {
// Different validation logic per message type
switch (message.type) {
case MessageType.CELL:
return this.validateCell(message);
case MessageType.POST:
return this.validatePost(message);
// ...
}
}
}
```
### 5. Repository Pattern
**LocalDatabase** acts as a repository abstracting storage:
```typescript
interface Repository<T> {
getById(id: string): T | null;
getAll(): T[];
save(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
// LocalDatabase implements repository pattern for each entity type
```
---
## Performance Considerations
### 1. In-Memory Caching
All reads are synchronous from in-memory cache:
```typescript
// Fast - synchronous cache access
const posts = Object.values(client.database.cache.posts);
// No IndexedDB reads during normal operation
```
### 2. Lazy Identity Resolution
Identities resolved on-demand with caching:
```typescript
// First call: async ENS lookup
const identity1 = await getIdentity(address);
// Subsequent calls: cache hit (fast)
const identity2 = await getIdentity(address); // same address
```
### 3. Debouncing
Identity lookups are debounced to avoid redundant calls:
```typescript
// Multiple rapid calls
getIdentity(address); // Starts timer
getIdentity(address); // Resets timer
getIdentity(address); // Resets timer
// Only executes once after 100ms
```
### 4. Batch Operations
IndexedDB writes are batched:
```typescript
// Single transaction for multiple writes
const tx = db.transaction(['cells', 'posts'], 'readwrite');
tx.objectStore('cells').put(cell);
tx.objectStore('posts').put(post);
await tx.complete;
```
### 5. Optimistic UI
Immediate feedback with pending states:
```typescript
// 1. Write to cache immediately
cache.posts[postId] = post;
// 2. Mark as pending
markPending(postId);
// 3. Update UI (shows post with "syncing" badge)
updateUI();
// 4. Send to network (async)
sendMessage(post);
// 5. Clear pending when confirmed
clearPending(postId);
```
---
## Security Model
### Threat Model
**Trusted**:
- User's device and browser
- User's wallet private key
**Untrusted**:
- Network peers
- Network infrastructure
- Message content
### Security Guarantees
1. **Message Authenticity**
- All messages signed with browser key
- Browser key authorized by wallet (for wallet users)
- Anonymous users sign with session key (no wallet)
2. **Message Integrity**
- Signatures cover entire message payload
- Any modification invalidates signature
3. **Non-Repudiation**
- Messages cryptographically tied to author
- Cannot deny authorship of signed messages
4. **Replay Protection**
- Deduplication prevents replayed messages
- Timestamps in message payloads
### Attack Resistance
**Impersonation**:
- ❌ Prevented: Cannot forge signature without private key
**Message Tampering**:
- ❌ Prevented: Modified messages fail signature verification
**Replay Attacks**:
- ✅ Mitigated: Deduplication based on (type:id:timestamp)
- ⚠️ Limited: Same message can be replayed with different timestamp
**Sybil Attacks**:
- ⚠️ Possible: Anonymous users can create multiple sessions
- ✅ Mitigated: ENS-verified users have higher trust/scoring
**DoS Attacks**:
- ⚠️ Possible: Can flood network with messages
- ✅ Mitigated: Client-side validation rejects malformed messages
---
**End of Architecture Guide**

View File

@ -0,0 +1,951 @@
## OpChan Core SDK (packages/core) — Building Decentralized Forums
This guide shows how to build a decentralized forum application using the Core SDK directly (without React). It covers project setup, client initialization, key delegation, network connectivity, content management, identity resolution, and persistence.
The examples assume you install and use the `@opchan/core` package.
---
### 1) Install and basic setup
```bash
npm i @opchan/core
```
Create a client instance and open the database:
```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
});
// IMPORTANT: Open database before use
await client.database.open();
```
---
### 2) Message synchronization
Set up listeners for incoming messages and network health:
```typescript
// Listen for all incoming messages
const unsubscribeMessages = client.messageManager.onMessageReceived(
async (message) => {
// Apply to local database
const wasNew = await client.database.applyMessage(message);
if (wasNew) {
console.log('New message received:', message.type);
updateUI();
}
}
);
// Monitor network health
const unsubscribeHealth = client.messageManager.onHealthChange(
(isHealthy) => {
console.log('Network status:', isHealthy ? 'Connected' : 'Disconnected');
updateNetworkIndicator(isHealthy);
}
);
// Monitor sync status
const unsubscribeSync = client.messageManager.onSyncStatus(
(status) => {
console.log('Sync status:', status);
}
);
// Clean up when done
function cleanup() {
unsubscribeMessages();
unsubscribeHealth();
unsubscribeSync();
}
```
---
### 3) Key delegation — wallet users
For wallet-connected users, create a delegation to reduce signature prompts:
```typescript
import { signMessage } from 'viem/accounts';
async function setupWalletDelegation(walletAddress: `0x${string}`) {
// Create delegation for 7 or 30 days
const success = await client.delegation.delegate(
walletAddress,
'7days', // or '30days'
async (message: string) => {
// Sign with wallet
return await signMessage({
message,
account: walletAddress
});
}
);
if (success) {
console.log('Delegation created successfully');
// Check delegation status
const status = await client.delegation.getStatus(walletAddress);
console.log('Delegation valid:', status.isValid);
console.log('Time remaining:', status.timeRemaining);
console.log('Expires at:', new Date(Date.now() + status.timeRemaining!));
}
}
```
---
### 4) Key delegation — anonymous users
For anonymous users (no wallet), create an anonymous delegation:
```typescript
async function setupAnonymousSession() {
// Create anonymous delegation (returns session ID)
const sessionId = await client.delegation.delegateAnonymous('7days');
console.log('Anonymous session ID:', sessionId);
// Create user object
const anonymousUser = {
address: sessionId,
displayName: 'Anonymous',
displayPreference: EDisplayPreference.WALLET_ADDRESS,
verificationStatus: EVerificationStatus.ANONYMOUS
};
// Store user in database
await client.database.storeUser(anonymousUser);
return anonymousUser;
}
```
---
### 5) Creating content — cells
Create a cell (requires ENS-verified wallet):
```typescript
async function createCell(
currentUser: User,
name: string,
description: string,
icon?: string
) {
const result = await client.forumActions.createCell(
{
name,
description,
icon,
currentUser,
isAuthenticated: true
},
() => {
// Callback when cache is updated
updateUI();
}
);
if (result.success) {
console.log('Cell created:', result.data);
return result.data;
} else {
console.error('Failed to create cell:', result.error);
return null;
}
}
```
---
### 6) Creating content — posts
Create a post in a cell:
```typescript
async function createPost(
currentUser: User,
cellId: string,
title: string,
content: string
) {
const result = await client.forumActions.createPost(
{
cellId,
title,
content,
currentUser,
isAuthenticated: true
},
() => updateUI()
);
if (result.success) {
console.log('Post created:', result.data);
// Mark as pending until network confirms
client.database.markPending(result.data!.id);
return result.data;
} else {
console.error('Failed to create post:', result.error);
return null;
}
}
```
---
### 7) Creating content — comments
Add a comment to a post:
```typescript
async function createComment(
currentUser: User,
postId: string,
content: string
) {
const result = await client.forumActions.createComment(
{
postId,
content,
currentUser,
isAuthenticated: true
},
() => updateUI()
);
if (result.success) {
console.log('Comment created:', result.data);
client.database.markPending(result.data!.id);
return result.data;
} else {
console.error('Failed to create comment:', result.error);
return null;
}
}
```
---
### 8) Voting
Vote on posts or comments:
```typescript
async function voteOnContent(
currentUser: User,
targetId: string,
isUpvote: boolean
) {
const result = await client.forumActions.vote(
{
targetId,
isUpvote,
currentUser,
isAuthenticated: true
},
() => updateUI()
);
if (result.success) {
console.log('Vote registered:', isUpvote ? 'upvote' : 'downvote');
} else {
console.error('Failed to vote:', result.error);
}
}
```
---
### 9) Moderation (cell owner only)
Moderate posts, comments, or users within a cell:
```typescript
async function moderatePost(
currentUser: User,
cellId: string,
postId: string,
reason: string
) {
const cell = client.database.cache.cells[cellId];
if (!cell || currentUser.address !== cell.author) {
console.error('Not authorized: Only cell owner can moderate');
return;
}
const result = await client.forumActions.moderatePost(
{
cellId,
postId,
reason,
currentUser,
isAuthenticated: true,
cellOwner: cell.author
},
() => updateUI()
);
if (result.success) {
console.log('Post moderated');
}
}
async function unmoderatePost(
currentUser: User,
cellId: string,
postId: string
) {
const cell = client.database.cache.cells[cellId];
const result = await client.forumActions.unmoderatePost(
{
cellId,
postId,
currentUser,
isAuthenticated: true,
cellOwner: cell.author
},
() => updateUI()
);
if (result.success) {
console.log('Post unmoderated');
}
}
// Similar methods exist for comments and users:
// - client.forumActions.moderateComment()
// - client.forumActions.unmoderateComment()
// - client.forumActions.moderateUser()
// - client.forumActions.unmoderateUser()
```
---
### 10) Reading cached data
Access cached content from the in-memory database:
```typescript
// Get all cells
const cells = Object.values(client.database.cache.cells);
console.log('Cells:', cells.length);
// Get all posts
const posts = Object.values(client.database.cache.posts);
// Filter posts by cell
const cellPosts = posts.filter(p => p.cellId === 'specific-cell-id');
// Get all comments
const comments = Object.values(client.database.cache.comments);
// Filter comments by post
const postComments = comments.filter(c => c.postId === 'specific-post-id');
// Get votes
const votes = Object.values(client.database.cache.votes);
// Get votes for specific content
const postVotes = votes.filter(v => v.targetId === 'post-id');
const upvotes = postVotes.filter(v => v.value === 1);
const downvotes = postVotes.filter(v => v.value === -1);
// Get moderations
const moderations = Object.values(client.database.cache.moderations);
// Check if post is moderated
const postModeration = moderations.find(
m => m.targetType === 'post' && m.targetId === 'post-id'
);
const isModerated = postModeration?.action === 'moderate';
```
---
### 11) Identity resolution
Resolve user identities (ENS names, call signs, etc.):
```typescript
// Get identity for a wallet address
const identity = await client.userIdentityService.getIdentity(
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
);
if (identity) {
console.log('Display name:', identity.displayName);
console.log('ENS name:', identity.ensName);
console.log('ENS avatar:', identity.ensAvatar);
console.log('Call sign:', identity.callSign);
console.log('Verification:', identity.verificationStatus);
}
// Force fresh resolution (bypass cache)
const freshIdentity = await client.userIdentityService.getIdentity(
address,
{ fresh: true }
);
// Get display name only
const displayName = client.userIdentityService.getDisplayName({
address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
ensName: 'alice.eth',
displayPreference: EDisplayPreference.CALL_SIGN
});
```
---
### 12) User profiles
Update user profiles (call sign and display preference):
```typescript
async function updateUserProfile(
userAddress: string,
callSign?: string,
displayPreference?: EDisplayPreference
) {
const result = await client.userIdentityService.updateProfile(
userAddress,
{
callSign,
displayPreference
}
);
if (result.ok) {
console.log('Profile updated:', result.identity);
return result.identity;
} else {
console.error('Failed to update profile:', result.error);
return null;
}
}
// Example: Set call sign
await updateUserProfile(
userAddress,
'alice',
EDisplayPreference.CALL_SIGN
);
```
---
### 13) Subscribe to identity changes
React to identity updates in real-time:
```typescript
const unsubscribe = client.userIdentityService.subscribe(
(address, identity) => {
console.log('Identity updated:', address);
if (identity) {
console.log('New display name:', identity.displayName);
console.log('New call sign:', identity.callSign);
// Update UI
updateUserDisplay(address, identity);
}
}
);
// Clean up
unsubscribe();
```
---
### 14) Relevance scoring
Calculate relevance scores for posts:
```typescript
import { transformPost } from '@opchan/core';
// Transform raw post message to enhanced post
const post = await transformPost(postMessage);
// Get votes and comments for scoring
const postVotes = Object.values(client.database.cache.votes)
.filter(v => v.targetId === post.id);
const postComments = Object.values(client.database.cache.comments)
.filter(c => c.postId === post.id);
// Get user verification status
const userVerificationStatus = {};
for (const [address, identity] of Object.entries(
client.database.cache.userIdentities
)) {
userVerificationStatus[address] = {
isVerified: identity.verificationStatus === 'ens-verified',
hasENS: !!identity.ensName,
ensName: identity.ensName
};
}
// Calculate score
const scoreDetails = client.relevance.calculatePostScore(
postMessage,
postVotes,
postComments,
userVerificationStatus,
client.database.cache.moderations
);
console.log('Relevance score:', scoreDetails.finalScore);
console.log('Score breakdown:', {
base: scoreDetails.baseScore,
engagement: scoreDetails.engagementScore,
authorBonus: scoreDetails.authorVerificationBonus,
upvoteBonus: scoreDetails.verifiedUpvoteBonus,
commenterBonus: scoreDetails.verifiedCommenterBonus,
timeDecay: scoreDetails.timeDecayMultiplier,
moderation: scoreDetails.moderationPenalty
});
```
---
### 15) Bookmarks
Manage user 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 all user bookmarks
const bookmarks = await client.database.getUserBookmarks(userId);
console.log('Total bookmarks:', bookmarks.length);
// Get bookmarks by type
const postBookmarks = bookmarks.filter(b => b.type === 'post');
const commentBookmarks = bookmarks.filter(b => b.type === 'comment');
// Check if content is bookmarked
const isBookmarked = client.database.isBookmarked(userId, 'post', postId);
// Remove bookmark
if (isBookmarked) {
const bookmarkId = `post:${postId}`;
await bookmarkService.removeBookmark(bookmarkId);
}
// Clear all bookmarks
const allBookmarks = await client.database.getUserBookmarks(userId);
for (const bookmark of allBookmarks) {
await bookmarkService.removeBookmark(bookmark.id);
}
```
---
### 16) Pending state management
Track pending operations for optimistic UI:
```typescript
// Mark content as pending
client.database.markPending(postId);
// Check if pending
const isPending = client.database.isPending(postId);
if (isPending) {
showSyncingIndicator(postId);
}
// Listen for pending changes
const unsubscribe = client.database.onPendingChange(() => {
// Update UI when pending state changes
updateAllPendingIndicators();
});
// Clear pending when confirmed
client.database.clearPending(postId);
```
---
### 17) Persistence and hydration
Load persisted data on app start:
```typescript
async function initializeApp() {
// Open database (hydrates from IndexedDB)
await client.database.open();
// Load stored user
const storedUser = await client.database.loadUser();
if (storedUser) {
console.log('Restored user session:', storedUser.displayName);
// Check if delegation is still valid
const delegationStatus = await client.delegation.getStatus(
storedUser.address
);
if (!delegationStatus.isValid) {
console.log('Delegation expired, need to re-authorize');
await client.database.clearUser();
await client.database.clearDelegation();
}
}
// Content is already hydrated from IndexedDB
console.log('Loaded from cache:', {
cells: Object.keys(client.database.cache.cells).length,
posts: Object.keys(client.database.cache.posts).length,
comments: Object.keys(client.database.cache.comments).length,
votes: Object.keys(client.database.cache.votes).length
});
}
```
---
### 18) Message validation
Validate messages before processing:
```typescript
import { MessageValidator } from '@opchan/core';
const validator = new MessageValidator();
// Validate a message
const isValid = await validator.isValidMessage(message);
if (!isValid) {
// Get detailed validation report
const report = await validator.getValidationReport(message);
console.error('Invalid message:', {
missingFields: report.missingFields,
invalidFields: report.invalidFields,
hasValidSignature: report.hasValidSignature,
errors: report.errors,
warnings: report.warnings
});
}
```
---
### 19) Network state management
Monitor and manage network connectivity:
```typescript
// Get current network status
const isReady = client.messageManager.isReady;
const health = client.messageManager.currentHealth;
console.log('Network ready:', isReady);
console.log('Network health:', health);
// Get sync state
const syncState = client.database.getSyncState();
console.log('Last sync:', new Date(syncState.lastSync || 0));
console.log('Is syncing:', syncState.isSyncing);
// Listen for health changes
client.messageManager.onHealthChange((isHealthy) => {
if (isHealthy) {
console.log('Network connected - messages will sync');
} else {
console.log('Network disconnected - working offline');
}
});
```
---
### 20) Complete application skeleton
```typescript
import {
OpChanClient,
EVerificationStatus,
EDisplayPreference,
transformPost,
transformComment,
type User
} from '@opchan/core';
class ForumApp {
private client: OpChanClient;
private currentUser: User | null = null;
private unsubscribers: (() => void)[] = [];
async initialize() {
// 1. Create client
this.client = new OpChanClient({
wakuConfig: {
contentTopic: '/opchan/1/messages/proto',
reliableChannelId: 'opchan-messages'
}
});
// 2. Open database (hydrates from IndexedDB)
await this.client.database.open();
// 3. Try to restore user session
this.currentUser = await this.client.database.loadUser();
// 4. If no session, start anonymous
if (!this.currentUser) {
await this.startAnonymousSession();
}
// 5. Set up listeners
this.setupListeners();
// 6. Initial render
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
};
await this.client.database.storeUser(this.currentUser);
}
private setupListeners() {
// Message listener
const unsubMsg = this.client.messageManager.onMessageReceived(
async (message) => {
await this.client.database.applyMessage(message);
this.render();
}
);
this.unsubscribers.push(unsubMsg);
// Health listener
const unsubHealth = this.client.messageManager.onHealthChange(
(isHealthy) => {
console.log('Network:', isHealthy ? 'Connected' : 'Disconnected');
this.updateNetworkStatus(isHealthy);
}
);
this.unsubscribers.push(unsubHealth);
// Identity listener
const unsubIdentity = this.client.userIdentityService.subscribe(
(address, identity) => {
console.log('Identity updated:', address, identity);
this.render();
}
);
this.unsubscribers.push(unsubIdentity);
// Pending listener
const unsubPending = this.client.database.onPendingChange(() => {
this.updatePendingIndicators();
});
this.unsubscribers.push(unsubPending);
}
async createPost(cellId: string, title: string, content: string) {
if (!this.currentUser) return;
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);
this.client.database.markPending(result.data!.id);
} else {
console.error('Failed:', result.error);
}
}
async vote(targetId: string, isUpvote: boolean) {
if (!this.currentUser) return;
await this.client.forumActions.vote(
{
targetId,
isUpvote,
currentUser: this.currentUser,
isAuthenticated: true
},
() => this.render()
);
}
private render() {
// Get data from cache
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);
// Transform and sort by relevance
Promise.all(posts.map(p => transformPost(p))).then(transformedPosts => {
const sorted = transformedPosts
.filter(p => p !== null)
.sort((a, b) => (b!.relevanceScore || 0) - (a!.relevanceScore || 0));
// Update DOM
this.renderCells(cells);
this.renderPosts(sorted);
this.renderComments(comments);
});
}
private renderCells(cells: any[]) {
console.log('Rendering', cells.length, 'cells');
// Update DOM here...
}
private renderPosts(posts: any[]) {
console.log('Rendering', posts.length, 'posts');
// Update DOM here...
}
private renderComments(comments: any[]) {
console.log('Rendering', comments.length, 'comments');
// Update DOM here...
}
private updateNetworkStatus(isHealthy: boolean) {
// Update network indicator in UI
const indicator = document.getElementById('network-status');
if (indicator) {
indicator.textContent = isHealthy ? '🟢 Connected' : '🔴 Offline';
}
}
private updatePendingIndicators() {
// Update all pending indicators
document.querySelectorAll('[data-pending]').forEach(el => {
const id = el.getAttribute('data-id');
if (id && this.client.database.isPending(id)) {
el.classList.add('syncing');
} else {
el.classList.remove('syncing');
}
});
}
cleanup() {
this.unsubscribers.forEach(unsub => unsub());
}
}
// Initialize app
const app = new ForumApp();
app.initialize().then(() => {
console.log('App initialized');
});
```
---
### 21) Best practices
- **Always open database**: Call `client.database.open()` before using the client
- **Set up message listener**: Subscribe to `onMessageReceived` early to stay synchronized
- **Monitor network health**: Use `onHealthChange` to show connection status
- **Use optimistic UI**: Mark items as pending during network operations
- **Cache identity lookups**: `UserIdentityService` automatically caches ENS resolution
- **Transform messages**: Use `transformPost/transformComment/transformCell` for enhanced data
- **Validate before storage**: `LocalDatabase.applyMessage` validates all messages
- **Handle delegation expiry**: Check `delegation.getStatus()` and re-authorize when needed
- **Persist user session**: Use `database.storeUser/loadUser` for session continuity
- **Clean up listeners**: Call unsubscribe functions when components unmount
---
### 22) Error handling
```typescript
// Wrap operations in try-catch
try {
const result = await client.forumActions.createPost(params, callback);
if (!result.success) {
// Handle business logic errors
showError(result.error);
}
} catch (error) {
// Handle unexpected errors
console.error('Unexpected error:', error);
showError('An unexpected error occurred');
}
// Check delegation before operations
const status = await client.delegation.getStatus(currentUser.address);
if (!status.isValid) {
showError('Delegation expired. Please re-authorize.');
await reauthorizeUser();
}
// Handle network errors
client.messageManager.onHealthChange((isHealthy) => {
if (!isHealthy) {
showWarning('Network disconnected. Working offline.');
}
});
```
---
### 23) Notes
- The core package is framework-agnostic - use with React, Vue, Svelte, or vanilla JS
- All content is stored locally and synchronized via Waku network
- Delegation lasts 7 or 30 days - users need to re-authorize after expiry
- Anonymous users can post/comment/vote but cannot create cells
- Cell creation requires ENS-verified wallet
- Moderation is cell-owner only
- All messages are cryptographically signed and verified
- IndexedDB persists data across sessions
---
**See the React package for a higher-level React integration layer built on top of this core SDK.**

View File

@ -0,0 +1,834 @@
# @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<Post[]> {
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.**

View File

@ -39,7 +39,7 @@
"dependencies": {
"@noble/ed25519": "^2.2.3",
"@noble/hashes": "^1.8.0",
"@waku/sdk": "0.0.37-339e26e.0",
"@waku/sdk": "0.0.37-3e3c511.0",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.2",
"uuid": "^11.1.0",