mirror of
https://github.com/logos-messaging/logos-messaging-frontend.git
synced 2026-01-08 16:53:12 +00:00
feat: address follow-ups $10
feat: address follow-ups
This commit is contained in:
commit
9112bd2ca7
1312
package-lock.json
generated
1312
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"serve": "serve ./out",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -15,7 +15,6 @@
|
|||||||
"next": "13.5.6",
|
"next": "13.5.6",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"uuid": "^9.0.1",
|
|
||||||
"zustand": "^4.4.4"
|
"zustand": "^4.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -28,6 +27,7 @@
|
|||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "13.5.6",
|
"eslint-config-next": "13.5.6",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
"serve": "^14.2.1",
|
||||||
"tailwindcss": "^3",
|
"tailwindcss": "^3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
import { Block, BlockTypes } from "@/components/Block";
|
import { Block, BlockTypes } from "@/components/Block";
|
||||||
import { Title } from "@/components/Title";
|
import { Title } from "@/components/Title";
|
||||||
import { Status } from "@/components/Status";
|
import { Status } from "@/components/Status";
|
||||||
import { useStore } from "@/hooks";
|
import { useStore, useWallet } from "@/hooks";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
|
||||||
export const Header: React.FunctionComponent<{}> = () => {
|
export const Header: React.FunctionComponent<{}> = () => {
|
||||||
const { appStatus } = useStore();
|
const { appStatus, wallet } = useStore();
|
||||||
|
const { onWalletConnect } = useWallet();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className="mb-5" type={BlockTypes.FlexHorizontal}>
|
<Block className="mb-5" type={BlockTypes.FlexHorizontal}>
|
||||||
<Title>Waku RLN</Title>
|
<Title>Waku RLN</Title>
|
||||||
|
<Button onClick={onWalletConnect}>
|
||||||
|
Connect Wallet
|
||||||
|
</Button>
|
||||||
</Block>
|
</Block>
|
||||||
<Status text="Application status" mark={appStatus} />
|
<Status text="Application status" mark={appStatus} />
|
||||||
|
{wallet && <p className="mt-3 text-sm">Wallet connected: {wallet}</p> }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,12 +2,11 @@ import React from "react";
|
|||||||
import { Block, BlockTypes } from "@/components/Block";
|
import { Block, BlockTypes } from "@/components/Block";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Subtitle } from "@/components/Subtitle";
|
import { Subtitle } from "@/components/Subtitle";
|
||||||
import { useRLN, useStore, useWallet } from "@/hooks";
|
import { useRLN, useStore } from "@/hooks";
|
||||||
import { useKeystore } from "@/hooks/useKeystore";
|
import { useKeystore } from "@/hooks/useKeystore";
|
||||||
|
|
||||||
export const Keystore: React.FunctionComponent<{}> = () => {
|
export const Keystore: React.FunctionComponent<{}> = () => {
|
||||||
const { keystoreCredentials } = useStore();
|
const { wallet, keystoreCredentials } = useStore();
|
||||||
const { onGenerateCredentials } = useWallet();
|
|
||||||
const { onReadCredentials, onRegisterCredentials } = useKeystore();
|
const { onReadCredentials, onRegisterCredentials } = useKeystore();
|
||||||
|
|
||||||
const { password, onPasswordChanged } = usePassword();
|
const { password, onPasswordChanged } = usePassword();
|
||||||
@ -64,15 +63,13 @@ export const Keystore: React.FunctionComponent<{}> = () => {
|
|||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block className="mt-4">
|
<Block className="mt-4">
|
||||||
<p className="text-s mb-2">Generate new credentials from wallet</p>
|
<p className="text-s mb-2">Generate new credentials from wallet and register on chain</p>
|
||||||
<Button onClick={onGenerateCredentials}>
|
|
||||||
Generate new credentials
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
className="ml-5"
|
disabled={!wallet || !password}
|
||||||
onClick={() => onRegisterCredentials(password)}
|
onClick={() => onRegisterCredentials(password)}
|
||||||
|
className={wallet && password ? "" : "cursor-not-allowed"}
|
||||||
>
|
>
|
||||||
Register credentials
|
Register new credentials
|
||||||
</Button>
|
</Button>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import React from "react";
|
|||||||
import { Block } from "@/components/Block";
|
import { Block } from "@/components/Block";
|
||||||
import { Subtitle } from "@/components/Subtitle";
|
import { Subtitle } from "@/components/Subtitle";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
|
import { Status } from "@/components/Status";
|
||||||
import { MessageContent, useWaku } from "@/hooks";
|
import { MessageContent, useWaku } from "@/hooks";
|
||||||
|
import { CONTENT_TOPIC } from "@/constants";
|
||||||
|
|
||||||
export const Waku: React.FunctionComponent<{}> = () => {
|
export const Waku: React.FunctionComponent<{}> = () => {
|
||||||
const { onSend, messages } = useWaku();
|
const { onSend, messages } = useWaku();
|
||||||
@ -20,9 +22,12 @@ export const Waku: React.FunctionComponent<{}> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Block className="mt-10">
|
<Block className="mt-10">
|
||||||
<Subtitle>
|
<Block>
|
||||||
Waku
|
<Subtitle>
|
||||||
</Subtitle>
|
Waku
|
||||||
|
</Subtitle>
|
||||||
|
<p className="text-sm">Content topic: {CONTENT_TOPIC}</p>
|
||||||
|
</Block>
|
||||||
|
|
||||||
<Block className="mt-4">
|
<Block className="mt-4">
|
||||||
<label
|
<label
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
type ButtonProps = {
|
type ButtonProps = {
|
||||||
children: any;
|
children: any;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
onClick?: (e?: any) => void;
|
onClick?: (e?: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button: React.FunctionComponent<ButtonProps> = (props) => {
|
export const Button: React.FunctionComponent<ButtonProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
disabled={props.disabled}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
className={`${
|
className={`${
|
||||||
props.className || ""
|
props.className || ""
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useStore } from "./useStore";
|
|||||||
import { useRLN } from "./useRLN";
|
import { useRLN } from "./useRLN";
|
||||||
import { SEPOLIA_CONTRACT } from "@waku/rln";
|
import { SEPOLIA_CONTRACT } from "@waku/rln";
|
||||||
import { StatusEventPayload } from "@/services/rln";
|
import { StatusEventPayload } from "@/services/rln";
|
||||||
|
import { SIGNATURE_MESSAGE } from "@/constants";
|
||||||
|
|
||||||
type UseKeystoreResult = {
|
type UseKeystoreResult = {
|
||||||
onReadCredentials: (hash: string, password: string) => void;
|
onReadCredentials: (hash: string, password: string) => void;
|
||||||
@ -12,20 +13,43 @@ type UseKeystoreResult = {
|
|||||||
export const useKeystore = (): UseKeystoreResult => {
|
export const useKeystore = (): UseKeystoreResult => {
|
||||||
const { rln } = useRLN();
|
const { rln } = useRLN();
|
||||||
const {
|
const {
|
||||||
credentials,
|
|
||||||
setActiveCredential,
|
setActiveCredential,
|
||||||
setActiveMembershipID,
|
setActiveMembershipID,
|
||||||
setAppStatus,
|
setAppStatus,
|
||||||
setCredentials,
|
setCredentials,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const generateCredentials = 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
|
||||||
|
);
|
||||||
|
return credentials;
|
||||||
|
};
|
||||||
|
|
||||||
const onRegisterCredentials = React.useCallback(
|
const onRegisterCredentials = React.useCallback(
|
||||||
async (password: string) => {
|
async (password: string) => {
|
||||||
if (!credentials || !rln?.rlnContract || !password) {
|
if (!rln?.rlnContract || !password) {
|
||||||
|
console.log(`Not registering - missing dependencies: contract-${!!rln?.rlnContract}, password-${!!password}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const credentials = await generateCredentials();
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
console.log("No credentials registered.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERING);
|
setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERING);
|
||||||
const membershipInfo = await rln.rlnContract.registerWithKey(
|
const membershipInfo = await rln.rlnContract.registerWithKey(
|
||||||
credentials
|
credentials
|
||||||
@ -42,7 +66,9 @@ export const useKeystore = (): UseKeystoreResult => {
|
|||||||
},
|
},
|
||||||
password
|
password
|
||||||
);
|
);
|
||||||
|
|
||||||
setActiveCredential(keystoreHash);
|
setActiveCredential(keystoreHash);
|
||||||
|
setCredentials(credentials);
|
||||||
setActiveMembershipID(membershipID);
|
setActiveMembershipID(membershipID);
|
||||||
rln.saveKeystore();
|
rln.saveKeystore();
|
||||||
setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERED);
|
setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERED);
|
||||||
@ -52,7 +78,7 @@ export const useKeystore = (): UseKeystoreResult => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[credentials, rln, setActiveCredential, setActiveMembershipID, setAppStatus]
|
[rln, setActiveCredential, setActiveMembershipID, setAppStatus]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReadCredentials = React.useCallback(
|
const onReadCredentials = React.useCallback(
|
||||||
@ -81,3 +107,7 @@ export const useKeystore = (): UseKeystoreResult => {
|
|||||||
onReadCredentials,
|
onReadCredentials,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function randomNumber(): number {
|
||||||
|
return Math.ceil(Math.random() * 1000);
|
||||||
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ type StoreResult = {
|
|||||||
|
|
||||||
wakuStatus: string;
|
wakuStatus: string;
|
||||||
setWakuStatus: (v: string) => void;
|
setWakuStatus: (v: string) => void;
|
||||||
|
|
||||||
|
wallet: string;
|
||||||
|
setWallet: (v: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_VALUE = "none";
|
const DEFAULT_VALUE = "none";
|
||||||
@ -41,6 +44,9 @@ export const useStore = create<StoreResult>((set) => {
|
|||||||
credentials: undefined,
|
credentials: undefined,
|
||||||
setCredentials: (v: undefined | IdentityCredential) =>
|
setCredentials: (v: undefined | IdentityCredential) =>
|
||||||
set((state) => ({ ...state, credentials: v })),
|
set((state) => ({ ...state, credentials: v })),
|
||||||
|
|
||||||
|
wallet: "",
|
||||||
|
setWallet: (v: string) => set((state) => ({ ...state, wallet: v })),
|
||||||
};
|
};
|
||||||
|
|
||||||
const wakuModule = {
|
const wakuModule = {
|
||||||
|
|||||||
@ -28,16 +28,16 @@ export const useWaku = () => {
|
|||||||
setMessages((prev) => [...prev, ...parsedMessaged]);
|
setMessages((prev) => [...prev, ...parsedMessaged]);
|
||||||
};
|
};
|
||||||
|
|
||||||
waku.filter.addEventListener(CONTENT_TOPIC, messageListener);
|
waku.relay.addEventListener(CONTENT_TOPIC, messageListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
waku.filter.removeEventListener(CONTENT_TOPIC, messageListener);
|
waku.relay.removeEventListener(CONTENT_TOPIC, messageListener);
|
||||||
};
|
};
|
||||||
}, [setMessages]);
|
}, [setMessages]);
|
||||||
|
|
||||||
const onSend = React.useCallback(
|
const onSend = React.useCallback(
|
||||||
async (nick: string, text: string) => {
|
async (nick: string, text: string) => {
|
||||||
await waku.lightPush.send({
|
await waku.relay.send({
|
||||||
version: 0,
|
version: 0,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
contentTopic: CONTENT_TOPIC,
|
contentTopic: CONTENT_TOPIC,
|
||||||
|
|||||||
@ -2,15 +2,14 @@ import React from "react";
|
|||||||
import { useStore } from "./useStore";
|
import { useStore } from "./useStore";
|
||||||
import { isEthereumEvenEmitterValid } from "@/utils/ethereum";
|
import { isEthereumEvenEmitterValid } from "@/utils/ethereum";
|
||||||
import { useRLN } from "./useRLN";
|
import { useRLN } from "./useRLN";
|
||||||
import { SIGNATURE_MESSAGE } from "@/constants";
|
|
||||||
|
|
||||||
type UseWalletResult = {
|
type UseWalletResult = {
|
||||||
onGenerateCredentials: () => void;
|
onWalletConnect: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWallet = (): UseWalletResult => {
|
export const useWallet = (): UseWalletResult => {
|
||||||
const { rln } = useRLN();
|
const { rln } = useRLN();
|
||||||
const { setEthAccount, setChainID, setCredentials } = useStore();
|
const { setEthAccount, setChainID, setWallet } = useStore();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const ethereum = window.ethereum;
|
const ethereum = window.ethereum;
|
||||||
@ -36,27 +35,29 @@ export const useWallet = (): UseWalletResult => {
|
|||||||
};
|
};
|
||||||
}, [setEthAccount, setChainID]);
|
}, [setEthAccount, setChainID]);
|
||||||
|
|
||||||
const onGenerateCredentials = React.useCallback(async () => {
|
const onWalletConnect = async () => {
|
||||||
if (!rln?.ethProvider) {
|
const ethereum = window.ethereum;
|
||||||
console.log("Cannot generate credentials, no provider found.");
|
|
||||||
|
if (!ethereum) {
|
||||||
|
console.log("No ethereum instance found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signer = rln.ethProvider.getSigner();
|
if (!rln?.rlnInstance) {
|
||||||
const signature = await signer.signMessage(
|
console.log("RLN instance is not initialized.");
|
||||||
`${SIGNATURE_MESSAGE}. Nonce: ${randomNumber()}`
|
return;
|
||||||
);
|
}
|
||||||
const credentials = await rln.rlnInstance?.generateSeededIdentityCredential(
|
|
||||||
signature
|
try {
|
||||||
);
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }) as unknown as string[];
|
||||||
setCredentials(credentials);
|
await rln.initRLNContract(rln.rlnInstance);
|
||||||
}, [rln, setCredentials]);
|
setWallet(accounts?.[0] || "");
|
||||||
|
} catch(error) {
|
||||||
|
console.error("Failed to conenct to wallet.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onGenerateCredentials,
|
onWalletConnect,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function randomNumber(): number {
|
|
||||||
return Math.ceil(Math.random() * 1000);
|
|
||||||
}
|
|
||||||
|
|||||||
2
src/react-app-env.d.ts
vendored
2
src/react-app-env.d.ts
vendored
@ -4,7 +4,7 @@ type EthereumEvents = "accountsChanged" | "chainChanged";
|
|||||||
type EthereumEventListener = (v: any) => void;
|
type EthereumEventListener = (v: any) => void;
|
||||||
|
|
||||||
type Ethereum = {
|
type Ethereum = {
|
||||||
request: () => void;
|
request: (v?: any) => Promise<void>;
|
||||||
on: (name: EthereumEvents, fn: EthereumEventListener) => void;
|
on: (name: EthereumEvents, fn: EthereumEventListener) => void;
|
||||||
removeListener: (name: EthereumEvents, fn: EthereumEventListener) => void;
|
removeListener: (name: EthereumEvents, fn: EthereumEventListener) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export enum RLNEventsNames {
|
|||||||
|
|
||||||
export enum StatusEventPayload {
|
export enum StatusEventPayload {
|
||||||
WASM_LOADING = "WASM Blob download in progress...",
|
WASM_LOADING = "WASM Blob download in progress...",
|
||||||
|
WASM_LOADED = "WASM Blob downloaded",
|
||||||
WASM_FAILED = "Failed to download WASM, check console",
|
WASM_FAILED = "Failed to download WASM, check console",
|
||||||
CONTRACT_LOADING = "Connecting to RLN contract",
|
CONTRACT_LOADING = "Connecting to RLN contract",
|
||||||
CONTRACT_FAILED = "Failed to connect to RLN contract",
|
CONTRACT_FAILED = "Failed to connect to RLN contract",
|
||||||
@ -63,10 +64,7 @@ export class RLN implements IRLN {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.initializing = true;
|
this.initializing = true;
|
||||||
const rlnInstance = await this.initRLNWasm();
|
await this.initRLNWasm();
|
||||||
await this.initRLNContract(rlnInstance);
|
|
||||||
|
|
||||||
this.emitStatusEvent(StatusEventPayload.RLN_INITIALIZED);
|
|
||||||
|
|
||||||
// emit keystore keys once app is ready
|
// emit keystore keys once app is ready
|
||||||
this.emitKeystoreKeys();
|
this.emitKeystoreKeys();
|
||||||
@ -75,11 +73,10 @@ export class RLN implements IRLN {
|
|||||||
this.initializing = false;
|
this.initializing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initRLNWasm(): Promise<RLNInstance> {
|
private async initRLNWasm(): Promise<void> {
|
||||||
this.emitStatusEvent(StatusEventPayload.WASM_LOADING);
|
this.emitStatusEvent(StatusEventPayload.WASM_LOADING);
|
||||||
try {
|
try {
|
||||||
this.rlnInstance = await create();
|
this.rlnInstance = await create();
|
||||||
return this.rlnInstance;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"Failed at fetching WASM and creating RLN instance: ",
|
"Failed at fetching WASM and creating RLN instance: ",
|
||||||
@ -88,9 +85,14 @@ export class RLN implements IRLN {
|
|||||||
this.emitStatusEvent(StatusEventPayload.WASM_FAILED);
|
this.emitStatusEvent(StatusEventPayload.WASM_FAILED);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
this.emitStatusEvent(StatusEventPayload.WASM_LOADED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initRLNContract(rlnInstance: RLNInstance): Promise<void> {
|
public async initRLNContract(rlnInstance: RLNInstance): Promise<void> {
|
||||||
|
if (this.rlnContract) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.emitStatusEvent(StatusEventPayload.CONTRACT_LOADING);
|
this.emitStatusEvent(StatusEventPayload.CONTRACT_LOADING);
|
||||||
try {
|
try {
|
||||||
this.rlnContract = await RLNContract.init(rlnInstance, {
|
this.rlnContract = await RLNContract.init(rlnInstance, {
|
||||||
@ -102,6 +104,7 @@ export class RLN implements IRLN {
|
|||||||
this.emitStatusEvent(StatusEventPayload.CONTRACT_FAILED);
|
this.emitStatusEvent(StatusEventPayload.CONTRACT_FAILED);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
this.emitStatusEvent(StatusEventPayload.RLN_INITIALIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private initKeystore(): Keystore {
|
private initKeystore(): Keystore {
|
||||||
|
|||||||
@ -1,71 +1,70 @@
|
|||||||
import { v4 as uuid } from "uuid";
|
import { PUBSUB_TOPIC } from "@/constants";
|
||||||
import {
|
|
||||||
PUBSUB_TOPIC,
|
|
||||||
} from "@/constants";
|
|
||||||
import { http } from "@/utils/http";
|
import { http } from "@/utils/http";
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
payload: string,
|
payload: string;
|
||||||
contentTopic: string,
|
contentTopic: string;
|
||||||
version: number,
|
version: number;
|
||||||
timestamp: number
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventListener = (event: CustomEvent) => void;
|
type EventListener = (event: CustomEvent) => void;
|
||||||
|
|
||||||
const SECOND = 1000;
|
const SECOND = 1000;
|
||||||
const LOCAL_NODE = "http://127.0.0.1:8645/";
|
const LOCAL_NODE = "http://127.0.0.1:8645";
|
||||||
const FILTER_URL = "/filter/v2/";
|
const RELAY = "/relay/v1";
|
||||||
const LIGHT_PUSH = "/lightpush/v1/";
|
|
||||||
|
|
||||||
class Filter {
|
const buildURL = (endpoint: string) => `${LOCAL_NODE}${endpoint}`;
|
||||||
private readonly internalEmitter = new EventTarget();
|
|
||||||
|
class Relay {
|
||||||
private readonly subscriptionsEmitter = new EventTarget();
|
private readonly subscriptionsEmitter = new EventTarget();
|
||||||
|
|
||||||
private contentTopicToRequestID: Map<string, string> = new Map();
|
|
||||||
private contentTopicListeners: Map<string, number> = new Map();
|
private contentTopicListeners: Map<string, number> = new Map();
|
||||||
|
|
||||||
// only one content topic subscriptions is possible now
|
// only one content topic subscriptions is possible now
|
||||||
private subscriptionRoutine: undefined | number;
|
private subscriptionRoutine: undefined | number;
|
||||||
|
|
||||||
constructor() {
|
constructor() {}
|
||||||
this.internalEmitter.addEventListener("subscribed", this.handleSubscribed.bind(this));
|
|
||||||
this.internalEmitter.addEventListener("unsubscribed", this.handleUnsubscribed.bind(this));
|
public addEventListener(contentTopic: string, fn: EventListener) {
|
||||||
|
this.handleSubscribed(contentTopic);
|
||||||
|
return this.subscriptionsEmitter.addEventListener(contentTopic, fn as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
public removeEventListener(contentTopic: string, fn: EventListener) {
|
||||||
if (numberOfListeners) {
|
this.handleUnsubscribed(contentTopic);
|
||||||
this.contentTopicListeners.set(contentTopic, numberOfListeners + 1);
|
return this.subscriptionsEmitter.removeEventListener(
|
||||||
return;
|
contentTopic,
|
||||||
}
|
fn as any
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const requestId = uuid();
|
private async handleSubscribed(contentTopic: string) {
|
||||||
await http.post(`${LOCAL_NODE}/${FILTER_URL}/subscriptions`, {
|
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
|
||||||
requestId,
|
|
||||||
contentFilters: [contentTopic],
|
// if nwaku node already subscribed to this content topic
|
||||||
pubsubTopic: PUBSUB_TOPIC
|
if (numberOfListeners) {
|
||||||
});
|
this.contentTopicListeners.set(contentTopic, numberOfListeners + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await http.post(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
|
||||||
|
|
||||||
this.subscriptionRoutine = window.setInterval(async () => {
|
this.subscriptionRoutine = window.setInterval(async () => {
|
||||||
await this.fetchMessages();
|
await this.fetchMessages();
|
||||||
}, SECOND);
|
}, SECOND);
|
||||||
|
|
||||||
this.contentTopicToRequestID.set(contentTopic, requestId);
|
|
||||||
this.contentTopicListeners.set(contentTopic, 1);
|
this.contentTopicListeners.set(contentTopic, 1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to subscribe node ${contentTopic}:`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUnsubscribed(_e: Event) {
|
private async handleUnsubscribed(contentTopic: string) {
|
||||||
const event = _e as CustomEvent;
|
|
||||||
const contentTopic = event.detail;
|
|
||||||
const requestId = this.contentTopicToRequestID.get(contentTopic);
|
|
||||||
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
|
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
|
||||||
|
|
||||||
if (!numberOfListeners || !requestId) {
|
if (!numberOfListeners) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,15 +73,14 @@ class Filter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await http.delete(`${LOCAL_NODE}/${FILTER_URL}/subscriptions`, {
|
try {
|
||||||
requestId,
|
await http.delete(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
|
||||||
contentFilters: [contentTopic],
|
} catch (error) {
|
||||||
pubsubTopic: PUBSUB_TOPIC
|
console.error(`Failed to unsubscribe node from ${contentTopic}:`, error);
|
||||||
});
|
}
|
||||||
|
|
||||||
clearInterval(this.subscriptionRoutine);
|
clearInterval(this.subscriptionRoutine);
|
||||||
this.contentTopicListeners.delete(contentTopic);
|
this.contentTopicListeners.delete(contentTopic);
|
||||||
this.contentTopicToRequestID.delete(contentTopic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchMessages(): Promise<void> {
|
private async fetchMessages(): Promise<void> {
|
||||||
@ -92,7 +90,9 @@ class Filter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await http.get(`${LOCAL_NODE}/${FILTER_URL}/${encodeURIComponent(contentTopic)}`);
|
const response = await http.get(
|
||||||
|
buildURL(`${RELAY}/messages/${encodeURIComponent(PUBSUB_TOPIC)}`)
|
||||||
|
);
|
||||||
const body: Message[] = await response.json();
|
const body: Message[] = await response.json();
|
||||||
|
|
||||||
if (!body || !body.length) {
|
if (!body || !body.length) {
|
||||||
@ -104,37 +104,11 @@ class Filter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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<void> {
|
public async send(message: Message): Promise<void> {
|
||||||
await http.post(`${LOCAL_NODE}/${LIGHT_PUSH}/message`, {
|
await http.post(buildURL(`${RELAY}/messages/${encodeURIComponent(PUBSUB_TOPIC)}`), message);
|
||||||
pubsubTopic: PUBSUB_TOPIC,
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const waku = {
|
export const waku = {
|
||||||
filter: new Filter(),
|
relay: new Relay(),
|
||||||
lightPush: new LightPush(),
|
};
|
||||||
};
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user