Merge pull request #16 from waku-org/weboko/follow-up-2

feat: add ability to subscribe to custom contentTopic + other stuff
This commit is contained in:
Sasha 2023-12-03 00:30:49 +01:00 committed by GitHub
commit 226e3327ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 548 additions and 250 deletions

59
package-lock.json generated
View File

@ -8,7 +8,7 @@
"name": "rln-js",
"version": "0.1.0",
"dependencies": {
"@waku/rln": "0.1.1-7e8cb89",
"@waku/rln": "0.1.1-60a5070",
"@waku/utils": "^0.0.12",
"ethers": "^5.7.2",
"next": "13.5.6",
@ -1424,13 +1424,13 @@
"dev": true
},
"node_modules/@waku/rln": {
"version": "0.1.1-7e8cb89",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-7e8cb89.tgz",
"integrity": "sha512-ToHwjodXoc1BrllRkIqC8/D73LqMlL4AHRdaSk4ttXjzdlqELdZYaJEwMtEBlupydNPf813RZ6V6hVuE6ttbTg==",
"version": "0.1.1-60a5070",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-60a5070.tgz",
"integrity": "sha512-c3rXqzuTTQVIn2l4gZKDG+/2UY2yb0XbumyrsuvX9fQhQxopCAsSi16CYePdG5HCpImXPNJEC3C3JakuiA3v9A==",
"dependencies": {
"@chainsafe/bls-keystore": "^3.0.0",
"@waku/utils": "^0.0.12",
"@waku/zerokit-rln-wasm": "^0.0.10",
"@waku/utils": "^0.0.13",
"@waku/zerokit-rln-wasm": "^0.0.13",
"ethereum-cryptography": "^2.1.2",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
@ -1441,6 +1441,19 @@
"node": ">=18"
}
},
"node_modules/@waku/rln/node_modules/@waku/utils": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.13.tgz",
"integrity": "sha512-sGZRJyYr7+QZpV2tlGJF48gKmwNdFha6rPKPgXiKDsz2YMhPlg70ffbGcND3bEfOwWmX4g/x5i3Oqwwl+HzwJw==",
"dependencies": {
"chai": "^4.3.8",
"debug": "^4.3.4",
"uint8arrays": "^4.0.4"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@waku/utils": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.12.tgz",
@ -1455,9 +1468,9 @@
}
},
"node_modules/@waku/zerokit-rln-wasm": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.10.tgz",
"integrity": "sha512-qegIK1P54mxEp59uTa8C0/zidUffLc2Iee61yiKRIuGJDui2mQ+0V+KzPSPImKpIoqfVLT192EqgZkqPmj8VEw=="
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA=="
},
"node_modules/@zeit/schemas": {
"version": "2.29.0",
@ -7796,18 +7809,30 @@
"dev": true
},
"@waku/rln": {
"version": "0.1.1-7e8cb89",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-7e8cb89.tgz",
"integrity": "sha512-ToHwjodXoc1BrllRkIqC8/D73LqMlL4AHRdaSk4ttXjzdlqELdZYaJEwMtEBlupydNPf813RZ6V6hVuE6ttbTg==",
"version": "0.1.1-60a5070",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-60a5070.tgz",
"integrity": "sha512-c3rXqzuTTQVIn2l4gZKDG+/2UY2yb0XbumyrsuvX9fQhQxopCAsSi16CYePdG5HCpImXPNJEC3C3JakuiA3v9A==",
"requires": {
"@chainsafe/bls-keystore": "^3.0.0",
"@waku/utils": "^0.0.12",
"@waku/zerokit-rln-wasm": "^0.0.10",
"@waku/utils": "^0.0.13",
"@waku/zerokit-rln-wasm": "^0.0.13",
"ethereum-cryptography": "^2.1.2",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"rlnjs": "^3.2.3",
"uuid": "^9.0.1"
},
"dependencies": {
"@waku/utils": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.13.tgz",
"integrity": "sha512-sGZRJyYr7+QZpV2tlGJF48gKmwNdFha6rPKPgXiKDsz2YMhPlg70ffbGcND3bEfOwWmX4g/x5i3Oqwwl+HzwJw==",
"requires": {
"chai": "^4.3.8",
"debug": "^4.3.4",
"uint8arrays": "^4.0.4"
}
}
}
},
"@waku/utils": {
@ -7821,9 +7846,9 @@
}
},
"@waku/zerokit-rln-wasm": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.10.tgz",
"integrity": "sha512-qegIK1P54mxEp59uTa8C0/zidUffLc2Iee61yiKRIuGJDui2mQ+0V+KzPSPImKpIoqfVLT192EqgZkqPmj8VEw=="
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA=="
},
"@zeit/schemas": {
"version": "2.29.0",

View File

@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@waku/rln": "0.1.1-7e8cb89",
"@waku/rln": "0.1.1-60a5070",
"@waku/utils": "^0.0.12",
"ethers": "^5.7.2",
"next": "13.5.6",

View File

@ -0,0 +1,24 @@
import { Block, BlockTypes } from "@/components/Block";
import { Title } from "@/components/Title";
import { Button } from "@/components/Button";
type HeaderProps = {
children?: React.ReactNode;
onWalletConnect?: () => void;
}
export const Header: React.FunctionComponent<HeaderProps> = (props) => {
return (
<>
<Block className="mb-5" type={BlockTypes.FlexHorizontal}>
<Title>Waku</Title>
{props.onWalletConnect && (
<Button onClick={props.onWalletConnect}>
Connect Wallet
</Button>
)}
</Block>
{props.children}
</>
);
};

View File

@ -0,0 +1,12 @@
import Link from "next/link";
import { Block } from "@/components/Block";
export const Menu: React.FunctionComponent<{}> = () => {
return (
<Block className="m-5 flex text-lg">
<p className="mr-5">{">"}</p>
<p className="mr-5"><Link href="/">Chat</Link></p>
<p><Link href="/keystore">Keystore</Link></p>
</Block>
);
};

162
src/app/components/Waku.tsx Normal file
View File

@ -0,0 +1,162 @@
import React from "react";
import { Block } from "@/components/Block";
import { Subtitle } from "@/components/Subtitle";
import { Button } from "@/components/Button";
import { MessageContent } from "@/hooks";
type WakuProps = {
onSend: (nick: string, text: string) => Promise<void>;
activeContentTopic: string;
messages: MessageContent[];
onActiveContentTopicChange: (contentTopic: string) => void;
}
export const Waku: React.FunctionComponent<WakuProps> = (props) => {
const {
nick,
text,
onNickChange,
onMessageChange,
resetText,
} = useMessage();
const {
contentTopic,
onContentTopicChange,
} = useContentTopic(props.activeContentTopic);
const onSendClick = async () => {
await props.onSend(nick, text);
resetText();
};
const renderedMessages = React.useMemo(
() => props.messages.map(renderMessage),
[props.messages]
);
return (
<Block className="mt-10 flex flex-col md:flex-row lg:flex-row">
<Block>
<Block>
<Subtitle>
Waku
</Subtitle>
<label
htmlFor="contentTopic-input"
className="block mb-2 mt-2 text-sm font-medium text-gray-900 dark:text-white"
>
Content topic
</label>
<input
type="text"
id="contentTopic-input"
value={contentTopic}
onChange={onContentTopicChange}
className="w-96 mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
<Button className="mt-1" onClick={() => { props.onActiveContentTopicChange(contentTopic); }}>Change</Button>
</Block>
<Block className="mt-4 mr-10 min-w-fit">
<label
htmlFor="nick-input"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Your nickname
</label>
<input
type="text"
id="nick-input"
placeholder="Choose a nickname"
value={nick}
onChange={onNickChange}
className="w-96 mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
</Block>
<Block className="mt-4">
<Block className="mb-2">
<label
htmlFor="message-input"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Message
</label>
<textarea
id="message-input"
value={text}
onChange={onMessageChange}
placeholder="Text your message here"
className="w-96 h-60 mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
</Block>
<Button onClick={onSendClick}>Send</Button>
</Block>
</Block>
<Block className="max-w-screen-md mt-10 md:mt-0">
<p className="text-l mb-4">Messages</p>
<div>
<ul>{renderedMessages}</ul>
</div>
</Block>
</Block>
);
};
function useContentTopic(globalContentTopic: string) {
const [contentTopic, setContentTopic] = React.useState<string>(globalContentTopic);
React.useEffect(() => {
setContentTopic(globalContentTopic);
}, [globalContentTopic]);
const onContentTopicChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
setContentTopic(e.currentTarget.value || "");
};
return {
contentTopic,
onContentTopicChange,
};
}
function useMessage() {
const [nick, setNick] = React.useState<string>("");
const [text, setText] = React.useState<string>("");
const onNickChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
setNick(e.currentTarget.value || "");
};
const onMessageChange = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
setText(e.currentTarget.value || "");
};
const resetText = () => {
setText("");
};
return {
nick,
text,
resetText,
onNickChange,
onMessageChange,
};
}
function renderMessage(content: MessageContent) {
return (
<li key={`${content.nick}-${content.timestamp}-${content.text}`} className="mb-4">
<p>
<span className="text-lg">{content.nick}</span>
<span className="text-sm font-bold">
({(new Date(content.timestamp)).toDateString()})
</span>
:
</p>
<p className="break-words">{content.text}</p>
</li>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,23 +0,0 @@
import { Block, BlockTypes } from "@/components/Block";
import { Title } from "@/components/Title";
import { Status } from "@/components/Status";
import { useStore, useWallet } from "@/hooks";
import { Button } from "@/components/Button";
export const Header: React.FunctionComponent<{}> = () => {
const { appStatus, wallet } = useStore();
const { onWalletConnect } = useWallet();
return (
<>
<Block className="mb-5" type={BlockTypes.FlexHorizontal}>
<Title>Waku RLN</Title>
<Button onClick={onWalletConnect}>
Connect Wallet
</Button>
</Block>
<Status text="Application status" mark={appStatus} />
{wallet && <p className="mt-3 text-sm">Wallet connected: {wallet}</p> }
</>
);
};

View File

@ -1,117 +0,0 @@
import React from "react";
import { Block } from "@/components/Block";
import { Subtitle } from "@/components/Subtitle";
import { Button } from "@/components/Button";
import { Status } from "@/components/Status";
import { MessageContent, useWaku } from "@/hooks";
import { CONTENT_TOPIC } from "@/constants";
export const Waku: React.FunctionComponent<{}> = () => {
const { onSend, messages } = useWaku();
const { nick, text, onNickChange, onMessageChange, resetText } = useMessage();
const onSendClick = async () => {
await onSend(nick, text);
resetText();
};
const renderedMessages = React.useMemo(
() => messages.map(renderMessage),
[messages]
);
return (
<Block className="mt-10">
<Block>
<Subtitle>
Waku
</Subtitle>
<p className="text-sm">Content topic: {CONTENT_TOPIC}</p>
</Block>
<Block className="mt-4">
<label
htmlFor="nick-input"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Your nickname
</label>
<input
type="text"
id="nick-input"
placeholder="Choose a nickname"
value={nick}
onChange={onNickChange}
className="w-full mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
</Block>
<Block className="mt-4">
<Block className="mb-2">
<label
htmlFor="message-input"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Message
</label>
<input
type="text"
id="message-input"
value={text}
onChange={onMessageChange}
placeholder="Text your message here"
className="w-full mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
</Block>
<Button onClick={onSendClick}>Send</Button>
</Block>
<Block className="mt-8">
<p className="text-l mb-4">Messages</p>
<div>
<ul>{renderedMessages}</ul>
</div>
</Block>
</Block>
);
};
function useMessage() {
const [nick, setNick] = React.useState<string>("");
const [text, setText] = React.useState<string>("");
const onNickChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
setNick(e.currentTarget.value || "");
};
const onMessageChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
setText(e.currentTarget.value || "");
};
const resetText = () => {
setText("");
};
return {
nick,
text,
resetText,
onNickChange,
onMessageChange,
};
}
function renderMessage(content: MessageContent) {
return (
<li key={`${content.nick}-${content.time}`} className="mb-4">
<p>
<span className="text-lg">{content.nick}</span>
<span className="text-sm font-bold">
({content.time})
</span>
:
</p>
<p>{content.text}</p>
</li>
);
}

View File

@ -1,16 +0,0 @@
"use client";
import { Header } from "./components/Header";
import { Waku } from "./components/Waku";
import { Keystore } from "./components/Keystore";
import { KeystoreDetails } from "./components/KeystoreDetails";
export default function Home() {
return (
<main className="flex min-h-screen flex-col p-24 font-mono max-w-screen-lg m-auto">
<Header />
<Keystore />
<KeystoreDetails />
<Waku />
</main>
);
}

32
src/app/keystore/page.tsx Normal file
View File

@ -0,0 +1,32 @@
"use client";
import { Header } from "@/app/components/Header";
import { Keystore } from "@/app/components/Keystore";
import { KeystoreDetails } from "@/app/components/KeystoreDetails";
import { useWallet } from "@/hooks";
import { Status } from "@/components/Status";
import { useStore } from "@/hooks";
export default function KeystorePage() {
const { onWalletConnect } = useWallet();
const { appStatus, wallet } = useStore();
if (typeof window !== "undefined" && !window?.ethereum) {
return (
<main className="flex min-h-screen flex-col p-6 font-mono max-w-screen-lg">
<Header />
<p className="text-xl">Seems you don't have MetaMask installed. Please, install and reload the page.</p>
</main>
);
}
return (
<main className="flex min-h-screen flex-col p-6 font-mono max-w-screen-lg">
<Header onWalletConnect={onWalletConnect}>
<Status text="Application status" mark={appStatus} />
{wallet && <p className="mt-3 text-sm">Wallet connected: {wallet}</p> }
</Header>
<Keystore />
<KeystoreDetails />
</main>
);
}

View File

@ -1,12 +1,13 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Menu } from "@/app/components/Menu";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "RLN Example",
description: "Showcases RLN, Keystore and generation of proofs",
title: "nwaku front-end",
description: "Send messages through you local node, register to RLN, read and export Keystore",
};
export default function RootLayout({
@ -16,7 +17,10 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<Menu />
{children}
</body>
</html>
);
}

View File

@ -1,4 +1,58 @@
import Home from "@/app/home/page";
"use client";
import { Header } from "@/app/components/Header";
import { Waku } from "@/app/components/Waku";
import { useWaku } from "@/hooks";
import { DebugInfo } from "@/services/waku";
export const dynamic = "force-static";
export default Home;
export default function Home() {
const {
onSend,
messages,
debugInfo,
contentTopic,
onContentTopicChange
} = useWaku();
return (
<main className="flex min-h-screen flex-col p-6 font-mono max-w-screen-lg">
<Header>
<DebugInfo value={debugInfo} />
</Header>
<Waku
onSend={onSend}
messages={messages}
activeContentTopic={contentTopic}
onActiveContentTopicChange={onContentTopicChange}
/>
</main>
);
}
type DebugInfoProps = {
value?: DebugInfo;
}
const DebugInfo: React.FunctionComponent<DebugInfoProps> = (props) => {
if (!props.value) {
return;
}
return (
<details className="border rounded p-2">
<summary className="cursor-pointer bg-gray-300 p-2 rounded-md">
<span className="font-bold">Show node info</span>
</summary>
<div className="mt-2 text-sm break-words">
<p className="mb-2">Health: {props.value.health}</p>
<p className="mb-2">Version: {props.value.version}</p>
<p className="mb-2">ENR URI: {props.value.enrUri}</p>
<p className="mb-2">Listen Addresses:</p>
<ul>
{props.value.listenAddresses.map((address, index) => (
<li key={index}>{address}</li>
))}
</ul>
</div>
</details>
);
}

View File

@ -12,16 +12,16 @@ export const useContract = (): UseContractResult => {
const onFetchContract = React.useCallback(async () => {
const fetchAccounts = new Promise<void>(async (resolve) => {
if (!rln) {
if (!rln || !rln?.ethProvider) {
console.log("Cannot fetch wallet, not provider found.");
resolve();
return;
}
try {
const accounts = await rln.ethProvider.send("eth_requestAccounts", []);
const accounts = await rln?.ethProvider.send("eth_requestAccounts", []);
setEthAccount(accounts[0] || "");
const network = await rln.ethProvider.getNetwork();
const network = await rln?.ethProvider.getNetwork();
setChainID(network.chainId);
} catch (error) {
console.error("Failed to connect to account: ", error);

View File

@ -25,7 +25,7 @@ export const useKeystore = (): UseKeystoreResult => {
return;
}
const signer = rln.ethProvider.getSigner();
const signer = rln?.ethProvider.getSigner();
const signature = await signer.signMessage(
`${SIGNATURE_MESSAGE}. Nonce: ${randomNumber()}`
);

View File

@ -1,55 +1,101 @@
import React from "react";
import { CONTENT_TOPIC } from "@/constants";
import { Message, waku } from "@/services/waku";
import { DebugInfo, Message, waku } from "@/services/waku";
export type MessageContent = {
nick: string;
text: string;
time: string;
timestamp: number;
};
export const useWaku = () => {
const [messages, setMessages] = React.useState<MessageContent[]>([]);
const [contentTopic, setContentTopic] = React.useState<string>(CONTENT_TOPIC);
const [messages, setMessages] = React.useState<Map<string, MessageContent>>(new Map());
const [debugInfo, setDebugInfo] = React.useState<undefined | DebugInfo>();
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));
const nextMessages = new Map(messages);
const newMessages: Message[] = event.detail;
return {
newMessages.forEach((m) => {
const payload = JSON.parse(atob(m.payload));
const message: MessageContent = {
nick: payload?.nick || "unknown",
text: payload?.text || "empty",
time: time.toDateString(),
timestamp: m.timestamp || Date.now(),
};
nextMessages.set(`${message.nick}-${message.timestamp}-${message.text}`, message);
});
setMessages((prev) => [...prev, ...parsedMessaged]);
setMessages(nextMessages);
};
waku.relay.addEventListener(CONTENT_TOPIC, messageListener);
waku.relay.addEventListener(contentTopic, messageListener);
return () => {
waku.relay.removeEventListener(CONTENT_TOPIC, messageListener);
waku.relay.removeEventListener(contentTopic, messageListener);
};
}, [setMessages]);
}, [messages, setMessages, contentTopic]);
React.useEffect(() => {
const debugInfoListener = (event: CustomEvent) => {
const debugInfo = event.detail;
if (!debugInfo) {
return;
}
setDebugInfo(debugInfo);
};
waku.debug.addEventListener("debug", debugInfoListener);
return () => {
waku.debug.removeEventListener("debug", debugInfoListener);
};
}, [debugInfo, setDebugInfo]);
const onSend = React.useCallback(
async (nick: string, text: string) => {
const timestamp = Date.now();
await waku.relay.send({
version: 0,
timestamp: Date.now(),
timestamp,
contentTopic: CONTENT_TOPIC,
payload: btoa(JSON.stringify({
nick,
text
})),
});
const id = `${nick}-${timestamp}-${text}`;
setMessages((prev) => {
if (prev.has(id)) {
return prev;
}
const next = new Map(prev);
next.set(id, { nick, timestamp, text });
return next;
});
},
[]
[setMessages]
);
return { onSend, messages };
};
const onContentTopicChange = async (nextContentTopic: string) => {
if (nextContentTopic === contentTopic) {
return;
}
setContentTopic(nextContentTopic);
};
return {
onSend,
debugInfo,
contentTopic,
onContentTopicChange,
messages: Array.from(messages.values())
};
};

View File

@ -37,7 +37,7 @@ type IRLN = {
export class RLN implements IRLN {
private readonly emitter = new EventTarget();
public readonly ethProvider: ethers.providers.Web3Provider;
public ethProvider: ethers.providers.Web3Provider | undefined;
public rlnInstance: undefined | RLNInstance;
public rlnContract: undefined | RLNContract;
@ -47,14 +47,6 @@ export class RLN implements IRLN {
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();
}
@ -64,6 +56,8 @@ export class RLN implements IRLN {
}
this.initializing = true;
this.initProvider();
await this.initRLNWasm();
// emit keystore keys once app is ready
@ -73,6 +67,21 @@ export class RLN implements IRLN {
this.initializing = false;
}
private initProvider() {
if (typeof window === "undefined") {
return;
}
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");
}
private async initRLNWasm(): Promise<void> {
this.emitStatusEvent(StatusEventPayload.WASM_LOADING);
try {
@ -89,7 +98,7 @@ export class RLN implements IRLN {
}
public async initRLNContract(rlnInstance: RLNInstance): Promise<void> {
if (this.rlnContract) {
if (this.rlnContract || !this.ethProvider) {
return;
}
@ -109,9 +118,13 @@ export class RLN implements IRLN {
private initKeystore(): Keystore {
const localKeystoreString = localStorage.getItem("keystore");
if (!localKeystoreString) {
return Keystore.create();
}
try {
return Keystore.fromString(localKeystoreString || "");
return Keystore.fromString(localKeystoreString || "") || Keystore.create();
} catch(error) {
return Keystore.create();
}

View File

@ -17,79 +17,59 @@ const RELAY = "/relay/v1";
const buildURL = (endpoint: string) => `${LOCAL_NODE}${endpoint}`;
class Relay {
private subscribing = false;
private readonly subscriptionsEmitter = new EventTarget();
private contentTopicListeners: Map<string, number> = new Map();
// only one content topic subscriptions is possible now
private subscriptionRoutine: undefined | number;
constructor() {}
public addEventListener(contentTopic: string, fn: EventListener) {
this.handleSubscribed(contentTopic);
this.subscribe();
return this.subscriptionsEmitter.addEventListener(contentTopic, fn as any);
}
public removeEventListener(contentTopic: string, fn: EventListener) {
this.handleUnsubscribed(contentTopic);
return this.subscriptionsEmitter.removeEventListener(
contentTopic,
fn as any
);
}
private async handleSubscribed(contentTopic: string) {
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
// if nwaku node already subscribed to this content topic
if (numberOfListeners) {
this.contentTopicListeners.set(contentTopic, numberOfListeners + 1);
private async subscribe() {
if (this.subscriptionRoutine || this.subscribing) {
return;
}
this.subscribing = true;
try {
await http.post(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
this.subscriptionRoutine = window.setInterval(async () => {
await this.fetchMessages();
}, 10 * SECOND);
this.contentTopicListeners.set(contentTopic, 1);
}, 5 * SECOND);
} catch (error) {
console.error(`Failed to subscribe node ${contentTopic}:`, error);
console.error(`Failed to subscribe node ${PUBSUB_TOPIC}:`, error);
}
this.subscribing = false;
}
private async handleUnsubscribed(contentTopic: string) {
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
if (!numberOfListeners) {
return;
}
if (numberOfListeners - 1 > 0) {
this.contentTopicListeners.set(contentTopic, numberOfListeners - 1);
public async unsubscribe() {
if (!this.subscriptionRoutine) {
return;
}
try {
await http.delete(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
} catch (error) {
console.error(`Failed to unsubscribe node from ${contentTopic}:`, error);
console.error(`Failed to unsubscribe node from ${PUBSUB_TOPIC}:`, error);
}
clearInterval(this.subscriptionRoutine);
this.contentTopicListeners.delete(contentTopic);
this.subscriptionRoutine = undefined;
}
private async fetchMessages(): Promise<void> {
const contentTopic = Array.from(this.contentTopicListeners.keys())[0];
if (!contentTopic) {
return;
}
const response = await http.get(
buildURL(`${RELAY}/messages/${encodeURIComponent(PUBSUB_TOPIC)}`)
);
@ -99,9 +79,27 @@ class Relay {
return;
}
this.subscriptionsEmitter.dispatchEvent(
new CustomEvent(contentTopic, { detail: body })
);
const messagesPerContentTopic = new Map<string, Message[]>();
body.forEach((m) => {
const contentTopic = m.contentTopic;
if (!contentTopic) {
return;
}
let messages = messagesPerContentTopic.get(contentTopic);
if (!messages) {
messages = [];
}
messages.push(m);
messagesPerContentTopic.set(contentTopic, messages);
});
Array.from(messagesPerContentTopic.entries()).forEach(([contentTopic, messages]) => {
this.subscriptionsEmitter.dispatchEvent(
new CustomEvent(contentTopic, { detail: messages })
);
});
}
public async send(message: Message): Promise<void> {
@ -109,6 +107,92 @@ class Relay {
}
}
type DebugInfoResponse = {
enrUri: string;
listenAddresses: string[];
}
export type DebugInfo = {
health: string;
version: string;
} & DebugInfoResponse;
class Debug {
private subscribing = false;
private readonly subscriptionsEmitter = new EventTarget();
private subscriptionRoutine: undefined | number;
constructor() {}
public addEventListener(event: string, fn: EventListener) {
this.subscribe();
return this.subscriptionsEmitter.addEventListener(event, fn as any);
}
public removeEventListener(event: string, fn: EventListener) {
return this.subscriptionsEmitter.removeEventListener(
event,
fn as any
);
}
private async subscribe() {
if (this.subscriptionRoutine || this.subscribing) {
return;
}
this.subscribing = true;
try {
await this.fetchParameters();
this.subscriptionRoutine = window.setInterval(async () => {
await this.fetchParameters();
}, 30 * SECOND);
} catch(error) {
console.error("Failed to fetch debug info:", error);
}
this.subscribing = false;
}
private async unsubscribe() {
if (!this.subscriptionRoutine) {
return;
}
clearInterval(this.subscriptionRoutine);
this.subscriptionRoutine = undefined;
}
private async fetchParameters(): Promise<void> {
const health = await this.fetchHealth();
const debug = await this.fetchDebugInfo();
const version = await this.fetchDebugVersion();
this.subscriptionsEmitter.dispatchEvent(
new CustomEvent("debug", { detail: {
health,
version,
...debug,
} })
);
}
private async fetchHealth(): Promise<string> {
const response = await http.get(buildURL(`/health`));
return response.text();
}
private async fetchDebugInfo(): Promise<DebugInfoResponse> {
const response = await http.get(buildURL(`/debug/v1/info`));
const body: DebugInfoResponse = await response.json();
return body;
}
private async fetchDebugVersion(): Promise<string> {
const response = await http.get(buildURL(`/debug/v1/version`));
return response.text();
}
}
export const waku = {
relay: new Relay(),
debug: new Debug(),
};

View File

@ -13,8 +13,6 @@ export const http = {
delete(url: string, body: any) {
return fetch(new URL(url), {
method: "DELETE",
mode: "no-cors",
referrerPolicy: "no-referrer",
headers: {
'Content-Type': 'text/plain',
},