feat: implement waku

This commit is contained in:
Danish Arora 2025-04-16 14:45:27 +05:30
parent 95401cdb5b
commit 56c9c8d889
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
12 changed files with 282 additions and 205 deletions

View File

@ -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 }>();

View File

@ -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;

View File

@ -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';

View File

@ -1,5 +1,5 @@
import { Cell, Post, Comment } from "../types/forum";
import { Cell, Post, Comment } from "../types";
export const mockCells: Cell[] = [
{

View File

@ -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;
}
}
/**
* 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;
}

View File

@ -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, string> = {
[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: ""
// };
];

View File

@ -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};
}
}

View File

@ -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);
}

View File

@ -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);
}

50
src/lib/waku/store.ts Normal file
View File

@ -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;

View File

@ -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<T extends OpchanMessage> = (message: T) => void;
/**
* Subscription object returned when registering listeners
*/
export interface Subscription {
unsubscribe: () => void;
value: 1 | -1;
}
/**

View File

@ -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[];
}