From 690cb1ae594e62b9b454302940cce4e28e4f5d33 Mon Sep 17 00:00:00 2001 From: Sasha Date: Fri, 27 Oct 2023 11:27:14 +0200 Subject: [PATCH] add contract hooks, add store fields, fix multiple downloads issue --- examples/rln-js/package-lock.json | 16 ++++ examples/rln-js/package.json | 1 + .../src/app/home/components/Blockchain.tsx | 12 ++- .../rln-js/src/app/home/components/Header.tsx | 5 +- .../src/app/home/components/Keystore.tsx | 7 +- .../app/home/components/KeystoreDetails.tsx | 18 +++-- examples/rln-js/src/hooks/index.ts | 2 + examples/rln-js/src/hooks/useContract.ts | 34 ++++++++ examples/rln-js/src/hooks/useRLN.ts | 4 +- examples/rln-js/src/hooks/useStore.ts | 20 +++++ examples/rln-js/src/hooks/useWallet.ts | 80 +++++++++++++++++++ examples/rln-js/src/react-app-env.d.ts | 14 ++++ examples/rln-js/src/services/rln.ts | 24 +++--- examples/rln-js/src/utils/ethereum.ts | 15 +++- 14 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 examples/rln-js/src/hooks/useContract.ts create mode 100644 examples/rln-js/src/hooks/useWallet.ts create mode 100644 examples/rln-js/src/react-app-env.d.ts diff --git a/examples/rln-js/package-lock.json b/examples/rln-js/package-lock.json index c8216ac..987c5a6 100644 --- a/examples/rln-js/package-lock.json +++ b/examples/rln-js/package-lock.json @@ -17,6 +17,7 @@ "zustand": "^4.4.4" }, "devDependencies": { + "@metamask/types": "^1.1.0", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -958,6 +959,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@metamask/types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@metamask/types/-/types-1.1.0.tgz", + "integrity": "sha512-EEV/GjlYkOSfSPnYXfOosxa3TqYtIW3fhg6jdw+cok/OhMgNn4wCfbENFqjytrHMU2f7ZKtBAvtiP5V8H44sSw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@next/env": { "version": "13.5.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", @@ -6742,6 +6752,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@metamask/types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@metamask/types/-/types-1.1.0.tgz", + "integrity": "sha512-EEV/GjlYkOSfSPnYXfOosxa3TqYtIW3fhg6jdw+cok/OhMgNn4wCfbENFqjytrHMU2f7ZKtBAvtiP5V8H44sSw==", + "dev": true + }, "@next/env": { "version": "13.5.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", diff --git a/examples/rln-js/package.json b/examples/rln-js/package.json index 719341e..302961d 100644 --- a/examples/rln-js/package.json +++ b/examples/rln-js/package.json @@ -18,6 +18,7 @@ "zustand": "^4.4.4" }, "devDependencies": { + "@metamask/types": "^1.1.0", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/examples/rln-js/src/app/home/components/Blockchain.tsx b/examples/rln-js/src/app/home/components/Blockchain.tsx index 52cd9cb..979bb44 100644 --- a/examples/rln-js/src/app/home/components/Blockchain.tsx +++ b/examples/rln-js/src/app/home/components/Blockchain.tsx @@ -1,23 +1,29 @@ import { Block, BlockTypes } from "@/components/Block"; import { Button } from "@/components/Button"; import { Subtitle } from "@/components/Subtitle"; +import { useContract, useStore } from "@/hooks"; export const Blockchain: React.FunctionComponent<{}> = () => { + const { ethAccount, lastMembershipID } = useStore(); + const { onFetchContract } = useContract(); + return ( Contract - +

Your address

- Not loaded yet + {ethAccount || "Not loaded yet"}

Latest membership ID on contract

- Not loaded yet + + {lastMembershipID === -1 ? "Not loaded yet" : lastMembershipID} +
); diff --git a/examples/rln-js/src/app/home/components/Header.tsx b/examples/rln-js/src/app/home/components/Header.tsx index 2742049..337d05e 100644 --- a/examples/rln-js/src/app/home/components/Header.tsx +++ b/examples/rln-js/src/app/home/components/Header.tsx @@ -2,16 +2,17 @@ import { Block, BlockTypes } from "@/components/Block"; import { Title } from "@/components/Title"; import { Button } from "@/components/Button"; import { Status } from "@/components/Status"; -import { useStore } from "@/hooks"; +import { useStore, useWallet } from "@/hooks"; export const Header: React.FunctionComponent<{}> = () => { const { appStatus } = useStore(); + const { onConnectWallet } = useWallet(); return ( <> Waku RLN - + diff --git a/examples/rln-js/src/app/home/components/Keystore.tsx b/examples/rln-js/src/app/home/components/Keystore.tsx index 43237a4..5463c3f 100644 --- a/examples/rln-js/src/app/home/components/Keystore.tsx +++ b/examples/rln-js/src/app/home/components/Keystore.tsx @@ -3,10 +3,11 @@ import { Block, BlockTypes } from "@/components/Block"; import { Button } from "@/components/Button"; import { Status } from "@/components/Status"; import { Subtitle } from "@/components/Subtitle"; -import { useStore } from "@/hooks"; +import { useStore, useWallet } from "@/hooks"; export const Keystore: React.FunctionComponent<{}> = () => { const { keystoreStatus, keystoreCredentials } = useStore(); + const { onGenerateCredentials } = useWallet(); const credentialsNodes = React.useMemo( () => @@ -46,7 +47,9 @@ export const Keystore: React.FunctionComponent<{}> = () => {

Generate new credentials from wallet

- +
diff --git a/examples/rln-js/src/app/home/components/KeystoreDetails.tsx b/examples/rln-js/src/app/home/components/KeystoreDetails.tsx index 0882e18..5e478c6 100644 --- a/examples/rln-js/src/app/home/components/KeystoreDetails.tsx +++ b/examples/rln-js/src/app/home/components/KeystoreDetails.tsx @@ -1,10 +1,14 @@ import { Block, BlockTypes } from "@/components/Block"; +import { useStore } from "@/hooks"; +import { bytesToHex } from "@waku/utils/bytes"; export const KeystoreDetails: React.FunctionComponent<{}> = () => { + const { credentials } = useStore(); + return ( -

Keystore

+

Keystore hash

none
@@ -15,23 +19,27 @@ export const KeystoreDetails: React.FunctionComponent<{}> = () => {

Secret Hash

- none + {renderBytes(credentials?.IDSecretHash)}

Commitment

- none + {renderBytes(credentials?.IDCommitment)}

Nullifier

- none + {renderBytes(credentials?.IDNullifier)}

Trapdoor

- none + {renderBytes(credentials?.IDTrapdoor)}
); }; + +function renderBytes(bytes: undefined | Uint8Array): string { + return bytes ? bytesToHex(bytes) : "none"; +} diff --git a/examples/rln-js/src/hooks/index.ts b/examples/rln-js/src/hooks/index.ts index b80e2df..73f5b9b 100644 --- a/examples/rln-js/src/hooks/index.ts +++ b/examples/rln-js/src/hooks/index.ts @@ -1,2 +1,4 @@ export { useStore } from "./useStore"; export { useRLN } from "./useRLN"; +export { useWallet } from "./useWallet"; +export { useContract } from "./useContract"; diff --git a/examples/rln-js/src/hooks/useContract.ts b/examples/rln-js/src/hooks/useContract.ts new file mode 100644 index 0000000..1542b0b --- /dev/null +++ b/examples/rln-js/src/hooks/useContract.ts @@ -0,0 +1,34 @@ +import React from "react"; +import { useStore } from "./useStore"; +import { useRLN } from "./useRLN"; + +type UseContractResult = { + onFetchContract: () => void; +}; + +export const useContract = (): UseContractResult => { + const { rln } = useRLN(); + const { setLastMembershipID } = useStore(); + + const onFetchContract = React.useCallback(async () => { + if (!rln?.rlnContract || !rln?.rlnInstance) { + console.log("Cannot fetch contract info, no contract found."); + return; + } + + // disable button + await rln.rlnContract.fetchMembers(rln.rlnInstance); + // enable button + rln.rlnContract.subscribeToMembers(rln.rlnInstance); + + const last = rln.rlnContract.members.at(-1); + + if (last) { + setLastMembershipID(last.index.toNumber()); + } + }, [rln, setLastMembershipID]); + + return { + onFetchContract, + }; +}; diff --git a/examples/rln-js/src/hooks/useRLN.ts b/examples/rln-js/src/hooks/useRLN.ts index 65fa6ce..a928c86 100644 --- a/examples/rln-js/src/hooks/useRLN.ts +++ b/examples/rln-js/src/hooks/useRLN.ts @@ -1,5 +1,5 @@ import React from "react"; -import { RLN, RLNEventsNames } from "@/services/rln"; +import { rln, RLN, RLNEventsNames } from "@/services/rln"; import { useStore } from "./useStore"; type RLNResult = { @@ -23,8 +23,6 @@ export const useRLN = (): RLNResult => { setKeystoreStatus(event?.detail); }; - const rln = new RLN(); - rln.addEventListener(RLNEventsNames.Status, statusListener); rln.addEventListener(RLNEventsNames.Keystore, keystoreListener); diff --git a/examples/rln-js/src/hooks/useStore.ts b/examples/rln-js/src/hooks/useStore.ts index b7ca404..d43cd55 100644 --- a/examples/rln-js/src/hooks/useStore.ts +++ b/examples/rln-js/src/hooks/useStore.ts @@ -1,8 +1,17 @@ import { create } from "zustand"; +import { IdentityCredential } from "@waku/rln"; type StoreResult = { appStatus: string; setAppStatus: (v: string) => void; + ethAccount: string; + setEthAccount: (v: string) => void; + chainID: number; + setChainID: (v: number) => void; + lastMembershipID: number; + setLastMembershipID: (v: number) => void; + credentials: undefined | IdentityCredential; + setCredentials: (v: undefined | IdentityCredential) => void; keystoreStatus: string; setKeystoreStatus: (v: string) => void; @@ -21,6 +30,17 @@ 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: -1, + setChainID: (v: number) => set((state) => ({ ...state, chainID: v })), + lastMembershipID: -1, + setLastMembershipID: (v: number) => + set((state) => ({ ...state, lastMembershipID: v })), + credentials: undefined, + setCredentials: (v: undefined | IdentityCredential) => + set((state) => ({ ...state, credentials: v })), }; const wakuModule = { diff --git a/examples/rln-js/src/hooks/useWallet.ts b/examples/rln-js/src/hooks/useWallet.ts new file mode 100644 index 0000000..c90aa59 --- /dev/null +++ b/examples/rln-js/src/hooks/useWallet.ts @@ -0,0 +1,80 @@ +import React from "react"; +import { useStore } from "./useStore"; +import { isEthereumEvenEmitterValid } from "@/utils/ethereum"; +import { useRLN } from "./useRLN"; +import { SIGNATURE_MESSAGE } from "@/constants"; + +type UseWalletResult = { + onConnectWallet: () => void; + 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 onConnectWallet = React.useCallback(async () => { + if (!rln?.ethProvider) { + console.log("Cannot connect wallet, no provider found."); + 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); + } + }, [rln, 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 { + onConnectWallet, + onGenerateCredentials, + }; +}; + +function randomNumber(): number { + return Math.ceil(Math.random() * 1000); +} diff --git a/examples/rln-js/src/react-app-env.d.ts b/examples/rln-js/src/react-app-env.d.ts new file mode 100644 index 0000000..abda36f --- /dev/null +++ b/examples/rln-js/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/examples/rln-js/src/services/rln.ts b/examples/rln-js/src/services/rln.ts index 10f03e8..628a957 100644 --- a/examples/rln-js/src/services/rln.ts +++ b/examples/rln-js/src/services/rln.ts @@ -34,17 +34,18 @@ type IRLN = { export class RLN implements IRLN { private readonly emitter = new EventTarget(); - private readonly ethProvider: ethers.providers.Web3Provider; + public readonly ethProvider: ethers.providers.Web3Provider; - private rlnInstance: undefined | RLNInstance; - private rlnContract: undefined | RLNContract; - private keystore: undefined | Keystore; + public rlnInstance: undefined | RLNInstance; + public rlnContract: undefined | RLNContract; + public keystore: undefined | Keystore; private initialized = false; + private initializing = false; public constructor() { - const ethereum = (window) - .ethereum as ethers.providers.ExternalProvider; + 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." @@ -54,19 +55,20 @@ export class RLN implements IRLN { } public async init(): Promise { - if (this.initialized) { - console.info("RLN is initialized."); + if (this.initialized || this.initializing) { return; } - // const rlnInstance = await this.initRLNWasm(); - // await this.initRLNContract(rlnInstance); + this.initializing = true; + const rlnInstance = await this.initRLNWasm(); + await this.initRLNContract(rlnInstance); this.emitStatusEvent(StatusEventPayload.RLN_INITIALIZED); this.initKeystore(); this.initialized = true; + this.initializing = false; } private async initRLNWasm(): Promise { @@ -131,3 +133,5 @@ export class RLN implements IRLN { ); } } + +export const rln = new RLN(); diff --git a/examples/rln-js/src/utils/ethereum.ts b/examples/rln-js/src/utils/ethereum.ts index 5d28415..6441202 100644 --- a/examples/rln-js/src/utils/ethereum.ts +++ b/examples/rln-js/src/utils/ethereum.ts @@ -1,6 +1,17 @@ -export const isBrowserProviderValid = (obj: any) => { +export const isBrowserProviderValid = (obj: any): boolean => { if (obj && typeof obj.request === "function") { return true; } - return true; + return false; +}; + +export const isEthereumEvenEmitterValid = (obj: any): boolean => { + if ( + obj && + typeof obj.on === "function" && + typeof obj.removeListener === "function" + ) { + return true; + } + return false; };