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