= (props) => (
+
+ {props.text}:{" "}
+
+ {props.mark}
+
+
+);
diff --git a/src/components/Subtitle.tsx b/src/components/Subtitle.tsx
new file mode 100644
index 0000000..081b06f
--- /dev/null
+++ b/src/components/Subtitle.tsx
@@ -0,0 +1,8 @@
+type SubtitleProps = {
+ children: any;
+ className?: string;
+};
+
+export const Subtitle: React.FunctionComponent = (props) => (
+ {props.children}
+);
diff --git a/src/components/Title.tsx b/src/components/Title.tsx
new file mode 100644
index 0000000..59d87d2
--- /dev/null
+++ b/src/components/Title.tsx
@@ -0,0 +1,8 @@
+type TitleProps = {
+ children: any;
+ className?: string;
+};
+
+export const Title: React.FunctionComponent = (props) => (
+ {props.children}
+);
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..8382b98
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,17 @@
+import protobuf from "protobufjs";
+
+export type ProtoChatMessageType = {
+ timestamp: number;
+ nick: string;
+ text: string;
+};
+
+export const ProtoChatMessage = new protobuf.Type("ChatMessage")
+ .add(new protobuf.Field("timestamp", 1, "uint64"))
+ .add(new protobuf.Field("nick", 2, "string"))
+ .add(new protobuf.Field("text", 3, "string"));
+
+export const CONTENT_TOPIC = "/toy-chat/2/luzhou/proto";
+
+export const SIGNATURE_MESSAGE =
+ "The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp";
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..cc8e5d1
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,5 @@
+export { useStore } from "./useStore";
+export { useRLN } from "./useRLN";
+export { useWallet } from "./useWallet";
+export { useContract } from "./useContract";
+export { useWaku } from "./useWaku";
diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts
new file mode 100644
index 0000000..d066900
--- /dev/null
+++ b/src/hooks/useContract.ts
@@ -0,0 +1,60 @@
+import React from "react";
+import { useStore } from "./useStore";
+import { useRLN } from "./useRLN";
+
+type UseContractResult = {
+ onFetchContract: () => void;
+};
+
+export const useContract = (): UseContractResult => {
+ const { rln } = useRLN();
+ const { setEthAccount, setChainID, setLastMembershipID } = useStore();
+
+ const onFetchContract = React.useCallback(async () => {
+ const fetchAccounts = new Promise(async (resolve) => {
+ if (!rln) {
+ console.log("Cannot fetch wallet, not provider found.");
+ resolve();
+ return;
+ }
+
+ try {
+ const accounts = await rln.ethProvider.send("eth_requestAccounts", []);
+ setEthAccount(accounts[0] || "");
+ const network = await rln.ethProvider.getNetwork();
+ setChainID(network.chainId);
+ } catch (error) {
+ console.error("Failed to connect to account: ", error);
+ }
+ resolve();
+ });
+
+ const fetchContract = new Promise(async (resolve) => {
+ if (!rln?.rlnContract || !rln?.rlnInstance) {
+ console.log("Cannot fetch contract info, no contract found.");
+ resolve();
+ return;
+ }
+
+ try {
+ await rln.rlnContract.fetchMembers(rln.rlnInstance);
+ rln.rlnContract.subscribeToMembers(rln.rlnInstance);
+
+ const last = rln.rlnContract.members.at(-1);
+
+ if (last) {
+ setLastMembershipID(last.index.toNumber());
+ }
+ } catch (error) {
+ console.error("Failed to fetch contract state: ", error);
+ }
+ resolve();
+ });
+
+ await Promise.any([fetchAccounts, fetchContract]);
+ }, [rln, setEthAccount, setChainID, setLastMembershipID]);
+
+ return {
+ onFetchContract,
+ };
+};
diff --git a/src/hooks/useKeystore.ts b/src/hooks/useKeystore.ts
new file mode 100644
index 0000000..ed22d0b
--- /dev/null
+++ b/src/hooks/useKeystore.ts
@@ -0,0 +1,83 @@
+import React from "react";
+import { useStore } from "./useStore";
+import { useRLN } from "./useRLN";
+import { SEPOLIA_CONTRACT } from "@waku/rln";
+import { StatusEventPayload } from "@/services/rln";
+
+type UseKeystoreResult = {
+ onReadCredentials: (hash: string, password: string) => void;
+ onRegisterCredentials: (password: string) => void;
+};
+
+export const useKeystore = (): UseKeystoreResult => {
+ const { rln } = useRLN();
+ const {
+ credentials,
+ setActiveCredential,
+ setActiveMembershipID,
+ setAppStatus,
+ setCredentials,
+ } = useStore();
+
+ const onRegisterCredentials = React.useCallback(
+ async (password: string) => {
+ if (!credentials || !rln?.rlnContract || !password) {
+ return;
+ }
+
+ try {
+ setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERING);
+ const membershipInfo = await rln.rlnContract.registerWithKey(
+ credentials
+ );
+ const membershipID = membershipInfo!.index.toNumber();
+ const keystoreHash = await rln.keystore.addCredential(
+ {
+ membership: {
+ treeIndex: membershipID,
+ chainId: SEPOLIA_CONTRACT.chainId,
+ address: SEPOLIA_CONTRACT.address,
+ },
+ identity: credentials,
+ },
+ password
+ );
+ setActiveCredential(keystoreHash);
+ setActiveMembershipID(membershipID);
+ rln.saveKeystore();
+ setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERED);
+ } catch (error) {
+ setAppStatus(StatusEventPayload.CREDENTIALS_FAILURE);
+ console.error("Failed to register to RLN Contract: ", error);
+ return;
+ }
+ },
+ [credentials, rln, setActiveCredential, setActiveMembershipID, setAppStatus]
+ );
+
+ const onReadCredentials = React.useCallback(
+ async (hash: string, password: string) => {
+ if (!rln || !hash || !password) {
+ return;
+ }
+
+ try {
+ const record = await rln.keystore.readCredential(hash, password);
+ if (record) {
+ setCredentials(record.identity);
+ setActiveCredential(hash);
+ setActiveMembershipID(record.membership.treeIndex);
+ }
+ } catch (error) {
+ console.error("Failed to read credentials from Keystore.");
+ return;
+ }
+ },
+ [rln, setActiveCredential, setActiveMembershipID, setCredentials]
+ );
+
+ return {
+ onRegisterCredentials,
+ onReadCredentials,
+ };
+};
diff --git a/src/hooks/useRLN.ts b/src/hooks/useRLN.ts
new file mode 100644
index 0000000..98322b3
--- /dev/null
+++ b/src/hooks/useRLN.ts
@@ -0,0 +1,50 @@
+"use client";
+import React from "react";
+import { rln, RLN, RLNEventsNames } from "@/services/rln";
+import { useStore } from "./useStore";
+
+type RLNResult = {
+ rln: undefined | RLN;
+};
+
+export const useRLN = (): RLNResult => {
+ const { setAppStatus, setKeystoreCredentials } = useStore();
+ const rlnRef = React.useRef(undefined);
+
+ React.useEffect(() => {
+ if (rlnRef.current || !rln) {
+ return;
+ }
+
+ let terminate = false;
+
+ const statusListener = (event: CustomEvent) => {
+ setAppStatus(event?.detail);
+ };
+ rln.addEventListener(RLNEventsNames.Status, statusListener);
+
+ const keystoreListener = (event: CustomEvent) => {
+ setKeystoreCredentials(event?.detail || []);
+ };
+ rln.addEventListener(RLNEventsNames.Keystore, keystoreListener);
+
+ const run = async () => {
+ if (terminate) {
+ return;
+ }
+ await rln?.init();
+ rlnRef.current = rln;
+ };
+
+ run();
+ return () => {
+ terminate = true;
+ rln?.removeEventListener(RLNEventsNames.Status, statusListener);
+ rln?.removeEventListener(RLNEventsNames.Keystore, keystoreListener);
+ };
+ }, [rlnRef, setAppStatus]);
+
+ return {
+ rln: rlnRef.current,
+ };
+};
diff --git a/src/hooks/useStore.ts b/src/hooks/useStore.ts
new file mode 100644
index 0000000..9277eab
--- /dev/null
+++ b/src/hooks/useStore.ts
@@ -0,0 +1,68 @@
+import { create } from "zustand";
+import { IdentityCredential } from "@waku/rln";
+
+type StoreResult = {
+ appStatus: string;
+ setAppStatus: (v: string) => void;
+ ethAccount: string;
+ setEthAccount: (v: string) => void;
+ chainID: undefined | number;
+ setChainID: (v: number) => void;
+ lastMembershipID: undefined | number;
+ setLastMembershipID: (v: number) => void;
+ credentials: undefined | IdentityCredential;
+ setCredentials: (v: undefined | IdentityCredential) => void;
+
+ activeCredential: string;
+ keystoreCredentials: string[];
+ setKeystoreCredentials: (v: string[]) => void;
+ setActiveCredential: (v: string) => void;
+ activeMembershipID: undefined | number;
+ setActiveMembershipID: (v: number) => void;
+
+ wakuStatus: string;
+ setWakuStatus: (v: string) => void;
+};
+
+const DEFAULT_VALUE = "none";
+
+export const useStore = create((set) => {
+ const generalModule = {
+ appStatus: DEFAULT_VALUE,
+ setAppStatus: (v: string) => set((state) => ({ ...state, appStatus: v })),
+
+ ethAccount: "",
+ setEthAccount: (v: string) => set((state) => ({ ...state, ethAccount: v })),
+ chainID: undefined,
+ setChainID: (v: number) => set((state) => ({ ...state, chainID: v })),
+ lastMembershipID: undefined,
+ setLastMembershipID: (v: number) =>
+ set((state) => ({ ...state, lastMembershipID: v })),
+ credentials: undefined,
+ setCredentials: (v: undefined | IdentityCredential) =>
+ set((state) => ({ ...state, credentials: v })),
+ };
+
+ const wakuModule = {
+ wakuStatus: DEFAULT_VALUE,
+ setWakuStatus: (v: string) => set((state) => ({ ...state, wakuStatus: v })),
+ };
+
+ const keystoreModule = {
+ activeCredential: DEFAULT_VALUE,
+ setActiveCredential: (v: string) =>
+ set((state) => ({ ...state, activeCredential: v })),
+ keystoreCredentials: [],
+ setKeystoreCredentials: (v: string[]) =>
+ set((state) => ({ ...state, keystoreCredentials: v })),
+ activeMembershipID: undefined,
+ setActiveMembershipID: (v: number) =>
+ set((state) => ({ ...state, activeMembershipID: v })),
+ };
+
+ return {
+ ...generalModule,
+ ...wakuModule,
+ ...keystoreModule,
+ };
+});
diff --git a/src/hooks/useWaku.ts b/src/hooks/useWaku.ts
new file mode 100644
index 0000000..4823b5e
--- /dev/null
+++ b/src/hooks/useWaku.ts
@@ -0,0 +1,67 @@
+import React from "react";
+import { waku, Waku, WakuEventsNames, MessageContent } from "@/services/waku";
+import { useStore } from "./useStore";
+import { useRLN } from "./useRLN";
+
+export const useWaku = () => {
+ const wakuRef = React.useRef();
+ const [messages, setMessages] = React.useState([]);
+
+ const { rln } = useRLN();
+ const { activeMembershipID, credentials, setWakuStatus } = useStore();
+
+ React.useEffect(() => {
+ if (!credentials || !activeMembershipID || !rln) {
+ return;
+ }
+
+ const statusListener = (event: CustomEvent) => {
+ setWakuStatus(event.detail || "");
+ };
+ waku.addEventListener(WakuEventsNames.Status, statusListener);
+
+ const messagesListener = (event: CustomEvent) => {
+ setMessages((prev) => [...prev, event.detail as MessageContent]);
+ };
+ waku.addEventListener(WakuEventsNames.Message, messagesListener);
+
+ let terminated = false;
+ const run = async () => {
+ if (terminated) {
+ return;
+ }
+
+ const options = {
+ rln,
+ credentials,
+ membershipID: activeMembershipID,
+ };
+
+ if (!wakuRef.current) {
+ await waku.init(options);
+ wakuRef.current = waku;
+ } else {
+ wakuRef.current.initEncoder(options);
+ }
+ };
+
+ run();
+ return () => {
+ terminated = true;
+ waku.removeEventListener(WakuEventsNames.Status, statusListener);
+ waku.removeEventListener(WakuEventsNames.Message, messagesListener);
+ };
+ }, [activeMembershipID, credentials, rln, setWakuStatus]);
+
+ const onSend = React.useCallback(
+ async (nick: string, text: string) => {
+ if (!wakuRef.current) {
+ return;
+ }
+ await wakuRef.current.sendMessage(nick, text);
+ },
+ [wakuRef]
+ );
+
+ return { onSend, messages };
+};
diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts
new file mode 100644
index 0000000..8eae0b4
--- /dev/null
+++ b/src/hooks/useWallet.ts
@@ -0,0 +1,62 @@
+import React from "react";
+import { useStore } from "./useStore";
+import { isEthereumEvenEmitterValid } from "@/utils/ethereum";
+import { useRLN } from "./useRLN";
+import { SIGNATURE_MESSAGE } from "@/constants";
+
+type UseWalletResult = {
+ onGenerateCredentials: () => void;
+};
+
+export const useWallet = (): UseWalletResult => {
+ const { rln } = useRLN();
+ const { setEthAccount, setChainID, setCredentials } = useStore();
+
+ React.useEffect(() => {
+ const ethereum = window.ethereum;
+ if (!isEthereumEvenEmitterValid(ethereum)) {
+ console.log("Cannot subscribe to ethereum events.");
+ return;
+ }
+
+ const onAccountsChanged = (accounts: string[]) => {
+ setEthAccount(accounts[0] || "");
+ };
+ ethereum.on("accountsChanged", onAccountsChanged);
+
+ const onChainChanged = (chainID: string) => {
+ const ID = parseInt(chainID, 16);
+ setChainID(ID);
+ };
+ ethereum.on("chainChanged", onChainChanged);
+
+ return () => {
+ ethereum.removeListener("chainChanged", onChainChanged);
+ ethereum.removeListener("accountsChanged", onAccountsChanged);
+ };
+ }, [setEthAccount, setChainID]);
+
+ const onGenerateCredentials = React.useCallback(async () => {
+ if (!rln?.ethProvider) {
+ console.log("Cannot generate credentials, no provider found.");
+ return;
+ }
+
+ const signer = rln.ethProvider.getSigner();
+ const signature = await signer.signMessage(
+ `${SIGNATURE_MESSAGE}. Nonce: ${randomNumber()}`
+ );
+ const credentials = await rln.rlnInstance?.generateSeededIdentityCredential(
+ signature
+ );
+ setCredentials(credentials);
+ }, [rln, setCredentials]);
+
+ return {
+ onGenerateCredentials,
+ };
+};
+
+function randomNumber(): number {
+ return Math.ceil(Math.random() * 1000);
+}
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000..abda36f
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1,14 @@
+///
+
+type EthereumEvents = "accountsChanged" | "chainChanged";
+type EthereumEventListener = (v: any) => void;
+
+type Ethereum = {
+ request: () => void;
+ on: (name: EthereumEvents, fn: EthereumEventListener) => void;
+ removeListener: (name: EthereumEvents, fn: EthereumEventListener) => void;
+};
+
+interface Window {
+ ethereum: Ethereum;
+}
diff --git a/src/services/rln.ts b/src/services/rln.ts
new file mode 100644
index 0000000..493547b
--- /dev/null
+++ b/src/services/rln.ts
@@ -0,0 +1,149 @@
+import { ethers } from "ethers";
+import {
+ create,
+ Keystore,
+ RLNDecoder,
+ RLNEncoder,
+ RLNContract,
+ SEPOLIA_CONTRACT,
+ RLNInstance,
+} from "@waku/rln";
+import { isBrowserProviderValid } from "@/utils/ethereum";
+
+export enum RLNEventsNames {
+ Status = "status",
+ Keystore = "keystore-changed",
+}
+
+export enum StatusEventPayload {
+ WASM_LOADING = "WASM Blob download in progress...",
+ WASM_FAILED = "Failed to download WASM, check console",
+ CONTRACT_LOADING = "Connecting to RLN contract",
+ CONTRACT_FAILED = "Failed to connect to RLN contract",
+ RLN_INITIALIZED = "RLN dependencies initialized",
+ KEYSTORE_LOCAL = "Keystore initialized from localStore",
+ KEYSTORE_NEW = "New Keystore was initialized",
+ CREDENTIALS_REGISTERING = "Registering credentials...",
+ CREDENTIALS_REGISTERED = "Registered credentials",
+ CREDENTIALS_FAILURE = "Failed to register credentials, check console",
+}
+
+type EventListener = (event: CustomEvent) => void;
+
+type IRLN = {
+ saveKeystore: () => void;
+ addEventListener: (name: RLNEventsNames, fn: EventListener) => void;
+ removeEventListener: (name: RLNEventsNames, fn: EventListener) => void;
+};
+
+export class RLN implements IRLN {
+ private readonly emitter = new EventTarget();
+ public readonly ethProvider: ethers.providers.Web3Provider;
+
+ public rlnInstance: undefined | RLNInstance;
+ public rlnContract: undefined | RLNContract;
+ public keystore: Keystore;
+
+ private initialized = false;
+ private initializing = false;
+
+ public constructor() {
+ const ethereum =
+ window.ethereum as unknown as ethers.providers.ExternalProvider;
+ if (!isBrowserProviderValid(ethereum)) {
+ throw Error(
+ "Invalid Ethereum provider present on the page. Check if MetaMask is connected."
+ );
+ }
+ this.ethProvider = new ethers.providers.Web3Provider(ethereum, "any");
+ this.keystore = this.initKeystore();
+ }
+
+ public async init(): Promise {
+ if (this.initialized || this.initializing) {
+ return;
+ }
+
+ this.initializing = true;
+ const rlnInstance = await this.initRLNWasm();
+ await this.initRLNContract(rlnInstance);
+
+ this.emitStatusEvent(StatusEventPayload.RLN_INITIALIZED);
+
+ // emit keystore keys once app is ready
+ this.emitKeystoreKeys();
+
+ this.initialized = true;
+ this.initializing = false;
+ }
+
+ private async initRLNWasm(): Promise {
+ this.emitStatusEvent(StatusEventPayload.WASM_LOADING);
+ try {
+ this.rlnInstance = await create();
+ return this.rlnInstance;
+ } catch (error) {
+ console.error(
+ "Failed at fetching WASM and creating RLN instance: ",
+ error
+ );
+ this.emitStatusEvent(StatusEventPayload.WASM_FAILED);
+ throw error;
+ }
+ }
+
+ private async initRLNContract(rlnInstance: RLNInstance): Promise {
+ this.emitStatusEvent(StatusEventPayload.CONTRACT_LOADING);
+ try {
+ this.rlnContract = await RLNContract.init(rlnInstance, {
+ registryAddress: SEPOLIA_CONTRACT.address,
+ provider: this.ethProvider.getSigner(),
+ });
+ } catch (error) {
+ console.error("Failed to connect to RLN contract: ", error);
+ this.emitStatusEvent(StatusEventPayload.CONTRACT_FAILED);
+ throw error;
+ }
+ }
+
+ private initKeystore(): Keystore {
+ const localKeystoreString = localStorage.getItem("keystore");
+ const _keystore = Keystore.fromString(localKeystoreString || "");
+
+ return _keystore || Keystore.create();
+ }
+
+ public addEventListener(name: RLNEventsNames, fn: EventListener) {
+ return this.emitter.addEventListener(name, fn as any);
+ }
+
+ public removeEventListener(name: RLNEventsNames, fn: EventListener) {
+ return this.emitter.removeEventListener(name, fn as any);
+ }
+
+ private emitStatusEvent(payload: StatusEventPayload) {
+ this.emitter.dispatchEvent(
+ new CustomEvent(RLNEventsNames.Status, { detail: payload })
+ );
+ }
+
+ private emitKeystoreKeys() {
+ const credentials = Object.keys(this.keystore.toObject().credentials || {});
+ this.emitter.dispatchEvent(
+ new CustomEvent(RLNEventsNames.Keystore, { detail: credentials })
+ );
+ }
+
+ public async saveKeystore() {
+ // localStorage.setItem("keystore", this.keystore.toString());
+ this.emitKeystoreKeys();
+ }
+
+ public importKeystore(value: string) {
+ this.keystore = Keystore.fromString(value) || Keystore.create();
+ this.saveKeystore();
+ }
+}
+
+// Next.js sometimes executes code in server env where there is no window object
+export const rln = typeof window === "undefined" ? undefined : new RLN();
diff --git a/src/services/waku.ts b/src/services/waku.ts
new file mode 100644
index 0000000..4d1a13c
--- /dev/null
+++ b/src/services/waku.ts
@@ -0,0 +1,202 @@
+import {
+ createLightNode,
+ createEncoder,
+ createDecoder,
+ IDecodedMessage,
+ LightNode,
+ waitForRemotePeer,
+} from "@waku/sdk";
+import {
+ CONTENT_TOPIC,
+ ProtoChatMessage,
+ ProtoChatMessageType,
+} from "@/constants";
+import {
+ RLNDecoder,
+ RLNEncoder,
+ IdentityCredential,
+ RLNInstance,
+ RLNContract,
+} from "@waku/rln";
+import { RLN } from "@/services/rln";
+
+type InitOptions = {
+ membershipID: number;
+ credentials: IdentityCredential;
+ rln: RLN;
+};
+
+export type MessageContent = {
+ nick: string;
+ text: string;
+ time: string;
+ proofStatus: string;
+};
+
+type SubscribeOptions = {
+ rlnContract: RLNContract;
+ node: LightNode;
+ decoder: RLNDecoder;
+};
+
+export enum WakuEventsNames {
+ Status = "status",
+ Message = "message",
+}
+
+export enum WakuStatusEventPayload {
+ INITIALIZING = "Initializing",
+ WAITING_FOR_PEERS = "Waiting for peers",
+ STARTING = "Starting the node",
+ READY = "Ready",
+}
+
+type EventListener = (event: CustomEvent) => void;
+
+interface IWaku {
+ init: (options: InitOptions) => void;
+ initEncoder: (options: InitOptions) => void;
+ addEventListener: (name: WakuEventsNames, fn: EventListener) => void;
+ removeEventListener: (name: WakuEventsNames, fn: EventListener) => void;
+}
+
+export class Waku implements IWaku {
+ private contentTopic = CONTENT_TOPIC;
+ private readonly emitter = new EventTarget();
+
+ public node: undefined | LightNode;
+
+ private encoder: undefined | RLNEncoder;
+ private decoder: undefined | RLNDecoder;
+
+ private initialized = false;
+ private initializing = false;
+
+ constructor() {}
+
+ public async init(options: InitOptions) {
+ if (this.initialized || this.initializing || !options.rln.rlnInstance) {
+ return;
+ }
+
+ this.initializing = true;
+
+ this.initEncoder(options);
+ this.decoder = new RLNDecoder(
+ options.rln.rlnInstance,
+ createDecoder(this.contentTopic)
+ );
+
+ if (!this.node) {
+ this.emitStatusEvent(WakuStatusEventPayload.INITIALIZING);
+ this.node = await createLightNode({ defaultBootstrap: true });
+ this.emitStatusEvent(WakuStatusEventPayload.STARTING);
+ await this.node.start();
+ this.emitStatusEvent(WakuStatusEventPayload.WAITING_FOR_PEERS);
+ await waitForRemotePeer(this.node);
+ this.emitStatusEvent(WakuStatusEventPayload.READY);
+
+ if (options.rln.rlnContract) {
+ await this.subscribeToMessages({
+ node: this.node,
+ decoder: this.decoder,
+ rlnContract: options.rln.rlnContract,
+ });
+ }
+ }
+
+ this.initialized = true;
+ this.initializing = false;
+ }
+
+ public initEncoder(options: InitOptions) {
+ const { rln, membershipID, credentials } = options;
+ if (!rln.rlnInstance) {
+ return;
+ }
+
+ this.encoder = new RLNEncoder(
+ createEncoder({
+ ephemeral: false,
+ contentTopic: this.contentTopic,
+ }),
+ rln.rlnInstance,
+ membershipID,
+ credentials
+ );
+ }
+
+ public async sendMessage(nick: string, text: string): Promise {
+ if (!this.node || !this.encoder) {
+ return;
+ }
+
+ const timestamp = new Date();
+ const msg = ProtoChatMessage.create({
+ text,
+ nick,
+ timestamp: Math.floor(timestamp.valueOf() / 1000),
+ });
+ const payload = ProtoChatMessage.encode(msg).finish();
+ console.log("Sending message with proof...");
+
+ await this.node.lightPush.send(this.encoder, { payload, timestamp });
+ console.log("Message sent!");
+ }
+
+ private async subscribeToMessages(options: SubscribeOptions) {
+ await options.node.filter.subscribe(options.decoder, (message) => {
+ try {
+ const { timestamp, nick, text } = ProtoChatMessage.decode(
+ message.payload
+ ) as unknown as ProtoChatMessageType;
+
+ let proofStatus = "no proof";
+ if (message.rateLimitProof) {
+ console.log("Proof received: ", message.rateLimitProof);
+
+ try {
+ console.time("Proof verification took:");
+ const res = message.verify(options.rlnContract.roots());
+ console.timeEnd("Proof verification took:");
+ proofStatus = res ? "verified" : "not verified";
+ } catch (error) {
+ proofStatus = "invalid";
+ console.error("Failed to verify proof: ", error);
+ }
+ }
+
+ this.emitMessageEvent({
+ nick,
+ text,
+ proofStatus,
+ time: new Date(timestamp).toDateString(),
+ });
+ } catch (error) {
+ console.error("Failed in subscription listener: ", error);
+ }
+ });
+ }
+
+ public addEventListener(name: WakuEventsNames, fn: EventListener) {
+ return this.emitter.addEventListener(name, fn as any);
+ }
+
+ public removeEventListener(name: WakuEventsNames, fn: EventListener) {
+ return this.emitter.removeEventListener(name, fn as any);
+ }
+
+ private emitStatusEvent(payload: WakuStatusEventPayload) {
+ this.emitter.dispatchEvent(
+ new CustomEvent(WakuEventsNames.Status, { detail: payload })
+ );
+ }
+
+ private emitMessageEvent(payload: MessageContent) {
+ this.emitter.dispatchEvent(
+ new CustomEvent(WakuEventsNames.Message, { detail: payload })
+ );
+ }
+}
+
+export const waku = new Waku();
diff --git a/src/utils/ethereum.ts b/src/utils/ethereum.ts
new file mode 100644
index 0000000..6441202
--- /dev/null
+++ b/src/utils/ethereum.ts
@@ -0,0 +1,17 @@
+export const isBrowserProviderValid = (obj: any): boolean => {
+ if (obj && typeof obj.request === "function") {
+ return true;
+ }
+ return false;
+};
+
+export const isEthereumEvenEmitterValid = (obj: any): boolean => {
+ if (
+ obj &&
+ typeof obj.on === "function" &&
+ typeof obj.removeListener === "function"
+ ) {
+ return true;
+ }
+ return false;
+};
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..e9a0944
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,20 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: [
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {
+ extend: {
+ backgroundImage: {
+ "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
+ "gradient-conic":
+ "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
+ },
+ },
+ },
+ plugins: [],
+};
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e59724b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}