diff --git a/examples/keystore-management/package-lock.json b/examples/keystore-management/package-lock.json index f946d8b..e38db54 100644 --- a/examples/keystore-management/package-lock.json +++ b/examples/keystore-management/package-lock.json @@ -8,7 +8,7 @@ "name": "waku-keystore-management", "version": "0.1.0", "dependencies": { - "@waku/rln": "0.0.2-a3e7f15.0", + "@waku/rln": "0.0.2-5c50ed7.0", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -2265,16 +2265,16 @@ } }, "node_modules/@waku/core": { - "version": "0.0.34-a3e7f15.0", - "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.34-a3e7f15.0.tgz", - "integrity": "sha512-KWx7Epz7yAZKt9SJdUPjbZYY90312omLMXzBV9C3I8H94Q7u8C3dQep4wZsTBB+VGnVsKLYEyVmdorGZIgcubg==", + "version": "0.0.34-5c50ed7.0", + "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.34-5c50ed7.0.tgz", + "integrity": "sha512-GWYRzLZTfWgwXrK/KCWU3bbsYiE7pCyjGHKOfTDnUX9Z0LEx9fs5nDF7LyK41uefrZ0c6zrth3TH+Rkawm5aHw==", "license": "MIT OR Apache-2.0", "dependencies": { "@libp2p/ping": "2.0.1", - "@waku/enr": "0.0.28-a3e7f15.0", - "@waku/interfaces": "0.0.29-a3e7f15.0", - "@waku/proto": "0.0.9-a3e7f15.0", - "@waku/utils": "0.0.22-a3e7f15.0", + "@waku/enr": "0.0.28-5c50ed7.0", + "@waku/interfaces": "0.0.29-5c50ed7.0", + "@waku/proto": "0.0.9-5c50ed7.0", + "@waku/utils": "0.0.22-5c50ed7.0", "debug": "^4.3.4", "it-all": "^3.0.4", "it-length-prefixed": "^9.0.4", @@ -2312,9 +2312,9 @@ } }, "node_modules/@waku/enr": { - "version": "0.0.28-a3e7f15.0", - "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.28-a3e7f15.0.tgz", - "integrity": "sha512-IdVrw04Bi/DJd7caYtYbIGnb26hZJdGMsB0hBaqwto+kgAjp0gzOBB+NSutiBIPsVH0ED4fr1FHjW/pIRjrCfA==", + "version": "0.0.28-5c50ed7.0", + "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.28-5c50ed7.0.tgz", + "integrity": "sha512-Ms4FSw/fvkC63yHlikXCSRNUbxcAph4Os4uveVEnevV96B9k5GIoy5VzkLQbimWEdplgNBTiPzwI47O2AhvDrw==", "license": "MIT OR Apache-2.0", "dependencies": { "@ethersproject/rlp": "^5.7.0", @@ -2322,7 +2322,7 @@ "@libp2p/peer-id": "^5.0.1", "@multiformats/multiaddr": "^12.0.0", "@noble/secp256k1": "^1.7.1", - "@waku/utils": "0.0.22-a3e7f15.0", + "@waku/utils": "0.0.22-5c50ed7.0", "debug": "^4.3.4", "js-sha3": "^0.9.2" }, @@ -2339,21 +2339,21 @@ } }, "node_modules/@waku/interfaces": { - "version": "0.0.29-a3e7f15.0", - "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.29-a3e7f15.0.tgz", - "integrity": "sha512-RgtxDuLDmzrQBq17fcFRVSpQoeiZocbNTXIhW39+zh5HhggYRu0694W1D2c3toGMTODggEyCfPdFLfC24cMfuQ==", + "version": "0.0.29-5c50ed7.0", + "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.29-5c50ed7.0.tgz", + "integrity": "sha512-yfu9+SYLHpA7z9U1VuYlbhQVb5hBfUnqvAT7cLSbBKW4S13+gHaalZ3SFpZy8hYTqiIA5MTvXysWlSfLv6zIyw==", "license": "MIT OR Apache-2.0", "dependencies": { - "@waku/proto": "0.0.9-a3e7f15.0" + "@waku/proto": "0.0.9-5c50ed7.0" }, "engines": { "node": ">=20" } }, "node_modules/@waku/proto": { - "version": "0.0.9-a3e7f15.0", - "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.9-a3e7f15.0.tgz", - "integrity": "sha512-nHUDg6QQ1OG1lEcx5o4HawkKWZl8eUd0gU7amk7reAJ64Y0I2UkblQt+FzUM3v8RinZnFUCweRKy1j+/ygiPPw==", + "version": "0.0.9-5c50ed7.0", + "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.9-5c50ed7.0.tgz", + "integrity": "sha512-3QfqbglgWjotrlzrNgtbiXDnxOOP68Bew2OZMhDW8bF7bUdBsq1Y8eih6eJPEcQ/8EP53OQ+gJ0FFv8BTaKLrw==", "license": "MIT OR Apache-2.0", "dependencies": { "protons-runtime": "^5.4.0" @@ -2363,14 +2363,15 @@ } }, "node_modules/@waku/rln": { - "version": "0.0.2-a3e7f15.0", - "resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.0.2-a3e7f15.0.tgz", - "integrity": "sha512-IK0CBJ16XBnODSc1uIaVmCMejSqQbVLEIzP6HdvrGH+9bJuyUzBLL7hkceAlMNPsOwZXb/fj0VR3DvzW1x9rKQ==", + "version": "0.0.2-5c50ed7.0", + "resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.0.2-5c50ed7.0.tgz", + "integrity": "sha512-8C1OevAJMgZn7FWjqsQIMS5PCgAKlr9Dcs4nyXCvTaHBptkFWzVfhVa7hdz2EpcOtlYD1HqsvDeT17S15m1lmA==", "license": "MIT OR Apache-2.0", "dependencies": { "@chainsafe/bls-keystore": "3.0.0", - "@waku/core": "0.0.34-a3e7f15.0", - "@waku/utils": "0.0.22-a3e7f15.0", + "@noble/hashes": "^1.2.0", + "@waku/core": "0.0.34-5c50ed7.0", + "@waku/utils": "0.0.22-5c50ed7.0", "@waku/zerokit-rln-wasm": "^0.0.13", "ethereum-cryptography": "^3.1.0", "ethers": "^5.7.2", @@ -2382,13 +2383,13 @@ } }, "node_modules/@waku/utils": { - "version": "0.0.22-a3e7f15.0", - "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.22-a3e7f15.0.tgz", - "integrity": "sha512-N3JiVD5mzJklheZAyVgMrE/RqrFQjUQ13b24c1XTLGgqrwXAVoLxPK47EQQK/hAt6OxX1ql2f4wHMYLP3mu1Tw==", + "version": "0.0.22-5c50ed7.0", + "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.22-5c50ed7.0.tgz", + "integrity": "sha512-hD+XO7lZ86OL9zFEzZvBy6pmgZlmNw7g1xeoui3FOauv6+zL///tarjLrmkPWh+eLJHdalVvTF9D/zkUWbK9SA==", "license": "MIT OR Apache-2.0", "dependencies": { "@noble/hashes": "^1.3.2", - "@waku/interfaces": "0.0.29-a3e7f15.0", + "@waku/interfaces": "0.0.29-5c50ed7.0", "chai": "^4.3.10", "debug": "^4.3.4", "uint8arrays": "^5.0.1" diff --git a/examples/keystore-management/package.json b/examples/keystore-management/package.json index dc6a0db..be054d1 100644 --- a/examples/keystore-management/package.json +++ b/examples/keystore-management/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@waku/rln": "0.0.2-a3e7f15.0", + "@waku/rln": "0.0.2-5c50ed7.0", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/examples/keystore-management/src/app/layout.tsx b/examples/keystore-management/src/app/layout.tsx index 6676a68..7c6a855 100644 --- a/examples/keystore-management/src/app/layout.tsx +++ b/examples/keystore-management/src/app/layout.tsx @@ -2,7 +2,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { WalletProvider } from "../contexts/WalletContext"; -import { RLNProvider } from "../contexts/RLNContext"; +import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2"; +import { RLNImplementationProvider } from "../contexts/RLNImplementationContext"; import { Header } from "../components/Header"; const geistSans = Geist({ @@ -31,14 +32,16 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > - -
-
-
- {children} -
-
-
+ + +
+
+
+ {children} +
+
+
+
diff --git a/examples/keystore-management/src/app/page.tsx b/examples/keystore-management/src/app/page.tsx index 00e891e..c6eb0fa 100644 --- a/examples/keystore-management/src/app/page.tsx +++ b/examples/keystore-management/src/app/page.tsx @@ -1,5 +1,6 @@ import RLNMembershipRegistration from '../components/RLNMembershipRegistration'; import { WalletInfo } from '../components/WalletInfo'; +import { RLNImplementationToggle } from '../components/RLNImplementationToggle'; export default function Home() { return ( @@ -9,6 +10,12 @@ export default function Home() {

Waku Keystore Management

+ {/* RLN Implementation Toggle */} +
+

RLN Implementation

+ +
+ {/* Wallet Information Section */}

Wallet Connection

diff --git a/examples/keystore-management/src/components/RLNImplementationToggle.tsx b/examples/keystore-management/src/components/RLNImplementationToggle.tsx new file mode 100644 index 0000000..5c730d5 --- /dev/null +++ b/examples/keystore-management/src/components/RLNImplementationToggle.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useRLNImplementation, RLNImplementationType } from '../contexts/RLNImplementationContext'; + +export function RLNImplementationToggle() { + const { implementation, setImplementation } = useRLNImplementation(); + + const handleToggle = (newImplementation: RLNImplementationType) => { + setImplementation(newImplementation); + }; + + return ( +
+ + RLN Implementation: + +
+ + +
+
+ {implementation === 'standard' ? ( + Using full RLN implementation + ) : ( + Using lightweight RLN implementation + )} +
+
+ ); +} diff --git a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx index 79cdbf2..bbfe1f7 100644 --- a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx +++ b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from 'react'; -import { useRLN } from '../contexts/RLNContext'; +import { useRLN } from '../contexts/RLNUnifiedContext2'; import { useWallet } from '../contexts/WalletContext'; import { DecryptedCredentials } from '@waku/rln'; diff --git a/examples/keystore-management/src/contexts/RLNFactory.tsx b/examples/keystore-management/src/contexts/RLNFactory.tsx new file mode 100644 index 0000000..6b7c9b8 --- /dev/null +++ b/examples/keystore-management/src/contexts/RLNFactory.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { createRLN, RLNLightInstance } from '@waku/rln'; +import { ethers } from 'ethers'; + +// Define a unified interface that both implementations must support +export interface UnifiedRLNInstance { + contract: { + address: string; + membershipFee?: () => Promise; + }; + start: (options: { signer: ethers.Signer }) => Promise; + // Both implementations use registerMembership but with different parameters + registerMembership: (options: { signature: string }) => Promise>; +} + +// Define a factory function that creates the appropriate RLN implementation +export async function createRLNImplementation(type: 'standard' | 'light'): Promise { + if (type === 'standard') { + // Create and return the standard RLN implementation + return await createRLN() as unknown as UnifiedRLNInstance; + } else { + // Create and return the light RLN implementation + return new RLNLightInstance() as unknown as UnifiedRLNInstance; + } +} diff --git a/examples/keystore-management/src/contexts/RLNImplementationContext.tsx b/examples/keystore-management/src/contexts/RLNImplementationContext.tsx new file mode 100644 index 0000000..8f1c367 --- /dev/null +++ b/examples/keystore-management/src/contexts/RLNImplementationContext.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { createContext, useContext, useState, ReactNode } from 'react'; + +// Define the implementation types +export type RLNImplementationType = 'standard' | 'light'; + +// Define the context type +interface RLNImplementationContextType { + implementation: RLNImplementationType; + setImplementation: (implementation: RLNImplementationType) => void; +} + +// Create the context +const RLNImplementationContext = createContext(undefined); + +// Create the provider component +export function RLNImplementationProvider({ children }: { children: ReactNode }) { + const [implementation, setImplementation] = useState('standard'); + + return ( + + {children} + + ); +} + +// Create a hook to use the context +export function useRLNImplementation() { + const context = useContext(RLNImplementationContext); + if (context === undefined) { + throw new Error('useRLNImplementation must be used within a RLNImplementationProvider'); + } + return context; +} diff --git a/examples/keystore-management/src/contexts/RLNLightContext.tsx b/examples/keystore-management/src/contexts/RLNLightContext.tsx new file mode 100644 index 0000000..cb08aed --- /dev/null +++ b/examples/keystore-management/src/contexts/RLNLightContext.tsx @@ -0,0 +1,297 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln'; +import { useWallet } from './WalletContext'; +import { ethers } from 'ethers'; + +// Constants +const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials"; +const ERC20_ABI = [ + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", + "function balanceOf(address account) view returns (uint256)" +]; + +// Linea Sepolia configuration +const LINEA_SEPOLIA_CONFIG = { + chainId: 59141, + tokenAddress: '0x185A0015aC462a0aECb81beCc0497b649a64B9ea' +}; + +interface RLNContextType { + rln: RLNLightInstance | RLNInstance | null; + isInitialized: boolean; + isStarted: boolean; + error: string | null; + initializeRLN: () => Promise; + registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>; + rateMinLimit: number; + rateMaxLimit: number; +} + +const RLNContext = createContext(undefined); + +export function RLNProvider({ children }: { children: ReactNode }) { + const { isConnected, signer } = useWallet(); + const [rln, setRln] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); + const [isStarted, setIsStarted] = useState(false); + const [error, setError] = useState(null); + const [rateMinLimit, setRateMinLimit] = useState(20); + const [rateMaxLimit, setRateMaxLimit] = useState(600); + + const ensureLineaSepoliaNetwork = async (): Promise => { + try { + console.log("Current network: unknown", await signer?.getChainId()); + + // Check if already on Linea Sepolia + if (await signer?.getChainId() === LINEA_SEPOLIA_CONFIG.chainId) { + console.log("Already on Linea Sepolia network"); + return true; + } + + // If not on Linea Sepolia, try to switch + console.log("Not on Linea Sepolia, attempting to switch..."); + + interface EthereumProvider { + request: (args: { + method: string; + params?: unknown[] + }) => Promise; + } + + // Get the provider from window.ethereum + const provider = window.ethereum as EthereumProvider | undefined; + + if (!provider) { + console.warn("No Ethereum provider found"); + return false; + } + + try { + // Request network switch + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: `0x${LINEA_SEPOLIA_CONFIG.chainId.toString(16)}` }], + }); + + console.log("Successfully switched to Linea Sepolia"); + return true; + } catch (switchError: unknown) { + console.error("Error switching network:", switchError); + return false; + } + } catch (err) { + console.error("Error checking or switching network:", err); + return false; + } + }; + + const initializeRLN = async () => { + console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer); + + try { + setError(null); + + if (!rln) { + console.log("Creating RLN instance..."); + + try { + const rlnInstance = new RLNLightInstance(); + + console.log("RLN instance created successfully:", !!rlnInstance); + setRln(rlnInstance); + + setIsInitialized(true); + console.log("isInitialized set to true"); + + // Update rate limits to match contract requirements + setRateMinLimit(20); // Contract minimum (RATE_LIMIT_PARAMS.MIN_RATE) + setRateMaxLimit(600); // Contract maximum (RATE_LIMIT_PARAMS.MAX_RATE) + } catch (createErr) { + console.error("Error creating RLN instance:", createErr); + throw createErr; + } + } else { + console.log("RLN instance already exists, skipping creation"); + } + + // Start RLN if wallet is connected + if (isConnected && signer && rln && !isStarted) { + console.log("Starting RLN with signer..."); + try { + // Initialize with localKeystore if available (just for reference in localStorage) + const localKeystore = localStorage.getItem("rln-keystore") || ""; + console.log("Local keystore available:", !!localKeystore); + + // Start RLN with signer + await rln.start({ signer }); + + setIsStarted(true); + console.log("RLN started successfully, isStarted set to true"); + } catch (startErr) { + console.error("Error starting RLN:", startErr); + throw startErr; + } + } else { + console.log("Skipping RLN start because:", { + isConnected, + hasSigner: !!signer, + hasRln: !!rln, + isAlreadyStarted: isStarted + }); + } + } catch (err) { + console.error('Error in initializeRLN:', err); + setError(err instanceof Error ? err.message : 'Failed to initialize RLN'); + } + }; + + const registerMembership = async (rateLimit: number) => { + console.log("registerMembership called with rate limit:", rateLimit); + + if (!rln || !isStarted) { + return { success: false, error: 'RLN not initialized or not started' }; + } + + if (!signer) { + return { success: false, error: 'No signer available' }; + } + + try { + // Validate rate limit + if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) { + return { + success: false, + error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}` + }; + } + + // Ensure we're on the correct network + const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); + if (!isOnLineaSepolia) { + console.warn("Could not switch to Linea Sepolia network. Registration may fail."); + } + + // Get user address and contract address + const userAddress = await signer.getAddress(); + + if (!rln.contract || !rln.contract.address) { + return { success: false, error: "RLN contract address not available. Cannot proceed with registration." }; + } + + const contractAddress = rln.contract.address; + const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress; + + // Create token contract instance + const tokenContract = new ethers.Contract( + tokenAddress, + ERC20_ABI, + signer + ); + + // Check token balance + const tokenBalance = await tokenContract.balanceOf(userAddress); + if (tokenBalance.isZero()) { + return { success: false, error: "You need tokens to register a membership. Your token balance is zero." }; + } + + // Check and approve token allowance if needed + const currentAllowance = await tokenContract.allowance(userAddress, contractAddress); + if (currentAllowance.eq(0)) { + console.log("Requesting token approval..."); + + // Approve a large amount (max uint256) + const maxUint256 = ethers.constants.MaxUint256; + + try { + const approveTx = await tokenContract.approve(contractAddress, maxUint256); + console.log("Approval transaction submitted:", approveTx.hash); + + // Wait for the transaction to be mined + await approveTx.wait(1); + console.log("Token approval confirmed"); + } catch (approvalErr) { + console.error("Error during token approval:", approvalErr); + return { + success: false, + error: `Failed to approve token: ${approvalErr instanceof Error ? approvalErr.message : String(approvalErr)}` + }; + } + } else { + console.log("Token allowance already sufficient"); + } + + // Generate signature for identity + const message = `${SIGNATURE_MESSAGE} ${Date.now()}`; + const signature = await signer.signMessage(message); + + const _credentials = await rln.registerMembership({signature: signature}); + if (!_credentials) { + throw new Error("Failed to register membership: No credentials returned"); + } + if (!_credentials.identity) { + throw new Error("Failed to register membership: Missing identity information"); + } + if (!_credentials.membership) { + throw new Error("Failed to register membership: Missing membership information"); + } + + return { success: true, credentials: _credentials }; + } catch (err) { + let errorMsg = "Failed to register membership"; + if (err instanceof Error) { + errorMsg = err.message; + } + + return { success: false, error: errorMsg }; + } + }; + + // Initialize RLN when wallet connects + useEffect(() => { + console.log("Wallet connection state changed:", { isConnected, hasSigner: !!signer }); + if (isConnected && signer) { + console.log("Wallet connected, attempting to initialize RLN"); + initializeRLN(); + } else { + console.log("Wallet not connected or no signer available, skipping RLN initialization"); + } + }, [isConnected, signer]); + + // Debug log for state changes + useEffect(() => { + console.log("RLN Context state:", { + isInitialized, + isStarted, + hasRln: !!rln, + error + }); + }, [isInitialized, isStarted, rln, error]); + + return ( + + {children} + + ); +} + +export function useRLN() { + const context = useContext(RLNContext); + if (context === undefined) { + throw new Error('useRLN must be used within an RLNProvider'); + } + return context; +} \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx b/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx new file mode 100644 index 0000000..2412857 --- /dev/null +++ b/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { ReactNode } from 'react'; +import { RLNProvider as StandardRLNProvider } from './RLNContext'; +import { RLNProvider as LightRLNProvider } from './RLNLightContext'; +import { useRLNImplementation } from './RLNImplementationContext'; + +// Create a unified provider that conditionally renders the appropriate provider +export function RLNUnifiedProvider({ children }: { children: ReactNode }) { + const { implementation } = useRLNImplementation(); + + // Render the appropriate provider based on the implementation + return ( + <> + {implementation === 'standard' ? ( + {children} + ) : ( + {children} + )} + + ); +} + diff --git a/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx b/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx new file mode 100644 index 0000000..7419ca4 --- /dev/null +++ b/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx @@ -0,0 +1,358 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { KeystoreEntity } from '@waku/rln'; +import { UnifiedRLNInstance } from './RLNFactory'; +import { useRLNImplementation } from './RLNImplementationContext'; +import { createRLNImplementation } from './RLNFactory'; +import { ethers } from 'ethers'; + +// Constants for RLN membership registration +const ERC20_ABI = [ + "function allowance(address owner, address spender) view returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", + "function balanceOf(address account) view returns (uint256)" +]; + +// Linea Sepolia configuration +const LINEA_SEPOLIA_CONFIG = { + chainId: 59141, + tokenAddress: '0x185A0015aC462a0aECb81beCc0497b649a64B9ea' +}; + +// Define the context type +interface RLNContextType { + rln: UnifiedRLNInstance | null; + isInitialized: boolean; + isStarted: boolean; + error: string | null; + initializeRLN: () => Promise; + registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: KeystoreEntity }>; + rateMinLimit: number; + rateMaxLimit: number; +} + +// Create the context +const RLNUnifiedContext = createContext(undefined); + +// Create the provider component +export function RLNUnifiedProvider({ children }: { children: ReactNode }) { + const { implementation } = useRLNImplementation(); + const [rln, setRln] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); + const [isStarted, setIsStarted] = useState(false); + const [error, setError] = useState(null); + const [rateMinLimit, setRateMinLimit] = useState(20); + const [rateMaxLimit, setRateMaxLimit] = useState(600); + + // Get the signer from window.ethereum + const [signer, setSigner] = useState(null); + const [isConnected, setIsConnected] = useState(false); + + // Listen for wallet connection + useEffect(() => { + const checkWallet = async () => { + try { + if (window.ethereum) { + const provider = new ethers.providers.Web3Provider(window.ethereum); + const accounts = await provider.listAccounts(); + + if (accounts.length > 0) { + const signer = provider.getSigner(); + setSigner(signer); + setIsConnected(true); + return; + } + } + + setSigner(null); + setIsConnected(false); + } catch (err) { + console.error("Error checking wallet:", err); + setSigner(null); + setIsConnected(false); + } + }; + + checkWallet(); + + // Listen for account changes + if (window.ethereum) { + window.ethereum.on('accountsChanged', checkWallet); + window.ethereum.on('chainChanged', checkWallet); + } + + return () => { + if (window.ethereum) { + window.ethereum.removeListener('accountsChanged', checkWallet); + window.ethereum.removeListener('chainChanged', checkWallet); + } + }; + }, []); + + // Reset RLN state when implementation changes + useEffect(() => { + setRln(null); + setIsInitialized(false); + setIsStarted(false); + setError(null); + }, [implementation]); + + const ensureLineaSepoliaNetwork = async (): Promise => { + try { + console.log("Current network: unknown", await signer?.getChainId()); + + // Check if already on Linea Sepolia + if (await signer?.getChainId() === LINEA_SEPOLIA_CONFIG.chainId) { + console.log("Already on Linea Sepolia network"); + return true; + } + + // If not on Linea Sepolia, try to switch + console.log("Not on Linea Sepolia, attempting to switch..."); + + interface EthereumProvider { + request: (args: { + method: string; + params?: unknown[] + }) => Promise; + } + + // Get the provider from window.ethereum + const provider = window.ethereum as EthereumProvider | undefined; + + if (!provider) { + console.warn("No Ethereum provider found"); + return false; + } + + try { + // Request network switch + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: `0x${LINEA_SEPOLIA_CONFIG.chainId.toString(16)}` }], + }); + + console.log("Successfully switched to Linea Sepolia"); + return true; + } catch (switchError: unknown) { + console.error("Error switching network:", switchError); + return false; + } + } catch (err) { + console.error("Error checking or switching network:", err); + return false; + } + }; + + const initializeRLN = async () => { + console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer); + + try { + setError(null); + + if (!rln) { + console.log(`Creating RLN ${implementation} instance...`); + + try { + // Use our factory to create the appropriate implementation + const rlnInstance = await createRLNImplementation(implementation); + + console.log("RLN instance created successfully:", !!rlnInstance); + setRln(rlnInstance); + + setIsInitialized(true); + console.log("isInitialized set to true"); + + // Update rate limits to match contract requirements + setRateMinLimit(20); // Contract minimum (RATE_LIMIT_PARAMS.MIN_RATE) + setRateMaxLimit(600); // Contract maximum (RATE_LIMIT_PARAMS.MAX_RATE) + } catch (createErr) { + console.error("Error creating RLN instance:", createErr); + throw createErr; + } + } else { + console.log("RLN instance already exists, skipping creation"); + } + + // Start RLN if wallet is connected + if (isConnected && signer && rln && !isStarted) { + console.log("Starting RLN with signer..."); + try { + // Initialize with localKeystore if available (just for reference in localStorage) + const localKeystore = localStorage.getItem("rln-keystore") || ""; + console.log("Local keystore available:", !!localKeystore); + + // Start RLN with signer + await rln.start({ signer }); + + setIsStarted(true); + console.log("RLN started successfully, isStarted set to true"); + } catch (startErr) { + console.error("Error starting RLN:", startErr); + throw startErr; + } + } else { + console.log("Skipping RLN start because:", { + isConnected, + hasSigner: !!signer, + hasRln: !!rln, + isAlreadyStarted: isStarted + }); + } + } catch (err) { + console.error('Error in initializeRLN:', err); + setError(err instanceof Error ? err.message : 'Failed to initialize RLN'); + } + }; + + const registerMembership = async (rateLimit: number) => { + console.log("registerMembership called with rate limit:", rateLimit); + + if (!rln || !isStarted) { + return { success: false, error: 'RLN not initialized or not started' }; + } + + if (!signer) { + return { success: false, error: 'No signer available' }; + } + + try { + // Validate rate limit + if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) { + return { + success: false, + error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}` + }; + } + + // Ensure we're on the correct network + const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); + if (!isOnLineaSepolia) { + console.warn("Could not switch to Linea Sepolia network. Registration may fail."); + } + + // Get user address and contract address + const userAddress = await signer.getAddress(); + + if (!rln.contract || !rln.contract.address) { + return { success: false, error: "RLN contract address not available. Cannot proceed with registration." }; + } + + const contractAddress = rln.contract.address; + const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress; + + // Create token contract instance + const tokenContract = new ethers.Contract( + tokenAddress, + ERC20_ABI, + signer + ); + + // Check token balance + const tokenBalance = await tokenContract.balanceOf(userAddress); + if (tokenBalance.isZero()) { + return { success: false, error: "You need tokens to register a membership. Your token balance is zero." }; + } + + // Check and approve token allowance if needed + const currentAllowance = await tokenContract.allowance(userAddress, contractAddress); + // Get membership fee - implementation may differ between standard and light + const membershipFee = await rln.contract.membershipFee?.() || ethers.utils.parseEther("0.01"); + + if (currentAllowance.lt(membershipFee)) { + console.log("Approving token allowance..."); + + try { + const approveTx = await tokenContract.approve(contractAddress, membershipFee); + await approveTx.wait(); + console.log("Token allowance approved"); + } catch (approveErr) { + console.error("Error approving token allowance:", approveErr); + return { success: false, error: "Failed to approve token allowance for membership registration." }; + } + } else { + console.log("Token allowance already sufficient"); + } + + // Register membership + console.log("Registering membership with rate limit:", rateLimit); + + try { + // Both implementations use registerMembership with a signature + // Generate signature for identity + const message = `Sign this message to generate your RLN credentials ${Date.now()}`; + const signature = await signer.signMessage(message); + + // Call registerMembership with the signature + const credentials = await rln.registerMembership({ + signature: signature + }) as unknown as KeystoreEntity; + + // Validate credentials + if (!credentials) { + throw new Error("Failed to register membership: No credentials returned"); + } + if (!credentials.identity) { + throw new Error("Failed to register membership: Missing identity information"); + } + if (!credentials.membership) { + throw new Error("Failed to register membership: Missing membership information"); + } + + console.log("Membership registered successfully"); + + // Store credentials in localStorage for reference + try { + localStorage.setItem("rln-keystore", JSON.stringify(credentials)); + } catch (storageErr) { + console.warn("Could not store credentials in localStorage:", storageErr); + } + + return { + success: true, + credentials: credentials + }; + } catch (registerErr) { + console.error("Error registering membership:", registerErr); + return { + success: false, + error: registerErr instanceof Error ? registerErr.message : "Failed to register membership" + }; + } + } catch (err) { + console.error("Error in registerMembership:", err); + return { + success: false, + error: err instanceof Error ? err.message : "An unknown error occurred during registration" + }; + } + }; + + // Create the context value + const contextValue: RLNContextType = { + rln, + isInitialized, + isStarted, + error, + initializeRLN, + registerMembership, + rateMinLimit, + rateMaxLimit + }; + + return ( + + {children} + + ); +} + +// Create a hook to use the context +export function useRLN() { + const context = useContext(RLNUnifiedContext); + if (context === undefined) { + throw new Error('useRLN must be used within a RLNUnifiedProvider'); + } + return context; +} diff --git a/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx b/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx new file mode 100644 index 0000000..25678bb --- /dev/null +++ b/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln'; +import { useRLNImplementation } from './RLNImplementationContext'; + +// Define a dummy context for when neither implementation is available +interface RLNContextType { + rln: RLNInstance | RLNLightInstance | null; + isInitialized: boolean; + isStarted: boolean; + error: string | null; + initializeRLN: () => Promise; + registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>; + rateMinLimit: number; + rateMaxLimit: number; +} + +// Create a dummy context with default values +const dummyRLNContext: RLNContextType = { + rln: null, + isInitialized: false, + isStarted: false, + error: 'RLN context not initialized', + initializeRLN: async () => { throw new Error('RLN context not initialized'); }, + registerMembership: async () => ({ success: false, error: 'RLN context not initialized' }), + rateMinLimit: 20, + rateMaxLimit: 600 +}; + +// Create a context to store the selected RLN implementation +const UnifiedRLNContext = createContext(dummyRLNContext); + +// Create a provider component that will fetch the appropriate implementation +export function UnifiedRLNProvider({ children }: { children: ReactNode }) { + const { implementation } = useRLNImplementation(); + const [contextValue, setContextValue] = useState(dummyRLNContext); + + useEffect(() => { + // This effect will run when the implementation changes + // We'll dynamically import the appropriate context module + const fetchContext = async () => { + try { + if (implementation === 'standard') { + // Import the standard RLN context + const standardModule = await import('./RLNContext'); + const standardRLNContext = standardModule.RLNContext; + + if (standardRLNContext) { + // Access the context value + const contextConsumer = standardRLNContext.Consumer; + contextConsumer(value => { + if (value) { + setContextValue(value); + } + }); + } + } else { + // Import the light RLN context + const lightModule = await import('./RLNLightContext'); + const lightRLNContext = lightModule.RLNContext; + + if (lightRLNContext) { + // Access the context value + const contextConsumer = lightRLNContext.Consumer; + contextConsumer(value => { + if (value) { + setContextValue(value); + } + }); + } + } + } catch (error) { + console.error('Error loading RLN context:', error); + } + }; + + fetchContext(); + }, [implementation]); + + return ( + + {children} + + ); +} + +// Create a hook to use the unified RLN context +export function useRLN() { + return useContext(UnifiedRLNContext); +}