= (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..9f4dcb4
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,5 @@
+export const CONTENT_TOPIC = "/toy-chat/2/luzhou/proto";
+export const PUBSUB_TOPIC = "/waku/2/default-waku/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..7e1c9f5
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,6 @@
+export { useStore } from "./useStore";
+export { useRLN } from "./useRLN";
+export { useWallet } from "./useWallet";
+export { useContract } from "./useContract";
+export { useWaku } from "./useWaku";
+export type { MessageContent } 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..6bf2300
--- /dev/null
+++ b/src/hooks/useWaku.ts
@@ -0,0 +1,55 @@
+import React from "react";
+import { CONTENT_TOPIC } from "@/constants";
+import { Message, waku } from "@/services/waku";
+
+export type MessageContent = {
+ nick: string;
+ text: string;
+ time: string;
+};
+
+export const useWaku = () => {
+ const [messages, setMessages] = React.useState([]);
+
+ React.useEffect(() => {
+ const messageListener = (event: CustomEvent) => {
+ const messages: Message[] = event.detail;
+ const parsedMessaged = messages.map((message) => {
+ const time = new Date(message.timestamp);
+ const payload = JSON.parse(atob(message.payload));
+
+ return {
+ nick: payload?.nick || "unknown",
+ text: payload?.text || "empty",
+ time: time.toDateString(),
+ };
+ });
+
+ setMessages((prev) => [...prev, ...parsedMessaged]);
+ };
+
+ waku.filter.addEventListener(CONTENT_TOPIC, messageListener);
+
+ return () => {
+ waku.filter.removeEventListener(CONTENT_TOPIC, messageListener);
+ };
+ }, [setMessages]);
+
+ const onSend = React.useCallback(
+ async (nick: string, text: string) => {
+ await waku.lightPush.send({
+ version: 0,
+ timestamp: Date.now(),
+ contentTopic: CONTENT_TOPIC,
+ payload: btoa(JSON.stringify({
+ nick,
+ text
+ })),
+ });
+ },
+ []
+ );
+
+ 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..5cc9904
--- /dev/null
+++ b/src/services/rln.ts
@@ -0,0 +1,147 @@
+import { ethers } from "ethers";
+import {
+ create,
+ Keystore,
+ 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..7a122b6
--- /dev/null
+++ b/src/services/waku.ts
@@ -0,0 +1,140 @@
+import { v4 as uuid } from "uuid";
+import {
+ PUBSUB_TOPIC,
+} from "@/constants";
+import { http } from "@/utils/http";
+
+export type Message = {
+ payload: string,
+ contentTopic: string,
+ version: number,
+ timestamp: number
+};
+
+type EventListener = (event: CustomEvent) => void;
+
+const SECOND = 1000;
+const LOCAL_NODE = "http://127.0.0.1:8645/";
+const FILTER_URL = "/filter/v2/";
+const LIGHT_PUSH = "/lightpush/v1/";
+
+class Filter {
+ private readonly internalEmitter = new EventTarget();
+ private readonly subscriptionsEmitter = new EventTarget();
+
+ private contentTopicToRequestID: Map = new Map();
+ private contentTopicListeners: Map = new Map();
+
+ // only one content topic subscriptions is possible now
+ private subscriptionRoutine: undefined | number;
+
+ constructor() {
+ this.internalEmitter.addEventListener("subscribed", this.handleSubscribed.bind(this));
+ this.internalEmitter.addEventListener("unsubscribed", this.handleUnsubscribed.bind(this));
+ }
+
+ private async handleSubscribed(_e: Event) {
+ const event = _e as CustomEvent;
+ const contentTopic = event.detail;
+ const numberOfListeners = this.contentTopicListeners.get(contentTopic);
+
+ // if nwaku node already subscribed to this content topic
+ if (numberOfListeners) {
+ this.contentTopicListeners.set(contentTopic, numberOfListeners + 1);
+ return;
+ }
+
+ const requestId = uuid();
+ await http.post(`${LOCAL_NODE}/${FILTER_URL}/subscriptions`, {
+ requestId,
+ contentFilters: [contentTopic],
+ pubsubTopic: PUBSUB_TOPIC
+ });
+
+ this.subscriptionRoutine = window.setInterval(async () => {
+ await this.fetchMessages();
+ }, SECOND);
+
+ this.contentTopicToRequestID.set(contentTopic, requestId);
+ this.contentTopicListeners.set(contentTopic, 1);
+ }
+
+ private async handleUnsubscribed(_e: Event) {
+ const event = _e as CustomEvent;
+ const contentTopic = event.detail;
+ const requestId = this.contentTopicToRequestID.get(contentTopic);
+ const numberOfListeners = this.contentTopicListeners.get(contentTopic);
+
+ if (!numberOfListeners || !requestId) {
+ return;
+ }
+
+ if (numberOfListeners - 1 > 0) {
+ this.contentTopicListeners.set(contentTopic, numberOfListeners - 1);
+ return;
+ }
+
+ await http.delete(`${LOCAL_NODE}/${FILTER_URL}/subscriptions`, {
+ requestId,
+ contentFilters: [contentTopic],
+ pubsubTopic: PUBSUB_TOPIC
+ });
+
+ clearInterval(this.subscriptionRoutine);
+ this.contentTopicListeners.delete(contentTopic);
+ this.contentTopicToRequestID.delete(contentTopic);
+ }
+
+ private async fetchMessages(): Promise {
+ const contentTopic = Object.keys(this.contentTopicListeners)[0];
+
+ if (!contentTopic) {
+ return;
+ }
+
+ const response = await http.get(`${LOCAL_NODE}/${FILTER_URL}/${encodeURIComponent(contentTopic)}`);
+ const body: Message[] = await response.json();
+
+ if (!body || !body.length) {
+ return;
+ }
+
+ this.subscriptionsEmitter.dispatchEvent(
+ new CustomEvent(contentTopic, { detail: body })
+ );
+ }
+
+ public addEventListener(contentTopic: string, fn: EventListener) {
+ this.emitSubscribedEvent(contentTopic);
+ return this.subscriptionsEmitter.addEventListener(contentTopic, fn as any);
+ }
+
+ public removeEventListener(contentTopic: string, fn: EventListener) {
+ this.emitUnsubscribedEvent(contentTopic);
+ return this.subscriptionsEmitter.removeEventListener(contentTopic, fn as any);
+ }
+
+ private emitSubscribedEvent(contentTopic: string) {
+ this.internalEmitter.dispatchEvent(new CustomEvent("subscribed", { detail: contentTopic }));
+ }
+
+ private emitUnsubscribedEvent(contentTopic: string) {
+ this.internalEmitter.dispatchEvent(new CustomEvent("unsubscribed", { detail: contentTopic }));
+ }
+}
+
+class LightPush {
+ constructor() {}
+
+ public async send(message: Message): Promise {
+ await http.post(`${LOCAL_NODE}/${LIGHT_PUSH}/message`, {
+ pubsubTopic: PUBSUB_TOPIC,
+ message,
+ });
+ }
+}
+
+export const waku = {
+ filter: new Filter(),
+ lightPush: new LightPush(),
+};
\ No newline at end of file
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/src/utils/http.ts b/src/utils/http.ts
new file mode 100644
index 0000000..4dcfa25
--- /dev/null
+++ b/src/utils/http.ts
@@ -0,0 +1,31 @@
+export const http = {
+ post(url: string, body: any) {
+ return fetch(new URL(url), {
+ method: "POST",
+ mode: "no-cors",
+ referrerPolicy: "no-referrer",
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ "Access-Control-Allow-Origin": "*",
+ },
+ body: JSON.stringify(body)
+ });
+ },
+ delete(url: string, body: any) {
+ return fetch(new URL(url), {
+ method: "DELETE",
+ mode: "no-cors",
+ referrerPolicy: "no-referrer",
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ "Access-Control-Allow-Origin": "*",
+ },
+ body: JSON.stringify(body)
+ });
+ },
+ get(url: string) {
+ return fetch(new URL(url));
+ }
+};
\ No newline at end of file
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"]
+}