mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
chore(core): docs
This commit is contained in:
parent
0bd7702395
commit
aeba9823a7
88
package-lock.json
generated
88
package-lock.json
generated
@ -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",
|
||||
|
||||
263
packages/core/QUICK_START.md
Normal file
263
packages/core/QUICK_START.md
Normal file
@ -0,0 +1,263 @@
|
||||
# OpChan Core - Quick Start
|
||||
|
||||
Get up and running with OpChan in 5 minutes.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @opchan/core
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Minimal Working Example
|
||||
|
||||
```typescript
|
||||
import { OpChanClient, EVerificationStatus, EDisplayPreference } from '@opchan/core';
|
||||
|
||||
// 1. Create client
|
||||
const client = new OpChanClient({
|
||||
wakuConfig: {
|
||||
contentTopic: '/opchan/1/messages/proto',
|
||||
reliableChannelId: 'opchan-messages'
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Open database
|
||||
await client.database.open();
|
||||
|
||||
// 3. Listen for messages
|
||||
client.messageManager.onMessageReceived(async (message) => {
|
||||
await client.database.applyMessage(message);
|
||||
console.log('Received:', message.type);
|
||||
});
|
||||
|
||||
// 4. Start anonymous session
|
||||
const sessionId = await client.delegation.delegateAnonymous('7days');
|
||||
const user = {
|
||||
address: sessionId,
|
||||
displayName: 'Anonymous',
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
verificationStatus: EVerificationStatus.ANONYMOUS
|
||||
};
|
||||
|
||||
// 5. Create a post
|
||||
const result = await client.forumActions.createPost(
|
||||
{
|
||||
cellId: 'general',
|
||||
title: 'Hello World',
|
||||
content: 'My first post!',
|
||||
currentUser: user,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => console.log('Post created!')
|
||||
);
|
||||
|
||||
// 6. Read posts
|
||||
const posts = Object.values(client.database.cache.posts);
|
||||
console.log('Posts:', posts.length);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Essential Operations
|
||||
|
||||
### Create Content
|
||||
|
||||
```typescript
|
||||
// Post
|
||||
await client.forumActions.createPost({
|
||||
cellId: 'general',
|
||||
title: 'Title',
|
||||
content: 'Content',
|
||||
currentUser: user,
|
||||
isAuthenticated: true
|
||||
}, () => {});
|
||||
|
||||
// Comment
|
||||
await client.forumActions.createComment({
|
||||
postId: 'post-id',
|
||||
content: 'Great post!',
|
||||
currentUser: user,
|
||||
isAuthenticated: true
|
||||
}, () => {});
|
||||
|
||||
// Vote
|
||||
await client.forumActions.vote({
|
||||
targetId: 'post-id',
|
||||
isUpvote: true,
|
||||
currentUser: user,
|
||||
isAuthenticated: true
|
||||
}, () => {});
|
||||
```
|
||||
|
||||
### Read Content
|
||||
|
||||
```typescript
|
||||
// All cells
|
||||
const cells = Object.values(client.database.cache.cells);
|
||||
|
||||
// All posts
|
||||
const posts = Object.values(client.database.cache.posts);
|
||||
|
||||
// Posts in a cell
|
||||
const cellPosts = posts.filter(p => p.cellId === 'cell-id');
|
||||
|
||||
// Comments on a post
|
||||
const comments = Object.values(client.database.cache.comments)
|
||||
.filter(c => c.postId === 'post-id');
|
||||
|
||||
// Votes on a post
|
||||
const votes = Object.values(client.database.cache.votes)
|
||||
.filter(v => v.targetId === 'post-id');
|
||||
```
|
||||
|
||||
### Network Status
|
||||
|
||||
```typescript
|
||||
// Check connection
|
||||
const isReady = client.messageManager.isReady;
|
||||
|
||||
// Listen for changes
|
||||
client.messageManager.onHealthChange((isHealthy) => {
|
||||
console.log(isHealthy ? 'Connected' : 'Offline');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Starter Template
|
||||
|
||||
```typescript
|
||||
import { OpChanClient, EVerificationStatus, EDisplayPreference, type User } from '@opchan/core';
|
||||
|
||||
class MyForumApp {
|
||||
private client: OpChanClient;
|
||||
private currentUser: User | null = null;
|
||||
|
||||
async init() {
|
||||
// Initialize
|
||||
this.client = new OpChanClient({
|
||||
wakuConfig: {
|
||||
contentTopic: '/opchan/1/messages/proto',
|
||||
reliableChannelId: 'opchan-messages'
|
||||
}
|
||||
});
|
||||
|
||||
await this.client.database.open();
|
||||
|
||||
// Set up listeners
|
||||
client.messageManager.onMessageReceived(async (message) => {
|
||||
await client.database.applyMessage(message);
|
||||
this.onNewMessage(message);
|
||||
});
|
||||
|
||||
// Start session
|
||||
const sessionId = await client.delegation.delegateAnonymous('7days');
|
||||
this.currentUser = {
|
||||
address: sessionId,
|
||||
displayName: 'Anonymous',
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
verificationStatus: EVerificationStatus.ANONYMOUS
|
||||
};
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
async createPost(cellId: string, title: string, content: string) {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
const result = await this.client.forumActions.createPost(
|
||||
{ cellId, title, content, currentUser: this.currentUser, isAuthenticated: true },
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Failed:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
private onNewMessage(message: any) {
|
||||
console.log('New message:', message.type);
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const posts = Object.values(this.client.database.cache.posts);
|
||||
console.log('Posts:', posts.length);
|
||||
// Update your UI here
|
||||
}
|
||||
}
|
||||
|
||||
// Start
|
||||
const app = new MyForumApp();
|
||||
await app.init();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Full Documentation**: See [README.md](./README.md)
|
||||
- **Getting Started Guide**: See [docs/getting-started.md](./docs/getting-started.md)
|
||||
- **API Reference**: See [docs/api-reference.md](./docs/api-reference.md)
|
||||
- **Architecture**: See [docs/architecture.md](./docs/architecture.md)
|
||||
- **Sample Apps**: See [docs/sample-apps.md](./docs/sample-apps.md)
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Wallet Connection
|
||||
|
||||
```typescript
|
||||
import { signMessage } from 'viem/accounts';
|
||||
|
||||
await client.delegation.delegate(
|
||||
walletAddress,
|
||||
'7days',
|
||||
async (msg) => await signMessage({ message: msg, account: walletAddress })
|
||||
);
|
||||
```
|
||||
|
||||
### ENS Resolution
|
||||
|
||||
```typescript
|
||||
import { createPublicClient, http } from 'viem';
|
||||
import { mainnet } from 'viem/chains';
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: mainnet,
|
||||
transport: http()
|
||||
});
|
||||
|
||||
client.userIdentityService.setPublicClient(publicClient);
|
||||
|
||||
const identity = await client.userIdentityService.getIdentity(address);
|
||||
console.log('ENS name:', identity?.ensName);
|
||||
```
|
||||
|
||||
### Relevance Scoring
|
||||
|
||||
```typescript
|
||||
import { transformPost } from '@opchan/core';
|
||||
|
||||
const post = await transformPost(postMessage);
|
||||
console.log('Relevance score:', post?.relevanceScore);
|
||||
```
|
||||
|
||||
### Bookmarks
|
||||
|
||||
```typescript
|
||||
import { BookmarkService } from '@opchan/core';
|
||||
|
||||
const bookmarkService = new BookmarkService();
|
||||
await bookmarkService.addPostBookmark(post, userId, cellId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ready to build!** 🚀
|
||||
|
||||
1230
packages/core/README.md
Normal file
1230
packages/core/README.md
Normal file
File diff suppressed because it is too large
Load Diff
327
packages/core/docs/INDEX.md
Normal file
327
packages/core/docs/INDEX.md
Normal file
@ -0,0 +1,327 @@
|
||||
# OpChan Core Documentation Index
|
||||
|
||||
Complete documentation for `@opchan/core` - the foundational SDK for building decentralized forums.
|
||||
|
||||
---
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **[Quick Start](../QUICK_START.md)** - Get started in 5 minutes
|
||||
- **[README](../README.md)** - Package overview and installation
|
||||
- **[Getting Started Guide](./getting-started.md)** - Comprehensive tutorial
|
||||
- **[API Reference](./api-reference.md)** - Complete API documentation
|
||||
- **[Architecture Guide](./architecture.md)** - System design and internals
|
||||
- **[Sample Applications](./sample-apps.md)** - Full working examples
|
||||
|
||||
---
|
||||
|
||||
## Documentation Overview
|
||||
|
||||
### For Beginners
|
||||
|
||||
Start here if you're new to OpChan:
|
||||
|
||||
1. **[Quick Start](../QUICK_START.md)** (5 min)
|
||||
- Minimal working example
|
||||
- Essential operations
|
||||
- Complete starter template
|
||||
|
||||
2. **[README](../README.md)** (15 min)
|
||||
- Package overview
|
||||
- Core concepts
|
||||
- Quick start guide
|
||||
- Usage patterns
|
||||
|
||||
3. **[Getting Started Guide](./getting-started.md)** (30 min)
|
||||
- Step-by-step tutorial
|
||||
- All features explained
|
||||
- Best practices
|
||||
- Complete application skeleton
|
||||
|
||||
### For Developers
|
||||
|
||||
Deep dive into building applications:
|
||||
|
||||
4. **[API Reference](./api-reference.md)** (Reference)
|
||||
- Complete API documentation
|
||||
- All classes and methods
|
||||
- Type definitions
|
||||
- Usage examples
|
||||
|
||||
5. **[Sample Applications](./sample-apps.md)** (Templates)
|
||||
- Minimal forum
|
||||
- Anonymous-first forum
|
||||
- Moderation dashboard
|
||||
- Identity explorer
|
||||
- Bookmark manager
|
||||
- Real-time feed
|
||||
- CLI tool
|
||||
|
||||
### For Advanced Users
|
||||
|
||||
Understanding the system:
|
||||
|
||||
6. **[Architecture Guide](./architecture.md)** (Deep Dive)
|
||||
- System overview
|
||||
- Core architecture
|
||||
- Data flow
|
||||
- Key subsystems
|
||||
- Cryptographic design
|
||||
- Storage strategy
|
||||
- Network layer
|
||||
- Design patterns
|
||||
- Performance considerations
|
||||
- Security model
|
||||
|
||||
---
|
||||
|
||||
## By Topic
|
||||
|
||||
### Getting Started
|
||||
|
||||
- [Installation](../README.md#installation)
|
||||
- [Quick Start](../QUICK_START.md)
|
||||
- [Basic Setup](./getting-started.md#1-install-and-basic-setup)
|
||||
- [First Application](../README.md#quick-start)
|
||||
|
||||
### Client Setup
|
||||
|
||||
- [OpChanClient Configuration](./api-reference.md#opchanclient)
|
||||
- [Opening Database](./getting-started.md#1-install-and-basic-setup)
|
||||
- [Message Synchronization](./getting-started.md#2-message-synchronization)
|
||||
- [Network Monitoring](./getting-started.md#19-network-state-management)
|
||||
|
||||
### Authentication & Identity
|
||||
|
||||
- [Key Delegation (Wallet)](./getting-started.md#3-key-delegation--wallet-users)
|
||||
- [Key Delegation (Anonymous)](./getting-started.md#4-key-delegation--anonymous-users)
|
||||
- [Identity Resolution](./getting-started.md#11-identity-resolution)
|
||||
- [User Profiles](./getting-started.md#12-user-profiles)
|
||||
- [ENS Integration](./api-reference.md#useridentityservice)
|
||||
|
||||
### Content Management
|
||||
|
||||
- [Creating Cells](./getting-started.md#5-creating-content--cells)
|
||||
- [Creating Posts](./getting-started.md#6-creating-content--posts)
|
||||
- [Creating Comments](./getting-started.md#7-creating-content--comments)
|
||||
- [Voting](./getting-started.md#8-voting)
|
||||
- [Reading Content](./getting-started.md#10-reading-cached-data)
|
||||
|
||||
### Moderation
|
||||
|
||||
- [Moderating Posts](./getting-started.md#9-moderation-cell-owner-only)
|
||||
- [Moderating Comments](./getting-started.md#9-moderation-cell-owner-only)
|
||||
- [Moderating Users](./getting-started.md#9-moderation-cell-owner-only)
|
||||
- [Moderation Dashboard Example](./sample-apps.md#moderation-dashboard)
|
||||
|
||||
### Advanced Features
|
||||
|
||||
- [Relevance Scoring](./getting-started.md#14-relevance-scoring)
|
||||
- [Bookmarks](./getting-started.md#15-bookmarks)
|
||||
- [Pending State Management](./getting-started.md#16-pending-state-management)
|
||||
- [Message Validation](./getting-started.md#18-message-validation)
|
||||
|
||||
### Architecture & Internals
|
||||
|
||||
- [System Overview](./architecture.md#system-overview)
|
||||
- [Core Architecture](./architecture.md#core-architecture)
|
||||
- [Data Flow](./architecture.md#data-flow)
|
||||
- [Delegation Subsystem](./architecture.md#1-delegation-subsystem)
|
||||
- [Storage Subsystem](./architecture.md#2-storage-subsystem)
|
||||
- [Identity Subsystem](./architecture.md#3-identity-subsystem)
|
||||
- [Relevance Subsystem](./architecture.md#4-relevance-subsystem)
|
||||
- [Cryptographic Design](./architecture.md#cryptographic-design)
|
||||
- [Network Layer](./architecture.md#network-layer)
|
||||
- [Security Model](./architecture.md#security-model)
|
||||
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
|
||||
### Core Classes
|
||||
|
||||
- **[OpChanClient](./api-reference.md#opchanclient)** - Main entry point
|
||||
- **[DelegationManager](./api-reference.md#delegationmanager)** - Key delegation
|
||||
- **[LocalDatabase](./api-reference.md#localdatabase)** - Storage and caching
|
||||
- **[ForumActions](./api-reference.md#forumactions)** - Content operations
|
||||
- **[UserIdentityService](./api-reference.md#useridentityservice)** - Identity resolution
|
||||
- **[RelevanceCalculator](./api-reference.md#relevancecalculator)** - Content scoring
|
||||
- **[MessageManager](./api-reference.md#messagemanager)** - Network layer
|
||||
- **[BookmarkService](./api-reference.md#bookmarkservice)** - Bookmarks
|
||||
- **[MessageValidator](./api-reference.md#messagevalidator)** - Validation
|
||||
|
||||
### Type Definitions
|
||||
|
||||
- [Core Types](./api-reference.md#type-definitions)
|
||||
- [User Types](./api-reference.md#user)
|
||||
- [Message Types](./api-reference.md#message-types)
|
||||
- [Forum Types](./api-reference.md#extended-forum-types)
|
||||
- [Delegation Types](./api-reference.md#delegationproof)
|
||||
|
||||
---
|
||||
|
||||
## Sample Application
|
||||
|
||||
### Complete Production-Ready Template
|
||||
|
||||
**[Complete Forum Application](./sample-apps.md)**
|
||||
|
||||
A comprehensive, production-ready forum demonstrating all features:
|
||||
|
||||
- ✅ Client initialization and configuration
|
||||
- ✅ Anonymous & wallet authentication
|
||||
- ✅ Session persistence across reloads
|
||||
- ✅ Content creation (cells, posts, comments)
|
||||
- ✅ Voting system (upvote/downvote)
|
||||
- ✅ Identity resolution (ENS, call signs)
|
||||
- ✅ Real-time message synchronization
|
||||
- ✅ Relevance scoring with breakdown
|
||||
- ✅ Moderation tools (cell owners)
|
||||
- ✅ Bookmark management
|
||||
- ✅ Network health monitoring
|
||||
- ✅ Optimistic UI with pending states
|
||||
- ✅ Proper error handling
|
||||
- ✅ Clean architecture
|
||||
|
||||
**Use this as a foundation for building your decentralized forum application.**
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Common Workflows
|
||||
|
||||
- [Anonymous User Flow](../README.md#anonymous-user-flow)
|
||||
- [Wallet User Flow](../README.md#wallet-user-flow)
|
||||
- [Creating Content](../README.md#pattern-1-complete-vanilla-js-application)
|
||||
- [Identity Resolution](../README.md#pattern-2-identity-resolution)
|
||||
- [Content Scoring](../README.md#pattern-3-content-scoring)
|
||||
- [Moderation](../README.md#pattern-4-moderation)
|
||||
- [Bookmarks](../README.md#pattern-5-bookmarks)
|
||||
|
||||
### Best Practices
|
||||
|
||||
- [Database Management](./getting-started.md#21-best-practices)
|
||||
- [Message Handling](./getting-started.md#21-best-practices)
|
||||
- [Network Monitoring](./getting-started.md#21-best-practices)
|
||||
- [Error Handling](./getting-started.md#22-error-handling)
|
||||
- [Performance Optimization](./architecture.md#performance-considerations)
|
||||
- [Security Considerations](./architecture.md#security-model)
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### External Links
|
||||
|
||||
- **GitHub Repository**: [opchan](https://github.com/your-org/opchan)
|
||||
- **React Package**: [@opchan/react](../../react/README.md)
|
||||
- **Waku Protocol**: [Waku Docs](https://docs.waku.org/)
|
||||
- **Viem**: [Viem Docs](https://viem.sh/)
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- [React Package Docs](../../react/README.md)
|
||||
- [React Getting Started](../../react/docs/getting-started.md)
|
||||
- [Project README](../../../README.md)
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Imports
|
||||
|
||||
```typescript
|
||||
// Core client
|
||||
import { OpChanClient } from '@opchan/core';
|
||||
|
||||
// Types
|
||||
import {
|
||||
EVerificationStatus,
|
||||
EDisplayPreference,
|
||||
type User,
|
||||
type Cell,
|
||||
type Post,
|
||||
type Comment,
|
||||
type Bookmark
|
||||
} from '@opchan/core';
|
||||
|
||||
// Utilities
|
||||
import {
|
||||
transformPost,
|
||||
transformComment,
|
||||
transformCell,
|
||||
BookmarkService,
|
||||
MessageValidator
|
||||
} from '@opchan/core';
|
||||
|
||||
// Services
|
||||
import {
|
||||
DelegationManager,
|
||||
LocalDatabase,
|
||||
ForumActions,
|
||||
UserIdentityService,
|
||||
RelevanceCalculator
|
||||
} from '@opchan/core';
|
||||
```
|
||||
|
||||
### Most Common Operations
|
||||
|
||||
```typescript
|
||||
// Initialize
|
||||
const client = new OpChanClient({ wakuConfig });
|
||||
await client.database.open();
|
||||
|
||||
// Anonymous session
|
||||
const sessionId = await client.delegation.delegateAnonymous('7days');
|
||||
|
||||
// Create post
|
||||
await client.forumActions.createPost(params, callback);
|
||||
|
||||
// Read posts
|
||||
const posts = Object.values(client.database.cache.posts);
|
||||
|
||||
// Get identity
|
||||
const identity = await client.userIdentityService.getIdentity(address);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
- [Messages not appearing](../README.md#messages-not-appearing)
|
||||
- [Database not persisting](../README.md#database-not-persisting)
|
||||
- [Identity not resolving](../README.md#identity-not-resolving)
|
||||
- [Delegation expired](./getting-started.md#22-error-handling)
|
||||
- [Network errors](./getting-started.md#22-error-handling)
|
||||
|
||||
### Debug Tips
|
||||
|
||||
1. Check delegation status: `await client.delegation.getStatus()`
|
||||
2. Monitor network health: `client.messageManager.onHealthChange()`
|
||||
3. Validate messages: `await validator.isValidMessage()`
|
||||
4. Check database: `await client.database.open()`
|
||||
5. Review logs: Enable verbose logging in browser console
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Documentation improvements are welcome! Please see the main project README for contribution guidelines.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See [LICENSE](../../../LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: December 2024
|
||||
|
||||
**Version**: 1.0.3
|
||||
|
||||
---
|
||||
|
||||
1791
packages/core/docs/api-reference.md
Normal file
1791
packages/core/docs/api-reference.md
Normal file
File diff suppressed because it is too large
Load Diff
967
packages/core/docs/architecture.md
Normal file
967
packages/core/docs/architecture.md
Normal file
@ -0,0 +1,967 @@
|
||||
# @opchan/core Architecture Guide
|
||||
|
||||
Deep dive into the architecture, design patterns, and implementation details of the OpChan Core SDK.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [System Overview](#system-overview)
|
||||
2. [Core Architecture](#core-architecture)
|
||||
3. [Data Flow](#data-flow)
|
||||
4. [Key Subsystems](#key-subsystems)
|
||||
5. [Cryptographic Design](#cryptographic-design)
|
||||
6. [Storage Strategy](#storage-strategy)
|
||||
7. [Network Layer](#network-layer)
|
||||
8. [Design Patterns](#design-patterns)
|
||||
9. [Performance Considerations](#performance-considerations)
|
||||
10. [Security Model](#security-model)
|
||||
|
||||
---
|
||||
|
||||
## System Overview
|
||||
|
||||
OpChan Core is a decentralized forum infrastructure built on three pillars:
|
||||
|
||||
1. **Cryptographic Identity** - Ed25519 key delegation with wallet authorization
|
||||
2. **P2P Messaging** - Waku protocol for decentralized communication
|
||||
3. **Local-First Storage** - IndexedDB with in-memory caching
|
||||
|
||||
### Design Philosophy
|
||||
|
||||
- **Local-First**: All data persisted locally, network as synchronization mechanism
|
||||
- **Optimistic UI**: Immediate feedback, eventual consistency
|
||||
- **Privacy-Preserving**: No centralized servers, peer-to-peer architecture
|
||||
- **Framework-Agnostic**: Pure TypeScript library, no UI framework dependencies
|
||||
|
||||
---
|
||||
|
||||
## Core Architecture
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ OpChanClient │
|
||||
│ Entry point orchestrating all subsystems │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────┼────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ MessageManager │ │ LocalDatabase │ │ DelegationManager│
|
||||
│ │ │ │ │ │
|
||||
│ - WakuNode │ │ - IndexedDB │ │ - Key Generation │
|
||||
│ - Reliable │ │ - In-memory │ │ - Signing │
|
||||
│ Messaging │ │ Cache │ │ - Verification │
|
||||
│ - Health Monitor │ │ - Validation │ │ │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
│ │ │
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Service Layer │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ ForumActions │ │ Identity │ │ Relevance │ │
|
||||
│ │ │ │ Service │ │ Calculator │ │
|
||||
│ │ - Create │ │ │ │ │ │
|
||||
│ │ - Moderate │ │ - ENS Lookup │ │ - Scoring │ │
|
||||
│ │ - Vote │ │ - Profiles │ │ - Time Decay │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component Responsibilities
|
||||
|
||||
#### OpChanClient
|
||||
- **Role**: Facade pattern - single entry point for all operations
|
||||
- **Responsibilities**:
|
||||
- Instantiate and wire all services
|
||||
- Configure environment
|
||||
- Initialize message manager with Waku config
|
||||
- Provide unified API surface
|
||||
|
||||
#### MessageManager
|
||||
- **Role**: Network abstraction layer
|
||||
- **Responsibilities**:
|
||||
- Manage Waku node lifecycle
|
||||
- Handle message sending/receiving
|
||||
- Monitor network health
|
||||
- Implement reliable messaging (retries, acknowledgments)
|
||||
- Manage subscriptions to content topics
|
||||
|
||||
#### LocalDatabase
|
||||
- **Role**: Persistence and caching layer
|
||||
- **Responsibilities**:
|
||||
- IndexedDB operations (async)
|
||||
- In-memory cache (sync)
|
||||
- Message validation
|
||||
- Deduplication
|
||||
- State management (pending, syncing)
|
||||
|
||||
#### DelegationManager
|
||||
- **Role**: Cryptographic signing system
|
||||
- **Responsibilities**:
|
||||
- Generate Ed25519 keypairs
|
||||
- Create wallet-authorized delegations
|
||||
- Sign messages with browser keys
|
||||
- Verify message signatures
|
||||
- Verify delegation proofs
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Outbound Message Flow (Creating Content)
|
||||
|
||||
```
|
||||
1. User Action
|
||||
└─> ForumActions.createPost()
|
||||
│
|
||||
├─> Validate permissions (wallet or anonymous)
|
||||
│
|
||||
├─> Create unsigned message with UUID
|
||||
│
|
||||
├─> DelegationManager.signMessage()
|
||||
│ ├─> Load cached delegation
|
||||
│ ├─> Check expiry
|
||||
│ ├─> Sign with browser private key (Ed25519)
|
||||
│ └─> Attach signature + browserPubKey + delegationProof
|
||||
│
|
||||
├─> LocalDatabase.applyMessage()
|
||||
│ ├─> Validate signature
|
||||
│ ├─> Check for duplicates
|
||||
│ ├─> Store in cache
|
||||
│ └─> Persist to IndexedDB
|
||||
│
|
||||
├─> LocalDatabase.markPending()
|
||||
│
|
||||
├─> Call updateCallback() [UI refresh]
|
||||
│
|
||||
└─> MessageManager.sendMessage()
|
||||
├─> Encode with protobuf
|
||||
├─> Send via Waku
|
||||
└─> Wait for acknowledgment
|
||||
└─> LocalDatabase.clearPending()
|
||||
```
|
||||
|
||||
### Inbound Message Flow (Receiving Content)
|
||||
|
||||
```
|
||||
1. Waku Network
|
||||
└─> WakuNodeManager receives message
|
||||
│
|
||||
├─> Decode protobuf
|
||||
│
|
||||
├─> ReliableMessaging handles deduplication
|
||||
│
|
||||
└─> MessageService.onMessageReceived() callback
|
||||
│
|
||||
└─> LocalDatabase.applyMessage()
|
||||
│
|
||||
├─> MessageValidator.isValidMessage()
|
||||
│ ├─> Check required fields
|
||||
│ ├─> Verify signature (Ed25519)
|
||||
│ ├─> If delegationProof:
|
||||
│ │ ├─> Verify auth message format
|
||||
│ │ ├─> Verify wallet signature (viem)
|
||||
│ │ └─> Check expiry (optional)
|
||||
│ └─> If anonymous:
|
||||
│ └─> Verify session ID format (UUID)
|
||||
│
|
||||
├─> Check duplicate (type:id:timestamp key)
|
||||
│
|
||||
├─> Store in appropriate cache collection
|
||||
│ ├─> cells[id]
|
||||
│ ├─> posts[id]
|
||||
│ ├─> comments[id]
|
||||
│ ├─> votes[targetId:author]
|
||||
│ ├─> moderations[key]
|
||||
│ └─> userIdentities[address]
|
||||
│
|
||||
├─> Persist to IndexedDB
|
||||
│
|
||||
├─> Update lastSync timestamp
|
||||
│
|
||||
└─> Return true (new message)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Subsystems
|
||||
|
||||
### 1. Delegation Subsystem
|
||||
|
||||
**Purpose**: Reduce wallet signature prompts while maintaining cryptographic security.
|
||||
|
||||
**Components**:
|
||||
|
||||
- **DelegationManager** - Core delegation logic
|
||||
- **DelegationStorage** - IndexedDB persistence
|
||||
- **DelegationCrypto** - Ed25519 signing/verification
|
||||
|
||||
**Delegation Process (Wallet)**:
|
||||
|
||||
```typescript
|
||||
// 1. Generate browser keypair
|
||||
const keypair = DelegationCrypto.generateKeypair();
|
||||
// { publicKey: string, privateKey: string }
|
||||
|
||||
// 2. Create authorization message
|
||||
const authMessage = `
|
||||
Authorize browser key:
|
||||
Public Key: ${keypair.publicKey}
|
||||
Wallet: ${walletAddress}
|
||||
Expires: ${new Date(expiryTimestamp).toISOString()}
|
||||
Nonce: ${nonce}
|
||||
`;
|
||||
|
||||
// 3. Sign with wallet (happens once)
|
||||
const walletSignature = await signFunction(authMessage);
|
||||
|
||||
// 4. Store delegation
|
||||
const delegation: WalletDelegationInfo = {
|
||||
authMessage,
|
||||
walletSignature,
|
||||
expiryTimestamp,
|
||||
walletAddress,
|
||||
browserPublicKey: keypair.publicKey,
|
||||
browserPrivateKey: keypair.privateKey,
|
||||
nonce
|
||||
};
|
||||
|
||||
await DelegationStorage.store(delegation);
|
||||
|
||||
// 5. Subsequent messages signed with browser key
|
||||
const signature = DelegationCrypto.signRaw(messageJson, privateKey);
|
||||
```
|
||||
|
||||
**Delegation Process (Anonymous)**:
|
||||
|
||||
```typescript
|
||||
// 1. Generate browser keypair
|
||||
const keypair = DelegationCrypto.generateKeypair();
|
||||
|
||||
// 2. Generate session ID (UUID)
|
||||
const sessionId = crypto.randomUUID();
|
||||
|
||||
// 3. Store anonymous delegation
|
||||
const delegation: AnonymousDelegationInfo = {
|
||||
sessionId,
|
||||
browserPublicKey: keypair.publicKey,
|
||||
browserPrivateKey: keypair.privateKey,
|
||||
expiryTimestamp,
|
||||
nonce
|
||||
};
|
||||
|
||||
// 4. No wallet signature required
|
||||
// User address = sessionId
|
||||
```
|
||||
|
||||
**Verification Process**:
|
||||
|
||||
```typescript
|
||||
// 1. Verify message signature
|
||||
const messagePayload = JSON.stringify({
|
||||
...message,
|
||||
signature: undefined,
|
||||
browserPubKey: undefined,
|
||||
delegationProof: undefined
|
||||
});
|
||||
|
||||
const signatureValid = DelegationCrypto.verifyRaw(
|
||||
messagePayload,
|
||||
message.signature,
|
||||
message.browserPubKey
|
||||
);
|
||||
|
||||
if (!signatureValid) return false;
|
||||
|
||||
// 2. Verify delegation authorization
|
||||
if (message.delegationProof) {
|
||||
// Wallet user - verify delegation proof
|
||||
const proofValid = await DelegationCrypto.verifyWalletSignature(
|
||||
message.delegationProof.authMessage,
|
||||
message.delegationProof.walletSignature,
|
||||
message.delegationProof.walletAddress
|
||||
);
|
||||
|
||||
// Check auth message contains browser key, wallet address, expiry
|
||||
const authMessageValid =
|
||||
message.delegationProof.authMessage.includes(message.browserPubKey) &&
|
||||
message.delegationProof.authMessage.includes(message.author) &&
|
||||
message.delegationProof.authMessage.includes(
|
||||
message.delegationProof.expiryTimestamp.toString()
|
||||
);
|
||||
|
||||
return proofValid && authMessageValid;
|
||||
} else {
|
||||
// Anonymous user - verify session ID format
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
message.author
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Storage Subsystem
|
||||
|
||||
**Purpose**: Fast local access with persistent storage.
|
||||
|
||||
**Two-Tier Architecture**:
|
||||
|
||||
1. **In-Memory Cache** (Tier 1)
|
||||
- Synchronous access
|
||||
- Fast reads for UI rendering
|
||||
- Volatile (resets on page reload)
|
||||
|
||||
2. **IndexedDB** (Tier 2)
|
||||
- Asynchronous access
|
||||
- Persistent across sessions
|
||||
- Hydrates cache on startup
|
||||
|
||||
**IndexedDB Schema**:
|
||||
|
||||
```typescript
|
||||
const schema = {
|
||||
// Content stores (keyPath = 'id')
|
||||
cells: { keyPath: 'id' },
|
||||
posts: { keyPath: 'id' },
|
||||
comments: { keyPath: 'id' },
|
||||
|
||||
// Votes (keyPath = 'key', composite: targetId:author)
|
||||
votes: { keyPath: 'key' },
|
||||
|
||||
// Moderations (keyPath = 'key', composite varies by type)
|
||||
moderations: { keyPath: 'key' },
|
||||
|
||||
// User identities (keyPath = 'address')
|
||||
userIdentities: {
|
||||
keyPath: 'address',
|
||||
indexes: []
|
||||
},
|
||||
|
||||
// Bookmarks (keyPath = 'id')
|
||||
bookmarks: {
|
||||
keyPath: 'id',
|
||||
indexes: [{ name: 'by_userId', keyPath: 'userId' }]
|
||||
},
|
||||
|
||||
// Auth/state stores (keyPath = 'key')
|
||||
userAuth: { keyPath: 'key' },
|
||||
delegation: { keyPath: 'key' },
|
||||
uiState: { keyPath: 'key' },
|
||||
meta: { keyPath: 'key' }
|
||||
};
|
||||
```
|
||||
|
||||
**Cache Synchronization**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Write Operation │
|
||||
│ │
|
||||
│ 1. Update in-memory cache (immediate) │
|
||||
│ 2. Write to IndexedDB (async, fire-and-forget) │
|
||||
│ 3. Notify listeners (for UI update) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Read Operation │
|
||||
│ │
|
||||
│ 1. Check in-memory cache (fast path) │
|
||||
│ 2. If not found, return null/empty │
|
||||
│ (IndexedDB only used for hydration on startup) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Startup Hydration │
|
||||
│ │
|
||||
│ 1. Open IndexedDB connection │
|
||||
│ 2. Load all stores in parallel │
|
||||
│ 3. Populate in-memory cache │
|
||||
│ 4. Ready for use │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Deduplication**:
|
||||
|
||||
Messages are deduplicated using a composite key:
|
||||
|
||||
```typescript
|
||||
const messageKey = `${message.type}:${message.id}:${message.timestamp}`;
|
||||
```
|
||||
|
||||
This allows the same logical message to be received multiple times without creating duplicates.
|
||||
|
||||
---
|
||||
|
||||
### 3. Identity Subsystem
|
||||
|
||||
**Purpose**: Resolve and cache user identities with ENS integration.
|
||||
|
||||
**Resolution Strategy**:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ Identity Resolution Flow │
|
||||
│ │
|
||||
│ 1. Check LocalDatabase cache │
|
||||
│ └─> If found and fresh (< 5 min): return │
|
||||
│ │
|
||||
│ 2. Check if Ethereum address │
|
||||
│ └─> If not (anonymous/UUID): return null │
|
||||
│ │
|
||||
│ 3. Resolve ENS via PublicClient │
|
||||
│ ├─> Get ENS name │
|
||||
│ ├─> Get ENS avatar │
|
||||
│ └─> Cache result for 5 minutes │
|
||||
│ │
|
||||
│ 4. Build UserIdentity object │
|
||||
│ ├─> address │
|
||||
│ ├─> ensName │
|
||||
│ ├─> ensAvatar │
|
||||
│ ├─> callSign (from profile messages) │
|
||||
│ ├─> displayPreference │
|
||||
│ ├─> displayName (computed) │
|
||||
│ ├─> verificationStatus (computed) │
|
||||
│ └─> lastUpdated │
|
||||
│ │
|
||||
│ 5. Store in LocalDatabase │
|
||||
│ │
|
||||
│ 6. Return identity │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Display Name Resolution**:
|
||||
|
||||
```typescript
|
||||
function getDisplayName(identity: UserIdentity): string {
|
||||
// Priority 1: Call sign (if preference is CALL_SIGN)
|
||||
if (
|
||||
identity.callSign &&
|
||||
identity.displayPreference === EDisplayPreference.CALL_SIGN
|
||||
) {
|
||||
return identity.callSign;
|
||||
}
|
||||
|
||||
// Priority 2: ENS name
|
||||
if (identity.ensName) {
|
||||
return identity.ensName;
|
||||
}
|
||||
|
||||
// Priority 3: Shortened address
|
||||
return `${identity.address.slice(0, 6)}...${identity.address.slice(-4)}`;
|
||||
}
|
||||
```
|
||||
|
||||
**Profile Updates**:
|
||||
|
||||
User profile updates are broadcast as USER_PROFILE_UPDATE messages:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: MessageType.USER_PROFILE_UPDATE,
|
||||
id: uuid(),
|
||||
timestamp: Date.now(),
|
||||
author: userAddress,
|
||||
callSign: 'alice', // optional
|
||||
displayPreference: EDisplayPreference.CALL_SIGN,
|
||||
signature: '...',
|
||||
browserPubKey: '...',
|
||||
delegationProof: { ... }
|
||||
}
|
||||
```
|
||||
|
||||
These messages update the `userIdentities` cache, propagating changes across the network.
|
||||
|
||||
---
|
||||
|
||||
### 4. Relevance Subsystem
|
||||
|
||||
**Purpose**: Score content based on engagement, verification, time, and moderation.
|
||||
|
||||
**Scoring Algorithm**:
|
||||
|
||||
```typescript
|
||||
interface RelevanceFactors {
|
||||
base: 100;
|
||||
engagement: {
|
||||
upvoteWeight: 10;
|
||||
commentWeight: 3;
|
||||
};
|
||||
verification: {
|
||||
authorBonus: 20; // ENS verified author
|
||||
upvoteBonus: 5; // Per ENS verified upvoter
|
||||
commenterBonus: 10; // Per ENS verified commenter
|
||||
};
|
||||
timeDecay: {
|
||||
halfLifeDays: 7;
|
||||
formula: 'exponential'; // exp(-0.693 * days / halfLife)
|
||||
};
|
||||
moderation: {
|
||||
penalty: 0.5; // 50% reduction if moderated
|
||||
};
|
||||
}
|
||||
|
||||
function calculateScore(
|
||||
post: Post,
|
||||
votes: Vote[],
|
||||
comments: Comment[],
|
||||
verifications: Map<address, boolean>,
|
||||
moderations: Map<postId, Moderation>
|
||||
): number {
|
||||
// Base score
|
||||
let score = 100;
|
||||
|
||||
// Engagement
|
||||
const upvotes = votes.filter(v => v.value === 1).length;
|
||||
const downvotes = votes.filter(v => v.value === -1).length;
|
||||
score += (upvotes * 10) + (comments.length * 3);
|
||||
|
||||
// Verification bonuses
|
||||
if (verifications.get(post.author)) {
|
||||
score += 20; // Author ENS verified
|
||||
}
|
||||
|
||||
const verifiedUpvoters = votes
|
||||
.filter(v => v.value === 1 && verifications.get(v.author))
|
||||
.length;
|
||||
score += verifiedUpvoters * 5;
|
||||
|
||||
const verifiedCommenters = new Set(
|
||||
comments
|
||||
.map(c => c.author)
|
||||
.filter(author => verifications.get(author))
|
||||
).size;
|
||||
score += verifiedCommenters * 10;
|
||||
|
||||
// Time decay (exponential)
|
||||
const daysOld = (Date.now() - post.timestamp) / (1000 * 60 * 60 * 24);
|
||||
const decay = Math.exp(-0.693 * daysOld / 7); // Half-life 7 days
|
||||
score *= decay;
|
||||
|
||||
// Moderation penalty
|
||||
if (moderations.has(post.id)) {
|
||||
score *= 0.5;
|
||||
}
|
||||
|
||||
return Math.max(0, score);
|
||||
}
|
||||
```
|
||||
|
||||
**Score Components Breakdown**:
|
||||
|
||||
```typescript
|
||||
interface RelevanceScoreDetails {
|
||||
baseScore: 100,
|
||||
engagementScore: (upvotes * 10) + (comments * 3),
|
||||
authorVerificationBonus: isENS ? 20 : 0,
|
||||
verifiedUpvoteBonus: verifiedUpvoters * 5,
|
||||
verifiedCommenterBonus: verifiedCommenters * 10,
|
||||
timeDecayMultiplier: exp(-0.693 * daysOld / 7),
|
||||
moderationPenalty: isModerated ? 0.5 : 1.0,
|
||||
finalScore: (base + engagement + bonuses) * decay * modPenalty
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cryptographic Design
|
||||
|
||||
### Key Hierarchy
|
||||
|
||||
```
|
||||
Wallet Private Key (User's wallet, never exposed)
|
||||
│
|
||||
├─> Signs authorization message
|
||||
│ └─> Stored in delegationProof.walletSignature
|
||||
│
|
||||
└─> Authorizes ─────────────────────────┐
|
||||
│
|
||||
Browser Private Key (Generated, stored locally)
|
||||
│ │
|
||||
├─> Signs all messages │
|
||||
│ └─> Stored in message.signature │
|
||||
│ │
|
||||
└─> Public key: message.browserPubKey ───┘
|
||||
```
|
||||
|
||||
### Cryptographic Primitives
|
||||
|
||||
**Ed25519** (via @noble/ed25519):
|
||||
- Browser key generation
|
||||
- Message signing
|
||||
- Signature verification
|
||||
|
||||
**ECDSA** (via viem):
|
||||
- Wallet signature verification
|
||||
- ENS resolution
|
||||
|
||||
### Message Signature Structure
|
||||
|
||||
```typescript
|
||||
// Unsigned message
|
||||
const unsignedMessage = {
|
||||
type: 'post',
|
||||
id: 'abc123',
|
||||
cellId: 'xyz789',
|
||||
title: 'Hello',
|
||||
content: 'World',
|
||||
timestamp: 1234567890,
|
||||
author: '0x...'
|
||||
};
|
||||
|
||||
// Message to sign (excludes signature fields)
|
||||
const messageToSign = JSON.stringify({
|
||||
...unsignedMessage,
|
||||
signature: undefined,
|
||||
browserPubKey: undefined,
|
||||
delegationProof: undefined
|
||||
});
|
||||
|
||||
// Sign with browser private key
|
||||
const signature = await ed25519.sign(
|
||||
sha512(messageToSign),
|
||||
browserPrivateKey
|
||||
);
|
||||
|
||||
// Signed message
|
||||
const signedMessage = {
|
||||
...unsignedMessage,
|
||||
signature: bytesToHex(signature),
|
||||
browserPubKey: bytesToHex(browserPublicKey),
|
||||
delegationProof: {
|
||||
authMessage: '...',
|
||||
walletSignature: '...',
|
||||
expiryTimestamp: 1234567890,
|
||||
walletAddress: '0x...'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Verification Logic
|
||||
|
||||
```typescript
|
||||
async function verifyMessage(message: OpchanMessage): Promise<boolean> {
|
||||
// 1. Verify message signature with browser public key
|
||||
const messagePayload = JSON.stringify({
|
||||
...message,
|
||||
signature: undefined,
|
||||
browserPubKey: undefined,
|
||||
delegationProof: undefined
|
||||
});
|
||||
|
||||
const signatureValid = await ed25519.verify(
|
||||
hexToBytes(message.signature),
|
||||
sha512(messagePayload),
|
||||
hexToBytes(message.browserPubKey)
|
||||
);
|
||||
|
||||
if (!signatureValid) return false;
|
||||
|
||||
// 2. Verify delegation authorization
|
||||
if (message.delegationProof) {
|
||||
// Wallet user - verify wallet signature on auth message
|
||||
const { authMessage, walletSignature, walletAddress } = message.delegationProof;
|
||||
|
||||
const walletSigValid = await verifyMessage({
|
||||
account: walletAddress,
|
||||
message: authMessage,
|
||||
signature: walletSignature
|
||||
});
|
||||
|
||||
if (!walletSigValid) return false;
|
||||
|
||||
// Verify auth message contains browser key and expiry
|
||||
if (!authMessage.includes(message.browserPubKey)) return false;
|
||||
if (!authMessage.includes(walletAddress)) return false;
|
||||
if (!authMessage.includes(message.delegationProof.expiryTimestamp.toString())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Anonymous user - verify session ID is valid UUID
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
message.author
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Layer
|
||||
|
||||
### Waku Protocol Integration
|
||||
|
||||
**Architecture**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ MessageManager │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ WakuNodeManager │ │
|
||||
│ │ - Create Waku node │ │
|
||||
│ │ - Connect to bootstrap peers │ │
|
||||
│ │ - Monitor health │ │
|
||||
│ │ - Emit health events │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ ReliableMessaging │ │
|
||||
│ │ - Store & Forward protocol │ │
|
||||
│ │ - Message deduplication │ │
|
||||
│ │ - Acknowledgments │ │
|
||||
│ │ - Retries │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ MessageService │ │
|
||||
│ │ - Send messages │ │
|
||||
│ │ - Receive subscriptions │ │
|
||||
│ │ - Codec management (protobuf) │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Content Topics**:
|
||||
|
||||
Messages are published/subscribed via content topics:
|
||||
|
||||
```typescript
|
||||
const contentTopic = '/opchan/1/messages/proto';
|
||||
|
||||
// All message types share the same content topic
|
||||
// Filtering happens at application layer based on message.type
|
||||
```
|
||||
|
||||
**Message Encoding**:
|
||||
|
||||
Messages are encoded with protobuf before transmission (handled by Waku SDK).
|
||||
|
||||
**Reliable Messaging**:
|
||||
|
||||
The `ReliableMessaging` layer provides:
|
||||
|
||||
1. **Store-and-Forward**: Messages cached for offline peers
|
||||
2. **Deduplication**: Filter out duplicate receives
|
||||
3. **Acknowledgments**: Confirm message delivery
|
||||
4. **Retries**: Resend on failure
|
||||
|
||||
---
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### 1. Singleton Pattern
|
||||
|
||||
**DelegationManager**, **LocalDatabase**, **MessageManager** are exported as singletons:
|
||||
|
||||
```typescript
|
||||
// Singleton instance
|
||||
export const delegationManager = new DelegationManager();
|
||||
export const localDatabase = new LocalDatabase();
|
||||
export default new DefaultMessageManager();
|
||||
```
|
||||
|
||||
### 2. Facade Pattern
|
||||
|
||||
**OpChanClient** acts as a facade, providing a simplified interface:
|
||||
|
||||
```typescript
|
||||
class OpChanClient {
|
||||
// Aggregates all services
|
||||
readonly delegation: DelegationManager;
|
||||
readonly database: LocalDatabase;
|
||||
readonly messageManager: DefaultMessageManager;
|
||||
readonly forumActions: ForumActions;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Observer Pattern
|
||||
|
||||
Event subscriptions throughout:
|
||||
|
||||
```typescript
|
||||
// MessageManager
|
||||
messageManager.onMessageReceived(callback);
|
||||
messageManager.onHealthChange(callback);
|
||||
messageManager.onSyncStatus(callback);
|
||||
|
||||
// UserIdentityService
|
||||
userIdentityService.subscribe(callback);
|
||||
|
||||
// LocalDatabase
|
||||
database.onPendingChange(callback);
|
||||
```
|
||||
|
||||
### 4. Strategy Pattern
|
||||
|
||||
**MessageValidator** validates different message types:
|
||||
|
||||
```typescript
|
||||
class MessageValidator {
|
||||
async isValidMessage(message: unknown): Promise<boolean> {
|
||||
// Different validation logic per message type
|
||||
switch (message.type) {
|
||||
case MessageType.CELL:
|
||||
return this.validateCell(message);
|
||||
case MessageType.POST:
|
||||
return this.validatePost(message);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Repository Pattern
|
||||
|
||||
**LocalDatabase** acts as a repository abstracting storage:
|
||||
|
||||
```typescript
|
||||
interface Repository<T> {
|
||||
getById(id: string): T | null;
|
||||
getAll(): T[];
|
||||
save(item: T): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
// LocalDatabase implements repository pattern for each entity type
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. In-Memory Caching
|
||||
|
||||
All reads are synchronous from in-memory cache:
|
||||
|
||||
```typescript
|
||||
// Fast - synchronous cache access
|
||||
const posts = Object.values(client.database.cache.posts);
|
||||
|
||||
// No IndexedDB reads during normal operation
|
||||
```
|
||||
|
||||
### 2. Lazy Identity Resolution
|
||||
|
||||
Identities resolved on-demand with caching:
|
||||
|
||||
```typescript
|
||||
// First call: async ENS lookup
|
||||
const identity1 = await getIdentity(address);
|
||||
|
||||
// Subsequent calls: cache hit (fast)
|
||||
const identity2 = await getIdentity(address); // same address
|
||||
```
|
||||
|
||||
### 3. Debouncing
|
||||
|
||||
Identity lookups are debounced to avoid redundant calls:
|
||||
|
||||
```typescript
|
||||
// Multiple rapid calls
|
||||
getIdentity(address); // Starts timer
|
||||
getIdentity(address); // Resets timer
|
||||
getIdentity(address); // Resets timer
|
||||
// Only executes once after 100ms
|
||||
```
|
||||
|
||||
### 4. Batch Operations
|
||||
|
||||
IndexedDB writes are batched:
|
||||
|
||||
```typescript
|
||||
// Single transaction for multiple writes
|
||||
const tx = db.transaction(['cells', 'posts'], 'readwrite');
|
||||
tx.objectStore('cells').put(cell);
|
||||
tx.objectStore('posts').put(post);
|
||||
await tx.complete;
|
||||
```
|
||||
|
||||
### 5. Optimistic UI
|
||||
|
||||
Immediate feedback with pending states:
|
||||
|
||||
```typescript
|
||||
// 1. Write to cache immediately
|
||||
cache.posts[postId] = post;
|
||||
|
||||
// 2. Mark as pending
|
||||
markPending(postId);
|
||||
|
||||
// 3. Update UI (shows post with "syncing" badge)
|
||||
updateUI();
|
||||
|
||||
// 4. Send to network (async)
|
||||
sendMessage(post);
|
||||
|
||||
// 5. Clear pending when confirmed
|
||||
clearPending(postId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
### Threat Model
|
||||
|
||||
**Trusted**:
|
||||
- User's device and browser
|
||||
- User's wallet private key
|
||||
|
||||
**Untrusted**:
|
||||
- Network peers
|
||||
- Network infrastructure
|
||||
- Message content
|
||||
|
||||
### Security Guarantees
|
||||
|
||||
1. **Message Authenticity**
|
||||
- All messages signed with browser key
|
||||
- Browser key authorized by wallet (for wallet users)
|
||||
- Anonymous users sign with session key (no wallet)
|
||||
|
||||
2. **Message Integrity**
|
||||
- Signatures cover entire message payload
|
||||
- Any modification invalidates signature
|
||||
|
||||
3. **Non-Repudiation**
|
||||
- Messages cryptographically tied to author
|
||||
- Cannot deny authorship of signed messages
|
||||
|
||||
4. **Replay Protection**
|
||||
- Deduplication prevents replayed messages
|
||||
- Timestamps in message payloads
|
||||
|
||||
### Attack Resistance
|
||||
|
||||
**Impersonation**:
|
||||
- ❌ Prevented: Cannot forge signature without private key
|
||||
|
||||
**Message Tampering**:
|
||||
- ❌ Prevented: Modified messages fail signature verification
|
||||
|
||||
**Replay Attacks**:
|
||||
- ✅ Mitigated: Deduplication based on (type:id:timestamp)
|
||||
- ⚠️ Limited: Same message can be replayed with different timestamp
|
||||
|
||||
**Sybil Attacks**:
|
||||
- ⚠️ Possible: Anonymous users can create multiple sessions
|
||||
- ✅ Mitigated: ENS-verified users have higher trust/scoring
|
||||
|
||||
**DoS Attacks**:
|
||||
- ⚠️ Possible: Can flood network with messages
|
||||
- ✅ Mitigated: Client-side validation rejects malformed messages
|
||||
|
||||
---
|
||||
|
||||
**End of Architecture Guide**
|
||||
|
||||
951
packages/core/docs/getting-started.md
Normal file
951
packages/core/docs/getting-started.md
Normal file
@ -0,0 +1,951 @@
|
||||
## OpChan Core SDK (packages/core) — Building Decentralized Forums
|
||||
|
||||
This guide shows how to build a decentralized forum application using the Core SDK directly (without React). It covers project setup, client initialization, key delegation, network connectivity, content management, identity resolution, and persistence.
|
||||
|
||||
The examples assume you install and use the `@opchan/core` package.
|
||||
|
||||
---
|
||||
|
||||
### 1) Install and basic setup
|
||||
|
||||
```bash
|
||||
npm i @opchan/core
|
||||
```
|
||||
|
||||
Create a client instance and open the database:
|
||||
|
||||
```typescript
|
||||
import { OpChanClient } from '@opchan/core';
|
||||
|
||||
const client = new OpChanClient({
|
||||
wakuConfig: {
|
||||
contentTopic: '/opchan/1/messages/proto',
|
||||
reliableChannelId: 'opchan-messages'
|
||||
},
|
||||
reownProjectId: 'your-reown-project-id' // Optional
|
||||
});
|
||||
|
||||
// IMPORTANT: Open database before use
|
||||
await client.database.open();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2) Message synchronization
|
||||
|
||||
Set up listeners for incoming messages and network health:
|
||||
|
||||
```typescript
|
||||
// Listen for all incoming messages
|
||||
const unsubscribeMessages = client.messageManager.onMessageReceived(
|
||||
async (message) => {
|
||||
// Apply to local database
|
||||
const wasNew = await client.database.applyMessage(message);
|
||||
if (wasNew) {
|
||||
console.log('New message received:', message.type);
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Monitor network health
|
||||
const unsubscribeHealth = client.messageManager.onHealthChange(
|
||||
(isHealthy) => {
|
||||
console.log('Network status:', isHealthy ? 'Connected' : 'Disconnected');
|
||||
updateNetworkIndicator(isHealthy);
|
||||
}
|
||||
);
|
||||
|
||||
// Monitor sync status
|
||||
const unsubscribeSync = client.messageManager.onSyncStatus(
|
||||
(status) => {
|
||||
console.log('Sync status:', status);
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up when done
|
||||
function cleanup() {
|
||||
unsubscribeMessages();
|
||||
unsubscribeHealth();
|
||||
unsubscribeSync();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3) Key delegation — wallet users
|
||||
|
||||
For wallet-connected users, create a delegation to reduce signature prompts:
|
||||
|
||||
```typescript
|
||||
import { signMessage } from 'viem/accounts';
|
||||
|
||||
async function setupWalletDelegation(walletAddress: `0x${string}`) {
|
||||
// Create delegation for 7 or 30 days
|
||||
const success = await client.delegation.delegate(
|
||||
walletAddress,
|
||||
'7days', // or '30days'
|
||||
async (message: string) => {
|
||||
// Sign with wallet
|
||||
return await signMessage({
|
||||
message,
|
||||
account: walletAddress
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log('Delegation created successfully');
|
||||
|
||||
// Check delegation status
|
||||
const status = await client.delegation.getStatus(walletAddress);
|
||||
console.log('Delegation valid:', status.isValid);
|
||||
console.log('Time remaining:', status.timeRemaining);
|
||||
console.log('Expires at:', new Date(Date.now() + status.timeRemaining!));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4) Key delegation — anonymous users
|
||||
|
||||
For anonymous users (no wallet), create an anonymous delegation:
|
||||
|
||||
```typescript
|
||||
async function setupAnonymousSession() {
|
||||
// Create anonymous delegation (returns session ID)
|
||||
const sessionId = await client.delegation.delegateAnonymous('7days');
|
||||
|
||||
console.log('Anonymous session ID:', sessionId);
|
||||
|
||||
// Create user object
|
||||
const anonymousUser = {
|
||||
address: sessionId,
|
||||
displayName: 'Anonymous',
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
verificationStatus: EVerificationStatus.ANONYMOUS
|
||||
};
|
||||
|
||||
// Store user in database
|
||||
await client.database.storeUser(anonymousUser);
|
||||
|
||||
return anonymousUser;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5) Creating content — cells
|
||||
|
||||
Create a cell (requires ENS-verified wallet):
|
||||
|
||||
```typescript
|
||||
async function createCell(
|
||||
currentUser: User,
|
||||
name: string,
|
||||
description: string,
|
||||
icon?: string
|
||||
) {
|
||||
const result = await client.forumActions.createCell(
|
||||
{
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => {
|
||||
// Callback when cache is updated
|
||||
updateUI();
|
||||
}
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Cell created:', result.data);
|
||||
return result.data;
|
||||
} else {
|
||||
console.error('Failed to create cell:', result.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6) Creating content — posts
|
||||
|
||||
Create a post in a cell:
|
||||
|
||||
```typescript
|
||||
async function createPost(
|
||||
currentUser: User,
|
||||
cellId: string,
|
||||
title: string,
|
||||
content: string
|
||||
) {
|
||||
const result = await client.forumActions.createPost(
|
||||
{
|
||||
cellId,
|
||||
title,
|
||||
content,
|
||||
currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => updateUI()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Post created:', result.data);
|
||||
|
||||
// Mark as pending until network confirms
|
||||
client.database.markPending(result.data!.id);
|
||||
|
||||
return result.data;
|
||||
} else {
|
||||
console.error('Failed to create post:', result.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7) Creating content — comments
|
||||
|
||||
Add a comment to a post:
|
||||
|
||||
```typescript
|
||||
async function createComment(
|
||||
currentUser: User,
|
||||
postId: string,
|
||||
content: string
|
||||
) {
|
||||
const result = await client.forumActions.createComment(
|
||||
{
|
||||
postId,
|
||||
content,
|
||||
currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => updateUI()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Comment created:', result.data);
|
||||
client.database.markPending(result.data!.id);
|
||||
return result.data;
|
||||
} else {
|
||||
console.error('Failed to create comment:', result.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8) Voting
|
||||
|
||||
Vote on posts or comments:
|
||||
|
||||
```typescript
|
||||
async function voteOnContent(
|
||||
currentUser: User,
|
||||
targetId: string,
|
||||
isUpvote: boolean
|
||||
) {
|
||||
const result = await client.forumActions.vote(
|
||||
{
|
||||
targetId,
|
||||
isUpvote,
|
||||
currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => updateUI()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Vote registered:', isUpvote ? 'upvote' : 'downvote');
|
||||
} else {
|
||||
console.error('Failed to vote:', result.error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9) Moderation (cell owner only)
|
||||
|
||||
Moderate posts, comments, or users within a cell:
|
||||
|
||||
```typescript
|
||||
async function moderatePost(
|
||||
currentUser: User,
|
||||
cellId: string,
|
||||
postId: string,
|
||||
reason: string
|
||||
) {
|
||||
const cell = client.database.cache.cells[cellId];
|
||||
|
||||
if (!cell || currentUser.address !== cell.author) {
|
||||
console.error('Not authorized: Only cell owner can moderate');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await client.forumActions.moderatePost(
|
||||
{
|
||||
cellId,
|
||||
postId,
|
||||
reason,
|
||||
currentUser,
|
||||
isAuthenticated: true,
|
||||
cellOwner: cell.author
|
||||
},
|
||||
() => updateUI()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Post moderated');
|
||||
}
|
||||
}
|
||||
|
||||
async function unmoderatePost(
|
||||
currentUser: User,
|
||||
cellId: string,
|
||||
postId: string
|
||||
) {
|
||||
const cell = client.database.cache.cells[cellId];
|
||||
|
||||
const result = await client.forumActions.unmoderatePost(
|
||||
{
|
||||
cellId,
|
||||
postId,
|
||||
currentUser,
|
||||
isAuthenticated: true,
|
||||
cellOwner: cell.author
|
||||
},
|
||||
() => updateUI()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Post unmoderated');
|
||||
}
|
||||
}
|
||||
|
||||
// Similar methods exist for comments and users:
|
||||
// - client.forumActions.moderateComment()
|
||||
// - client.forumActions.unmoderateComment()
|
||||
// - client.forumActions.moderateUser()
|
||||
// - client.forumActions.unmoderateUser()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10) Reading cached data
|
||||
|
||||
Access cached content from the in-memory database:
|
||||
|
||||
```typescript
|
||||
// Get all cells
|
||||
const cells = Object.values(client.database.cache.cells);
|
||||
console.log('Cells:', cells.length);
|
||||
|
||||
// Get all posts
|
||||
const posts = Object.values(client.database.cache.posts);
|
||||
|
||||
// Filter posts by cell
|
||||
const cellPosts = posts.filter(p => p.cellId === 'specific-cell-id');
|
||||
|
||||
// Get all comments
|
||||
const comments = Object.values(client.database.cache.comments);
|
||||
|
||||
// Filter comments by post
|
||||
const postComments = comments.filter(c => c.postId === 'specific-post-id');
|
||||
|
||||
// Get votes
|
||||
const votes = Object.values(client.database.cache.votes);
|
||||
|
||||
// Get votes for specific content
|
||||
const postVotes = votes.filter(v => v.targetId === 'post-id');
|
||||
const upvotes = postVotes.filter(v => v.value === 1);
|
||||
const downvotes = postVotes.filter(v => v.value === -1);
|
||||
|
||||
// Get moderations
|
||||
const moderations = Object.values(client.database.cache.moderations);
|
||||
|
||||
// Check if post is moderated
|
||||
const postModeration = moderations.find(
|
||||
m => m.targetType === 'post' && m.targetId === 'post-id'
|
||||
);
|
||||
const isModerated = postModeration?.action === 'moderate';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11) Identity resolution
|
||||
|
||||
Resolve user identities (ENS names, call signs, etc.):
|
||||
|
||||
```typescript
|
||||
// Get identity for a wallet address
|
||||
const identity = await client.userIdentityService.getIdentity(
|
||||
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
|
||||
);
|
||||
|
||||
if (identity) {
|
||||
console.log('Display name:', identity.displayName);
|
||||
console.log('ENS name:', identity.ensName);
|
||||
console.log('ENS avatar:', identity.ensAvatar);
|
||||
console.log('Call sign:', identity.callSign);
|
||||
console.log('Verification:', identity.verificationStatus);
|
||||
}
|
||||
|
||||
// Force fresh resolution (bypass cache)
|
||||
const freshIdentity = await client.userIdentityService.getIdentity(
|
||||
address,
|
||||
{ fresh: true }
|
||||
);
|
||||
|
||||
// Get display name only
|
||||
const displayName = client.userIdentityService.getDisplayName({
|
||||
address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
||||
ensName: 'alice.eth',
|
||||
displayPreference: EDisplayPreference.CALL_SIGN
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12) User profiles
|
||||
|
||||
Update user profiles (call sign and display preference):
|
||||
|
||||
```typescript
|
||||
async function updateUserProfile(
|
||||
userAddress: string,
|
||||
callSign?: string,
|
||||
displayPreference?: EDisplayPreference
|
||||
) {
|
||||
const result = await client.userIdentityService.updateProfile(
|
||||
userAddress,
|
||||
{
|
||||
callSign,
|
||||
displayPreference
|
||||
}
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
console.log('Profile updated:', result.identity);
|
||||
return result.identity;
|
||||
} else {
|
||||
console.error('Failed to update profile:', result.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Set call sign
|
||||
await updateUserProfile(
|
||||
userAddress,
|
||||
'alice',
|
||||
EDisplayPreference.CALL_SIGN
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13) Subscribe to identity changes
|
||||
|
||||
React to identity updates in real-time:
|
||||
|
||||
```typescript
|
||||
const unsubscribe = client.userIdentityService.subscribe(
|
||||
(address, identity) => {
|
||||
console.log('Identity updated:', address);
|
||||
|
||||
if (identity) {
|
||||
console.log('New display name:', identity.displayName);
|
||||
console.log('New call sign:', identity.callSign);
|
||||
|
||||
// Update UI
|
||||
updateUserDisplay(address, identity);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14) Relevance scoring
|
||||
|
||||
Calculate relevance scores for posts:
|
||||
|
||||
```typescript
|
||||
import { transformPost } from '@opchan/core';
|
||||
|
||||
// Transform raw post message to enhanced post
|
||||
const post = await transformPost(postMessage);
|
||||
|
||||
// Get votes and comments for scoring
|
||||
const postVotes = Object.values(client.database.cache.votes)
|
||||
.filter(v => v.targetId === post.id);
|
||||
const postComments = Object.values(client.database.cache.comments)
|
||||
.filter(c => c.postId === post.id);
|
||||
|
||||
// Get user verification status
|
||||
const userVerificationStatus = {};
|
||||
for (const [address, identity] of Object.entries(
|
||||
client.database.cache.userIdentities
|
||||
)) {
|
||||
userVerificationStatus[address] = {
|
||||
isVerified: identity.verificationStatus === 'ens-verified',
|
||||
hasENS: !!identity.ensName,
|
||||
ensName: identity.ensName
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate score
|
||||
const scoreDetails = client.relevance.calculatePostScore(
|
||||
postMessage,
|
||||
postVotes,
|
||||
postComments,
|
||||
userVerificationStatus,
|
||||
client.database.cache.moderations
|
||||
);
|
||||
|
||||
console.log('Relevance score:', scoreDetails.finalScore);
|
||||
console.log('Score breakdown:', {
|
||||
base: scoreDetails.baseScore,
|
||||
engagement: scoreDetails.engagementScore,
|
||||
authorBonus: scoreDetails.authorVerificationBonus,
|
||||
upvoteBonus: scoreDetails.verifiedUpvoteBonus,
|
||||
commenterBonus: scoreDetails.verifiedCommenterBonus,
|
||||
timeDecay: scoreDetails.timeDecayMultiplier,
|
||||
moderation: scoreDetails.moderationPenalty
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 15) Bookmarks
|
||||
|
||||
Manage user bookmarks:
|
||||
|
||||
```typescript
|
||||
import { BookmarkService } from '@opchan/core';
|
||||
|
||||
const bookmarkService = new BookmarkService();
|
||||
|
||||
// Add post bookmark
|
||||
await bookmarkService.addPostBookmark(post, userId, cellId);
|
||||
|
||||
// Add comment bookmark
|
||||
await bookmarkService.addCommentBookmark(comment, userId, postId);
|
||||
|
||||
// Get all user bookmarks
|
||||
const bookmarks = await client.database.getUserBookmarks(userId);
|
||||
console.log('Total bookmarks:', bookmarks.length);
|
||||
|
||||
// Get bookmarks by type
|
||||
const postBookmarks = bookmarks.filter(b => b.type === 'post');
|
||||
const commentBookmarks = bookmarks.filter(b => b.type === 'comment');
|
||||
|
||||
// Check if content is bookmarked
|
||||
const isBookmarked = client.database.isBookmarked(userId, 'post', postId);
|
||||
|
||||
// Remove bookmark
|
||||
if (isBookmarked) {
|
||||
const bookmarkId = `post:${postId}`;
|
||||
await bookmarkService.removeBookmark(bookmarkId);
|
||||
}
|
||||
|
||||
// Clear all bookmarks
|
||||
const allBookmarks = await client.database.getUserBookmarks(userId);
|
||||
for (const bookmark of allBookmarks) {
|
||||
await bookmarkService.removeBookmark(bookmark.id);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 16) Pending state management
|
||||
|
||||
Track pending operations for optimistic UI:
|
||||
|
||||
```typescript
|
||||
// Mark content as pending
|
||||
client.database.markPending(postId);
|
||||
|
||||
// Check if pending
|
||||
const isPending = client.database.isPending(postId);
|
||||
if (isPending) {
|
||||
showSyncingIndicator(postId);
|
||||
}
|
||||
|
||||
// Listen for pending changes
|
||||
const unsubscribe = client.database.onPendingChange(() => {
|
||||
// Update UI when pending state changes
|
||||
updateAllPendingIndicators();
|
||||
});
|
||||
|
||||
// Clear pending when confirmed
|
||||
client.database.clearPending(postId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 17) Persistence and hydration
|
||||
|
||||
Load persisted data on app start:
|
||||
|
||||
```typescript
|
||||
async function initializeApp() {
|
||||
// Open database (hydrates from IndexedDB)
|
||||
await client.database.open();
|
||||
|
||||
// Load stored user
|
||||
const storedUser = await client.database.loadUser();
|
||||
if (storedUser) {
|
||||
console.log('Restored user session:', storedUser.displayName);
|
||||
|
||||
// Check if delegation is still valid
|
||||
const delegationStatus = await client.delegation.getStatus(
|
||||
storedUser.address
|
||||
);
|
||||
|
||||
if (!delegationStatus.isValid) {
|
||||
console.log('Delegation expired, need to re-authorize');
|
||||
await client.database.clearUser();
|
||||
await client.database.clearDelegation();
|
||||
}
|
||||
}
|
||||
|
||||
// Content is already hydrated from IndexedDB
|
||||
console.log('Loaded from cache:', {
|
||||
cells: Object.keys(client.database.cache.cells).length,
|
||||
posts: Object.keys(client.database.cache.posts).length,
|
||||
comments: Object.keys(client.database.cache.comments).length,
|
||||
votes: Object.keys(client.database.cache.votes).length
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 18) Message validation
|
||||
|
||||
Validate messages before processing:
|
||||
|
||||
```typescript
|
||||
import { MessageValidator } from '@opchan/core';
|
||||
|
||||
const validator = new MessageValidator();
|
||||
|
||||
// Validate a message
|
||||
const isValid = await validator.isValidMessage(message);
|
||||
|
||||
if (!isValid) {
|
||||
// Get detailed validation report
|
||||
const report = await validator.getValidationReport(message);
|
||||
|
||||
console.error('Invalid message:', {
|
||||
missingFields: report.missingFields,
|
||||
invalidFields: report.invalidFields,
|
||||
hasValidSignature: report.hasValidSignature,
|
||||
errors: report.errors,
|
||||
warnings: report.warnings
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 19) Network state management
|
||||
|
||||
Monitor and manage network connectivity:
|
||||
|
||||
```typescript
|
||||
// Get current network status
|
||||
const isReady = client.messageManager.isReady;
|
||||
const health = client.messageManager.currentHealth;
|
||||
|
||||
console.log('Network ready:', isReady);
|
||||
console.log('Network health:', health);
|
||||
|
||||
// Get sync state
|
||||
const syncState = client.database.getSyncState();
|
||||
console.log('Last sync:', new Date(syncState.lastSync || 0));
|
||||
console.log('Is syncing:', syncState.isSyncing);
|
||||
|
||||
// Listen for health changes
|
||||
client.messageManager.onHealthChange((isHealthy) => {
|
||||
if (isHealthy) {
|
||||
console.log('Network connected - messages will sync');
|
||||
} else {
|
||||
console.log('Network disconnected - working offline');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 20) Complete application skeleton
|
||||
|
||||
```typescript
|
||||
import {
|
||||
OpChanClient,
|
||||
EVerificationStatus,
|
||||
EDisplayPreference,
|
||||
transformPost,
|
||||
transformComment,
|
||||
type User
|
||||
} from '@opchan/core';
|
||||
|
||||
class ForumApp {
|
||||
private client: OpChanClient;
|
||||
private currentUser: User | null = null;
|
||||
private unsubscribers: (() => void)[] = [];
|
||||
|
||||
async initialize() {
|
||||
// 1. Create client
|
||||
this.client = new OpChanClient({
|
||||
wakuConfig: {
|
||||
contentTopic: '/opchan/1/messages/proto',
|
||||
reliableChannelId: 'opchan-messages'
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Open database (hydrates from IndexedDB)
|
||||
await this.client.database.open();
|
||||
|
||||
// 3. Try to restore user session
|
||||
this.currentUser = await this.client.database.loadUser();
|
||||
|
||||
// 4. If no session, start anonymous
|
||||
if (!this.currentUser) {
|
||||
await this.startAnonymousSession();
|
||||
}
|
||||
|
||||
// 5. Set up listeners
|
||||
this.setupListeners();
|
||||
|
||||
// 6. Initial render
|
||||
this.render();
|
||||
}
|
||||
|
||||
private async startAnonymousSession() {
|
||||
const sessionId = await this.client.delegation.delegateAnonymous('7days');
|
||||
this.currentUser = {
|
||||
address: sessionId,
|
||||
displayName: 'Anonymous',
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
verificationStatus: EVerificationStatus.ANONYMOUS
|
||||
};
|
||||
await this.client.database.storeUser(this.currentUser);
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
// Message listener
|
||||
const unsubMsg = this.client.messageManager.onMessageReceived(
|
||||
async (message) => {
|
||||
await this.client.database.applyMessage(message);
|
||||
this.render();
|
||||
}
|
||||
);
|
||||
this.unsubscribers.push(unsubMsg);
|
||||
|
||||
// Health listener
|
||||
const unsubHealth = this.client.messageManager.onHealthChange(
|
||||
(isHealthy) => {
|
||||
console.log('Network:', isHealthy ? 'Connected' : 'Disconnected');
|
||||
this.updateNetworkStatus(isHealthy);
|
||||
}
|
||||
);
|
||||
this.unsubscribers.push(unsubHealth);
|
||||
|
||||
// Identity listener
|
||||
const unsubIdentity = this.client.userIdentityService.subscribe(
|
||||
(address, identity) => {
|
||||
console.log('Identity updated:', address, identity);
|
||||
this.render();
|
||||
}
|
||||
);
|
||||
this.unsubscribers.push(unsubIdentity);
|
||||
|
||||
// Pending listener
|
||||
const unsubPending = this.client.database.onPendingChange(() => {
|
||||
this.updatePendingIndicators();
|
||||
});
|
||||
this.unsubscribers.push(unsubPending);
|
||||
}
|
||||
|
||||
async createPost(cellId: string, title: string, content: string) {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
const result = await this.client.forumActions.createPost(
|
||||
{
|
||||
cellId,
|
||||
title,
|
||||
content,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Post created:', result.data);
|
||||
this.client.database.markPending(result.data!.id);
|
||||
} else {
|
||||
console.error('Failed:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
async vote(targetId: string, isUpvote: boolean) {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
await this.client.forumActions.vote(
|
||||
{
|
||||
targetId,
|
||||
isUpvote,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
}
|
||||
|
||||
private render() {
|
||||
// Get data from cache
|
||||
const cells = Object.values(this.client.database.cache.cells);
|
||||
const posts = Object.values(this.client.database.cache.posts);
|
||||
const comments = Object.values(this.client.database.cache.comments);
|
||||
|
||||
// Transform and sort by relevance
|
||||
Promise.all(posts.map(p => transformPost(p))).then(transformedPosts => {
|
||||
const sorted = transformedPosts
|
||||
.filter(p => p !== null)
|
||||
.sort((a, b) => (b!.relevanceScore || 0) - (a!.relevanceScore || 0));
|
||||
|
||||
// Update DOM
|
||||
this.renderCells(cells);
|
||||
this.renderPosts(sorted);
|
||||
this.renderComments(comments);
|
||||
});
|
||||
}
|
||||
|
||||
private renderCells(cells: any[]) {
|
||||
console.log('Rendering', cells.length, 'cells');
|
||||
// Update DOM here...
|
||||
}
|
||||
|
||||
private renderPosts(posts: any[]) {
|
||||
console.log('Rendering', posts.length, 'posts');
|
||||
// Update DOM here...
|
||||
}
|
||||
|
||||
private renderComments(comments: any[]) {
|
||||
console.log('Rendering', comments.length, 'comments');
|
||||
// Update DOM here...
|
||||
}
|
||||
|
||||
private updateNetworkStatus(isHealthy: boolean) {
|
||||
// Update network indicator in UI
|
||||
const indicator = document.getElementById('network-status');
|
||||
if (indicator) {
|
||||
indicator.textContent = isHealthy ? '🟢 Connected' : '🔴 Offline';
|
||||
}
|
||||
}
|
||||
|
||||
private updatePendingIndicators() {
|
||||
// Update all pending indicators
|
||||
document.querySelectorAll('[data-pending]').forEach(el => {
|
||||
const id = el.getAttribute('data-id');
|
||||
if (id && this.client.database.isPending(id)) {
|
||||
el.classList.add('syncing');
|
||||
} else {
|
||||
el.classList.remove('syncing');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.unsubscribers.forEach(unsub => unsub());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app
|
||||
const app = new ForumApp();
|
||||
app.initialize().then(() => {
|
||||
console.log('App initialized');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 21) Best practices
|
||||
|
||||
- **Always open database**: Call `client.database.open()` before using the client
|
||||
- **Set up message listener**: Subscribe to `onMessageReceived` early to stay synchronized
|
||||
- **Monitor network health**: Use `onHealthChange` to show connection status
|
||||
- **Use optimistic UI**: Mark items as pending during network operations
|
||||
- **Cache identity lookups**: `UserIdentityService` automatically caches ENS resolution
|
||||
- **Transform messages**: Use `transformPost/transformComment/transformCell` for enhanced data
|
||||
- **Validate before storage**: `LocalDatabase.applyMessage` validates all messages
|
||||
- **Handle delegation expiry**: Check `delegation.getStatus()` and re-authorize when needed
|
||||
- **Persist user session**: Use `database.storeUser/loadUser` for session continuity
|
||||
- **Clean up listeners**: Call unsubscribe functions when components unmount
|
||||
|
||||
---
|
||||
|
||||
### 22) Error handling
|
||||
|
||||
```typescript
|
||||
// Wrap operations in try-catch
|
||||
try {
|
||||
const result = await client.forumActions.createPost(params, callback);
|
||||
|
||||
if (!result.success) {
|
||||
// Handle business logic errors
|
||||
showError(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors
|
||||
console.error('Unexpected error:', error);
|
||||
showError('An unexpected error occurred');
|
||||
}
|
||||
|
||||
// Check delegation before operations
|
||||
const status = await client.delegation.getStatus(currentUser.address);
|
||||
if (!status.isValid) {
|
||||
showError('Delegation expired. Please re-authorize.');
|
||||
await reauthorizeUser();
|
||||
}
|
||||
|
||||
// Handle network errors
|
||||
client.messageManager.onHealthChange((isHealthy) => {
|
||||
if (!isHealthy) {
|
||||
showWarning('Network disconnected. Working offline.');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 23) Notes
|
||||
|
||||
- The core package is framework-agnostic - use with React, Vue, Svelte, or vanilla JS
|
||||
- All content is stored locally and synchronized via Waku network
|
||||
- Delegation lasts 7 or 30 days - users need to re-authorize after expiry
|
||||
- Anonymous users can post/comment/vote but cannot create cells
|
||||
- Cell creation requires ENS-verified wallet
|
||||
- Moderation is cell-owner only
|
||||
- All messages are cryptographically signed and verified
|
||||
- IndexedDB persists data across sessions
|
||||
|
||||
---
|
||||
|
||||
**See the React package for a higher-level React integration layer built on top of this core SDK.**
|
||||
|
||||
834
packages/core/docs/sample-app.md
Normal file
834
packages/core/docs/sample-app.md
Normal file
@ -0,0 +1,834 @@
|
||||
# @opchan/core Sample Application
|
||||
|
||||
Complete, production-ready forum application demonstrating all features of the OpChan Core SDK.
|
||||
|
||||
---
|
||||
|
||||
## Complete Forum Application
|
||||
|
||||
A full-featured, production-ready decentralized forum demonstrating all features of the OpChan Core SDK including authentication, content management, moderation, identity resolution, bookmarks, and real-time updates.
|
||||
|
||||
### Features Demonstrated
|
||||
|
||||
- ✅ **Client Initialization** - Setup and configuration
|
||||
- ✅ **Anonymous & Wallet Authentication** - Dual authentication modes
|
||||
- ✅ **Session Persistence** - Restore sessions across page loads
|
||||
- ✅ **Content Creation** - Posts, comments, and cells
|
||||
- ✅ **Voting System** - Upvote/downvote functionality
|
||||
- ✅ **Identity Resolution** - ENS lookup and call signs
|
||||
- ✅ **Real-Time Updates** - Live message synchronization
|
||||
- ✅ **Relevance Scoring** - Content ranking algorithm
|
||||
- ✅ **Moderation** - Cell owner moderation tools
|
||||
- ✅ **Bookmarks** - Save and manage favorite content
|
||||
- ✅ **Network Monitoring** - Connection health tracking
|
||||
- ✅ **Pending States** - Optimistic UI with sync indicators
|
||||
|
||||
### Application Structure
|
||||
|
||||
```typescript
|
||||
import {
|
||||
OpChanClient,
|
||||
EVerificationStatus,
|
||||
EDisplayPreference,
|
||||
BookmarkService,
|
||||
transformPost,
|
||||
type User,
|
||||
type Post,
|
||||
type Comment,
|
||||
type Cell
|
||||
} from '@opchan/core';
|
||||
import { createPublicClient, http } from 'viem';
|
||||
import { mainnet } from 'viem/chains';
|
||||
import { signMessage } from 'viem/accounts';
|
||||
|
||||
class CompleteForum {
|
||||
// Core components
|
||||
private client: OpChanClient;
|
||||
private bookmarkService: BookmarkService;
|
||||
private currentUser: User | null = null;
|
||||
|
||||
// Event handlers
|
||||
private unsubscribers: (() => void)[] = [];
|
||||
|
||||
constructor() {
|
||||
this.client = new OpChanClient({
|
||||
wakuConfig: {
|
||||
contentTopic: '/opchan/1/messages/proto',
|
||||
reliableChannelId: 'opchan-messages'
|
||||
},
|
||||
reownProjectId: process.env.REOWN_PROJECT_ID
|
||||
});
|
||||
|
||||
this.bookmarkService = new BookmarkService();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
async initialize() {
|
||||
console.log('🚀 Initializing OpChan Forum...\n');
|
||||
|
||||
// 1. Open database (hydrates from IndexedDB)
|
||||
await this.client.database.open();
|
||||
console.log('✅ Database opened');
|
||||
|
||||
// 2. Set up ENS resolution
|
||||
const publicClient = createPublicClient({
|
||||
chain: mainnet,
|
||||
transport: http()
|
||||
});
|
||||
this.client.userIdentityService.setPublicClient(publicClient);
|
||||
console.log('✅ ENS resolution configured');
|
||||
|
||||
// 3. Set up event listeners
|
||||
this.setupListeners();
|
||||
console.log('✅ Event listeners configured');
|
||||
|
||||
// 4. Restore or create session
|
||||
await this.restoreSession();
|
||||
|
||||
// 5. Initial render
|
||||
await this.render();
|
||||
|
||||
console.log('\n✅ Forum initialized successfully!\n');
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
// Message listener - handles all incoming messages
|
||||
const msgUnsub = this.client.messageManager.onMessageReceived(
|
||||
async (message) => {
|
||||
const wasNew = await this.client.database.applyMessage(message);
|
||||
if (wasNew) {
|
||||
console.log(`📨 New ${message.type} received`);
|
||||
await this.render();
|
||||
}
|
||||
}
|
||||
);
|
||||
this.unsubscribers.push(msgUnsub);
|
||||
|
||||
// Network health listener
|
||||
const healthUnsub = this.client.messageManager.onHealthChange(
|
||||
(isHealthy) => {
|
||||
console.log(isHealthy ? '🟢 Network: Connected' : '🔴 Network: Offline');
|
||||
}
|
||||
);
|
||||
this.unsubscribers.push(healthUnsub);
|
||||
|
||||
// Identity updates listener
|
||||
const identityUnsub = this.client.userIdentityService.subscribe(
|
||||
(address, identity) => {
|
||||
if (identity) {
|
||||
console.log(`👤 Identity updated: ${identity.displayName}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
this.unsubscribers.push(identityUnsub);
|
||||
|
||||
// Pending state listener
|
||||
const pendingUnsub = this.client.database.onPendingChange(() => {
|
||||
this.updatePendingIndicators();
|
||||
});
|
||||
this.unsubscribers.push(pendingUnsub);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUTHENTICATION
|
||||
// ============================================================================
|
||||
|
||||
private async restoreSession() {
|
||||
// Try to load stored user
|
||||
const storedUser = await this.client.database.loadUser();
|
||||
|
||||
if (storedUser) {
|
||||
// Validate delegation
|
||||
const status = await this.client.delegation.getStatus(storedUser.address);
|
||||
|
||||
if (status.isValid) {
|
||||
this.currentUser = storedUser;
|
||||
console.log(`👤 Restored session: ${storedUser.displayName}`);
|
||||
} else {
|
||||
console.log('⚠️ Delegation expired, starting new session');
|
||||
await this.startAnonymousSession();
|
||||
}
|
||||
} else {
|
||||
await this.startAnonymousSession();
|
||||
}
|
||||
}
|
||||
|
||||
async startAnonymousSession() {
|
||||
const sessionId = await this.client.delegation.delegateAnonymous('7days');
|
||||
|
||||
this.currentUser = {
|
||||
address: sessionId,
|
||||
displayName: 'Anonymous',
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
verificationStatus: EVerificationStatus.ANONYMOUS
|
||||
};
|
||||
|
||||
await this.client.database.storeUser(this.currentUser);
|
||||
console.log('👤 Started anonymous session');
|
||||
}
|
||||
|
||||
async connectWallet(walletAddress: `0x${string}`) {
|
||||
console.log('\n🔐 Connecting wallet...');
|
||||
|
||||
// Create delegation
|
||||
const success = await this.client.delegation.delegate(
|
||||
walletAddress,
|
||||
'7days',
|
||||
async (message) => {
|
||||
// Sign with wallet (this would use your wallet provider)
|
||||
console.log('📝 Please sign the authorization message...');
|
||||
return await signMessage({ message, account: walletAddress });
|
||||
}
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
console.error('❌ Failed to create delegation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get identity
|
||||
const identity = await this.client.userIdentityService.getIdentity(
|
||||
walletAddress,
|
||||
{ fresh: true }
|
||||
);
|
||||
|
||||
if (!identity) {
|
||||
console.error('❌ Failed to resolve identity');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentUser = {
|
||||
address: walletAddress,
|
||||
displayName: identity.displayName,
|
||||
displayPreference: identity.displayPreference,
|
||||
verificationStatus: identity.verificationStatus,
|
||||
ensName: identity.ensName,
|
||||
ensAvatar: identity.ensAvatar,
|
||||
callSign: identity.callSign
|
||||
};
|
||||
|
||||
await this.client.database.storeUser(this.currentUser);
|
||||
console.log(`✅ Wallet connected: ${identity.displayName}`);
|
||||
console.log(` Verification: ${identity.verificationStatus}\n`);
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this.client.delegation.clear();
|
||||
await this.client.database.clearUser();
|
||||
this.currentUser = null;
|
||||
console.log('👋 Disconnected');
|
||||
}
|
||||
|
||||
async setCallSign(callSign: string) {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
const result = await this.client.userIdentityService.updateProfile(
|
||||
this.currentUser.address,
|
||||
{
|
||||
callSign,
|
||||
displayPreference: EDisplayPreference.CALL_SIGN
|
||||
}
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
this.currentUser.callSign = callSign;
|
||||
this.currentUser.displayName = callSign;
|
||||
this.currentUser.displayPreference = EDisplayPreference.CALL_SIGN;
|
||||
await this.client.database.storeUser(this.currentUser);
|
||||
console.log(`✅ Call sign set: ${callSign}`);
|
||||
} else {
|
||||
console.error('❌ Failed to set call sign:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONTENT CREATION
|
||||
// ============================================================================
|
||||
|
||||
async createCell(name: string, description: string, icon?: string) {
|
||||
if (!this.currentUser) {
|
||||
console.error('❌ Not authenticated');
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await this.client.forumActions.createCell(
|
||||
{
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Cell created: ${name}`);
|
||||
this.client.database.markPending(result.data!.id);
|
||||
return result.data;
|
||||
} else {
|
||||
console.error(`❌ Failed to create cell: ${result.error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createPost(cellId: string, title: string, content: string) {
|
||||
if (!this.currentUser) {
|
||||
console.error('❌ Not authenticated');
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await this.client.forumActions.createPost(
|
||||
{
|
||||
cellId,
|
||||
title,
|
||||
content,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Post created: ${title}`);
|
||||
this.client.database.markPending(result.data!.id);
|
||||
return result.data;
|
||||
} else {
|
||||
console.error(`❌ Failed to create post: ${result.error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createComment(postId: string, content: string) {
|
||||
if (!this.currentUser) {
|
||||
console.error('❌ Not authenticated');
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await this.client.forumActions.createComment(
|
||||
{
|
||||
postId,
|
||||
content,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Comment created');
|
||||
this.client.database.markPending(result.data!.id);
|
||||
return result.data;
|
||||
} else {
|
||||
console.error(`❌ Failed to create comment: ${result.error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VOTING
|
||||
// ============================================================================
|
||||
|
||||
async vote(targetId: string, isUpvote: boolean) {
|
||||
if (!this.currentUser) {
|
||||
console.error('❌ Not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.client.forumActions.vote(
|
||||
{
|
||||
targetId,
|
||||
isUpvote,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ ${isUpvote ? 'Upvoted' : 'Downvoted'}`);
|
||||
} else {
|
||||
console.error(`❌ Failed to vote: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MODERATION
|
||||
// ============================================================================
|
||||
|
||||
canModerate(cellId: string): boolean {
|
||||
if (!this.currentUser) return false;
|
||||
const cell = this.client.database.cache.cells[cellId];
|
||||
return cell?.author === this.currentUser.address;
|
||||
}
|
||||
|
||||
async moderatePost(cellId: string, postId: string, reason: string) {
|
||||
if (!this.currentUser || !this.canModerate(cellId)) {
|
||||
console.error('❌ Not authorized to moderate');
|
||||
return;
|
||||
}
|
||||
|
||||
const cell = this.client.database.cache.cells[cellId];
|
||||
const result = await this.client.forumActions.moderatePost(
|
||||
{
|
||||
cellId,
|
||||
postId,
|
||||
reason,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true,
|
||||
cellOwner: cell.author
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Post moderated');
|
||||
} else {
|
||||
console.error(`❌ Failed to moderate: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async unmoderatePost(cellId: string, postId: string) {
|
||||
if (!this.currentUser || !this.canModerate(cellId)) {
|
||||
console.error('❌ Not authorized');
|
||||
return;
|
||||
}
|
||||
|
||||
const cell = this.client.database.cache.cells[cellId];
|
||||
const result = await this.client.forumActions.unmoderatePost(
|
||||
{
|
||||
cellId,
|
||||
postId,
|
||||
currentUser: this.currentUser,
|
||||
isAuthenticated: true,
|
||||
cellOwner: cell.author
|
||||
},
|
||||
() => this.render()
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Post unmoderated');
|
||||
} else {
|
||||
console.error(`❌ Failed to unmoderate: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BOOKMARKS
|
||||
// ============================================================================
|
||||
|
||||
async bookmarkPost(postId: string) {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
const post = this.client.database.cache.posts[postId];
|
||||
if (!post) {
|
||||
console.error('❌ Post not found');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.bookmarkService.addPostBookmark(
|
||||
post,
|
||||
this.currentUser.address,
|
||||
post.cellId
|
||||
);
|
||||
console.log(`✅ Bookmarked: ${post.title}`);
|
||||
}
|
||||
|
||||
async removeBookmark(bookmarkId: string) {
|
||||
await this.bookmarkService.removeBookmark(bookmarkId);
|
||||
console.log('✅ Bookmark removed');
|
||||
}
|
||||
|
||||
isBookmarked(type: 'post' | 'comment', targetId: string): boolean {
|
||||
if (!this.currentUser) return false;
|
||||
return this.client.database.isBookmarked(
|
||||
this.currentUser.address,
|
||||
type,
|
||||
targetId
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DATA ACCESS
|
||||
// ============================================================================
|
||||
|
||||
async getSortedPosts(cellId?: string): Promise<Post[]> {
|
||||
let posts = Object.values(this.client.database.cache.posts);
|
||||
|
||||
if (cellId) {
|
||||
posts = posts.filter(p => p.cellId === cellId);
|
||||
}
|
||||
|
||||
// Transform and score posts
|
||||
const transformedPosts = await Promise.all(
|
||||
posts.map(async p => {
|
||||
const transformed = await transformPost(p);
|
||||
if (!transformed) return null;
|
||||
|
||||
// Calculate relevance
|
||||
const votes = Object.values(this.client.database.cache.votes)
|
||||
.filter(v => v.targetId === p.id);
|
||||
const comments = Object.values(this.client.database.cache.comments)
|
||||
.filter(c => c.postId === p.id);
|
||||
|
||||
const userVerificationStatus = {};
|
||||
for (const [addr, identity] of Object.entries(
|
||||
this.client.database.cache.userIdentities
|
||||
)) {
|
||||
userVerificationStatus[addr] = {
|
||||
isVerified: identity.verificationStatus === 'ens-verified',
|
||||
hasENS: !!identity.ensName,
|
||||
ensName: identity.ensName
|
||||
};
|
||||
}
|
||||
|
||||
const scoreDetails = this.client.relevance.calculatePostScore(
|
||||
p,
|
||||
votes,
|
||||
comments,
|
||||
userVerificationStatus,
|
||||
this.client.database.cache.moderations
|
||||
);
|
||||
|
||||
transformed.relevanceScore = scoreDetails.finalScore;
|
||||
transformed.relevanceDetails = scoreDetails;
|
||||
|
||||
return transformed;
|
||||
})
|
||||
);
|
||||
|
||||
return transformedPosts
|
||||
.filter((p): p is Post => p !== null)
|
||||
.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
|
||||
}
|
||||
|
||||
getCells(): Cell[] {
|
||||
return Object.values(this.client.database.cache.cells);
|
||||
}
|
||||
|
||||
getComments(postId: string): Comment[] {
|
||||
return Object.values(this.client.database.cache.comments)
|
||||
.filter(c => c.postId === postId)
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
async getMyBookmarks() {
|
||||
if (!this.currentUser) return [];
|
||||
return await this.client.database.getUserBookmarks(this.currentUser.address);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UI RENDERING
|
||||
// ============================================================================
|
||||
|
||||
private async render() {
|
||||
console.clear();
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(' OPCHAN FORUM ');
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
|
||||
// Network status
|
||||
const isHealthy = this.client.messageManager.isReady;
|
||||
console.log(`Network: ${isHealthy ? '🟢 Connected' : '🔴 Offline'}`);
|
||||
|
||||
// User status
|
||||
if (this.currentUser) {
|
||||
console.log(`User: ${this.currentUser.displayName} (${this.currentUser.verificationStatus})`);
|
||||
} else {
|
||||
console.log('User: Not authenticated');
|
||||
}
|
||||
|
||||
console.log('\n───────────────────────────────────────────────────────────\n');
|
||||
|
||||
// Cells
|
||||
const cells = this.getCells();
|
||||
console.log(`📁 CELLS (${cells.length})\n`);
|
||||
cells.forEach(cell => {
|
||||
const posts = Object.values(this.client.database.cache.posts)
|
||||
.filter(p => p.cellId === cell.id);
|
||||
console.log(` ${cell.icon || '📁'} ${cell.name} (${posts.length} posts)`);
|
||||
console.log(` ${cell.description}`);
|
||||
if (this.canModerate(cell.id)) {
|
||||
console.log(` 👮 You can moderate this cell`);
|
||||
}
|
||||
console.log();
|
||||
});
|
||||
|
||||
// Posts (top 10 by relevance)
|
||||
const posts = await this.getSortedPosts();
|
||||
console.log(`\n📝 TOP POSTS (${posts.length} total)\n`);
|
||||
|
||||
posts.slice(0, 10).forEach((post, index) => {
|
||||
const isPending = this.client.database.isPending(post.id);
|
||||
const score = post.relevanceScore?.toFixed(0) || '0';
|
||||
const upvotes = post.upvotes?.length || 0;
|
||||
const downvotes = post.downvotes?.length || 0;
|
||||
const comments = this.getComments(post.id).length;
|
||||
const isBookmarked = this.isBookmarked('post', post.id);
|
||||
|
||||
console.log(`${index + 1}. [Score: ${score}] ${post.title}${isPending ? ' ⏳' : ''}`);
|
||||
console.log(` by ${post.author.slice(0, 8)}... | Cell: ${post.cellId}`);
|
||||
console.log(` ⬆️ ${upvotes} ⬇️ ${downvotes} 💬 ${comments}${isBookmarked ? ' 🔖' : ''}`);
|
||||
|
||||
if (post.relevanceDetails) {
|
||||
const details = post.relevanceDetails;
|
||||
console.log(` 📊 Base: ${details.baseScore} + Engagement: ${details.engagementScore.toFixed(0)} × Decay: ${(details.timeDecayMultiplier * 100).toFixed(0)}%`);
|
||||
}
|
||||
|
||||
console.log(` ${post.content.slice(0, 80)}${post.content.length > 80 ? '...' : ''}`);
|
||||
console.log();
|
||||
});
|
||||
|
||||
// Bookmarks
|
||||
if (this.currentUser) {
|
||||
const bookmarks = await this.getMyBookmarks();
|
||||
if (bookmarks.length > 0) {
|
||||
console.log(`\n🔖 MY BOOKMARKS (${bookmarks.length})\n`);
|
||||
bookmarks.slice(0, 5).forEach(b => {
|
||||
console.log(` ${b.title || b.id}`);
|
||||
});
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
}
|
||||
|
||||
private updatePendingIndicators() {
|
||||
// In a real UI, this would update visual indicators
|
||||
// For console, we just re-render
|
||||
this.render();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLEANUP
|
||||
// ============================================================================
|
||||
|
||||
cleanup() {
|
||||
this.unsubscribers.forEach(unsub => unsub());
|
||||
console.log('👋 Forum cleaned up');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USAGE EXAMPLE
|
||||
// ============================================================================
|
||||
|
||||
async function main() {
|
||||
const forum = new CompleteForum();
|
||||
|
||||
try {
|
||||
// Initialize
|
||||
await forum.initialize();
|
||||
|
||||
// Wait a moment for content to load
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Example: Set a call sign for anonymous user
|
||||
await forum.setCallSign('alice');
|
||||
|
||||
// Example: Create a post
|
||||
await forum.createPost(
|
||||
'general',
|
||||
'Hello OpChan!',
|
||||
'This is a comprehensive example of the OpChan forum application.'
|
||||
);
|
||||
|
||||
// Example: Vote on a post (would need actual post ID)
|
||||
// await forum.vote('post-id', true);
|
||||
|
||||
// Example: Bookmark a post
|
||||
// await forum.bookmarkPost('post-id');
|
||||
|
||||
// Keep running and listening for updates
|
||||
console.log('\n✅ Forum is running. Press Ctrl+C to exit.\n');
|
||||
|
||||
// In a real application, this would be kept alive by your UI framework
|
||||
// For this example, we'll just wait
|
||||
await new Promise(() => {}); // Wait forever
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
} finally {
|
||||
forum.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the application
|
||||
main();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Features Explained
|
||||
|
||||
### 1. **Robust Initialization**
|
||||
|
||||
The application properly initializes all components in order:
|
||||
- Opens database and hydrates from IndexedDB
|
||||
- Configures ENS resolution
|
||||
- Sets up event listeners
|
||||
- Restores or creates user session
|
||||
|
||||
### 2. **Dual Authentication**
|
||||
|
||||
Supports both anonymous and wallet-connected users:
|
||||
- Anonymous users get a UUID session ID
|
||||
- Wallet users connect with ENS verification
|
||||
- Sessions persist across page reloads
|
||||
- Delegation expiry is checked on restoration
|
||||
|
||||
### 3. **Real-Time Synchronization**
|
||||
|
||||
All events are properly wired:
|
||||
- Incoming messages trigger UI updates
|
||||
- Network health changes are logged
|
||||
- Identity updates propagate automatically
|
||||
- Pending states tracked for optimistic UI
|
||||
|
||||
### 4. **Complete Content Management**
|
||||
|
||||
Full CRUD operations:
|
||||
- Create cells (ENS-verified users only)
|
||||
- Create posts and comments (all authenticated users)
|
||||
- Vote on content
|
||||
- Moderation tools for cell owners
|
||||
|
||||
### 5. **Advanced Features**
|
||||
|
||||
Production-ready functionality:
|
||||
- Relevance scoring with detailed breakdown
|
||||
- Bookmark management
|
||||
- Identity resolution with ENS
|
||||
- Call sign support
|
||||
- Pending state indicators
|
||||
|
||||
### 6. **Proper Error Handling**
|
||||
|
||||
All operations include error handling:
|
||||
- Permission checks before actions
|
||||
- Validation of delegation status
|
||||
- User-friendly error messages
|
||||
- Graceful degradation
|
||||
|
||||
### 7. **Clean Architecture**
|
||||
|
||||
Well-organized code structure:
|
||||
- Logical section separation
|
||||
- Event-driven design
|
||||
- Proper cleanup on exit
|
||||
- Reusable methods
|
||||
|
||||
---
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
npm install @opchan/core viem
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
export REOWN_PROJECT_ID="your-project-id"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```typescript
|
||||
// Save as forum.ts
|
||||
// Run with: npx tsx forum.ts
|
||||
```
|
||||
|
||||
### Integration with UI
|
||||
|
||||
This application can be easily adapted for:
|
||||
- **React**: Convert methods to hooks, use state management
|
||||
- **Vue**: Use reactive refs and computed properties
|
||||
- **Svelte**: Use stores and reactive statements
|
||||
- **Web Components**: Create custom elements
|
||||
- **CLI**: Interactive command-line interface
|
||||
|
||||
---
|
||||
|
||||
## Extending the Application
|
||||
|
||||
### Add Wallet Support
|
||||
|
||||
```typescript
|
||||
import { useWalletClient } from 'wagmi';
|
||||
|
||||
async connectWithWagmi() {
|
||||
const { data: walletClient } = useWalletClient();
|
||||
if (!walletClient) return;
|
||||
|
||||
await this.connectWallet(walletClient.account.address);
|
||||
}
|
||||
```
|
||||
|
||||
### Add Search Functionality
|
||||
|
||||
```typescript
|
||||
searchPosts(query: string): Post[] {
|
||||
const posts = Object.values(this.client.database.cache.posts);
|
||||
return posts.filter(p =>
|
||||
p.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||
p.content.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Add Notifications
|
||||
|
||||
```typescript
|
||||
private setupListeners() {
|
||||
this.client.messageManager.onMessageReceived(async (message) => {
|
||||
const wasNew = await this.client.database.applyMessage(message);
|
||||
if (wasNew && message.type === 'comment') {
|
||||
this.notify(`New comment on your post!`);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Considerations
|
||||
|
||||
### Performance
|
||||
|
||||
- Cache is in-memory for fast reads
|
||||
- IndexedDB persistence for reliability
|
||||
- Lazy loading of identities
|
||||
- Debounced identity lookups
|
||||
|
||||
### Security
|
||||
|
||||
- All messages cryptographically signed
|
||||
- Delegation proofs verified
|
||||
- Permission checks enforced
|
||||
- Wallet signatures required for delegation
|
||||
|
||||
### Scalability
|
||||
|
||||
- Event-driven architecture
|
||||
- Efficient data structures
|
||||
- Minimal re-renders
|
||||
- Optimistic UI updates
|
||||
|
||||
### Reliability
|
||||
|
||||
- Session persistence
|
||||
- Delegation expiry handling
|
||||
- Network disconnect handling
|
||||
- Error recovery
|
||||
|
||||
---
|
||||
|
||||
**This is a production-ready template demonstrating all features of the OpChan Core SDK. Use it as a foundation for building your decentralized forum application.**
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user