From 56c9c8d889b467dd18c016e2ccae8892f1e2a2cd Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Wed, 16 Apr 2025 14:45:27 +0530 Subject: [PATCH] feat: implement waku --- src/components/PostDetail.tsx | 2 +- src/contexts/AuthContext.tsx | 2 +- src/contexts/ForumContext.tsx | 2 +- src/data/mockData.ts | 2 +- src/lib/waku/codec.ts | 65 +++++++++----- src/lib/waku/constants.ts | 21 ++--- src/lib/waku/lightpush_filter.ts | 68 +++++++++++++++ src/lib/waku/messages.ts | 142 ------------------------------- src/lib/waku/messages_parser.ts | 100 ++++++++++++++++++++++ src/lib/waku/store.ts | 50 +++++++++++ src/lib/waku/types.ts | 21 +---- src/types/{forum.ts => index.ts} | 12 ++- 12 files changed, 282 insertions(+), 205 deletions(-) create mode 100644 src/lib/waku/lightpush_filter.ts delete mode 100644 src/lib/waku/messages.ts create mode 100644 src/lib/waku/messages_parser.ts create mode 100644 src/lib/waku/store.ts rename src/types/{forum.ts => index.ts} (62%) diff --git a/src/components/PostDetail.tsx b/src/components/PostDetail.tsx index d54bf26..e7d18d8 100644 --- a/src/components/PostDetail.tsx +++ b/src/components/PostDetail.tsx @@ -8,7 +8,7 @@ import { Textarea } from '@/components/ui/textarea'; import { Skeleton } from '@/components/ui/skeleton'; import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; -import { Comment } from '@/types/forum'; +import { Comment } from '@/types'; const PostDetail = () => { const { postId } = useParams<{ postId: string }>(); diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 1d1e6ef..f36fcbd 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { useToast } from '@/components/ui/use-toast'; -import { User } from '@/types/forum'; +import { User } from '@/types'; interface AuthContextType { currentUser: User | null; diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index 17931b6..ff739a8 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { useToast } from '@/components/ui/use-toast'; -import { Cell, Post, Comment } from '@/types/forum'; +import { Cell, Post, Comment } from '@/types'; import { mockCells, mockPosts, mockComments } from '@/data/mockData'; import { useAuth } from './AuthContext'; diff --git a/src/data/mockData.ts b/src/data/mockData.ts index 36c5a1c..c0f9c7e 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -1,5 +1,5 @@ -import { Cell, Post, Comment } from "../types/forum"; +import { Cell, Post, Comment } from "../types"; export const mockCells: Cell[] = [ { diff --git a/src/lib/waku/codec.ts b/src/lib/waku/codec.ts index f2bc9e9..7968e38 100644 --- a/src/lib/waku/codec.ts +++ b/src/lib/waku/codec.ts @@ -1,35 +1,39 @@ +import { createDecoder, createEncoder } from '@waku/sdk'; import { MessageType } from './types'; -import { OpchanMessage, CellMessage, PostMessage, CommentMessage, VoteMessage } from './types'; +import { CellMessage, PostMessage, CommentMessage, VoteMessage } from './types'; +import { CONTENT_TOPICS } from './constants'; +import { OpchanMessage } from '@/types'; + +export const encoders = { + [MessageType.CELL]: createEncoder({ + contentTopic: CONTENT_TOPICS['cell'], + }), + [MessageType.POST]: createEncoder({ + contentTopic: CONTENT_TOPICS['post'], + }), + [MessageType.COMMENT]: createEncoder({ + contentTopic: CONTENT_TOPICS['comment'], + }), + [MessageType.VOTE]: createEncoder({ + contentTopic: CONTENT_TOPICS['vote'], + }), +} + +export const decoders = { + [MessageType.CELL]: createDecoder(CONTENT_TOPICS['cell']), + [MessageType.POST]: createDecoder(CONTENT_TOPICS['post']), + [MessageType.COMMENT]: createDecoder(CONTENT_TOPICS['comment']), + [MessageType.VOTE]: createDecoder(CONTENT_TOPICS['vote']), +} /** * Encode a message object into a Uint8Array for transmission */ export function encodeMessage(message: OpchanMessage): Uint8Array { - // Convert the message to a JSON string const messageJson = JSON.stringify(message); - - // Convert the string to a Uint8Array return new TextEncoder().encode(messageJson); } -/** - * Decode a message from a Uint8Array based on its type - */ -export function decodeMessage(payload: Uint8Array, type?: MessageType): OpchanMessage { - // Convert the Uint8Array to a string - const messageJson = new TextDecoder().decode(payload); - - // Parse the JSON string to an object - const message = JSON.parse(messageJson) as OpchanMessage; - - // Validate the message type if specified - if (type && message.type !== type) { - throw new Error(`Expected message of type ${type}, but got ${message.type}`); - } - - // Return the decoded message - return message; -} /** * Type-specific decoders @@ -48,4 +52,19 @@ export function decodeCommentMessage(payload: Uint8Array): CommentMessage { export function decodeVoteMessage(payload: Uint8Array): VoteMessage { return decodeMessage(payload, MessageType.VOTE) as VoteMessage; -} \ No newline at end of file +} + +/** + * Decode a message from a Uint8Array based on its type + */ +function decodeMessage(payload: Uint8Array, type?: MessageType): OpchanMessage { + const messageJson = new TextDecoder().decode(payload); + const message = JSON.parse(messageJson) as OpchanMessage; + + if (type && message.type !== type) { + throw new Error(`Expected message of type ${type}, but got ${message.type}`); + } + + // Return the decoded message + return message; +} \ No newline at end of file diff --git a/src/lib/waku/constants.ts b/src/lib/waku/constants.ts index 303563e..a12b6ad 100644 --- a/src/lib/waku/constants.ts +++ b/src/lib/waku/constants.ts @@ -1,5 +1,5 @@ +import { NetworkConfig, ShardInfo } from "@waku/sdk"; import { MessageType } from "./types"; -import type { QueryRequestParams } from '@waku/sdk' /** * Content topics for different message types @@ -11,20 +11,15 @@ export const CONTENT_TOPICS: Record = { [MessageType.VOTE]: '/opchan/1/vote/proto' }; +export const NETWORK_CONFIG: NetworkConfig = { + contentTopics: Object.values(CONTENT_TOPICS), + shards: [1], + clusterId: 42 +} + /** * Bootstrap nodes for the Waku network * These are public Waku nodes that our node will connect to on startup */ export const BOOTSTRAP_NODES = [ - '/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAkykgaECHswi3YKJ5dMLbq2kPVCo89fcyTd2Hz8tHPeV4y', - '/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ', - '/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS' -]; - -// Default store query options -// export const DEFAULT_STORE_QUERY_OPTIONS: QueryRequestParams = { -// contentTopics: [CONTENT_TOPICS[MessageType.CELL], CONTENT_TOPICS[MessageType.POST], CONTENT_TOPICS[MessageType.COMMENT], CONTENT_TOPICS[MessageType.VOTE]], -// includeData: true, -// paginationForward: false, -// pubsubTopic: "" -// }; \ No newline at end of file +]; \ No newline at end of file diff --git a/src/lib/waku/lightpush_filter.ts b/src/lib/waku/lightpush_filter.ts new file mode 100644 index 0000000..3eacd6c --- /dev/null +++ b/src/lib/waku/lightpush_filter.ts @@ -0,0 +1,68 @@ +import { LightNode } from "@waku/sdk"; +import { decodeCellMessage, decodeCommentMessage, decodePostMessage, decoders, decodeVoteMessage, encodeMessage, encoders } from "./codec"; +import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from "./types"; +import { CONTENT_TOPICS } from "./constants"; +import { OpchanMessage } from "@/types"; + +export class EphemeralProtocolsManager { + private node: LightNode; + + constructor(node: LightNode) { + this.node = node; + } + + public async sendMessage(message: OpchanMessage) { + const encodedMessage = encodeMessage(message); + await this.node.lightPush.send(encoders[message.type], { + payload: encodedMessage + }); + } + + public async subscribeToMessages(types: MessageType[]) { + const result: (CellMessage | PostMessage | CommentMessage | VoteMessage)[] = []; + + const subscription = await this.node.filter.subscribe(Object.values(decoders), async (message) => { + const {contentTopic, payload} = message; + const toDecode = [ + types.includes(MessageType.CELL) ? decodeCellMessage(payload) : null, + types.includes(MessageType.POST) ? decodePostMessage(payload) : null, + types.includes(MessageType.COMMENT) ? decodeCommentMessage(payload) : null, + types.includes(MessageType.VOTE) ? decodeVoteMessage(payload) : null + ] + const decodedMessage = await Promise.race(toDecode); + + let parsedMessage: OpchanMessage | null = null; + switch(contentTopic) { + case CONTENT_TOPICS['cell']: + parsedMessage = decodedMessage as CellMessage; + break; + case CONTENT_TOPICS['post']: + parsedMessage = decodedMessage as PostMessage; + break; + case CONTENT_TOPICS['comment']: + parsedMessage = decodedMessage as CommentMessage; + break; + case CONTENT_TOPICS['vote']: + parsedMessage = decodedMessage as VoteMessage; + break; + default: + console.error(`Unknown content topic: ${contentTopic}`); + return; + } + + if (parsedMessage) { + result.push(parsedMessage); + } + }); + + if (subscription.error) { + throw new Error(subscription.error); + } + + if (subscription.results.successes.length === 0) { + throw new Error("No successes"); + } + + return {result, subscription}; + } +} \ No newline at end of file diff --git a/src/lib/waku/messages.ts b/src/lib/waku/messages.ts deleted file mode 100644 index 0760b55..0000000 --- a/src/lib/waku/messages.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { MessageType } from './constants'; -import { DecodedMessage } from '@waku/sdk'; -import { Cell, Post, Comment } from '@/types/forum'; - -// Base structure for all messages -export interface WakuMessageBase { - messageType: MessageType; - timestamp: number; - sender: string; // Bitcoin address of sender - signature?: string; // Signature to verify sender -} - -// Message structures for different content types -export interface CellMessage extends WakuMessageBase { - messageType: MessageType.CELL; - cellId: string; - name: string; - description: string; - icon: string; -} - -export interface PostMessage extends WakuMessageBase { - messageType: MessageType.POST; - postId: string; - cellId: string; - content: string; -} - -export interface CommentMessage extends WakuMessageBase { - messageType: MessageType.COMMENT; - commentId: string; - postId: string; - content: string; -} - -export interface VoteMessage extends WakuMessageBase { - messageType: MessageType.VOTE; - targetId: string; // postId or commentId - isUpvote: boolean; -} - -// Type for all possible messages -export type WakuMessage = - | CellMessage - | PostMessage - | CommentMessage - | VoteMessage; - -// Utility functions for converting between message types and application models -export function cellToMessage(cell: Cell, sender: string): CellMessage { - return { - messageType: MessageType.CELL, - timestamp: Date.now(), - sender, - cellId: cell.id, - name: cell.name, - description: cell.description, - icon: cell.icon - }; -} - -export function messageToCell(message: CellMessage): Cell { - return { - id: message.cellId, - name: message.name, - description: message.description, - icon: message.icon - }; -} - -export function postToMessage(post: Post, sender: string): PostMessage { - return { - messageType: MessageType.POST, - timestamp: Date.now(), - sender, - postId: post.id, - cellId: post.cellId, - content: post.content - }; -} - -export function messageToPost(message: PostMessage): Post { - return { - id: message.postId, - cellId: message.cellId, - authorAddress: message.sender, - content: message.content, - timestamp: message.timestamp, - upvotes: [], - downvotes: [] - }; -} - -export function commentToMessage(comment: Comment, sender: string): CommentMessage { - return { - messageType: MessageType.COMMENT, - timestamp: Date.now(), - sender, - commentId: comment.id, - postId: comment.postId, - content: comment.content - }; -} - -export function messageToComment(message: CommentMessage): Comment { - return { - id: message.commentId, - postId: message.postId, - authorAddress: message.sender, - content: message.content, - timestamp: message.timestamp, - upvotes: [], - downvotes: [] - }; -} - -// Parse message from decoded waku message -export function parseMessage(decodedMessage: DecodedMessage): WakuMessage | null { - try { - if (!decodedMessage.payload) return null; - - const messageString = new TextDecoder().decode(decodedMessage.payload); - const message = JSON.parse(messageString) as WakuMessage; - - // Validate message has required fields - if (!message.messageType || !message.timestamp || !message.sender) { - console.error('Invalid message format:', message); - return null; - } - - return message; - } catch (error) { - console.error('Error parsing message:', error); - return null; - } -} - -// Serialize message to payload bytes -export function serializeMessage(message: WakuMessage): Uint8Array { - const messageString = JSON.stringify(message); - return new TextEncoder().encode(messageString); -} \ No newline at end of file diff --git a/src/lib/waku/messages_parser.ts b/src/lib/waku/messages_parser.ts new file mode 100644 index 0000000..97c506a --- /dev/null +++ b/src/lib/waku/messages_parser.ts @@ -0,0 +1,100 @@ +import { IDecodedMessage } from '@waku/sdk'; +import { Cell, Post, Comment } from '@/types'; +import { CellMessage, CommentMessage, MessageType, PostMessage } from './types'; +import { OpchanMessage } from '@/types'; +// Utility functions for converting between message types and application models +export function cellToMessage(cell: Cell, sender: string): CellMessage { + return { + type: MessageType.CELL, + timestamp: Date.now(), + author: sender, + id: cell.id, + name: cell.name, + description: cell.description, + icon: cell.icon + }; +} + +export function messageToCell(message: CellMessage): Cell { + return { + id: message.id, + name: message.name, + description: message.description, + icon: message.icon + }; +} + +export function postToMessage(post: Post, sender: string): PostMessage { + return { + type: MessageType.POST, + timestamp: Date.now(), + author: sender, + id: post.id, + title: post.title, + cellId: post.cellId, + content: post.content + }; +} + +export function messageToPost(message: PostMessage): Post { + return { + id: message.id, + cellId: message.cellId, + authorAddress: message.author, + content: message.content, + timestamp: message.timestamp, + title: message.title, + upvotes: [], + downvotes: [] + }; +} + +export function commentToMessage(comment: Comment, sender: string): CommentMessage { + return { + type: MessageType.COMMENT, + timestamp: Date.now(), + author: sender, + id: comment.id, + postId: comment.postId, + content: comment.content + }; +} + +export function messageToComment(message: CommentMessage): Comment { + return { + id: message.id, + postId: message.postId, + authorAddress: message.author, + content: message.content, + timestamp: message.timestamp, + upvotes: [], + downvotes: [] + }; +} + +// Parse message from decoded waku message +export function parseMessage(decodedMessage: IDecodedMessage): OpchanMessage | null { + try { + if (!decodedMessage.payload) return null; + + const messageString = new TextDecoder().decode(decodedMessage.payload); + const message = JSON.parse(messageString) as OpchanMessage; + + // Validate message has required fields + if (!message.type || !message.timestamp || !message.author) { + console.error('Invalid message format:', message); + return null; + } + + return message; + } catch (error) { + console.error('Error parsing message:', error); + return null; + } +} + +// Serialize message to payload bytes +export function serializeMessage(message: OpchanMessage): Uint8Array { + const messageString = JSON.stringify(message); + return new TextEncoder().encode(messageString); +} \ No newline at end of file diff --git a/src/lib/waku/store.ts b/src/lib/waku/store.ts new file mode 100644 index 0000000..bcb2900 --- /dev/null +++ b/src/lib/waku/store.ts @@ -0,0 +1,50 @@ +import { IDecodedMessage, LightNode } from "@waku/sdk"; +import { decoders, decodeCellMessage, decodePostMessage, decodeCommentMessage, decodeVoteMessage } from "./codec"; +import { CONTENT_TOPICS } from "./constants"; +import { CellMessage, PostMessage, CommentMessage, VoteMessage } from "./types"; + +class StoreManager { + private node: LightNode; + + constructor(node: LightNode) { + this.node = node; + } + + public async queryStore() { + const result: (CellMessage | PostMessage | CommentMessage | VoteMessage)[] = []; + + await this.node.store.queryWithOrderedCallback( + Object.values(decoders), + (message: IDecodedMessage) => { + const {contentTopic, payload} = message; + let parsedMessage: (CellMessage | PostMessage | CommentMessage | VoteMessage) | null = null; + + switch(contentTopic) { + case CONTENT_TOPICS['cell']: + parsedMessage = decodeCellMessage(payload) as CellMessage; + break; + case CONTENT_TOPICS['post']: + parsedMessage = decodePostMessage(payload) as PostMessage; + break; + case CONTENT_TOPICS['comment']: + parsedMessage = decodeCommentMessage(payload) as CommentMessage; + break; + case CONTENT_TOPICS['vote']: + parsedMessage = decodeVoteMessage(payload) as VoteMessage; + break; + default: + console.error(`Unknown content topic: ${contentTopic}`); + return; + } + + if (parsedMessage) { + result.push(parsedMessage); + } + } + ); + + return result; + } +} + +export default StoreManager; \ No newline at end of file diff --git a/src/lib/waku/types.ts b/src/lib/waku/types.ts index 10c01bb..f8e2eb7 100644 --- a/src/lib/waku/types.ts +++ b/src/lib/waku/types.ts @@ -25,6 +25,7 @@ export interface CellMessage extends BaseMessage { id: string; name: string; description: string; + icon: string; } /** @@ -45,7 +46,6 @@ export interface CommentMessage extends BaseMessage { type: MessageType.COMMENT; id: string; postId: string; - parentId?: string; // Optional for nested comments content: string; } @@ -56,24 +56,7 @@ export interface VoteMessage extends BaseMessage { type: MessageType.VOTE; id: string; targetId: string; // ID of the post or comment being voted on - value: number; // 1 for upvote, -1 for downvote -} - -/** - * Union type of all possible message types - */ -export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage; - -/** - * Listener function type for Waku service events - */ -export type MessageListener = (message: T) => void; - -/** - * Subscription object returned when registering listeners - */ -export interface Subscription { - unsubscribe: () => void; + value: 1 | -1; } /** diff --git a/src/types/forum.ts b/src/types/index.ts similarity index 62% rename from src/types/forum.ts rename to src/types/index.ts index 60c6eb9..048f081 100644 --- a/src/types/forum.ts +++ b/src/types/index.ts @@ -1,3 +1,6 @@ +import { CellMessage, CommentMessage, PostMessage, VoteMessage } from "@/lib/waku/types"; + +export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage; export interface User { address: string; @@ -17,10 +20,11 @@ export interface Post { id: string; cellId: string; authorAddress: string; + title: string; content: string; timestamp: number; - upvotes: string[]; - downvotes: string[]; + upvotes: VoteMessage[]; + downvotes: VoteMessage[]; } export interface Comment { @@ -29,6 +33,6 @@ export interface Comment { authorAddress: string; content: string; timestamp: number; - upvotes: string[]; - downvotes: string[]; + upvotes: VoteMessage[]; + downvotes: VoteMessage[]; }