feat: address follow-ups $10

feat: address follow-ups
This commit is contained in:
Sasha 2023-11-25 00:09:02 +01:00 committed by GitHub
commit 9112bd2ca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1460 additions and 126 deletions

1312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"serve": "serve ./out",
"lint": "next lint"
},
"dependencies": {
@ -15,7 +15,6 @@
"next": "13.5.6",
"react": "^18",
"react-dom": "^18",
"uuid": "^9.0.1",
"zustand": "^4.4.4"
},
"devDependencies": {
@ -28,6 +27,7 @@
"eslint": "^8",
"eslint-config-next": "13.5.6",
"postcss": "^8",
"serve": "^14.2.1",
"tailwindcss": "^3",
"typescript": "^5"
}

View File

@ -1,17 +1,23 @@
import { Block, BlockTypes } from "@/components/Block";
import { Title } from "@/components/Title";
import { Status } from "@/components/Status";
import { useStore } from "@/hooks";
import { useStore, useWallet } from "@/hooks";
import { Button } from "@/components/Button";
export const Header: React.FunctionComponent<{}> = () => {
const { appStatus } = useStore();
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

@ -2,12 +2,11 @@ import React from "react";
import { Block, BlockTypes } from "@/components/Block";
import { Button } from "@/components/Button";
import { Subtitle } from "@/components/Subtitle";
import { useRLN, useStore, useWallet } from "@/hooks";
import { useRLN, useStore } from "@/hooks";
import { useKeystore } from "@/hooks/useKeystore";
export const Keystore: React.FunctionComponent<{}> = () => {
const { keystoreCredentials } = useStore();
const { onGenerateCredentials } = useWallet();
const { wallet, keystoreCredentials } = useStore();
const { onReadCredentials, onRegisterCredentials } = useKeystore();
const { password, onPasswordChanged } = usePassword();
@ -64,15 +63,13 @@ export const Keystore: React.FunctionComponent<{}> = () => {
</Block>
<Block className="mt-4">
<p className="text-s mb-2">Generate new credentials from wallet</p>
<Button onClick={onGenerateCredentials}>
Generate new credentials
</Button>
<p className="text-s mb-2">Generate new credentials from wallet and register on chain</p>
<Button
className="ml-5"
disabled={!wallet || !password}
onClick={() => onRegisterCredentials(password)}
className={wallet && password ? "" : "cursor-not-allowed"}
>
Register credentials
Register new credentials
</Button>
</Block>

View File

@ -2,7 +2,9 @@ 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();
@ -20,9 +22,12 @@ export const Waku: React.FunctionComponent<{}> = () => {
return (
<Block className="mt-10">
<Subtitle>
Waku
</Subtitle>
<Block>
<Subtitle>
Waku
</Subtitle>
<p className="text-sm">Content topic: {CONTENT_TOPIC}</p>
</Block>
<Block className="mt-4">
<label

View File

@ -1,12 +1,14 @@
type ButtonProps = {
children: any;
className?: string;
disabled?: boolean;
onClick?: (e?: any) => void;
};
export const Button: React.FunctionComponent<ButtonProps> = (props) => {
return (
<button
disabled={props.disabled}
onClick={props.onClick}
className={`${
props.className || ""

View File

@ -3,6 +3,7 @@ import { useStore } from "./useStore";
import { useRLN } from "./useRLN";
import { SEPOLIA_CONTRACT } from "@waku/rln";
import { StatusEventPayload } from "@/services/rln";
import { SIGNATURE_MESSAGE } from "@/constants";
type UseKeystoreResult = {
onReadCredentials: (hash: string, password: string) => void;
@ -12,20 +13,43 @@ type UseKeystoreResult = {
export const useKeystore = (): UseKeystoreResult => {
const { rln } = useRLN();
const {
credentials,
setActiveCredential,
setActiveMembershipID,
setAppStatus,
setCredentials,
} = 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(
async (password: string) => {
if (!credentials || !rln?.rlnContract || !password) {
if (!rln?.rlnContract || !password) {
console.log(`Not registering - missing dependencies: contract-${!!rln?.rlnContract}, password-${!!password}`);
return;
}
try {
const credentials = await generateCredentials();
if (!credentials) {
console.log("No credentials registered.");
return;
}
setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERING);
const membershipInfo = await rln.rlnContract.registerWithKey(
credentials
@ -42,7 +66,9 @@ export const useKeystore = (): UseKeystoreResult => {
},
password
);
setActiveCredential(keystoreHash);
setCredentials(credentials);
setActiveMembershipID(membershipID);
rln.saveKeystore();
setAppStatus(StatusEventPayload.CREDENTIALS_REGISTERED);
@ -52,7 +78,7 @@ export const useKeystore = (): UseKeystoreResult => {
return;
}
},
[credentials, rln, setActiveCredential, setActiveMembershipID, setAppStatus]
[rln, setActiveCredential, setActiveMembershipID, setAppStatus]
);
const onReadCredentials = React.useCallback(
@ -81,3 +107,7 @@ export const useKeystore = (): UseKeystoreResult => {
onReadCredentials,
};
};
function randomNumber(): number {
return Math.ceil(Math.random() * 1000);
}

View File

@ -22,6 +22,9 @@ type StoreResult = {
wakuStatus: string;
setWakuStatus: (v: string) => void;
wallet: string;
setWallet: (v: string) => void;
};
const DEFAULT_VALUE = "none";
@ -41,6 +44,9 @@ export const useStore = create<StoreResult>((set) => {
credentials: undefined,
setCredentials: (v: undefined | IdentityCredential) =>
set((state) => ({ ...state, credentials: v })),
wallet: "",
setWallet: (v: string) => set((state) => ({ ...state, wallet: v })),
};
const wakuModule = {

View File

@ -28,16 +28,16 @@ export const useWaku = () => {
setMessages((prev) => [...prev, ...parsedMessaged]);
};
waku.filter.addEventListener(CONTENT_TOPIC, messageListener);
waku.relay.addEventListener(CONTENT_TOPIC, messageListener);
return () => {
waku.filter.removeEventListener(CONTENT_TOPIC, messageListener);
waku.relay.removeEventListener(CONTENT_TOPIC, messageListener);
};
}, [setMessages]);
const onSend = React.useCallback(
async (nick: string, text: string) => {
await waku.lightPush.send({
await waku.relay.send({
version: 0,
timestamp: Date.now(),
contentTopic: CONTENT_TOPIC,

View File

@ -2,15 +2,14 @@ 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;
onWalletConnect: () => void;
};
export const useWallet = (): UseWalletResult => {
const { rln } = useRLN();
const { setEthAccount, setChainID, setCredentials } = useStore();
const { setEthAccount, setChainID, setWallet } = useStore();
React.useEffect(() => {
const ethereum = window.ethereum;
@ -36,27 +35,29 @@ export const useWallet = (): UseWalletResult => {
};
}, [setEthAccount, setChainID]);
const onGenerateCredentials = React.useCallback(async () => {
if (!rln?.ethProvider) {
console.log("Cannot generate credentials, no provider found.");
const onWalletConnect = async () => {
const ethereum = window.ethereum;
if (!ethereum) {
console.log("No ethereum instance 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]);
if (!rln?.rlnInstance) {
console.log("RLN instance is not initialized.");
return;
}
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }) as unknown as string[];
await rln.initRLNContract(rln.rlnInstance);
setWallet(accounts?.[0] || "");
} catch(error) {
console.error("Failed to conenct to wallet.");
}
};
return {
onGenerateCredentials,
onWalletConnect,
};
};
function randomNumber(): number {
return Math.ceil(Math.random() * 1000);
}

View File

@ -4,7 +4,7 @@ type EthereumEvents = "accountsChanged" | "chainChanged";
type EthereumEventListener = (v: any) => void;
type Ethereum = {
request: () => void;
request: (v?: any) => Promise<void>;
on: (name: EthereumEvents, fn: EthereumEventListener) => void;
removeListener: (name: EthereumEvents, fn: EthereumEventListener) => void;
};

View File

@ -15,6 +15,7 @@ export enum RLNEventsNames {
export enum StatusEventPayload {
WASM_LOADING = "WASM Blob download in progress...",
WASM_LOADED = "WASM Blob downloaded",
WASM_FAILED = "Failed to download WASM, check console",
CONTRACT_LOADING = "Connecting to RLN contract",
CONTRACT_FAILED = "Failed to connect to RLN contract",
@ -63,10 +64,7 @@ export class RLN implements IRLN {
}
this.initializing = true;
const rlnInstance = await this.initRLNWasm();
await this.initRLNContract(rlnInstance);
this.emitStatusEvent(StatusEventPayload.RLN_INITIALIZED);
await this.initRLNWasm();
// emit keystore keys once app is ready
this.emitKeystoreKeys();
@ -75,11 +73,10 @@ export class RLN implements IRLN {
this.initializing = false;
}
private async initRLNWasm(): Promise<RLNInstance> {
private async initRLNWasm(): Promise<void> {
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: ",
@ -88,9 +85,14 @@ export class RLN implements IRLN {
this.emitStatusEvent(StatusEventPayload.WASM_FAILED);
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);
try {
this.rlnContract = await RLNContract.init(rlnInstance, {
@ -102,6 +104,7 @@ export class RLN implements IRLN {
this.emitStatusEvent(StatusEventPayload.CONTRACT_FAILED);
throw error;
}
this.emitStatusEvent(StatusEventPayload.RLN_INITIALIZED);
}
private initKeystore(): Keystore {

View File

@ -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";
export type Message = {
payload: string,
contentTopic: string,
version: number,
timestamp: number
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/";
const LOCAL_NODE = "http://127.0.0.1:8645";
const RELAY = "/relay/v1";
class Filter {
private readonly internalEmitter = new EventTarget();
const buildURL = (endpoint: string) => `${LOCAL_NODE}${endpoint}`;
class Relay {
private readonly subscriptionsEmitter = new EventTarget();
private contentTopicToRequestID: Map<string, string> = new Map();
private contentTopicListeners: Map<string, number> = 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));
constructor() {}
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
if (numberOfListeners) {
this.contentTopicListeners.set(contentTopic, numberOfListeners + 1);
return;
}
public removeEventListener(contentTopic: string, fn: EventListener) {
this.handleUnsubscribed(contentTopic);
return this.subscriptionsEmitter.removeEventListener(
contentTopic,
fn as any
);
}
const requestId = uuid();
await http.post(`${LOCAL_NODE}/${FILTER_URL}/subscriptions`, {
requestId,
contentFilters: [contentTopic],
pubsubTopic: PUBSUB_TOPIC
});
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);
return;
}
try {
await http.post(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
this.subscriptionRoutine = window.setInterval(async () => {
await this.fetchMessages();
}, SECOND);
this.contentTopicToRequestID.set(contentTopic, requestId);
this.contentTopicListeners.set(contentTopic, 1);
} catch (error) {
console.error(`Failed to subscribe node ${contentTopic}:`, error);
}
}
private async handleUnsubscribed(_e: Event) {
const event = _e as CustomEvent;
const contentTopic = event.detail;
const requestId = this.contentTopicToRequestID.get(contentTopic);
private async handleUnsubscribed(contentTopic: string) {
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
if (!numberOfListeners || !requestId) {
if (!numberOfListeners) {
return;
}
@ -74,15 +73,14 @@ class Filter {
return;
}
await http.delete(`${LOCAL_NODE}/${FILTER_URL}/subscriptions`, {
requestId,
contentFilters: [contentTopic],
pubsubTopic: PUBSUB_TOPIC
});
try {
await http.delete(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
} catch (error) {
console.error(`Failed to unsubscribe node from ${contentTopic}:`, error);
}
clearInterval(this.subscriptionRoutine);
this.contentTopicListeners.delete(contentTopic);
this.contentTopicToRequestID.delete(contentTopic);
}
private async fetchMessages(): Promise<void> {
@ -92,7 +90,9 @@ class Filter {
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();
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> {
await http.post(`${LOCAL_NODE}/${LIGHT_PUSH}/message`, {
pubsubTopic: PUBSUB_TOPIC,
message,
});
await http.post(buildURL(`${RELAY}/messages/${encodeURIComponent(PUBSUB_TOPIC)}`), message);
}
}
export const waku = {
filter: new Filter(),
lightPush: new LightPush(),
};
relay: new Relay(),
};