chore(react): auto install core when installing react

This commit is contained in:
Danish Arora 2025-12-12 15:09:53 -05:00
parent aeba9823a7
commit 759aff01d0
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
2 changed files with 835 additions and 1 deletions

View File

@ -0,0 +1,834 @@
# @opchan/core Sample Application
Complete, production-ready forum application demonstrating all features of the OpChan Core SDK.
---
## Complete Forum Application
A full-featured, production-ready decentralized forum demonstrating all features of the OpChan Core SDK including authentication, content management, moderation, identity resolution, bookmarks, and real-time updates.
### Features Demonstrated
- ✅ **Client Initialization** - Setup and configuration
- ✅ **Anonymous & Wallet Authentication** - Dual authentication modes
- ✅ **Session Persistence** - Restore sessions across page loads
- ✅ **Content Creation** - Posts, comments, and cells
- ✅ **Voting System** - Upvote/downvote functionality
- ✅ **Identity Resolution** - ENS lookup and call signs
- ✅ **Real-Time Updates** - Live message synchronization
- ✅ **Relevance Scoring** - Content ranking algorithm
- ✅ **Moderation** - Cell owner moderation tools
- ✅ **Bookmarks** - Save and manage favorite content
- ✅ **Network Monitoring** - Connection health tracking
- ✅ **Pending States** - Optimistic UI with sync indicators
### Application Structure
```typescript
import {
OpChanClient,
EVerificationStatus,
EDisplayPreference,
BookmarkService,
transformPost,
type User,
type Post,
type Comment,
type Cell
} from '@opchan/core';
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { signMessage } from 'viem/accounts';
class CompleteForum {
// Core components
private client: OpChanClient;
private bookmarkService: BookmarkService;
private currentUser: User | null = null;
// Event handlers
private unsubscribers: (() => void)[] = [];
constructor() {
this.client = new OpChanClient({
wakuConfig: {
contentTopic: '/opchan/1/messages/proto',
reliableChannelId: 'opchan-messages'
},
reownProjectId: process.env.REOWN_PROJECT_ID
});
this.bookmarkService = new BookmarkService();
}
// ============================================================================
// INITIALIZATION
// ============================================================================
async initialize() {
console.log('🚀 Initializing OpChan Forum...\n');
// 1. Open database (hydrates from IndexedDB)
await this.client.database.open();
console.log('✅ Database opened');
// 2. Set up ENS resolution
const publicClient = createPublicClient({
chain: mainnet,
transport: http()
});
this.client.userIdentityService.setPublicClient(publicClient);
console.log('✅ ENS resolution configured');
// 3. Set up event listeners
this.setupListeners();
console.log('✅ Event listeners configured');
// 4. Restore or create session
await this.restoreSession();
// 5. Initial render
await this.render();
console.log('\n✅ Forum initialized successfully!\n');
}
private setupListeners() {
// Message listener - handles all incoming messages
const msgUnsub = this.client.messageManager.onMessageReceived(
async (message) => {
const wasNew = await this.client.database.applyMessage(message);
if (wasNew) {
console.log(`📨 New ${message.type} received`);
await this.render();
}
}
);
this.unsubscribers.push(msgUnsub);
// Network health listener
const healthUnsub = this.client.messageManager.onHealthChange(
(isHealthy) => {
console.log(isHealthy ? '🟢 Network: Connected' : '🔴 Network: Offline');
}
);
this.unsubscribers.push(healthUnsub);
// Identity updates listener
const identityUnsub = this.client.userIdentityService.subscribe(
(address, identity) => {
if (identity) {
console.log(`👤 Identity updated: ${identity.displayName}`);
}
}
);
this.unsubscribers.push(identityUnsub);
// Pending state listener
const pendingUnsub = this.client.database.onPendingChange(() => {
this.updatePendingIndicators();
});
this.unsubscribers.push(pendingUnsub);
}
// ============================================================================
// AUTHENTICATION
// ============================================================================
private async restoreSession() {
// Try to load stored user
const storedUser = await this.client.database.loadUser();
if (storedUser) {
// Validate delegation
const status = await this.client.delegation.getStatus(storedUser.address);
if (status.isValid) {
this.currentUser = storedUser;
console.log(`👤 Restored session: ${storedUser.displayName}`);
} else {
console.log('⚠️ Delegation expired, starting new session');
await this.startAnonymousSession();
}
} else {
await this.startAnonymousSession();
}
}
async startAnonymousSession() {
const sessionId = await this.client.delegation.delegateAnonymous('7days');
this.currentUser = {
address: sessionId,
displayName: 'Anonymous',
displayPreference: EDisplayPreference.WALLET_ADDRESS,
verificationStatus: EVerificationStatus.ANONYMOUS
};
await this.client.database.storeUser(this.currentUser);
console.log('👤 Started anonymous session');
}
async connectWallet(walletAddress: `0x${string}`) {
console.log('\n🔐 Connecting wallet...');
// Create delegation
const success = await this.client.delegation.delegate(
walletAddress,
'7days',
async (message) => {
// Sign with wallet (this would use your wallet provider)
console.log('📝 Please sign the authorization message...');
return await signMessage({ message, account: walletAddress });
}
);
if (!success) {
console.error('❌ Failed to create delegation');
return;
}
// Get identity
const identity = await this.client.userIdentityService.getIdentity(
walletAddress,
{ fresh: true }
);
if (!identity) {
console.error('❌ Failed to resolve identity');
return;
}
this.currentUser = {
address: walletAddress,
displayName: identity.displayName,
displayPreference: identity.displayPreference,
verificationStatus: identity.verificationStatus,
ensName: identity.ensName,
ensAvatar: identity.ensAvatar,
callSign: identity.callSign
};
await this.client.database.storeUser(this.currentUser);
console.log(`✅ Wallet connected: ${identity.displayName}`);
console.log(` Verification: ${identity.verificationStatus}\n`);
}
async disconnect() {
await this.client.delegation.clear();
await this.client.database.clearUser();
this.currentUser = null;
console.log('👋 Disconnected');
}
async setCallSign(callSign: string) {
if (!this.currentUser) return;
const result = await this.client.userIdentityService.updateProfile(
this.currentUser.address,
{
callSign,
displayPreference: EDisplayPreference.CALL_SIGN
}
);
if (result.ok) {
this.currentUser.callSign = callSign;
this.currentUser.displayName = callSign;
this.currentUser.displayPreference = EDisplayPreference.CALL_SIGN;
await this.client.database.storeUser(this.currentUser);
console.log(`✅ Call sign set: ${callSign}`);
} else {
console.error('❌ Failed to set call sign:', result.error);
}
}
// ============================================================================
// CONTENT CREATION
// ============================================================================
async createCell(name: string, description: string, icon?: string) {
if (!this.currentUser) {
console.error('❌ Not authenticated');
return null;
}
const result = await this.client.forumActions.createCell(
{
name,
description,
icon,
currentUser: this.currentUser,
isAuthenticated: true
},
() => this.render()
);
if (result.success) {
console.log(`✅ Cell created: ${name}`);
this.client.database.markPending(result.data!.id);
return result.data;
} else {
console.error(`❌ Failed to create cell: ${result.error}`);
return null;
}
}
async createPost(cellId: string, title: string, content: string) {
if (!this.currentUser) {
console.error('❌ Not authenticated');
return null;
}
const result = await this.client.forumActions.createPost(
{
cellId,
title,
content,
currentUser: this.currentUser,
isAuthenticated: true
},
() => this.render()
);
if (result.success) {
console.log(`✅ Post created: ${title}`);
this.client.database.markPending(result.data!.id);
return result.data;
} else {
console.error(`❌ Failed to create post: ${result.error}`);
return null;
}
}
async createComment(postId: string, content: string) {
if (!this.currentUser) {
console.error('❌ Not authenticated');
return null;
}
const result = await this.client.forumActions.createComment(
{
postId,
content,
currentUser: this.currentUser,
isAuthenticated: true
},
() => this.render()
);
if (result.success) {
console.log('✅ Comment created');
this.client.database.markPending(result.data!.id);
return result.data;
} else {
console.error(`❌ Failed to create comment: ${result.error}`);
return null;
}
}
// ============================================================================
// VOTING
// ============================================================================
async vote(targetId: string, isUpvote: boolean) {
if (!this.currentUser) {
console.error('❌ Not authenticated');
return;
}
const result = await this.client.forumActions.vote(
{
targetId,
isUpvote,
currentUser: this.currentUser,
isAuthenticated: true
},
() => this.render()
);
if (result.success) {
console.log(`✅ ${isUpvote ? 'Upvoted' : 'Downvoted'}`);
} else {
console.error(`❌ Failed to vote: ${result.error}`);
}
}
// ============================================================================
// MODERATION
// ============================================================================
canModerate(cellId: string): boolean {
if (!this.currentUser) return false;
const cell = this.client.database.cache.cells[cellId];
return cell?.author === this.currentUser.address;
}
async moderatePost(cellId: string, postId: string, reason: string) {
if (!this.currentUser || !this.canModerate(cellId)) {
console.error('❌ Not authorized to moderate');
return;
}
const cell = this.client.database.cache.cells[cellId];
const result = await this.client.forumActions.moderatePost(
{
cellId,
postId,
reason,
currentUser: this.currentUser,
isAuthenticated: true,
cellOwner: cell.author
},
() => this.render()
);
if (result.success) {
console.log('✅ Post moderated');
} else {
console.error(`❌ Failed to moderate: ${result.error}`);
}
}
async unmoderatePost(cellId: string, postId: string) {
if (!this.currentUser || !this.canModerate(cellId)) {
console.error('❌ Not authorized');
return;
}
const cell = this.client.database.cache.cells[cellId];
const result = await this.client.forumActions.unmoderatePost(
{
cellId,
postId,
currentUser: this.currentUser,
isAuthenticated: true,
cellOwner: cell.author
},
() => this.render()
);
if (result.success) {
console.log('✅ Post unmoderated');
} else {
console.error(`❌ Failed to unmoderate: ${result.error}`);
}
}
// ============================================================================
// BOOKMARKS
// ============================================================================
async bookmarkPost(postId: string) {
if (!this.currentUser) return;
const post = this.client.database.cache.posts[postId];
if (!post) {
console.error('❌ Post not found');
return;
}
await this.bookmarkService.addPostBookmark(
post,
this.currentUser.address,
post.cellId
);
console.log(`✅ Bookmarked: ${post.title}`);
}
async removeBookmark(bookmarkId: string) {
await this.bookmarkService.removeBookmark(bookmarkId);
console.log('✅ Bookmark removed');
}
isBookmarked(type: 'post' | 'comment', targetId: string): boolean {
if (!this.currentUser) return false;
return this.client.database.isBookmarked(
this.currentUser.address,
type,
targetId
);
}
// ============================================================================
// DATA ACCESS
// ============================================================================
async getSortedPosts(cellId?: string): Promise<Post[]> {
let posts = Object.values(this.client.database.cache.posts);
if (cellId) {
posts = posts.filter(p => p.cellId === cellId);
}
// Transform and score posts
const transformedPosts = await Promise.all(
posts.map(async p => {
const transformed = await transformPost(p);
if (!transformed) return null;
// Calculate relevance
const votes = Object.values(this.client.database.cache.votes)
.filter(v => v.targetId === p.id);
const comments = Object.values(this.client.database.cache.comments)
.filter(c => c.postId === p.id);
const userVerificationStatus = {};
for (const [addr, identity] of Object.entries(
this.client.database.cache.userIdentities
)) {
userVerificationStatus[addr] = {
isVerified: identity.verificationStatus === 'ens-verified',
hasENS: !!identity.ensName,
ensName: identity.ensName
};
}
const scoreDetails = this.client.relevance.calculatePostScore(
p,
votes,
comments,
userVerificationStatus,
this.client.database.cache.moderations
);
transformed.relevanceScore = scoreDetails.finalScore;
transformed.relevanceDetails = scoreDetails;
return transformed;
})
);
return transformedPosts
.filter((p): p is Post => p !== null)
.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
}
getCells(): Cell[] {
return Object.values(this.client.database.cache.cells);
}
getComments(postId: string): Comment[] {
return Object.values(this.client.database.cache.comments)
.filter(c => c.postId === postId)
.sort((a, b) => a.timestamp - b.timestamp);
}
async getMyBookmarks() {
if (!this.currentUser) return [];
return await this.client.database.getUserBookmarks(this.currentUser.address);
}
// ============================================================================
// UI RENDERING
// ============================================================================
private async render() {
console.clear();
console.log('═══════════════════════════════════════════════════════════');
console.log(' OPCHAN FORUM ');
console.log('═══════════════════════════════════════════════════════════\n');
// Network status
const isHealthy = this.client.messageManager.isReady;
console.log(`Network: ${isHealthy ? '🟢 Connected' : '🔴 Offline'}`);
// User status
if (this.currentUser) {
console.log(`User: ${this.currentUser.displayName} (${this.currentUser.verificationStatus})`);
} else {
console.log('User: Not authenticated');
}
console.log('\n───────────────────────────────────────────────────────────\n');
// Cells
const cells = this.getCells();
console.log(`📁 CELLS (${cells.length})\n`);
cells.forEach(cell => {
const posts = Object.values(this.client.database.cache.posts)
.filter(p => p.cellId === cell.id);
console.log(` ${cell.icon || '📁'} ${cell.name} (${posts.length} posts)`);
console.log(` ${cell.description}`);
if (this.canModerate(cell.id)) {
console.log(` 👮 You can moderate this cell`);
}
console.log();
});
// Posts (top 10 by relevance)
const posts = await this.getSortedPosts();
console.log(`\n📝 TOP POSTS (${posts.length} total)\n`);
posts.slice(0, 10).forEach((post, index) => {
const isPending = this.client.database.isPending(post.id);
const score = post.relevanceScore?.toFixed(0) || '0';
const upvotes = post.upvotes?.length || 0;
const downvotes = post.downvotes?.length || 0;
const comments = this.getComments(post.id).length;
const isBookmarked = this.isBookmarked('post', post.id);
console.log(`${index + 1}. [Score: ${score}] ${post.title}${isPending ? ' ⏳' : ''}`);
console.log(` by ${post.author.slice(0, 8)}... | Cell: ${post.cellId}`);
console.log(` ⬆️ ${upvotes} ⬇️ ${downvotes} 💬 ${comments}${isBookmarked ? ' 🔖' : ''}`);
if (post.relevanceDetails) {
const details = post.relevanceDetails;
console.log(` 📊 Base: ${details.baseScore} + Engagement: ${details.engagementScore.toFixed(0)} × Decay: ${(details.timeDecayMultiplier * 100).toFixed(0)}%`);
}
console.log(` ${post.content.slice(0, 80)}${post.content.length > 80 ? '...' : ''}`);
console.log();
});
// Bookmarks
if (this.currentUser) {
const bookmarks = await this.getMyBookmarks();
if (bookmarks.length > 0) {
console.log(`\n🔖 MY BOOKMARKS (${bookmarks.length})\n`);
bookmarks.slice(0, 5).forEach(b => {
console.log(` ${b.title || b.id}`);
});
console.log();
}
}
console.log('═══════════════════════════════════════════════════════════\n');
}
private updatePendingIndicators() {
// In a real UI, this would update visual indicators
// For console, we just re-render
this.render();
}
// ============================================================================
// CLEANUP
// ============================================================================
cleanup() {
this.unsubscribers.forEach(unsub => unsub());
console.log('👋 Forum cleaned up');
}
}
// ============================================================================
// USAGE EXAMPLE
// ============================================================================
async function main() {
const forum = new CompleteForum();
try {
// Initialize
await forum.initialize();
// Wait a moment for content to load
await new Promise(resolve => setTimeout(resolve, 2000));
// Example: Set a call sign for anonymous user
await forum.setCallSign('alice');
// Example: Create a post
await forum.createPost(
'general',
'Hello OpChan!',
'This is a comprehensive example of the OpChan forum application.'
);
// Example: Vote on a post (would need actual post ID)
// await forum.vote('post-id', true);
// Example: Bookmark a post
// await forum.bookmarkPost('post-id');
// Keep running and listening for updates
console.log('\n✅ Forum is running. Press Ctrl+C to exit.\n');
// In a real application, this would be kept alive by your UI framework
// For this example, we'll just wait
await new Promise(() => {}); // Wait forever
} catch (error) {
console.error('❌ Error:', error);
} finally {
forum.cleanup();
}
}
// Run the application
main();
```
---
## Key Features Explained
### 1. **Robust Initialization**
The application properly initializes all components in order:
- Opens database and hydrates from IndexedDB
- Configures ENS resolution
- Sets up event listeners
- Restores or creates user session
### 2. **Dual Authentication**
Supports both anonymous and wallet-connected users:
- Anonymous users get a UUID session ID
- Wallet users connect with ENS verification
- Sessions persist across page reloads
- Delegation expiry is checked on restoration
### 3. **Real-Time Synchronization**
All events are properly wired:
- Incoming messages trigger UI updates
- Network health changes are logged
- Identity updates propagate automatically
- Pending states tracked for optimistic UI
### 4. **Complete Content Management**
Full CRUD operations:
- Create cells (ENS-verified users only)
- Create posts and comments (all authenticated users)
- Vote on content
- Moderation tools for cell owners
### 5. **Advanced Features**
Production-ready functionality:
- Relevance scoring with detailed breakdown
- Bookmark management
- Identity resolution with ENS
- Call sign support
- Pending state indicators
### 6. **Proper Error Handling**
All operations include error handling:
- Permission checks before actions
- Validation of delegation status
- User-friendly error messages
- Graceful degradation
### 7. **Clean Architecture**
Well-organized code structure:
- Logical section separation
- Event-driven design
- Proper cleanup on exit
- Reusable methods
---
## Running the Application
### Prerequisites
```bash
npm install @opchan/core viem
```
### Environment Variables
```bash
export REOWN_PROJECT_ID="your-project-id"
```
### Run
```typescript
// Save as forum.ts
// Run with: npx tsx forum.ts
```
### Integration with UI
This application can be easily adapted for:
- **React**: Convert methods to hooks, use state management
- **Vue**: Use reactive refs and computed properties
- **Svelte**: Use stores and reactive statements
- **Web Components**: Create custom elements
- **CLI**: Interactive command-line interface
---
## Extending the Application
### Add Wallet Support
```typescript
import { useWalletClient } from 'wagmi';
async connectWithWagmi() {
const { data: walletClient } = useWalletClient();
if (!walletClient) return;
await this.connectWallet(walletClient.account.address);
}
```
### Add Search Functionality
```typescript
searchPosts(query: string): Post[] {
const posts = Object.values(this.client.database.cache.posts);
return posts.filter(p =>
p.title.toLowerCase().includes(query.toLowerCase()) ||
p.content.toLowerCase().includes(query.toLowerCase())
);
}
```
### Add Notifications
```typescript
private setupListeners() {
this.client.messageManager.onMessageReceived(async (message) => {
const wasNew = await this.client.database.applyMessage(message);
if (wasNew && message.type === 'comment') {
this.notify(`New comment on your post!`);
}
});
}
```
---
## Production Considerations
### Performance
- Cache is in-memory for fast reads
- IndexedDB persistence for reliability
- Lazy loading of identities
- Debounced identity lookups
### Security
- All messages cryptographically signed
- Delegation proofs verified
- Permission checks enforced
- Wallet signatures required for delegation
### Scalability
- Event-driven architecture
- Efficient data structures
- Minimal re-renders
- Optimistic UI updates
### Reliability
- Session persistence
- Delegation expiry handling
- Network disconnect handling
- Error recovery
---
**This is a production-ready template demonstrating all features of the OpChan Core SDK. Use it as a foundation for building your decentralized forum application.**

View File

@ -33,7 +33,7 @@
"react-dom": ">=18.0.0"
},
"dependencies": {
"@opchan/core": "file:../core"
"@opchan/core": "^1.0.3"
},
"devDependencies": {
"typescript": "^5.5.3",