From aeba9823a71190aeb17049785d08c61e67322187 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Fri, 12 Dec 2025 15:08:52 -0500 Subject: [PATCH] chore(core): docs --- package-lock.json | 88 +- packages/core/QUICK_START.md | 263 ++++ packages/core/README.md | 1230 +++++++++++++++++ packages/core/docs/INDEX.md | 327 +++++ packages/core/docs/api-reference.md | 1791 +++++++++++++++++++++++++ packages/core/docs/architecture.md | 967 +++++++++++++ packages/core/docs/getting-started.md | 951 +++++++++++++ packages/core/docs/sample-app.md | 834 ++++++++++++ packages/core/package.json | 2 +- 9 files changed, 6408 insertions(+), 45 deletions(-) create mode 100644 packages/core/QUICK_START.md create mode 100644 packages/core/README.md create mode 100644 packages/core/docs/INDEX.md create mode 100644 packages/core/docs/api-reference.md create mode 100644 packages/core/docs/architecture.md create mode 100644 packages/core/docs/getting-started.md create mode 100644 packages/core/docs/sample-app.md diff --git a/package-lock.json b/package-lock.json index a277927..90f8bb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/core/QUICK_START.md b/packages/core/QUICK_START.md new file mode 100644 index 0000000..3d25180 --- /dev/null +++ b/packages/core/QUICK_START.md @@ -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!** ๐Ÿš€ + diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a16ab99 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,1230 @@ +# @opchan/core + +Core browser library for building decentralized forum applications with cryptographic identity management and peer-to-peer messaging. + +## Overview + +`@opchan/core` provides the foundational infrastructure for the OpChan protocol, featuring: + +- ๐Ÿ” **Cryptographic Identity** - Ed25519 key delegation with wallet signatures +- ๐Ÿ“ก **Waku Messaging** - Peer-to-peer communication via Waku network +- ๐Ÿ’พ **Local-First Storage** - IndexedDB persistence with in-memory caching +- โš–๏ธ **Content Management** - Cells, posts, comments, and voting system +- ๐ŸŽฏ **Relevance Scoring** - Multi-factor content ranking algorithm +- ๐Ÿ›ก๏ธ **Message Validation** - Cryptographic verification of all content +- ๐Ÿ‘ค **Identity Resolution** - ENS integration and user profiles +- ๐Ÿ”– **Bookmarks** - Client-side bookmark management + +## Installation + +```bash +npm install @opchan/core +``` + +## Quick Start + +### 1. Initialize Client + +```typescript +import { OpChanClient } from '@opchan/core'; + +const client = new OpChanClient({ + wakuConfig: { + contentTopic: '/opchan/1/messages/proto', + reliableChannelId: 'opchan-messages' + }, + reownProjectId: 'your-reown-project-id' // Optional, for WalletConnect +}); + +// Open database +await client.database.open(); +``` + +### 2. Set Up Message Listening + +```typescript +// Listen for incoming messages +client.messageManager.onMessageReceived(async (message) => { + // Apply message to local database + await client.database.applyMessage(message); + console.log('Received message:', message.type); +}); + +// Monitor network health +client.messageManager.onHealthChange((isHealthy) => { + console.log('Network health:', isHealthy ? 'Connected' : 'Disconnected'); +}); +``` + +### 3. Delegate Signing Keys (Wallet Users) + +```typescript +import { signMessage } from 'viem/accounts'; + +// Generate delegation for 7 days +const success = await client.delegation.delegate( + walletAddress, + '7days', + async (message) => { + return await signMessage({ message, account: walletAddress }); + } +); + +if (success) { + console.log('Delegation created successfully'); +} +``` + +### 4. Create Content + +```typescript +// Create a cell (requires ENS verification) +const cellResult = await client.forumActions.createCell( + { + name: 'Tech Discussion', + description: 'A place for tech talk', + icon: '๐Ÿš€', + currentUser: user, + isAuthenticated: true + }, + () => refreshUI() +); + +// Create a post +const postResult = await client.forumActions.createPost( + { + cellId: 'cell-id', + title: 'Hello World', + content: 'My first post!', + currentUser: user, + isAuthenticated: true + }, + () => refreshUI() +); + +// Add a comment +const commentResult = await client.forumActions.createComment( + { + postId: 'post-id', + content: 'Great post!', + currentUser: user, + isAuthenticated: true + }, + () => refreshUI() +); + +// Vote on content +await client.forumActions.vote( + { + targetId: 'post-id', + isUpvote: true, + currentUser: user, + isAuthenticated: true + }, + () => refreshUI() +); +``` + +### 5. Access Cached Data + +```typescript +// Get all cells +const cells = Object.values(client.database.cache.cells); + +// Get all posts +const posts = Object.values(client.database.cache.posts); + +// Get posts for a specific cell +const cellPosts = posts.filter(post => post.cellId === 'cell-id'); + +// Get comments for a post +const comments = Object.values(client.database.cache.comments) + .filter(comment => comment.postId === 'post-id'); + +// Get votes for a post +const votes = Object.values(client.database.cache.votes) + .filter(vote => vote.targetId === 'post-id'); +``` + +## Core Concepts + +### Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ OpChanClient โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ MessageManager (Waku Network) โ”‚ โ”‚ +โ”‚ โ”‚ - WakuNodeManager: Connection handling โ”‚ โ”‚ +โ”‚ โ”‚ - ReliableMessaging: Message delivery โ”‚ โ”‚ +โ”‚ โ”‚ - MessageService: Send/receive orchestration โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ†• โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ LocalDatabase (IndexedDB + Cache) โ”‚ โ”‚ +โ”‚ โ”‚ - In-memory cache for fast reads โ”‚ โ”‚ +โ”‚ โ”‚ - IndexedDB for persistence โ”‚ โ”‚ +โ”‚ โ”‚ - Message validation and deduplication โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ†• โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Services Layer โ”‚ โ”‚ +โ”‚ โ”‚ - ForumActions: Content creation โ”‚ โ”‚ +โ”‚ โ”‚ - UserIdentityService: Identity resolution โ”‚ โ”‚ +โ”‚ โ”‚ - DelegationManager: Key management โ”‚ โ”‚ +โ”‚ โ”‚ - RelevanceCalculator: Content scoring โ”‚ โ”‚ +โ”‚ โ”‚ - BookmarkService: Bookmark management โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Message Flow + +1. **Outbound**: User action โ†’ Sign with delegated key โ†’ Store in LocalDatabase โ†’ Send via Waku +2. **Inbound**: Waku receives โ†’ Validate signature โ†’ Validate delegation proof โ†’ Store in LocalDatabase โ†’ Notify listeners + +### Key Delegation + +OpChan uses a two-tier signing system: + +- **Wallet Key**: Used once to authorize a browser key +- **Browser Key**: Used for all subsequent messages + +Benefits: +- Reduces wallet prompts from dozens to one +- Messages still cryptographically verifiable +- Works with anonymous sessions (no wallet required) + +## API Reference + +### OpChanClient + +Main client class that orchestrates all services. + +```typescript +class OpChanClient { + readonly config: OpChanClientConfig; + readonly messageManager: DefaultMessageManager; + readonly database: LocalDatabase; + readonly forumActions: ForumActions; + readonly relevance: RelevanceCalculator; + readonly messageService: MessageService; + readonly userIdentityService: UserIdentityService; + readonly delegation: DelegationManager; +} +``` + +**Configuration:** + +```typescript +interface OpChanClientConfig { + wakuConfig: WakuConfig; + reownProjectId?: string; +} + +interface WakuConfig { + contentTopic: string; + reliableChannelId: string; +} +``` + +--- + +### DelegationManager + +Manages cryptographic key delegation and message signing. + +#### Methods + +**`delegate(address, duration, signFunction)`** + +Create a wallet-signed delegation for browser keys. + +```typescript +async delegate( + address: `0x${string}`, + duration: '7days' | '30days', + signFunction: (message: string) => Promise +): Promise +``` + +**Example:** +```typescript +const success = await client.delegation.delegate( + '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', + '7days', + async (msg) => await wallet.signMessage(msg) +); +``` + +--- + +**`delegateAnonymous(duration)`** + +Create an anonymous delegation (no wallet required). + +```typescript +async delegateAnonymous( + duration: '7days' | '30days' = '7days' +): Promise +``` + +Returns a session ID for the anonymous user. + +--- + +**`signMessage(message)`** + +Sign a message with the delegated browser key. + +```typescript +async signMessage( + message: UnsignedMessage +): Promise +``` + +--- + +**`verify(message)`** + +Verify a signed message's authenticity. + +```typescript +async verify(message: OpchanMessage): Promise +``` + +--- + +**`getStatus(currentAddress?)`** + +Get current delegation status. + +```typescript +async getStatus( + currentAddress?: string +): Promise +``` + +**Returns:** +```typescript +interface DelegationFullStatus { + hasDelegation: boolean; + isValid: boolean; + timeRemaining?: number; + publicKey?: string; + address?: `0x${string}`; + proof?: DelegationProof; +} +``` + +--- + +**`clear()`** + +Clear stored delegation. + +```typescript +async clear(): Promise +``` + +--- + +### LocalDatabase + +IndexedDB-backed local storage with in-memory caching. + +#### Properties + +**`cache`** - In-memory cache of all content + +```typescript +interface LocalDatabaseCache { + cells: { [id: string]: CellMessage }; + posts: { [id: string]: PostMessage }; + comments: { [id: string]: CommentMessage }; + votes: { [key: string]: VoteMessage }; + moderations: { [key: string]: ModerateMessage }; + userIdentities: { [address: string]: UserIdentityCache }; + bookmarks: { [id: string]: Bookmark }; +} +``` + +#### Methods + +**`open()`** + +Open database and hydrate cache from IndexedDB. + +```typescript +async open(): Promise +``` + +--- + +**`applyMessage(message)`** + +Apply an incoming message to the database. + +```typescript +async applyMessage(message: unknown): Promise +``` + +Returns `true` if message was newly processed and stored. + +--- + +**`storeUser(user)` / `loadUser()` / `clearUser()`** + +Persist user authentication state. + +```typescript +async storeUser(user: User): Promise +async loadUser(): Promise +async clearUser(): Promise +``` + +--- + +**`storeDelegation(delegation)` / `loadDelegation()` / `clearDelegation()`** + +Persist delegation information. + +```typescript +async storeDelegation(delegation: DelegationInfo): Promise +async loadDelegation(): Promise +async clearDelegation(): Promise +``` + +--- + +**`markPending(id)` / `clearPending(id)` / `isPending(id)`** + +Track pending message synchronization. + +```typescript +markPending(id: string): void +clearPending(id: string): void +isPending(id: string): boolean +onPendingChange(listener: () => void): () => void +``` + +--- + +**`addBookmark(bookmark)` / `removeBookmark(id)`** + +Manage bookmarks. + +```typescript +async addBookmark(bookmark: Bookmark): Promise +async removeBookmark(bookmarkId: string): Promise +async getUserBookmarks(userId: string): Promise +isBookmarked(userId: string, type: 'post' | 'comment', targetId: string): boolean +``` + +--- + +### ForumActions + +High-level actions for content creation and moderation. + +#### Content Creation + +**`createCell(params, updateCallback)`** + +Create a new cell (requires ENS verification). + +```typescript +async createCell( + params: { + name: string; + description: string; + icon?: string; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: Cell; error?: string }> +``` + +--- + +**`createPost(params, updateCallback)`** + +Create a new post. + +```typescript +async createPost( + params: { + cellId: string; + title: string; + content: string; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: Post; error?: string }> +``` + +--- + +**`createComment(params, updateCallback)`** + +Create a new comment. + +```typescript +async createComment( + params: { + postId: string; + content: string; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: Comment; error?: string }> +``` + +--- + +#### Voting + +**`vote(params, updateCallback)`** + +Vote on a post or comment. + +```typescript +async vote( + params: { + targetId: string; + isUpvote: boolean; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: boolean; error?: string }> +``` + +--- + +#### Moderation + +**`moderatePost(params, updateCallback)`** + +Moderate a post (cell owner only). + +```typescript +async moderatePost( + params: { + cellId: string; + postId: string; + reason?: string; + currentUser: User | null; + isAuthenticated: boolean; + cellOwner: string; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: boolean; error?: string }> +``` + +Similar methods exist for: `unmoderatePost`, `moderateComment`, `unmoderateComment`, `moderateUser`, `unmoderateUser` + +--- + +### UserIdentityService + +Manages user identity resolution and profiles. + +#### Methods + +**`getIdentity(address, opts?)`** + +Get user identity with optional fresh resolution. + +```typescript +async getIdentity( + address: string, + opts?: { fresh?: boolean } +): Promise +``` + +**Returns:** +```typescript +interface UserIdentity { + address: `0x${string}`; + ensName?: string; + ensAvatar?: string; + callSign?: string; + displayPreference: EDisplayPreference; + displayName: string; + lastUpdated: number; + verificationStatus: EVerificationStatus; +} +``` + +--- + +**`updateProfile(address, updates)`** + +Update user profile (call sign and display preference). + +```typescript +async updateProfile( + address: string, + updates: { + callSign?: string; + displayPreference?: EDisplayPreference; + } +): Promise<{ ok: true; identity: UserIdentity } | { ok: false; error: Error }> +``` + +--- + +**`getDisplayName(params)`** + +Get display name for a user based on their preferences. + +```typescript +getDisplayName({ + address: string, + ensName?: string | null, + displayPreference?: EDisplayPreference +}): string +``` + +--- + +**`subscribe(listener)`** + +Subscribe to identity changes. + +```typescript +subscribe( + listener: (address: string, identity: UserIdentity | null) => void +): () => void +``` + +--- + +### RelevanceCalculator + +Calculates content relevance scores based on multiple factors. + +#### Method + +**`calculatePostScore(post, votes, comments, userVerificationStatus, moderatedPosts)`** + +Calculate relevance score for a post. + +```typescript +calculatePostScore( + post: PostMessage, + votes: VoteMessage[], + comments: CommentMessage[], + userVerificationStatus: UserVerificationStatus, + moderatedPosts: { [postId: string]: ModerateMessage } +): RelevanceScoreDetails +``` + +**Score Factors:** +- **Base Score**: 100 points for all content +- **Engagement**: 10 points per upvote, 3 points per comment +- **Verification Bonuses**: + - Author ENS verified: +20 points + - Each verified upvote: +5 points + - Each verified commenter: +10 points +- **Time Decay**: Exponential decay over 7 days (half-life) +- **Moderation Penalty**: -50% for moderated content + +--- + +### MessageManager + +Manages Waku network connectivity and message transmission. + +#### Methods + +**`sendMessage(message, statusCallback?)`** + +Send a message via Waku network. + +```typescript +async sendMessage( + message: OpchanMessage, + statusCallback?: (status: MessageStatus) => void +): Promise +``` + +--- + +**`onMessageReceived(callback)`** + +Subscribe to incoming messages. + +```typescript +onMessageReceived( + callback: (message: OpchanMessage) => void +): () => void +``` + +--- + +**`onHealthChange(callback)`** + +Monitor network health. + +```typescript +onHealthChange( + callback: (isHealthy: boolean) => void +): () => void +``` + +--- + +**`onSyncStatus(callback)`** + +Monitor synchronization status. + +```typescript +onSyncStatus( + callback: (status: SyncStatus) => void +): () => void +``` + +--- + +## Type Definitions + +### Core Types + +```typescript +// User verification levels +enum EVerificationStatus { + ANONYMOUS = 'anonymous', + WALLET_UNCONNECTED = 'wallet-unconnected', + WALLET_CONNECTED = 'wallet-connected', + ENS_VERIFIED = 'ens-verified', +} + +// User display preferences +enum EDisplayPreference { + CALL_SIGN = 'call-sign', + WALLET_ADDRESS = 'wallet-address', +} + +// User object +interface User { + address: string; // 0x${string} for wallet, UUID for anonymous + ensName?: string; + ensAvatar?: string; + callSign?: string; + displayPreference: EDisplayPreference; + displayName: string; + verificationStatus: EVerificationStatus; + lastChecked?: number; +} +``` + +### Message Types + +```typescript +// All messages include signature fields +interface SignedMessage { + signature: string; + browserPubKey: string; + delegationProof?: DelegationProof; // Present for wallet users only +} + +// Message types +enum MessageType { + CELL = 'cell', + POST = 'post', + COMMENT = 'comment', + VOTE = 'vote', + MODERATE = 'moderate', + USER_PROFILE_UPDATE = 'user_profile_update', +} + +// Cell message +interface CellMessage { + type: MessageType.CELL; + id: string; + name: string; + description: string; + icon?: string; + timestamp: number; + author: string; +} + +// Post message +interface PostMessage { + type: MessageType.POST; + id: string; + cellId: string; + title: string; + content: string; + timestamp: number; + author: string; +} + +// Comment message +interface CommentMessage { + type: MessageType.COMMENT; + id: string; + postId: string; + content: string; + timestamp: number; + author: string; +} + +// Vote message +interface VoteMessage { + type: MessageType.VOTE; + id: string; + targetId: string; // Post or comment ID + value: 1 | -1; // Upvote or downvote + timestamp: number; + author: string; +} +``` + +### Extended Forum Types + +```typescript +// Extended cell with computed fields +interface Cell extends CellMessage { + relevanceScore?: number; + activeMemberCount?: number; + recentActivity?: number; + postCount?: number; +} + +// Extended post with computed fields +interface Post extends PostMessage { + authorAddress: string; + upvotes: VoteMessage[]; + downvotes: VoteMessage[]; + moderated?: boolean; + moderatedBy?: string; + moderationReason?: string; + relevanceScore?: number; + verifiedUpvotes?: number; + verifiedCommenters?: string[]; + voteScore?: number; +} + +// Extended comment with computed fields +interface Comment extends CommentMessage { + authorAddress: string; + upvotes: VoteMessage[]; + downvotes: VoteMessage[]; + moderated?: boolean; + moderatedBy?: string; + moderationReason?: string; + relevanceScore?: number; + voteScore?: number; +} +``` + +## Usage Patterns + +### Pattern 1: Complete Vanilla JS Application + +```typescript +import { OpChanClient, EVerificationStatus } from '@opchan/core'; + +// Initialize +const client = new OpChanClient({ + wakuConfig: { + contentTopic: '/opchan/1/messages/proto', + reliableChannelId: 'opchan-messages' + } +}); + +await client.database.open(); + +// Set up message listener +client.messageManager.onMessageReceived(async (message) => { + await client.database.applyMessage(message); + renderUI(); +}); + +// Create delegation +const sessionId = await client.delegation.delegateAnonymous('7days'); +const user = { + address: sessionId, + displayName: 'Anonymous', + displayPreference: EDisplayPreference.WALLET_ADDRESS, + verificationStatus: EVerificationStatus.ANONYMOUS +}; + +// Create content +const result = await client.forumActions.createPost( + { + cellId: 'general', + title: 'Hello', + content: 'First post!', + currentUser: user, + isAuthenticated: true + }, + () => renderUI() +); + +// Render UI +function renderUI() { + const posts = Object.values(client.database.cache.posts); + document.getElementById('posts').innerHTML = posts + .map(post => `
${post.title}
`) + .join(''); +} +``` + +--- + +### Pattern 2: Identity Resolution + +```typescript +// Resolve user identity +const identity = await client.userIdentityService.getIdentity( + '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', + { fresh: true } +); + +console.log('Display name:', identity?.displayName); +console.log('ENS name:', identity?.ensName); +console.log('Verification:', identity?.verificationStatus); + +// Update profile +const result = await client.userIdentityService.updateProfile( + userAddress, + { + callSign: 'alice', + displayPreference: EDisplayPreference.CALL_SIGN + } +); + +if (result.ok) { + console.log('Profile updated:', result.identity); +} + +// Subscribe to changes +const unsubscribe = client.userIdentityService.subscribe( + (address, identity) => { + console.log('Identity updated:', address, identity); + } +); +``` + +--- + +### Pattern 3: Content Scoring + +```typescript +import { transformPost } from '@opchan/core'; + +// Transform raw post message to enhanced post +const post = await transformPost(postMessage); + +// Calculate relevance +const score = client.relevance.calculatePostScore( + postMessage, + votes, + comments, + userVerificationStatus, + moderatedPosts +); + +console.log('Relevance score:', score.finalScore); +console.log('Score breakdown:', { + base: score.baseScore, + engagement: score.engagementScore, + verificationBonus: score.authorVerificationBonus + + score.verifiedUpvoteBonus + + score.verifiedCommenterBonus, + timeDecay: score.timeDecayMultiplier, + moderation: score.moderationPenalty +}); +``` + +--- + +### Pattern 4: Moderation + +```typescript +// Check if user can moderate +const cellOwner = cell.author; +const canModerate = currentUser.address === cellOwner; + +if (canModerate) { + // Moderate a post + await client.forumActions.moderatePost( + { + cellId: cell.id, + postId: post.id, + reason: 'Spam', + currentUser, + isAuthenticated: true, + cellOwner + }, + () => refreshUI() + ); + + // Unmoderate a post + await client.forumActions.unmoderatePost( + { + cellId: cell.id, + postId: post.id, + currentUser, + isAuthenticated: true, + cellOwner + }, + () => refreshUI() + ); +} +``` + +--- + +### Pattern 5: Bookmarks + +```typescript +import { BookmarkService } from '@opchan/core'; + +const bookmarkService = new BookmarkService(); + +// Add post bookmark +await bookmarkService.addPostBookmark( + post, + userId, + cellId +); + +// Add comment bookmark +await bookmarkService.addCommentBookmark( + comment, + userId, + postId +); + +// Get user bookmarks +const bookmarks = await client.database.getUserBookmarks(userId); + +// Check if bookmarked +const isBookmarked = client.database.isBookmarked( + userId, + 'post', + postId +); + +// Remove bookmark +await bookmarkService.removeBookmark(bookmarkId); +``` + +--- + +## Complete Example + +Here's a complete vanilla JavaScript application using `@opchan/core`: + +```typescript +import { + OpChanClient, + EVerificationStatus, + EDisplayPreference, + transformPost, + transformComment +} from '@opchan/core'; + +class OpChanApp { + private client: OpChanClient; + private currentUser: any = null; + + async initialize() { + // Create client + this.client = new OpChanClient({ + wakuConfig: { + contentTopic: '/opchan/1/messages/proto', + reliableChannelId: 'opchan-messages' + } + }); + + // Open database + await this.client.database.open(); + + // Set up listeners + this.setupListeners(); + + // Create anonymous session + await this.startAnonymousSession(); + + // Initial render + this.render(); + } + + private setupListeners() { + // Message listener + this.client.messageManager.onMessageReceived(async (message) => { + await this.client.database.applyMessage(message); + this.render(); + }); + + // Health listener + this.client.messageManager.onHealthChange((isHealthy) => { + console.log('Network:', isHealthy ? 'Connected' : 'Disconnected'); + this.render(); + }); + } + + private async startAnonymousSession() { + const sessionId = await this.client.delegation.delegateAnonymous('7days'); + this.currentUser = { + address: sessionId, + displayName: 'Anonymous', + displayPreference: EDisplayPreference.WALLET_ADDRESS, + verificationStatus: EVerificationStatus.ANONYMOUS + }; + } + + private async createPost(cellId: string, title: string, content: string) { + const result = await this.client.forumActions.createPost( + { + cellId, + title, + content, + currentUser: this.currentUser, + isAuthenticated: true + }, + () => this.render() + ); + + if (result.success) { + console.log('Post created:', result.data); + } else { + console.error('Failed to create post:', result.error); + } + } + + private async vote(targetId: string, isUpvote: boolean) { + await this.client.forumActions.vote( + { + targetId, + isUpvote, + currentUser: this.currentUser, + isAuthenticated: true + }, + () => this.render() + ); + } + + private render() { + const cells = Object.values(this.client.database.cache.cells); + const posts = Object.values(this.client.database.cache.posts); + const comments = Object.values(this.client.database.cache.comments); + + console.log('Cells:', cells.length); + console.log('Posts:', posts.length); + console.log('Comments:', comments.length); + + // Update DOM here... + } +} + +// Start app +const app = new OpChanApp(); +app.initialize(); +``` + +## Advanced Topics + +### Message Validation + +All messages are validated before storage: + +```typescript +import { MessageValidator } from '@opchan/core'; + +const validator = new MessageValidator(); + +// Validate message +const isValid = await validator.isValidMessage(message); + +// Get detailed validation report +const report = await validator.getValidationReport(message); +console.log('Validation report:', { + isValid: report.isValid, + missingFields: report.missingFields, + invalidFields: report.invalidFields, + hasValidSignature: report.hasValidSignature, + errors: report.errors, + warnings: report.warnings +}); +``` + +### Custom Transformers + +Transform raw messages into enhanced domain objects: + +```typescript +import { transformPost, transformComment, transformCell } from '@opchan/core'; + +// Transform post with votes and comments +const enhancedPost = await transformPost(postMessage); + +// Transform comment with votes +const enhancedComment = await transformComment(commentMessage); + +// Transform cell with stats +const enhancedCell = await transformCell(cellMessage); +``` + +### Network Configuration + +Configure Waku network parameters: + +```typescript +const client = new OpChanClient({ + wakuConfig: { + contentTopic: '/my-app/1/messages/proto', + reliableChannelId: 'my-app-messages', + // Additional Waku options... + } +}); +``` + +## Best Practices + +1. **Always open database before use**: Call `client.database.open()` on initialization +2. **Listen for messages**: Set up `onMessageReceived` listener to stay synchronized +3. **Handle delegation expiry**: Check delegation status and refresh when needed +4. **Validate user permissions**: Use `ForumActions` validation or implement custom checks +5. **Use transformers for UI**: Transform raw messages to enhanced types for rendering +6. **Monitor network health**: Subscribe to health changes for connection indicators +7. **Implement optimistic UI**: Mark items as pending during network operations +8. **Cache identity lookups**: Use `UserIdentityService` for efficient identity resolution + +## Troubleshooting + +### Messages not appearing + +- **Check delegation**: Ensure delegation is valid with `delegation.getStatus()` +- **Verify network**: Monitor `onHealthChange` for connectivity issues +- **Check validation**: Messages may be rejected if signature is invalid + +### Database not persisting + +- **Call open()**: Ensure `database.open()` was called before use +- **Check browser support**: IndexedDB must be available +- **Clear cache**: Try `database.clearAll()` to reset + +### Identity not resolving + +- **Set public client**: Call `userIdentityService.setPublicClient(publicClient)` for ENS +- **Force refresh**: Use `getIdentity(address, { fresh: true })` to bypass cache +- **Check network**: ENS resolution requires network connectivity + +## License + +MIT + +--- + +**Built for decentralized communities** ๐ŸŒ + diff --git a/packages/core/docs/INDEX.md b/packages/core/docs/INDEX.md new file mode 100644 index 0000000..0253949 --- /dev/null +++ b/packages/core/docs/INDEX.md @@ -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 + +--- + diff --git a/packages/core/docs/api-reference.md b/packages/core/docs/api-reference.md new file mode 100644 index 0000000..263e318 --- /dev/null +++ b/packages/core/docs/api-reference.md @@ -0,0 +1,1791 @@ +# @opchan/core API Reference + +Complete API documentation for all classes, interfaces, and utilities in the OpChan Core SDK. + +--- + +## Table of Contents + +1. [OpChanClient](#opchanclient) +2. [DelegationManager](#delegationmanager) +3. [LocalDatabase](#localdatabase) +4. [ForumActions](#forumactions) +5. [UserIdentityService](#useridentityservice) +6. [RelevanceCalculator](#relevancecalculator) +7. [MessageManager](#messagemanager) +8. [BookmarkService](#bookmarkservice) +9. [MessageValidator](#messagevalidator) +10. [Type Definitions](#type-definitions) +11. [Utility Functions](#utility-functions) + +--- + +## OpChanClient + +Main client class that orchestrates all services and provides a unified interface. + +### Constructor + +```typescript +constructor(config: OpChanClientConfig) +``` + +**Parameters:** + +- `config.wakuConfig` - Waku network configuration + - `contentTopic` - Content topic for Waku messages (e.g., `/opchan/1/messages/proto`) + - `reliableChannelId` - Channel ID for reliable messaging (e.g., `opchan-messages`) +- `config.reownProjectId` - Optional Reown/WalletConnect project ID + +**Example:** + +```typescript +const client = new OpChanClient({ + wakuConfig: { + contentTopic: '/opchan/1/messages/proto', + reliableChannelId: 'opchan-messages' + }, + reownProjectId: 'your-project-id' +}); +``` + +### Properties + +#### `config: OpChanClientConfig` + +Client configuration object passed to constructor. + +#### `messageManager: DefaultMessageManager` + +Manages Waku network connectivity and message transmission. + +#### `database: LocalDatabase` + +IndexedDB-backed local storage with in-memory caching. + +#### `forumActions: ForumActions` + +High-level actions for content creation and moderation. + +#### `relevance: RelevanceCalculator` + +Content relevance scoring algorithm. + +#### `messageService: MessageService` + +Low-level message signing and broadcasting. + +#### `userIdentityService: UserIdentityService` + +User identity resolution and profile management. + +#### `delegation: DelegationManager` + +Cryptographic key delegation system. + +--- + +## DelegationManager + +Manages browser key delegation with wallet signatures or anonymous sessions. + +### Methods + +#### `delegate(address, duration, signFunction)` + +Create a wallet-signed delegation authorizing browser keys. + +**Signature:** + +```typescript +async delegate( + address: `0x${string}`, + duration: '7days' | '30days' = '7days', + signFunction: (message: string) => Promise +): Promise +``` + +**Parameters:** + +- `address` - Wallet address to delegate from +- `duration` - Delegation validity period ('7days' or '30days') +- `signFunction` - Function that signs the authorization message with wallet + +**Returns:** `true` if delegation created successfully, `false` otherwise + +**Example:** + +```typescript +import { signMessage } from 'viem/accounts'; + +const success = await client.delegation.delegate( + '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', + '7days', + async (message) => await signMessage({ message, account: walletAddress }) +); +``` + +**Flow:** + +1. Generates Ed25519 browser keypair +2. Creates authorization message with expiry timestamp and nonce +3. Signs authorization message with wallet (via `signFunction`) +4. Stores delegation in IndexedDB +5. Returns success status + +--- + +#### `delegateAnonymous(duration)` + +Create an anonymous delegation without wallet signature. + +**Signature:** + +```typescript +async delegateAnonymous( + duration: '7days' | '30days' = '7days' +): Promise +``` + +**Parameters:** + +- `duration` - Delegation validity period + +**Returns:** Session ID (UUID) for the anonymous user + +**Example:** + +```typescript +const sessionId = await client.delegation.delegateAnonymous('7days'); +// sessionId: "a3f5c2d1-8e9b-4c7a-b6d5-3e2f1a0b9c8d" +``` + +**Flow:** + +1. Generates Ed25519 browser keypair +2. Generates UUID session ID +3. Creates expiry timestamp and nonce +4. Stores anonymous delegation in IndexedDB +5. Returns session ID as user address + +--- + +#### `signMessage(message)` + +Sign a message with the delegated browser key. + +**Signature:** + +```typescript +async signMessage( + message: UnsignedMessage +): Promise +``` + +**Parameters:** + +- `message` - Unsigned message object (cell, post, comment, vote, etc.) + +**Returns:** Signed message with signature and delegation proof, or `null` if delegation invalid + +**Example:** + +```typescript +const unsignedPost = { + type: MessageType.POST, + id: 'post-id', + cellId: 'cell-id', + title: 'Hello', + content: 'World', + timestamp: Date.now(), + author: userAddress +}; + +const signed = await client.delegation.signMessage(unsignedPost); +if (signed) { + // Message is now signed and ready to send + await client.messageManager.sendMessage(signed); +} +``` + +**Signature Process:** + +1. Retrieves cached delegation (or loads from storage) +2. Checks if delegation is valid (not expired) +3. Creates message payload (excluding signature fields) +4. Signs payload with browser private key (Ed25519) +5. Attaches signature, browser public key, and delegation proof +6. Returns signed message + +--- + +#### `verify(message)` + +Verify a signed message's authenticity. + +**Signature:** + +```typescript +async verify(message: OpchanMessage): Promise +``` + +**Parameters:** + +- `message` - Signed message to verify + +**Returns:** `true` if message is valid, `false` otherwise + +**Example:** + +```typescript +const isValid = await client.delegation.verify(message); +if (isValid) { + await client.database.applyMessage(message); +} +``` + +**Verification Process:** + +1. Checks required fields (signature, browserPubKey, author) +2. Verifies message signature with browser public key +3. If wallet delegation: verifies delegation proof +4. If anonymous: validates session ID format (UUID) +5. Returns validation result + +--- + +#### `verifyWithReason(message)` + +Verify message and return detailed validation reasons. + +**Signature:** + +```typescript +async verifyWithReason( + message: OpchanMessage +): Promise<{ isValid: boolean; reasons: string[] }> +``` + +**Example:** + +```typescript +const result = await client.delegation.verifyWithReason(message); +if (!result.isValid) { + console.error('Validation failed:', result.reasons); +} +``` + +--- + +#### `getStatus(currentAddress?)` + +Get current delegation status. + +**Signature:** + +```typescript +async getStatus( + currentAddress?: string +): Promise +``` + +**Parameters:** + +- `currentAddress` - Optional address to check against stored delegation + +**Returns:** + +```typescript +interface DelegationFullStatus { + hasDelegation: boolean; + isValid: boolean; + timeRemaining?: number; // milliseconds + publicKey?: string; + address?: `0x${string}`; + proof?: DelegationProof; +} +``` + +**Example:** + +```typescript +const status = await client.delegation.getStatus(userAddress); + +if (!status.isValid) { + console.log('Delegation expired or invalid'); + // Re-authorize user +} else { + console.log(`Valid for ${Math.round(status.timeRemaining! / 3600000)} more hours`); +} +``` + +--- + +#### `clear()` + +Clear stored delegation from IndexedDB. + +**Signature:** + +```typescript +async clear(): Promise +``` + +**Example:** + +```typescript +await client.delegation.clear(); +// User needs to re-authorize +``` + +--- + +## LocalDatabase + +IndexedDB-backed local storage with in-memory caching for fast reads. + +### Properties + +#### `cache: LocalDatabaseCache` + +In-memory cache of all content. Fast synchronous access. + +```typescript +interface LocalDatabaseCache { + cells: { [id: string]: CellMessage }; + posts: { [id: string]: PostMessage }; + comments: { [id: string]: CommentMessage }; + votes: { [key: string]: VoteMessage }; + moderations: { [key: string]: ModerateMessage }; + userIdentities: { [address: string]: UserIdentityCache }; + bookmarks: { [id: string]: Bookmark }; +} +``` + +**Example:** + +```typescript +// Synchronous access to cached data +const cells = Object.values(client.database.cache.cells); +const posts = Object.values(client.database.cache.posts); + +// Filter by relationship +const cellPosts = posts.filter(p => p.cellId === 'cell-id'); +``` + +### Methods + +#### `open()` + +Open IndexedDB and hydrate in-memory cache. + +**Signature:** + +```typescript +async open(): Promise +``` + +**Example:** + +```typescript +const client = new OpChanClient(config); +await client.database.open(); // MUST call before use +``` + +**Hydration Process:** + +1. Opens IndexedDB connection +2. Loads all cells, posts, comments, votes, moderations from stores +3. Populates in-memory cache +4. Loads pending message IDs +5. Ready for use + +--- + +#### `applyMessage(message)` + +Validate and store an incoming message. + +**Signature:** + +```typescript +async applyMessage(message: unknown): Promise +``` + +**Parameters:** + +- `message` - Message to validate and store + +**Returns:** `true` if message was newly processed and stored, `false` if invalid or duplicate + +**Example:** + +```typescript +client.messageManager.onMessageReceived(async (message) => { + const wasNew = await client.database.applyMessage(message); + if (wasNew) { + console.log('New message stored:', message.type); + updateUI(); + } +}); +``` + +**Process:** + +1. Validates message signature and structure +2. Checks for duplicates (message key = `type:id:timestamp`) +3. Stores in appropriate cache collection +4. Persists to IndexedDB +5. Updates last sync timestamp +6. Returns whether message was new + +--- + +#### `updateCache(message)` + +Alias for `applyMessage()`. For backward compatibility. + +--- + +#### `clear()` + +Clear all in-memory cache (does not affect IndexedDB). + +**Signature:** + +```typescript +clear(): void +``` + +--- + +#### `clearAll()` + +Clear both in-memory cache and all IndexedDB stores. + +**Signature:** + +```typescript +async clearAll(): Promise +``` + +**Example:** + +```typescript +// Complete reset +await client.database.clearAll(); +await client.database.open(); +``` + +--- + +### User Storage + +#### `storeUser(user)` / `loadUser()` / `clearUser()` + +Persist user authentication state. + +**Signatures:** + +```typescript +async storeUser(user: User): Promise +async loadUser(): Promise +async clearUser(): Promise +``` + +**Example:** + +```typescript +// Store current user +await client.database.storeUser(currentUser); + +// Load on app start +const restoredUser = await client.database.loadUser(); +if (restoredUser) { + console.log('Restored session:', restoredUser.displayName); +} + +// Clear on logout +await client.database.clearUser(); +``` + +**User Expiry:** Stored user expires after 24 hours. `loadUser()` returns `null` if expired. + +--- + +### Delegation Storage + +#### `storeDelegation(delegation)` / `loadDelegation()` / `clearDelegation()` + +Persist delegation information. + +**Signatures:** + +```typescript +async storeDelegation(delegation: DelegationInfo): Promise +async loadDelegation(): Promise +async clearDelegation(): Promise +``` + +--- + +### Pending State + +#### `markPending(id)` / `clearPending(id)` / `isPending(id)` / `onPendingChange(listener)` + +Track pending message synchronization for optimistic UI. + +**Signatures:** + +```typescript +markPending(id: string): void +clearPending(id: string): void +isPending(id: string): boolean +onPendingChange(listener: () => void): () => void +``` + +**Example:** + +```typescript +// Mark as pending when creating +const post = await createPost(...); +client.database.markPending(post.id); + +// Show pending indicator +if (client.database.isPending(post.id)) { + showSyncingBadge(post.id); +} + +// Listen for changes +const unsubscribe = client.database.onPendingChange(() => { + updateAllPendingIndicators(); +}); + +// Clear when confirmed via network +client.database.clearPending(post.id); +``` + +--- + +### Sync State + +#### `getSyncState()` / `setSyncing(isSyncing)` / `updateLastSync(timestamp)` + +Manage synchronization state. + +**Signatures:** + +```typescript +getSyncState(): { lastSync: number | null; isSyncing: boolean } +setSyncing(isSyncing: boolean): void +updateLastSync(timestamp: number): void +``` + +**Example:** + +```typescript +const { lastSync, isSyncing } = client.database.getSyncState(); + +if (isSyncing) { + showSyncSpinner(); +} + +if (lastSync) { + console.log('Last synced:', new Date(lastSync)); +} +``` + +--- + +### Bookmarks + +#### `addBookmark(bookmark)` / `removeBookmark(id)` / `getUserBookmarks(userId)` + +Manage user bookmarks. + +**Signatures:** + +```typescript +async addBookmark(bookmark: Bookmark): Promise +async removeBookmark(bookmarkId: string): Promise +async getUserBookmarks(userId: string): Promise +async getUserBookmarksByType(userId: string, type: 'post' | 'comment'): Promise +isBookmarked(userId: string, type: 'post' | 'comment', targetId: string): boolean +getBookmark(bookmarkId: string): Bookmark | undefined +getAllBookmarks(): Bookmark[] +``` + +**Example:** + +```typescript +// Add bookmark +await client.database.addBookmark({ + id: `post:${postId}`, + type: 'post', + targetId: postId, + userId: currentUser.address, + createdAt: Date.now(), + title: post.title, + author: post.author, + cellId: post.cellId +}); + +// Check if bookmarked +const isBookmarked = client.database.isBookmarked( + userId, + 'post', + postId +); + +// Get all user bookmarks +const bookmarks = await client.database.getUserBookmarks(userId); + +// Remove bookmark +await client.database.removeBookmark(`post:${postId}`); +``` + +--- + +### User Identity Cache + +#### `upsertUserIdentity(address, record)` + +Update user identity in centralized cache. + +**Signature:** + +```typescript +async upsertUserIdentity( + address: string, + record: Partial & { lastUpdated?: number } +): Promise +``` + +**Example:** + +```typescript +await client.database.upsertUserIdentity(address, { + ensName: 'alice.eth', + ensAvatar: 'https://...', + verificationStatus: 'ens-verified', + lastUpdated: Date.now() +}); +``` + +--- + +### UI State + +#### `storeUIState(key, value)` / `loadUIState(key)` / `clearUIState(key)` + +Persist arbitrary UI state to IndexedDB. + +**Signatures:** + +```typescript +async storeUIState(key: string, value: unknown): Promise +async loadUIState(key: string): Promise +async clearUIState(key: string): Promise +``` + +**Example:** + +```typescript +// Store theme preference +await client.database.storeUIState('theme', 'dark'); + +// Load on app start +const theme = await client.database.loadUIState('theme'); + +// Clear +await client.database.clearUIState('theme'); +``` + +--- + +## ForumActions + +High-level actions for content creation, voting, and moderation. + +### Content Creation + +#### `createCell(params, updateCallback)` + +Create a new cell. Requires ENS-verified wallet. + +**Signature:** + +```typescript +async createCell( + params: { + name: string; + description: string; + icon?: string; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: Cell; error?: string }> +``` + +**Parameters:** + +- `params.name` - Cell name (required) +- `params.description` - Cell description (required) +- `params.icon` - Optional emoji/icon for cell +- `params.currentUser` - Current user object +- `params.isAuthenticated` - Whether user is authenticated +- `updateCallback` - Function called when cache is updated (for UI refresh) + +**Returns:** Result object with success status, created cell, or error message + +**Permissions:** Only ENS-verified users can create cells + +**Example:** + +```typescript +const result = await client.forumActions.createCell( + { + name: 'Tech Discussion', + description: 'A place for tech enthusiasts', + icon: '๐Ÿ’ป', + currentUser, + isAuthenticated: true + }, + () => { + // Refresh UI + renderCells(); + } +); + +if (result.success) { + console.log('Cell created:', result.data); +} else { + console.error('Error:', result.error); +} +``` + +--- + +#### `createPost(params, updateCallback)` + +Create a new post in a cell. + +**Signature:** + +```typescript +async createPost( + params: { + cellId: string; + title: string; + content: string; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: Post; error?: string }> +``` + +**Permissions:** Wallet-connected or anonymous users + +**Example:** + +```typescript +const result = await client.forumActions.createPost( + { + cellId: 'cell-id', + title: 'My First Post', + content: 'Hello, OpChan!', + currentUser, + isAuthenticated: true + }, + () => renderPosts() +); +``` + +--- + +#### `createComment(params, updateCallback)` + +Add a comment to a post. + +**Signature:** + +```typescript +async createComment( + params: { + postId: string; + content: string; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: Comment; error?: string }> +``` + +**Permissions:** Wallet-connected or anonymous users + +**Example:** + +```typescript +const result = await client.forumActions.createComment( + { + postId: 'post-id', + content: 'Great post!', + currentUser, + isAuthenticated: true + }, + () => renderComments() +); +``` + +--- + +### Voting + +#### `vote(params, updateCallback)` + +Vote on a post or comment. + +**Signature:** + +```typescript +async vote( + params: { + targetId: string; + isUpvote: boolean; + currentUser: User | null; + isAuthenticated: boolean; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: boolean; error?: string }> +``` + +**Permissions:** Wallet-connected or anonymous users + +**Example:** + +```typescript +// Upvote +await client.forumActions.vote( + { + targetId: postId, + isUpvote: true, + currentUser, + isAuthenticated: true + }, + () => updateVoteCount() +); + +// Downvote +await client.forumActions.vote( + { + targetId: commentId, + isUpvote: false, + currentUser, + isAuthenticated: true + }, + () => updateVoteCount() +); +``` + +--- + +### Moderation + +#### `moderatePost(params, updateCallback)` + +Moderate a post (hide it). + +**Signature:** + +```typescript +async moderatePost( + params: { + cellId: string; + postId: string; + reason?: string; + currentUser: User | null; + isAuthenticated: boolean; + cellOwner: string; + }, + updateCallback: () => void +): Promise<{ success: boolean; data?: boolean; error?: string }> +``` + +**Permissions:** Cell owner only + +**Similar Methods:** + +- `unmoderatePost()` - Remove moderation from post +- `moderateComment()` - Moderate a comment +- `unmoderateComment()` - Remove moderation from comment +- `moderateUser()` - Moderate user in a cell +- `unmoderateUser()` - Remove user moderation + +**Example:** + +```typescript +const cell = client.database.cache.cells[cellId]; + +if (currentUser.address === cell.author) { + await client.forumActions.moderatePost( + { + cellId, + postId, + reason: 'Spam', + currentUser, + isAuthenticated: true, + cellOwner: cell.author + }, + () => renderPosts() + ); +} +``` + +--- + +## UserIdentityService + +Manages user identity resolution, ENS lookup, and profile management. + +### Methods + +#### `getIdentity(address, opts?)` + +Get user identity with ENS resolution and caching. + +**Signature:** + +```typescript +async getIdentity( + address: string, + opts?: { fresh?: boolean } +): Promise +``` + +**Parameters:** + +- `address` - Wallet address or session ID +- `opts.fresh` - If `true`, bypass cache and resolve fresh + +**Returns:** + +```typescript +interface UserIdentity { + address: `0x${string}`; + ensName?: string; + ensAvatar?: string; + callSign?: string; + displayPreference: EDisplayPreference; + displayName: string; + lastUpdated: number; + verificationStatus: EVerificationStatus; +} +``` + +**Example:** + +```typescript +// Cached lookup (fast) +const identity = await client.userIdentityService.getIdentity(address); + +// Fresh lookup (bypasses cache) +const freshIdentity = await client.userIdentityService.getIdentity( + address, + { fresh: true } +); + +if (identity) { + console.log('Display name:', identity.displayName); + console.log('ENS name:', identity.ensName); + console.log('Call sign:', identity.callSign); + console.log('Verification:', identity.verificationStatus); +} +``` + +**Caching Strategy:** + +1. Checks LocalDatabase cache first +2. If not found or stale, resolves from ENS +3. Stores result in cache +4. Returns identity + +--- + +#### `setPublicClient(publicClient)` + +Set viem PublicClient for ENS resolution. + +**Signature:** + +```typescript +setPublicClient(publicClient: PublicClient): void +``` + +**Example:** + +```typescript +import { createPublicClient, http } from 'viem'; +import { mainnet } from 'viem/chains'; + +const publicClient = createPublicClient({ + chain: mainnet, + transport: http() +}); + +client.userIdentityService.setPublicClient(publicClient); +``` + +--- + +#### `updateProfile(address, updates)` + +Update user profile (call sign and display preference). + +**Signature:** + +```typescript +async updateProfile( + address: string, + updates: { + callSign?: string; + displayPreference?: EDisplayPreference; + } +): Promise<{ ok: true; identity: UserIdentity } | { ok: false; error: Error }> +``` + +**Example:** + +```typescript +const result = await client.userIdentityService.updateProfile( + userAddress, + { + callSign: 'alice', + displayPreference: EDisplayPreference.CALL_SIGN + } +); + +if (result.ok) { + console.log('Profile updated:', result.identity); +} else { + console.error('Failed:', result.error); +} +``` + +**Process:** + +1. Creates USER_PROFILE_UPDATE message +2. Signs with delegated key +3. Broadcasts to network +4. Updates LocalDatabase +5. Returns updated identity + +--- + +#### `getDisplayName(params)` + +Get display name for a user based on their preferences. + +**Signature:** + +```typescript +getDisplayName({ + address: string, + ensName?: string | null, + displayPreference?: EDisplayPreference +}): string +``` + +**Example:** + +```typescript +const displayName = client.userIdentityService.getDisplayName({ + address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', + ensName: 'alice.eth', + displayPreference: EDisplayPreference.CALL_SIGN +}); + +// If user has call sign and preference is CALL_SIGN: "alice" +// If user has ENS: "alice.eth" +// Otherwise: "0x742d...0bEb" +``` + +--- + +#### `subscribe(listener)` + +Subscribe to identity changes. + +**Signature:** + +```typescript +subscribe( + listener: (address: string, identity: UserIdentity | null) => void +): () => void +``` + +**Returns:** Unsubscribe function + +**Example:** + +```typescript +const unsubscribe = client.userIdentityService.subscribe( + (address, identity) => { + console.log('Identity updated:', address); + if (identity) { + updateUserDisplay(address, identity.displayName); + } + } +); + +// Clean up +unsubscribe(); +``` + +--- + +#### `getAll()` + +Get all cached user identities. + +**Signature:** + +```typescript +getAll(): UserIdentity[] +``` + +--- + +#### `refreshIdentity(address)` + +Force refresh of a user's identity. + +**Signature:** + +```typescript +async refreshIdentity(address: string): Promise +``` + +--- + +## RelevanceCalculator + +Calculates content relevance scores based on multiple factors. + +### Method + +#### `calculatePostScore(post, votes, comments, userVerificationStatus, moderatedPosts)` + +Calculate relevance score for a post. + +**Signature:** + +```typescript +calculatePostScore( + post: PostMessage, + votes: VoteMessage[], + comments: CommentMessage[], + userVerificationStatus: UserVerificationStatus, + moderatedPosts: { [postId: string]: ModerateMessage } +): RelevanceScoreDetails +``` + +**Parameters:** + +- `post` - Post message to score +- `votes` - All votes for this post +- `comments` - All comments on this post +- `userVerificationStatus` - Verification status of all users (for bonuses) +- `moderatedPosts` - Moderation records + +**Returns:** + +```typescript +interface RelevanceScoreDetails { + baseScore: number; // 100 + engagementScore: number; // upvotes*10 + comments*3 + authorVerificationBonus: number; // +20 if ENS verified + verifiedUpvoteBonus: number; // +5 per verified upvoter + verifiedCommenterBonus: number; // +10 per verified commenter + timeDecayMultiplier: number; // Exponential decay (half-life 7 days) + moderationPenalty: number; // -50% if moderated + finalScore: number; // Combined total + isVerified: boolean; + upvotes: number; + comments: number; + verifiedUpvotes: number; + verifiedCommenters: number; + daysOld: number; + isModerated: boolean; +} +``` + +**Scoring Formula:** + +``` +base = 100 +engagement = (upvotes * 10) + (comments * 3) +verification = (author ENS ? 20 : 0) + + (verified upvoters * 5) + + (verified commenters * 10) +timeDecay = exp(-0.693 * daysOld / 7) +moderation = isModerated ? 0.5 : 1.0 + +finalScore = (base + engagement + verification) * timeDecay * moderation +``` + +**Example:** + +```typescript +const post = client.database.cache.posts['post-id']; +const votes = Object.values(client.database.cache.votes) + .filter(v => v.targetId === post.id); +const comments = Object.values(client.database.cache.comments) + .filter(c => c.postId === post.id); + +const userVerificationStatus = {}; +for (const [addr, identity] of Object.entries(client.database.cache.userIdentities)) { + userVerificationStatus[addr] = { + isVerified: identity.verificationStatus === 'ens-verified', + hasENS: !!identity.ensName, + ensName: identity.ensName + }; +} + +const score = client.relevance.calculatePostScore( + post, + votes, + comments, + userVerificationStatus, + client.database.cache.moderations +); + +console.log('Final score:', score.finalScore); +console.log('Breakdown:', { + base: score.baseScore, + engagement: score.engagementScore, + verification: score.authorVerificationBonus + + score.verifiedUpvoteBonus + + score.verifiedCommenterBonus, + timeDecay: score.timeDecayMultiplier, + moderation: score.moderationPenalty +}); +``` + +--- + +## MessageManager + +Manages Waku network connectivity and message transmission. + +### Properties + +#### `isReady: boolean` + +Whether Waku node is ready to send/receive messages. + +#### `currentHealth: HealthStatus` + +Current network health status. + +### Methods + +#### `sendMessage(message, statusCallback?)` + +Send a message via Waku network. + +**Signature:** + +```typescript +async sendMessage( + message: OpchanMessage, + statusCallback?: (status: MessageStatus) => void +): Promise +``` + +**Example:** + +```typescript +const signed = await client.delegation.signMessage(unsignedMessage); +if (signed) { + await client.messageManager.sendMessage(signed, (status) => { + console.log('Message status:', status); + }); +} +``` + +--- + +#### `onMessageReceived(callback)` + +Subscribe to incoming messages. + +**Signature:** + +```typescript +onMessageReceived( + callback: (message: OpchanMessage) => void +): () => void +``` + +**Returns:** Unsubscribe function + +**Example:** + +```typescript +const unsubscribe = client.messageManager.onMessageReceived( + async (message) => { + await client.database.applyMessage(message); + console.log('Received:', message.type); + } +); +``` + +--- + +#### `onHealthChange(callback)` + +Monitor network health changes. + +**Signature:** + +```typescript +onHealthChange( + callback: (isHealthy: boolean) => void +): () => void +``` + +**Example:** + +```typescript +client.messageManager.onHealthChange((isHealthy) => { + if (isHealthy) { + showConnectedIndicator(); + } else { + showDisconnectedIndicator(); + } +}); +``` + +--- + +#### `onSyncStatus(callback)` + +Monitor synchronization status. + +**Signature:** + +```typescript +onSyncStatus( + callback: (status: SyncStatus) => void +): () => void +``` + +--- + +## BookmarkService + +Service for managing user bookmarks (posts and comments). + +### Methods + +#### `addPostBookmark(post, userId, cellId?)` + +Add a post to bookmarks. + +**Signature:** + +```typescript +async addPostBookmark( + post: Post, + userId: string, + cellId?: string +): Promise +``` + +--- + +#### `addCommentBookmark(comment, userId, postId?)` + +Add a comment to bookmarks. + +**Signature:** + +```typescript +async addCommentBookmark( + comment: Comment, + userId: string, + postId?: string +): Promise +``` + +--- + +#### `removeBookmark(bookmarkId)` + +Remove a bookmark by ID. + +**Signature:** + +```typescript +async removeBookmark(bookmarkId: string): Promise +``` + +**Example:** + +```typescript +import { BookmarkService } from '@opchan/core'; + +const bookmarkService = new BookmarkService(); + +// Add post bookmark +await bookmarkService.addPostBookmark(post, userId, cellId); + +// Check if bookmarked +const isBookmarked = client.database.isBookmarked(userId, 'post', postId); + +// Remove bookmark +await bookmarkService.removeBookmark(`post:${postId}`); +``` + +--- + +## MessageValidator + +Validates message signatures and structure. + +### Methods + +#### `isValidMessage(message)` + +Check if message is valid. + +**Signature:** + +```typescript +async isValidMessage(message: unknown): Promise +``` + +--- + +#### `getValidationReport(message)` + +Get detailed validation report. + +**Signature:** + +```typescript +async getValidationReport(message: unknown): Promise<{ + isValid: boolean; + missingFields: string[]; + invalidFields: string[]; + hasValidSignature: boolean; + errors: string[]; + warnings: string[]; +}> +``` + +**Example:** + +```typescript +import { MessageValidator } from '@opchan/core'; + +const validator = new MessageValidator(); + +const report = await validator.getValidationReport(message); + +if (!report.isValid) { + console.error('Validation failed:'); + console.error('Missing fields:', report.missingFields); + console.error('Invalid fields:', report.invalidFields); + console.error('Errors:', report.errors); +} +``` + +--- + +## Type Definitions + +### Enums + +#### `EVerificationStatus` + +User verification levels. + +```typescript +enum EVerificationStatus { + ANONYMOUS = 'anonymous', + WALLET_UNCONNECTED = 'wallet-unconnected', + WALLET_CONNECTED = 'wallet-connected', + ENS_VERIFIED = 'ens-verified', +} +``` + +--- + +#### `EDisplayPreference` + +User display name preference. + +```typescript +enum EDisplayPreference { + CALL_SIGN = 'call-sign', + WALLET_ADDRESS = 'wallet-address', +} +``` + +--- + +#### `MessageType` + +Message types in the protocol. + +```typescript +enum MessageType { + CELL = 'cell', + POST = 'post', + COMMENT = 'comment', + VOTE = 'vote', + MODERATE = 'moderate', + USER_PROFILE_UPDATE = 'user_profile_update', +} +``` + +--- + +#### `EModerationAction` + +Moderation actions. + +```typescript +enum EModerationAction { + MODERATE = 'moderate', + UNMODERATE = 'unmoderate', +} +``` + +--- + +### Core Interfaces + +#### `User` + +User object representing authenticated or anonymous user. + +```typescript +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; + browserPubKey?: string; + delegationSignature?: string; + delegationExpiry?: number; +} +``` + +--- + +#### `Cell` + +Extended cell with computed fields. + +```typescript +interface Cell extends CellMessage { + relevanceScore?: number; + activeMemberCount?: number; + recentActivity?: number; + postCount?: number; + relevanceDetails?: RelevanceScoreDetails; +} +``` + +--- + +#### `Post` + +Extended post with votes and moderation. + +```typescript +interface Post extends PostMessage { + authorAddress: string; + upvotes: VoteMessage[]; + downvotes: VoteMessage[]; + moderated?: boolean; + moderatedBy?: string; + moderationReason?: string; + moderationTimestamp?: number; + relevanceScore?: number; + verifiedUpvotes?: number; + verifiedCommenters?: string[]; + relevanceDetails?: RelevanceScoreDetails; + voteScore?: number; +} +``` + +--- + +#### `Comment` + +Extended comment with votes and moderation. + +```typescript +interface Comment extends CommentMessage { + authorAddress: string; + upvotes: VoteMessage[]; + downvotes: VoteMessage[]; + moderated?: boolean; + moderatedBy?: string; + moderationReason?: string; + moderationTimestamp?: number; + relevanceScore?: number; + relevanceDetails?: RelevanceScoreDetails; + voteScore?: number; +} +``` + +--- + +#### `Bookmark` + +Bookmark data structure. + +```typescript +interface Bookmark { + id: string; // `${type}:${targetId}` + type: BookmarkType; + targetId: string; + userId: string; + createdAt: number; + title?: string; + author?: string; + cellId?: string; + postId?: string; +} +``` + +--- + +### Message Types + +#### `OpchanMessage` + +Union type of all signed message types. + +```typescript +type OpchanMessage = ( + | CellMessage + | PostMessage + | CommentMessage + | VoteMessage + | ModerateMessage + | UserProfileUpdateMessage +) & SignedMessage; +``` + +--- + +#### `SignedMessage` + +Signature fields present on all messages. + +```typescript +interface SignedMessage { + signature: string; // Ed25519 signature + browserPubKey: string; // Browser public key + delegationProof?: DelegationProof; // Optional for anonymous +} +``` + +--- + +#### `DelegationProof` + +Proof that browser key was authorized by wallet. + +```typescript +interface DelegationProof { + authMessage: string; // Message signed by wallet + walletSignature: string; // Wallet's signature + expiryTimestamp: number; // When delegation expires + walletAddress: string; // Wallet that signed +} +``` + +--- + +## Utility Functions + +### `transformPost(postMessage)` + +Transform raw post message to enhanced Post type. + +**Signature:** + +```typescript +async transformPost(postMessage: PostMessage): Promise +``` + +--- + +### `transformComment(commentMessage)` + +Transform raw comment message to enhanced Comment type. + +**Signature:** + +```typescript +async transformComment(commentMessage: CommentMessage): Promise +``` + +--- + +### `transformCell(cellMessage)` + +Transform raw cell message to enhanced Cell type. + +**Signature:** + +```typescript +async transformCell(cellMessage: CellMessage): Promise +``` + +**Example:** + +```typescript +import { transformPost, transformComment, transformCell } from '@opchan/core'; + +// Transform messages +const post = await transformPost(postMessage); +const comment = await transformComment(commentMessage); +const cell = await transformCell(cellMessage); + +// Use enhanced types +if (post) { + console.log('Upvotes:', post.upvotes.length); + console.log('Downvotes:', post.downvotes.length); + console.log('Vote score:', post.voteScore); + console.log('Relevance:', post.relevanceScore); +} +``` + +--- + +## Complete Type Reference + +For complete TypeScript type definitions, see: + +- `packages/core/src/types/forum.ts` - Forum-specific types +- `packages/core/src/types/identity.ts` - Identity and user types +- `packages/core/src/types/waku.ts` - Message and network types +- `packages/core/src/lib/delegation/types.ts` - Delegation types + +--- + +**End of API Reference** + diff --git a/packages/core/docs/architecture.md b/packages/core/docs/architecture.md new file mode 100644 index 0000000..e684fb4 --- /dev/null +++ b/packages/core/docs/architecture.md @@ -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, + moderations: Map +): 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 { + // 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 { + // 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 { + getById(id: string): T | null; + getAll(): T[]; + save(item: T): Promise; + delete(id: string): Promise; +} + +// 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** + diff --git a/packages/core/docs/getting-started.md b/packages/core/docs/getting-started.md new file mode 100644 index 0000000..2715dc8 --- /dev/null +++ b/packages/core/docs/getting-started.md @@ -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.** + diff --git a/packages/core/docs/sample-app.md b/packages/core/docs/sample-app.md new file mode 100644 index 0000000..68ac150 --- /dev/null +++ b/packages/core/docs/sample-app.md @@ -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 { + 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.** + diff --git a/packages/core/package.json b/packages/core/package.json index 0d32e6c..7856de5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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",