add contract hooks, add store fields, fix multiple downloads issue

This commit is contained in:
Sasha 2023-10-27 11:27:14 +02:00
parent 0be9df014f
commit 690cb1ae59
No known key found for this signature in database
14 changed files with 225 additions and 27 deletions

View File

@ -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",

View File

@ -18,6 +18,7 @@
"zustand": "^4.4.4"
},
"devDependencies": {
"@metamask/types": "^1.1.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",

View File

@ -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 (
<Block className="mt-10">
<Block className="mb-3" type={BlockTypes.FlexHorizontal}>
<Subtitle>Contract</Subtitle>
<Button>Fetch state</Button>
<Button onClick={onFetchContract}>Fetch state</Button>
</Block>
<Block type={BlockTypes.FlexHorizontal}>
<p>Your address</p>
<code>Not loaded yet</code>
<code>{ethAccount || "Not loaded yet"}</code>
</Block>
<Block type={BlockTypes.FlexHorizontal}>
<p>Latest membership ID on contract</p>
<code>Not loaded yet</code>
<code>
{lastMembershipID === -1 ? "Not loaded yet" : lastMembershipID}
</code>
</Block>
</Block>
);

View File

@ -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 (
<>
<Block className="mb-5" type={BlockTypes.FlexHorizontal}>
<Title>Waku RLN</Title>
<Button>Connect Wallet</Button>
<Button onClick={onConnectWallet}>Connect Wallet</Button>
</Block>
<Status text="Application status" mark={appStatus} />
</>

View File

@ -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<{}> = () => {
<Block className="mt-4">
<p className="text-s mb-2">Generate new credentials from wallet</p>
<Button>Generate new credentials</Button>
<Button onClick={onGenerateCredentials}>
Generate new credentials
</Button>
<Button className="ml-5">Register credentials</Button>
</Block>

View File

@ -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 (
<Block className="mt-5">
<Block className="mt-3" type={BlockTypes.FlexHorizontal}>
<p>Keystore</p>
<p>Keystore hash</p>
<code>none</code>
</Block>
@ -15,23 +19,27 @@ export const KeystoreDetails: React.FunctionComponent<{}> = () => {
<Block className="mt-3" type={BlockTypes.FlexHorizontal}>
<p>Secret Hash</p>
<code>none</code>
<code>{renderBytes(credentials?.IDSecretHash)}</code>
</Block>
<Block className="mt-3" type={BlockTypes.FlexHorizontal}>
<p>Commitment</p>
<code>none</code>
<code>{renderBytes(credentials?.IDCommitment)}</code>
</Block>
<Block className="mt-3" type={BlockTypes.FlexHorizontal}>
<p>Nullifier</p>
<code>none</code>
<code>{renderBytes(credentials?.IDNullifier)}</code>
</Block>
<Block className="mt-3" type={BlockTypes.FlexHorizontal}>
<p>Trapdoor</p>
<code>none</code>
<code>{renderBytes(credentials?.IDTrapdoor)}</code>
</Block>
</Block>
);
};
function renderBytes(bytes: undefined | Uint8Array): string {
return bytes ? bytesToHex(bytes) : "none";
}

View File

@ -1,2 +1,4 @@
export { useStore } from "./useStore";
export { useRLN } from "./useRLN";
export { useWallet } from "./useWallet";
export { useContract } from "./useContract";

View File

@ -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,
};
};

View File

@ -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);

View File

@ -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<StoreResult>((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 = {

View File

@ -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);
}

14
examples/rln-js/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
/// <reference types="react-scripts" />
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;
}

View File

@ -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 = (<any>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<void> {
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<RLNInstance> {
@ -131,3 +133,5 @@ export class RLN implements IRLN {
);
}
}
export const rln = new RLN();

View File

@ -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;
};