From c33ebfb8740f3cf1bc48778cae51580c51f230ea Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 3 Apr 2025 20:47:02 +0530 Subject: [PATCH] chore: cleanup and improvements --- .../keystore-management/package-lock.json | 56 +-- examples/keystore-management/package.json | 2 +- .../keystore-management/src/app/globals.css | 2 +- .../keystore-management/src/app/layout.tsx | 51 +-- examples/keystore-management/src/app/page.tsx | 57 +-- .../src/components/Header.tsx | 10 +- .../src/components/Layout.tsx | 51 +++ .../components/RLNImplementationToggle.tsx | 62 ++- .../components/RLNMembershipRegistration.tsx | 398 ------------------ .../src/components/RLNinitButton.tsx | 65 +++ .../Tabs/KeystoreTab/KeystoreManagement.tsx | 156 +++++++ .../MembershipTab/MembershipRegistration.tsx | 264 ++++++++++++ .../src/components/Tabs/TabNavigation.tsx | 40 ++ .../src/components/WalletInfo.tsx | 122 ++++-- .../src/contexts/AppStateContext.tsx | 62 +++ .../src/contexts/RLNUnifiedContext.tsx | 23 - .../src/contexts/RLNUnifiedHook.tsx | 93 ---- .../keystore-management/src/contexts/index.ts | 15 + .../{ => keystore}/KeystoreContext.tsx | 33 +- .../src/contexts/keystore/index.ts | 1 + .../RLNContext.tsx} | 289 +++++-------- .../{ => rln}/RLNImplementationContext.tsx | 9 +- .../implementations/factory.tsx} | 9 +- .../src/contexts/rln/implementations/index.ts | 3 + .../implementations/light.tsx} | 86 +--- .../implementations/standard.tsx} | 87 +--- .../src/contexts/rln/index.ts | 3 + .../src/contexts/rln/utils/network.ts | 68 +++ .../contexts/{ => wallet}/WalletContext.tsx | 0 .../src/contexts/wallet/index.ts | 1 + 30 files changed, 1040 insertions(+), 1078 deletions(-) create mode 100644 examples/keystore-management/src/components/Layout.tsx delete mode 100644 examples/keystore-management/src/components/RLNMembershipRegistration.tsx create mode 100644 examples/keystore-management/src/components/RLNinitButton.tsx create mode 100644 examples/keystore-management/src/components/Tabs/KeystoreTab/KeystoreManagement.tsx create mode 100644 examples/keystore-management/src/components/Tabs/MembershipTab/MembershipRegistration.tsx create mode 100644 examples/keystore-management/src/components/Tabs/TabNavigation.tsx create mode 100644 examples/keystore-management/src/contexts/AppStateContext.tsx delete mode 100644 examples/keystore-management/src/contexts/RLNUnifiedContext.tsx delete mode 100644 examples/keystore-management/src/contexts/RLNUnifiedHook.tsx create mode 100644 examples/keystore-management/src/contexts/index.ts rename examples/keystore-management/src/contexts/{ => keystore}/KeystoreContext.tsx (84%) create mode 100644 examples/keystore-management/src/contexts/keystore/index.ts rename examples/keystore-management/src/contexts/{RLNUnifiedContext2.tsx => rln/RLNContext.tsx} (56%) rename examples/keystore-management/src/contexts/{ => rln}/RLNImplementationContext.tsx (84%) rename examples/keystore-management/src/contexts/{RLNFactory.tsx => rln/implementations/factory.tsx} (79%) create mode 100644 examples/keystore-management/src/contexts/rln/implementations/index.ts rename examples/keystore-management/src/contexts/{RLNLightContext.tsx => rln/implementations/light.tsx} (73%) rename examples/keystore-management/src/contexts/{RLNZerokitContext.tsx => rln/implementations/standard.tsx} (72%) create mode 100644 examples/keystore-management/src/contexts/rln/index.ts create mode 100644 examples/keystore-management/src/contexts/rln/utils/network.ts rename examples/keystore-management/src/contexts/{ => wallet}/WalletContext.tsx (100%) create mode 100644 examples/keystore-management/src/contexts/wallet/index.ts diff --git a/examples/keystore-management/package-lock.json b/examples/keystore-management/package-lock.json index 9c1cff0..8c4f764 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.1.5-6198efb.0", + "@waku/rln": "0.1.5-ad0e277.0", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -2309,16 +2309,16 @@ } }, "node_modules/@waku/core": { - "version": "0.0.35-6198efb.0", - "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.35-6198efb.0.tgz", - "integrity": "sha512-9ta28XPCs9mOT9DmaewxHuZyWVME5bqRsF8Hardrd4QaHVLEaE5nlqi1nF7PoxdwF+l+XmZ2x/gqwgDee6qPwg==", + "version": "0.0.35-ad0e277.0", + "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.35-ad0e277.0.tgz", + "integrity": "sha512-IfgWE/Kc8jpcmO6PGsLzySHAWwoDQtIY9gsxWtoqPnll1cE9ylxfGVh0je4o58E5F0XGw+wm9TdD149hq30cJQ==", "license": "MIT OR Apache-2.0", "dependencies": { "@libp2p/ping": "2.0.1", - "@waku/enr": "0.0.29-6198efb.0", - "@waku/interfaces": "0.0.30-6198efb.0", - "@waku/proto": "0.0.10-6198efb.0", - "@waku/utils": "0.0.23-6198efb.0", + "@waku/enr": "0.0.29-ad0e277.0", + "@waku/interfaces": "0.0.30-ad0e277.0", + "@waku/proto": "0.0.10-ad0e277.0", + "@waku/utils": "0.0.23-ad0e277.0", "debug": "^4.3.4", "it-all": "^3.0.4", "it-length-prefixed": "^9.0.4", @@ -2356,9 +2356,9 @@ } }, "node_modules/@waku/enr": { - "version": "0.0.29-6198efb.0", - "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.29-6198efb.0.tgz", - "integrity": "sha512-vInEV3d2LV1fakhXIACYn4PBZwYLZNpKYqDMU1YJQ3yoQv6nHl0emeGz7GviyXh+2qyLFjEKzesqhLht41kP/A==", + "version": "0.0.29-ad0e277.0", + "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.29-ad0e277.0.tgz", + "integrity": "sha512-3IoFVU3XX7rEge3VFMl5r3wc0XVnZTvheRuFYSm0HgPrMls23PmAd7IdFLOMq5e9CGWAbWx0fdT3StzIdN2PbQ==", "license": "MIT OR Apache-2.0", "dependencies": { "@ethersproject/rlp": "^5.7.0", @@ -2366,7 +2366,7 @@ "@libp2p/peer-id": "^5.0.1", "@multiformats/multiaddr": "^12.0.0", "@noble/secp256k1": "^1.7.1", - "@waku/utils": "0.0.23-6198efb.0", + "@waku/utils": "0.0.23-ad0e277.0", "debug": "^4.3.4", "js-sha3": "^0.9.2" }, @@ -2383,21 +2383,21 @@ } }, "node_modules/@waku/interfaces": { - "version": "0.0.30-6198efb.0", - "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.30-6198efb.0.tgz", - "integrity": "sha512-K+DIqbSFct0/1RYg2QdpviXOps9mDo04quW7jvlUvKP9qVBDvDUBlZrvfw+kIHr77g0DzyYLganuLv/Ocd13rw==", + "version": "0.0.30-ad0e277.0", + "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.30-ad0e277.0.tgz", + "integrity": "sha512-Xg0vupz9y+PGNKVsJ3AodpcxWLkGafLPser4H/0SYvwiBKR0doEs08wfRdXY83WID1GZPVkA3jcIm0rAnPZWSw==", "license": "MIT OR Apache-2.0", "dependencies": { - "@waku/proto": "0.0.10-6198efb.0" + "@waku/proto": "0.0.10-ad0e277.0" }, "engines": { "node": ">=20" } }, "node_modules/@waku/proto": { - "version": "0.0.10-6198efb.0", - "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.10-6198efb.0.tgz", - "integrity": "sha512-PQktYd1wgQ9Ihdyf+u950ugJL3LUfLh0kEvEHlSLhD9fOsipaegPBR7EsK78cecpZP1hBxuOZwjr5coO3byWlg==", + "version": "0.0.10-ad0e277.0", + "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.10-ad0e277.0.tgz", + "integrity": "sha512-F4RKTcdX3v+E3/TxdINPWpsQEjst1VXlslJdNXC1LXPRqd87S7r2e47PXafM7TGBdOitIIPw8Xf4MxbPFGa1Gg==", "license": "MIT OR Apache-2.0", "dependencies": { "protons-runtime": "^5.4.0" @@ -2407,15 +2407,15 @@ } }, "node_modules/@waku/rln": { - "version": "0.1.5-6198efb.0", - "resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.5-6198efb.0.tgz", - "integrity": "sha512-v/x79fKH6M1E4qjpRWdGErSGt6p7LBLc9tt7G/ZFWJY8afBHbtOH3JrJ0ZqDc4oZsQbRRMGVLJ6WbJIDkQtoYA==", + "version": "0.1.5-ad0e277.0", + "resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.5-ad0e277.0.tgz", + "integrity": "sha512-2QxvEhZoZjsP0J8wg6BAv7GTvDMeOzD7a46WR6M3Gld39TNf+2hQMzED5I91QxjcoAvMMbphD8FiH4X5tqzM5g==", "license": "MIT OR Apache-2.0", "dependencies": { "@chainsafe/bls-keystore": "3.0.0", "@noble/hashes": "^1.2.0", - "@waku/core": "0.0.35-6198efb.0", - "@waku/utils": "0.0.23-6198efb.0", + "@waku/core": "0.0.35-ad0e277.0", + "@waku/utils": "0.0.23-ad0e277.0", "@waku/zerokit-rln-wasm": "^0.0.13", "chai": "^5.1.2", "chai-as-promised": "^8.0.1", @@ -2432,13 +2432,13 @@ } }, "node_modules/@waku/utils": { - "version": "0.0.23-6198efb.0", - "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.23-6198efb.0.tgz", - "integrity": "sha512-K7RNG8ngHYpyVwIHeXaIAi52FO1fJM/h4gurxII331segXrnSXSYtnv3jwO3HDnoTMX+o21ddM0VfyzxilM3NQ==", + "version": "0.0.23-ad0e277.0", + "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.23-ad0e277.0.tgz", + "integrity": "sha512-r3nef/L4fZx2GA7byUAOFLSjhVXuH22FFgmEp7+/jzbAjKAcVIaCfG2M09SnNDbxc5uViKeEE6ODIfw+k+2rZw==", "license": "MIT OR Apache-2.0", "dependencies": { "@noble/hashes": "^1.3.2", - "@waku/interfaces": "0.0.30-6198efb.0", + "@waku/interfaces": "0.0.30-ad0e277.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 018c6aa..0643827 100644 --- a/examples/keystore-management/package.json +++ b/examples/keystore-management/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@waku/rln": "0.1.5-6198efb.0", + "@waku/rln": "0.1.5-ad0e277.0", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/examples/keystore-management/src/app/globals.css b/examples/keystore-management/src/app/globals.css index 6b717ad..a45bba3 100644 --- a/examples/keystore-management/src/app/globals.css +++ b/examples/keystore-management/src/app/globals.css @@ -18,4 +18,4 @@ body { color: var(--foreground); background: var(--background); font-family: Arial, Helvetica, sans-serif; -} +} \ No newline at end of file diff --git a/examples/keystore-management/src/app/layout.tsx b/examples/keystore-management/src/app/layout.tsx index b9a21d5..c9f1b7b 100644 --- a/examples/keystore-management/src/app/layout.tsx +++ b/examples/keystore-management/src/app/layout.tsx @@ -1,19 +1,12 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Inter } from 'next/font/google' import "./globals.css"; -import { WalletProvider } from "../contexts/WalletContext"; -import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2"; -import { RLNImplementationProvider } from "../contexts/RLNImplementationContext"; -import { KeystoreProvider } from "../contexts/KeystoreContext"; +import { WalletProvider, RLNImplementationProvider, KeystoreProvider, RLNProvider } from "../contexts/index"; import { Header } from "../components/Header"; +import { AppStateProvider } from "../contexts/AppStateContext"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const inter = Inter({ + variable: "--font-inter", subsets: ["latin"], }); @@ -30,23 +23,25 @@ export default function RootLayout({ return ( - - - - -
-
-
- {children} -
-
-
-
-
-
+ + + + + +
+
+
+ {children} +
+
+
+
+
+
+
); -} +} \ No newline at end of file diff --git a/examples/keystore-management/src/app/page.tsx b/examples/keystore-management/src/app/page.tsx index d4b743f..7a945fd 100644 --- a/examples/keystore-management/src/app/page.tsx +++ b/examples/keystore-management/src/app/page.tsx @@ -1,50 +1,15 @@ -import RLNMembershipRegistration from '../components/RLNMembershipRegistration'; -import { WalletInfo } from '../components/WalletInfo'; -import { RLNImplementationToggle } from '../components/RLNImplementationToggle'; -import KeystoreManager from '../components/KeystoreManager'; +"use client"; + +import React from 'react'; +import { Layout } from '../components/Layout'; +import { MembershipRegistration } from '../components/Tabs/MembershipTab/MembershipRegistration'; +import { KeystoreManagement } from '../components/Tabs/KeystoreTab/KeystoreManagement'; export default function Home() { return ( -
-
-
-

Waku Keystore Management

- -
- {/* RLN Implementation Toggle */} -
-

RLN Implementation

- -
- - {/* Wallet Information Section */} -
-

Wallet Connection

- -
- - {/* RLN Membership Registration Section */} -
-

RLN Membership

-

- Register a new RLN membership to participate in Waku RLN Relay without exposing your private key on your node. - Set your desired rate limit for messages per epoch. -

- -
- - {/* Keystore Management Section */} -
-

Keystore Management

-

- Export your keystore credentials to use them with your Waku node or import existing credentials. - Keep your keystores safe as they contain your membership information. -

- -
-
-
-
-
+ + + + ); -} +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/Header.tsx b/examples/keystore-management/src/components/Header.tsx index daeec74..2e6d204 100644 --- a/examples/keystore-management/src/components/Header.tsx +++ b/examples/keystore-management/src/components/Header.tsx @@ -5,14 +5,12 @@ import { WalletInfo } from './WalletInfo'; export function Header() { return ( -
-
+
+
-

Waku Keystore Management

-
-
- +

Waku Keystore Management

+
); diff --git a/examples/keystore-management/src/components/Layout.tsx b/examples/keystore-management/src/components/Layout.tsx new file mode 100644 index 0000000..78b6fb0 --- /dev/null +++ b/examples/keystore-management/src/components/Layout.tsx @@ -0,0 +1,51 @@ +"use client"; + +import React, { Children, isValidElement } from 'react'; +import { TabNavigation, TabItem } from './Tabs/TabNavigation'; +import { useAppState } from '../contexts/AppStateContext'; + +const tabs: TabItem[] = [ + { + id: 'membership', + label: 'Membership Registration', + }, + { + id: 'keystore', + label: 'Keystore Management', + }, +]; + +const componentToTabId: Record = { + MembershipRegistration: 'membership', + KeystoreManagement: 'keystore', +}; + +export function Layout({ children }: { children: React.ReactNode }) { + const { activeTab, setActiveTab } = useAppState(); + const childrenArray = Children.toArray(children); + + return ( +
+
+ +
+ {childrenArray.map((child) => { + if (isValidElement(child) && typeof child.type === 'function') { + const componentName = child.type.name; + const tabId = componentToTabId[componentName]; + + if (tabId === activeTab) { + return child; + } + } + return null; + })} +
+
+
+ ); +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/RLNImplementationToggle.tsx b/examples/keystore-management/src/components/RLNImplementationToggle.tsx index 5c730d5..3552397 100644 --- a/examples/keystore-management/src/components/RLNImplementationToggle.tsx +++ b/examples/keystore-management/src/components/RLNImplementationToggle.tsx @@ -1,50 +1,44 @@ "use client"; -import { useRLNImplementation, RLNImplementationType } from '../contexts/RLNImplementationContext'; +import React from 'react'; +import { useRLNImplementation } from '../contexts/rln'; 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 - )} -
+

+ {implementation === 'light' + ? 'Light implementation, without Zerokit. Instant initalisation.' + : 'Standard implementation, with Zerokit. Initialisation takes 10-15 seconds for WASM module' + } +

); -} +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx deleted file mode 100644 index 57d2a15..0000000 --- a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx +++ /dev/null @@ -1,398 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { useRLN } from '../contexts/RLNUnifiedContext2'; -import { useWallet } from '../contexts/WalletContext'; -import { KeystoreEntity } from '@waku/rln'; - -export default function RLNMembershipRegistration() { - const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN(); - const { isConnected, address, chainId } = useWallet(); - - const [rateLimit, setRateLimit] = useState(rateMinLimit); - const [isRegistering, setIsRegistering] = useState(false); - const [isInitializing, setIsInitializing] = useState(false); - const [saveToKeystore, setSaveToKeystore] = useState(true); - const [keystorePassword, setKeystorePassword] = useState(''); - const [registrationResult, setRegistrationResult] = useState<{ - success?: boolean; - error?: string; - txHash?: string; - warning?: string; - credentials?: KeystoreEntity; - keystoreHash?: string; - }>({}); - - const isLineaSepolia = chainId === 59141; - - const handleRateLimitChange = (e: React.ChangeEvent) => { - const value = parseInt(e.target.value); - setRateLimit(isNaN(value) ? rateMinLimit : value); - }; - - const handleInitializeRLN = async () => { - setIsInitializing(true); - try { - await initializeRLN(); - } catch (err) { - console.error("Error initializing RLN:", err); - } finally { - setIsInitializing(false); - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!isConnected) { - setRegistrationResult({ success: false, error: 'Please connect your wallet first' }); - return; - } - - if (!isInitialized || !isStarted) { - setRegistrationResult({ success: false, error: 'RLN is not initialized' }); - return; - } - - if (!isLineaSepolia) { - setRegistrationResult({ success: false, error: 'Please switch to Linea Sepolia network' }); - return; - } - - // Validate keystore password if saving to keystore - if (saveToKeystore && keystorePassword.length < 8) { - setRegistrationResult({ - success: false, - error: 'Keystore password must be at least 8 characters long' - }); - return; - } - - setIsRegistering(true); - setRegistrationResult({}); - - try { - setRegistrationResult({ - success: undefined, - warning: 'Please check your wallet to sign the registration message.' - }); - - // Pass save options if saving to keystore - const saveOptions = saveToKeystore ? { password: keystorePassword } : undefined; - - const result = await registerMembership(rateLimit, saveOptions); - - setRegistrationResult({ - ...result, - credentials: result.credentials - }); - - // Clear password field after successful registration - if (result.success) { - setKeystorePassword(''); - } - } catch (error) { - setRegistrationResult({ - success: false, - error: error instanceof Error ? error.message : 'Registration failed' - }); - } finally { - setIsRegistering(false); - } - }; - - return ( -
-

- RLN Membership Registration -

- - {/* Network Warning */} - {isConnected && !isLineaSepolia && ( -
-

- Warning: You are not connected to Linea Sepolia network. Please switch networks to register. -

-
- )} - - {/* Informational Box */} -
-

- About RLN Membership on Linea Sepolia -

-

- RLN (Rate Limiting Nullifier) membership allows you to participate in Waku RLN Relay with rate limiting protection, - without exposing your private keys on your node. -

-

- This application is configured to use the Linea Sepolia testnet for RLN registrations. -

-

- When you register, your wallet will sign a message that will be used to generate a cryptographic identity - for your membership. This allows your node to prove it has permission to send messages without revealing your identity. -

-
- - {/* Initialization Status */} -
-
-
-

- RLN Status: - - {isInitialized && isStarted ? "Ready" : "Not Initialized"} - -

-
- {isConnected && (!isInitialized || !isStarted) && ( - - )} -
- {error && ( -

{error}

- )} -
- - {isInitialized && !isStarted && ( -
-

- Note: RLN is partially initialized. You can still proceed with registration, but some advanced features might be limited. -

-
- )} - - {!isConnected ? ( -
- Please connect your wallet to register a membership -
- ) : !isInitialized || !isStarted ? ( -
- Please initialize RLN before registering a membership -
- ) : ( - <> -
-
- -
- - {rateLimit} -
-

- Select a rate limit between {rateMinLimit} and {rateMaxLimit} -

-
- - {/* Keystore Options */} -
-
- setSaveToKeystore(e.target.checked)} - className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" - /> - -
- - {saveToKeystore && ( -
- - setKeystorePassword(e.target.value)} - placeholder="Enter password to encrypt your keystore" - className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" - minLength={8} - /> -

- The password will be used to encrypt your RLN credentials in the keystore. - You will need this password to decrypt your credentials later. -

-
- )} -
- - {address && ( -
-

Registration Details:

-

Connected Address: {address.slice(0, 8)}...{address.slice(-6)}

-

When you register, your wallet will sign a secure message containing a random nonce. This signature will be used to generate your RLN credentials without exposing your private key.

-
- )} - - -
- - {registrationResult.warning && registrationResult.success === undefined && ( -
-

Important:

-

{registrationResult.warning}

-
- You'll need to sign a message with your wallet to complete the registration. -
-
- )} - - {registrationResult.success === true && ( -
-

Registration submitted!

- {registrationResult.txHash && ( -
-

- {registrationResult.txHash} -

- {registrationResult.txHash.startsWith('0x') && ( -

- - View on Linea Sepolia Explorer - -

- )} -
- )} - {registrationResult.warning && ( -

- Note: {registrationResult.warning} -

- )} -

- Your RLN membership is now registered and can be used with your Waku node. -

- - {registrationResult.credentials && ( - <>
-

Your RLN Credentials:

-
-

Identity:

-

- ID Commitment: {Buffer.from(registrationResult.credentials.identity.IDCommitment).toString('hex')} -

-

- ID Secret Hash: {Buffer.from(registrationResult.credentials.identity.IDSecretHash).toString('hex')} -

-

- ID Nullifier: {Buffer.from(registrationResult.credentials.identity.IDNullifier).toString('hex')} -

-

- ID Trapdoor: {Buffer.from(registrationResult.credentials.identity.IDTrapdoor).toString('hex')} -

- -

Membership:

-

- Chain ID: {registrationResult.credentials.membership.chainId} -

-

- Contract Address: {registrationResult.credentials.membership.address} -

-

- Tree Index: {registrationResult.credentials.membership.treeIndex} -

-

- Rate Limit: {registrationResult.credentials.membership.rateLimit} -

-
-

- These credentials are your proof of membership. Store them securely. -

-
- - )} - {registrationResult.keystoreHash && ( -

- Credentials saved to keystore! -
- Hash: {registrationResult.keystoreHash.slice(0, 10)}...{registrationResult.keystoreHash.slice(-8)} -

- )} -
- )} - - {registrationResult.success === false && ( -
-

Registration failed

-

{registrationResult.error}

- {registrationResult.error?.includes("field") && ( -
-

- This is a mathematical constraint in the zero-knowledge proof system. - Your wallet's signatures produce values that aren't compatible with the RLN cryptographic system. -

-

Recommended solution:

-

Please try using a different wallet address for registration. Different wallet addresses - generate different signatures, and some are more compatible with the RLN cryptographic system.

-
- )} -
- )} - - )} - - {/* Debug Info (For Development) */} -
-

Debug Info:

-

Wallet Connected: {isConnected ? "Yes" : "No"}

-

RLN Initialized: {isInitialized ? "Yes" : "No"}

-

RLN Started: {isStarted ? "Yes" : "No"}

-

Min Rate: {rateMinLimit}, Max Rate: {rateMaxLimit}

-

Current Rate Limit: {rateLimit}

-
-
- ); -} \ No newline at end of file diff --git a/examples/keystore-management/src/components/RLNinitButton.tsx b/examples/keystore-management/src/components/RLNinitButton.tsx new file mode 100644 index 0000000..c918a05 --- /dev/null +++ b/examples/keystore-management/src/components/RLNinitButton.tsx @@ -0,0 +1,65 @@ +"use client"; + +import React from 'react'; +import { useRLN } from '../contexts/rln'; + +export function RLNInitButton() { + const { initializeRLN, isInitialized, isStarted, error, isLoading } = useRLN(); + + const handleInitialize = async () => { + try { + await initializeRLN(); + } catch (err) { + console.error('Error initializing RLN:', err); + } + }; + + const getButtonText = () => { + if (isLoading) return 'Initializing...'; + if (isInitialized && isStarted) return 'RLN Initialized'; + return 'Initialize RLN'; + }; + + return ( +
+ + {error && ( +

+ {error} +

+ )} +
+ ); +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/Tabs/KeystoreTab/KeystoreManagement.tsx b/examples/keystore-management/src/components/Tabs/KeystoreTab/KeystoreManagement.tsx new file mode 100644 index 0000000..99849d4 --- /dev/null +++ b/examples/keystore-management/src/components/Tabs/KeystoreTab/KeystoreManagement.tsx @@ -0,0 +1,156 @@ +"use client"; + +import React, { useState } from 'react'; +import { useKeystore } from '../../../contexts/keystore'; +import { useAppState } from '../../../contexts/AppStateContext'; +import { useRLN } from '../../../contexts/rln'; +import { saveKeystoreToFile, readKeystoreFromFile } from '../../../utils/fileUtils'; + +export function KeystoreManagement() { + const { + hasStoredCredentials, + storedCredentialsHashes, + error, + exportCredential, + importKeystore, + removeCredential + } = useKeystore(); + const { setGlobalError } = useAppState(); + const { isInitialized, isStarted } = useRLN(); + const [exportPassword, setExportPassword] = useState(''); + const [selectedCredential, setSelectedCredential] = useState(null); + + React.useEffect(() => { + if (error) { + setGlobalError(error); + } + }, [error, setGlobalError]); + + const handleExportCredential = async (hash: string) => { + try { + if (!exportPassword) { + setGlobalError('Please enter your keystore password to export'); + return; + } + const keystoreJson = await exportCredential(hash, exportPassword); + saveKeystoreToFile(keystoreJson, `waku-rln-credential-${hash.slice(0, 8)}.json`); + setExportPassword(''); + setSelectedCredential(null); + } catch (err) { + setGlobalError(err instanceof Error ? err.message : 'Failed to export credential'); + } + }; + + const handleImport = async () => { + try { + const keystoreJson = await readKeystoreFromFile(); + const success = importKeystore(keystoreJson); + if (!success) { + setGlobalError('Failed to import keystore'); + } + } catch (err) { + setGlobalError(err instanceof Error ? err.message : 'Failed to import keystore'); + } + }; + + const handleRemoveCredential = (hash: string) => { + try { + removeCredential(hash); + } catch (err) { + setGlobalError(err instanceof Error ? err.message : 'Failed to remove credential'); + } + }; + + return ( +
+
+

+ Keystore Management +

+ +
+ {/* Import Action */} +
+ +
+ + {/* RLN Status */} + {!isInitialized || !isStarted ? ( +
+

+ ⚠️ Please initialize RLN before managing credentials +

+
+ ) : null} + + {/* Stored Credentials */} +
+

+ Stored Credentials +

+ + {hasStoredCredentials ? ( +
+ {storedCredentialsHashes.map((hash) => ( +
+
+
+ + {hash} + +
+ + +
+
+ + {selectedCredential === hash && ( +
+ setExportPassword(e.target.value)} + placeholder="Enter keystore password" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + +
+ )} +
+
+ ))} +
+ ) : ( +

+ No credentials stored +

+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/Tabs/MembershipTab/MembershipRegistration.tsx b/examples/keystore-management/src/components/Tabs/MembershipTab/MembershipRegistration.tsx new file mode 100644 index 0000000..9ad31de --- /dev/null +++ b/examples/keystore-management/src/components/Tabs/MembershipTab/MembershipRegistration.tsx @@ -0,0 +1,264 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { RLNImplementationToggle } from '../../RLNImplementationToggle'; +import { KeystoreEntity } from '@waku/rln'; +import { useAppState } from '@/contexts/AppStateContext'; +import { useRLN } from '@/contexts/rln/RLNContext'; +import { useWallet } from '@/contexts/wallet'; +import { RLNInitButton } from '@/components/RLNinitButton'; + +export function MembershipRegistration() { + const { setGlobalError } = useAppState(); + const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error } = useRLN(); + const { isConnected, chainId } = useWallet(); + + const [rateLimit, setRateLimit] = useState(rateMinLimit); + const [isRegistering, setIsRegistering] = useState(false); + const [saveToKeystore, setSaveToKeystore] = useState(true); + const [keystorePassword, setKeystorePassword] = useState(''); + const [registrationResult, setRegistrationResult] = useState<{ + success?: boolean; + error?: string; + txHash?: string; + warning?: string; + credentials?: KeystoreEntity; + keystoreHash?: string; + }>({}); + + const isLineaSepolia = chainId === 59141; + + useEffect(() => { + if (error) { + setGlobalError(error); + } + }, [error, setGlobalError]); + + const handleRateLimitChange = (e: React.ChangeEvent) => { + const value = parseInt(e.target.value); + setRateLimit(isNaN(value) ? rateMinLimit : value); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!isConnected) { + setRegistrationResult({ success: false, error: 'Please connect your wallet first' }); + return; + } + + if (!isInitialized || !isStarted) { + setRegistrationResult({ success: false, error: 'RLN is not initialized' }); + return; + } + + if (!isLineaSepolia) { + setRegistrationResult({ success: false, error: 'Please switch to Linea Sepolia network' }); + return; + } + + // Validate keystore password if saving to keystore + if (saveToKeystore && keystorePassword.length < 8) { + setRegistrationResult({ + success: false, + error: 'Keystore password must be at least 8 characters long' + }); + return; + } + + setIsRegistering(true); + setRegistrationResult({}); + + try { + setRegistrationResult({ + success: undefined, + warning: 'Please check your wallet to sign the registration message.' + }); + + // Pass save options if saving to keystore + const saveOptions = saveToKeystore ? { password: keystorePassword } : undefined; + + const result = await registerMembership(rateLimit, saveOptions); + + setRegistrationResult({ + ...result, + credentials: result.credentials + }); + + // Clear password field after successful registration + if (result.success) { + setKeystorePassword(''); + } + } catch (error) { + setRegistrationResult({ + success: false, + error: error instanceof Error ? error.message : 'Registration failed' + }); + } finally { + setIsRegistering(false); + } + }; + + return ( +
+
+

+ RLN Membership Registration +

+
+
+ +
+ + {/* Network Warning */} + {isConnected && !isLineaSepolia && ( +
+

+ Warning: You are not connected to Linea Sepolia network. Please switch networks to register. +

+
+ )} + + {/* Informational Box */} +
+

+ About RLN Membership on Linea Sepolia +

+

+ RLN (Rate Limiting Nullifier) membership allows you to participate in Waku RLN Relay with rate limiting protection, + without exposing your private keys on your node. +

+

+ This application is configured to use the Linea Sepolia testnet for RLN registrations. +

+

+ When you register, your wallet will sign a message that will be used to generate a cryptographic identity + for your membership. This allows your node to prove it has permission to send messages without revealing your identity. +

+
+ +
+ +
+ + {!isConnected ? ( +
+ Please connect your wallet to register a membership +
+ ) : !isInitialized || !isStarted ? ( +
+ Please initialize RLN before registering a membership +
+ ) : ( +
+
+ +
+ + + {rateLimit} + +
+
+ +
+
+ setSaveToKeystore(e.target.checked)} + className="h-4 w-4 text-blue-600 rounded border-gray-300" + /> + +
+ {saveToKeystore && ( +
+ + setKeystorePassword(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + placeholder="Enter password to encrypt credentials" + /> +
+ )} +
+ + +
+ )} + + {/* Registration Result */} + {registrationResult.warning && ( +
+

+ {registrationResult.warning} +

+
+ )} + {registrationResult.error && ( +
+

+ {registrationResult.error} +

+
+ )} + {registrationResult.success && ( +
+

+ ✓ Membership registered successfully! +

+ {registrationResult.txHash && ( +

+ Transaction Hash: {registrationResult.txHash} +

+ )} + {registrationResult.keystoreHash && ( +

+ Credentials saved to keystore with hash: {registrationResult.keystoreHash} +

+ )} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/Tabs/TabNavigation.tsx b/examples/keystore-management/src/components/Tabs/TabNavigation.tsx new file mode 100644 index 0000000..ec45504 --- /dev/null +++ b/examples/keystore-management/src/components/Tabs/TabNavigation.tsx @@ -0,0 +1,40 @@ +"use client"; + +import React from 'react'; + +export type TabItem = { + id: string; + label: string; +}; + +interface TabNavigationProps { + tabs: TabItem[]; + activeTab: string; + onTabChange: (tabId: string) => void; +} + +export function TabNavigation({ tabs, activeTab, onTabChange }: TabNavigationProps) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/WalletInfo.tsx b/examples/keystore-management/src/components/WalletInfo.tsx index 3b81d89..2b1a443 100644 --- a/examples/keystore-management/src/components/WalletInfo.tsx +++ b/examples/keystore-management/src/components/WalletInfo.tsx @@ -1,7 +1,7 @@ "use client"; -import React from 'react'; -import { useWallet } from '../contexts/WalletContext'; +import React, { useState, useRef, useEffect } from 'react'; +import { useWallet } from '../contexts/index'; function getNetworkName(chainId: number | null): string { if (!chainId) return 'Unknown'; @@ -23,6 +23,20 @@ interface ProviderRpcError extends Error { export function WalletInfo() { const { isConnected, address, balance, chainId, connectWallet, disconnectWallet, error } = useWallet(); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); // Function to switch to Linea Sepolia network const switchToLineaSepolia = async () => { @@ -32,13 +46,11 @@ export function WalletInfo() { } try { - // Try to switch to Linea Sepolia await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: LINEA_SEPOLIA_CHAIN_ID }], }); } catch (err) { - // If the error code is 4902, the chain hasn't been added to MetaMask const providerError = err as ProviderRpcError; if (providerError.code === 4902) { try { @@ -70,64 +82,90 @@ export function WalletInfo() { // Check if user is on unsupported network const isUnsupportedNetwork = isConnected && chainId !== 59141; + if (!isConnected) { + return ( +
+ +
+ ); + } + return ( -
+
{error && ( -
+

{error}

)} - {isConnected ? ( -
-
-
-
- Address: - - {address} - + + + {isOpen && ( +
+
+
+
+ Address: + {address}
-
- Network: - +
+ Network: + {getNetworkName(chainId)}
-
- Balance: - +
+ Balance: + {balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}
+
+ +
+ {isUnsupportedNetwork && ( + + )}
- - {isUnsupportedNetwork && ( -
- -
- )} -
- ) : ( -
-
)}
diff --git a/examples/keystore-management/src/contexts/AppStateContext.tsx b/examples/keystore-management/src/contexts/AppStateContext.tsx new file mode 100644 index 0000000..1f0b25d --- /dev/null +++ b/examples/keystore-management/src/contexts/AppStateContext.tsx @@ -0,0 +1,62 @@ +"use client"; + +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +interface AppState { + isLoading: boolean; + globalError: string | null; + setIsLoading: (loading: boolean) => void; + setGlobalError: (error: string | null) => void; + activeTab: string; + setActiveTab: (tab: string) => void; +} + +const AppStateContext = createContext(undefined); + +export function AppStateProvider({ children }: { children: ReactNode }) { + const [isLoading, setIsLoading] = useState(false); + const [globalError, setGlobalError] = useState(null); + const [activeTab, setActiveTab] = useState('membership'); + + const value = { + isLoading, + setIsLoading, + globalError, + setGlobalError, + activeTab, + setActiveTab, + }; + + return ( + + {children} + {isLoading && ( +
+
+
+ Loading... +
+
+ )} + {globalError && ( +
+ {globalError} + +
+ )} +
+ ); +} + +export function useAppState() { + const context = useContext(AppStateContext); + if (context === undefined) { + throw new Error('useAppState must be used within an AppStateProvider'); + } + 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 deleted file mode 100644 index 6c96150..0000000 --- a/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import { ReactNode } from 'react'; -import { RLNProvider as StandardRLNProvider } from './RLNZerokitContext'; -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/RLNUnifiedHook.tsx b/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx deleted file mode 100644 index 7449120..0000000 --- a/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"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 hook - const standardModule = await import('./RLNZerokitContext'); - const { useRLN: useStandardRLN } = standardModule; - - // Create a temporary component to access the context - function TempComponent() { - const context = useStandardRLN(); - setContextValue(context); - return null; - } - - // Render the component within the provider - const { RLNProvider } = standardModule; - return ; - } else { - // Import the light RLN hook - const lightModule = await import('./RLNLightContext'); - const { useRLN: useLightRLN } = lightModule; - - // Create a temporary component to access the context - function TempComponent() { - const context = useLightRLN(); - setContextValue(context); - return null; - } - - // Render the component within the provider - const { RLNProvider } = lightModule; - return ; - } - } 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); -} diff --git a/examples/keystore-management/src/contexts/index.ts b/examples/keystore-management/src/contexts/index.ts new file mode 100644 index 0000000..cac44a5 --- /dev/null +++ b/examples/keystore-management/src/contexts/index.ts @@ -0,0 +1,15 @@ +// Re-export wallet context +export { WalletProvider, useWallet } from './wallet'; + +// Re-export keystore context +export { KeystoreProvider, useKeystore } from './keystore'; + +// Re-export RLN contexts +export { + RLNImplementationProvider, + useRLNImplementation, + type RLNImplementationType, + RLNProvider, + type UnifiedRLNInstance, + useRLN +} from './rln'; \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/KeystoreContext.tsx b/examples/keystore-management/src/contexts/keystore/KeystoreContext.tsx similarity index 84% rename from examples/keystore-management/src/contexts/KeystoreContext.tsx rename to examples/keystore-management/src/contexts/keystore/KeystoreContext.tsx index 98746e4..0fdd442 100644 --- a/examples/keystore-management/src/contexts/KeystoreContext.tsx +++ b/examples/keystore-management/src/contexts/keystore/KeystoreContext.tsx @@ -11,8 +11,7 @@ interface KeystoreContextType { hasStoredCredentials: boolean; storedCredentialsHashes: string[]; saveCredentials: (credentials: KeystoreEntity, password: string) => Promise; - loadCredential: (hash: string, password: string) => Promise; - exportKeystore: () => string; + exportCredential: (hash: string, password: string) => Promise; importKeystore: (keystoreJson: string) => boolean; removeCredential: (hash: string) => void; } @@ -83,25 +82,24 @@ export function KeystoreProvider({ children }: { children: ReactNode }) { } }; - const loadCredential = async (hash: string, password: string): Promise => { + const exportCredential = async (hash: string, password: string): Promise => { if (!keystore) { throw new Error("Keystore not initialized"); } - try { - return await keystore.readCredential(hash, password); - } catch (err) { - console.error("Error loading credential:", err); - throw err; + // Create a new keystore instance for the single credential + const singleCredentialKeystore = Keystore.create(); + + // Get the credential from the main keystore + const credential = await keystore.readCredential(hash, password); + if (!credential) { + throw new Error("Credential not found"); } - }; - - const exportKeystore = (): string => { - if (!keystore) { - throw new Error("Keystore not initialized"); - } - - return keystore.toString(); + + // Add the credential to the new keystore + await singleCredentialKeystore.addCredential(credential, password); + console.log("Single credential keystore:", singleCredentialKeystore.toString()); + return singleCredentialKeystore.toString(); }; const importKeystore = (keystoreJson: string): boolean => { @@ -138,8 +136,7 @@ export function KeystoreProvider({ children }: { children: ReactNode }) { hasStoredCredentials: storedCredentialsHashes.length > 0, storedCredentialsHashes, saveCredentials, - loadCredential, - exportKeystore, + exportCredential, importKeystore, removeCredential }; diff --git a/examples/keystore-management/src/contexts/keystore/index.ts b/examples/keystore-management/src/contexts/keystore/index.ts new file mode 100644 index 0000000..8e3cc3e --- /dev/null +++ b/examples/keystore-management/src/contexts/keystore/index.ts @@ -0,0 +1 @@ +export { KeystoreProvider, useKeystore } from './KeystoreContext'; \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx b/examples/keystore-management/src/contexts/rln/RLNContext.tsx similarity index 56% rename from examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx rename to examples/keystore-management/src/contexts/rln/RLNContext.tsx index c7e2141..037a325 100644 --- a/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx +++ b/examples/keystore-management/src/contexts/rln/RLNContext.tsx @@ -1,27 +1,13 @@ "use client"; -import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; import { KeystoreEntity } from '@waku/rln'; -import { UnifiedRLNInstance } from './RLNFactory'; +import { createRLNImplementation, UnifiedRLNInstance } from './implementations'; import { useRLNImplementation } from './RLNImplementationContext'; -import { createRLNImplementation } from './RLNFactory'; import { ethers } from 'ethers'; -import { useKeystore } from './KeystoreContext'; +import { useKeystore } from '../keystore'; +import { ERC20_ABI, LINEA_SEPOLIA_CONFIG, ensureLineaSepoliaNetwork } from './utils/network'; -// 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; @@ -39,18 +25,18 @@ interface RLNContextType { getCurrentRateLimit: () => Promise; getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>; saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise; + isLoading: boolean; } -// Create the context -const RLNUnifiedContext = createContext(undefined); +const RLNContext = createContext(undefined); -// Create the provider component -export function RLNUnifiedProvider({ children }: { children: ReactNode }) { +export function RLNProvider({ 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 [isLoading, setIsLoading] = useState(false); // Get the signer from window.ethereum const [signer, setSigner] = useState(null); @@ -109,64 +95,17 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { 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 () => { + const initializeRLN = useCallback(async () => { console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer); try { setError(null); + setIsLoading(true); 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); @@ -182,7 +121,6 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { 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 { @@ -215,8 +153,18 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { } catch (err) { console.error('Error in initializeRLN:', err); setError(err instanceof Error ? err.message : 'Failed to initialize RLN'); + } finally { + setIsLoading(false); } - }; + }, [isConnected, signer, implementation, rln, isStarted]); + + // Auto-initialize effect for Light implementation + useEffect(() => { + if (implementation === 'light' && isConnected && signer && !isInitialized && !isStarted && !isLoading) { + console.log('Auto-initializing Light RLN implementation...'); + initializeRLN(); + } + }, [implementation, isConnected, signer, isInitialized, isStarted, isLoading, initializeRLN]); const getCurrentRateLimit = async (): Promise => { try { @@ -251,23 +199,21 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { setRateMinLimit(minLimit); setRateMaxLimit(maxLimit); - return { - success: true, - rateMinLimit: minLimit, - rateMaxLimit: maxLimit + return { + success: true, + rateMinLimit: minLimit, + rateMaxLimit: maxLimit }; - } catch (error) { - console.error("Error getting rate limits bounds:", error); + } catch (err) { return { success: false, - rateMinLimit: 0, - rateMaxLimit: 0, - error: 'Failed to get rate limits bounds' + rateMinLimit: rateMinLimit, + rateMaxLimit: rateMaxLimit, + error: err instanceof Error ? err.message : 'Failed to get rate limits' }; } - } + }; - // Save credentials to keystore const saveCredentialsToKeystore = async (credentials: KeystoreEntity, password: string): Promise => { try { return await saveToKeystore(credentials, password); @@ -277,13 +223,7 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { } }; - // Update registerMembership to optionally save credentials to keystore - const registerMembership = async (rateLimit: number, saveOptions?: { password: string }): Promise<{ - success: boolean; - error?: string; - credentials?: KeystoreEntity; - keystoreHash?: string; - }> => { + const registerMembership = async (rateLimit: number, saveOptions?: { password: string }) => { console.log("registerMembership called with rate limit:", rateLimit); if (!rln || !isStarted) { @@ -295,13 +235,6 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { } try { - console.log("im here") - const rateMinLimit = await rln.contract.getMinRateLimit(); - const rateMaxLimit = await rln.contract.getMaxRateLimit(); - console.log({ - rateMinLimit, - rateMaxLimit - }) // Validate rate limit if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) { return { @@ -309,11 +242,10 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}` }; } - rln.contract.setRateLimit(rateLimit); - console.log("Rate limit set to:", rateLimit); + await rln.contract.setRateLimit(rateLimit); // Ensure we're on the correct network - const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); + const isOnLineaSepolia = await ensureLineaSepoliaNetwork(signer); if (!isOnLineaSepolia) { console.warn("Could not switch to Linea Sepolia network. Registration may fail."); } @@ -343,111 +275,98 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { // 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..."); + 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, 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." }; + 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"); } - // Register membership - console.log("Registering membership with rate limit:", rateLimit); + // Generate signature for identity + const timestamp = Date.now(); + const message = `Sign this message to generate your RLN credentials ${timestamp}`; + const signature = await signer.signMessage(message); - 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"); + // Register membership + console.log("Registering membership..."); + const credentials = await rln.registerMembership({ + signature: signature + }); + console.log("Credentials:", credentials); + + // If we have save options, save to keystore + let keystoreHash: string | undefined; + if (saveOptions && saveOptions.password && credentials) { + try { + const credentialsEntity = credentials as KeystoreEntity; + keystoreHash = await saveCredentialsToKeystore(credentialsEntity, saveOptions.password); + console.log("Credentials saved to keystore with hash:", keystoreHash); + } catch (saveErr) { + console.error("Error saving credentials to keystore:", saveErr); + // Continue without failing the overall registration } - 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"); - - // If saveOptions provided, save to keystore - let keystoreHash: string | undefined; - if (saveOptions?.password) { - try { - keystoreHash = await saveCredentialsToKeystore(credentials, saveOptions.password); - console.log("Credentials saved to keystore with hash:", keystoreHash); - } catch (keystoreErr) { - console.warn("Could not save credentials to keystore:", keystoreErr); - } - } - - return { - success: true, - credentials, - keystoreHash - }; - } 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" + success: true, + credentials: credentials as KeystoreEntity, + keystoreHash }; + } catch (err) { + console.error("Error registering membership:", err); + + let errorMsg = "Failed to register membership"; + if (err instanceof Error) { + errorMsg = err.message; + } + + return { success: false, error: errorMsg }; } }; - // Create the context value - const contextValue: RLNContextType = { - rln, - isInitialized, - isStarted, - error, - initializeRLN, - registerMembership, - getCurrentRateLimit, - getRateLimitsBounds, - rateMinLimit, - rateMaxLimit, - saveCredentialsToKeystore, - }; - return ( - + {children} - + ); } -// Create a hook to use the context export function useRLN() { - const context = useContext(RLNUnifiedContext); + const context = useContext(RLNContext); if (context === undefined) { - throw new Error('useRLN must be used within a RLNUnifiedProvider'); + throw new Error('useRLN must be used within a RLNProvider'); } return context; -} - +} \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/RLNImplementationContext.tsx b/examples/keystore-management/src/contexts/rln/RLNImplementationContext.tsx similarity index 84% rename from examples/keystore-management/src/contexts/RLNImplementationContext.tsx rename to examples/keystore-management/src/contexts/rln/RLNImplementationContext.tsx index 8f1c367..222181f 100644 --- a/examples/keystore-management/src/contexts/RLNImplementationContext.tsx +++ b/examples/keystore-management/src/contexts/rln/RLNImplementationContext.tsx @@ -2,21 +2,17 @@ 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'); + const [implementation, setImplementation] = useState('light'); return ( @@ -25,11 +21,10 @@ export function RLNImplementationProvider({ children }: { children: ReactNode }) ); } -// 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; -} +} \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/RLNFactory.tsx b/examples/keystore-management/src/contexts/rln/implementations/factory.tsx similarity index 79% rename from examples/keystore-management/src/contexts/RLNFactory.tsx rename to examples/keystore-management/src/contexts/rln/implementations/factory.tsx index 48a689f..9423c59 100644 --- a/examples/keystore-management/src/contexts/RLNFactory.tsx +++ b/examples/keystore-management/src/contexts/rln/implementations/factory.tsx @@ -3,7 +3,6 @@ import { createRLN, MembershipInfo, RLNLightInstance } from '@waku/rln'; import { ethers } from 'ethers'; -// Define a unified interface that both implementations must support export interface UnifiedRLNInstance { contract: { address: string; @@ -22,17 +21,13 @@ export interface UnifiedRLNInstance { registerMembership: (idCommitment: string, rateLimit?: number) => 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 { +export async function createRLNImplementation(type: 'standard' | 'light' = '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; } -} +} \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/rln/implementations/index.ts b/examples/keystore-management/src/contexts/rln/implementations/index.ts new file mode 100644 index 0000000..0a033cd --- /dev/null +++ b/examples/keystore-management/src/contexts/rln/implementations/index.ts @@ -0,0 +1,3 @@ +export { RLNProvider as StandardRLNProvider, useRLN as useStandardRLN } from './standard'; +export { RLNProvider as LightRLNProvider, useRLN as useLightRLN } from './light'; +export { createRLNImplementation, type UnifiedRLNInstance } from './factory'; \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/RLNLightContext.tsx b/examples/keystore-management/src/contexts/rln/implementations/light.tsx similarity index 73% rename from examples/keystore-management/src/contexts/RLNLightContext.tsx rename to examples/keystore-management/src/contexts/rln/implementations/light.tsx index bd83846..b95c9ee 100644 --- a/examples/keystore-management/src/contexts/RLNLightContext.tsx +++ b/examples/keystore-management/src/contexts/rln/implementations/light.tsx @@ -1,23 +1,10 @@ "use client"; import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; -import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln'; -import { useWallet } from './WalletContext'; +import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln'; +import { useWallet } from '../../wallet'; 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' -}; +import { ensureLineaSepoliaNetwork, ERC20_ABI, SIGNATURE_MESSAGE } from '../utils/network'; interface RLNContextType { rln: RLNLightInstance | RLNInstance | null; @@ -41,53 +28,6 @@ export function RLNProvider({ children }: { children: ReactNode }) { const [rateMinLimit, setRateMinLimit] = useState(0); const [rateMaxLimit, setRateMaxLimit] = useState(0); - 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 = useCallback(async () => { console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer); @@ -172,7 +112,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { await rln.contract?.setRateLimit(rateLimit); // Ensure we're on the correct network - const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); + const isOnLineaSepolia = await ensureLineaSepoliaNetwork(signer); if (!isOnLineaSepolia) { console.warn("Could not switch to Linea Sepolia network. Registration may fail."); } @@ -185,7 +125,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { } const contractAddress = rln.contract.address; - const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress; + const tokenAddress = '0x185A0015aC462a0aECb81beCc0497b649a64B9ea'; // Linea Sepolia token address // Create token contract instance const tokenContract = new ethers.Contract( @@ -258,20 +198,8 @@ export function RLNProvider({ children }: { children: ReactNode }) { 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"); } - }, [initializeRLN, isConnected, signer]); - - // Debug log for state changes - useEffect(() => { - console.log("RLN Context state:", { - isInitialized, - isStarted, - hasRln: !!rln, - error - }); - }, [isInitialized, isStarted, rln, error]); + }, [isConnected, signer, initializeRLN]); return ( => { - try { - console.log("Current network: unknown", await signer?.getChainId()); - - // Check if already on Linea Sepolia - if (await signer?.getChainId() === LINEA_CONTRACT.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_CONTRACT.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); @@ -163,7 +107,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { rln.contract?.setRateLimit(rateLimit); // Ensure we're on the correct network - const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); + const isOnLineaSepolia = await ensureLineaSepoliaNetwork(signer); if (!isOnLineaSepolia) { console.warn("Could not switch to Linea Sepolia network. Registration may fail."); } @@ -243,27 +187,6 @@ export function RLNProvider({ children }: { children: ReactNode }) { } }; - // 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 ( Promise; +} + +// Function to ensure the wallet is connected to Linea Sepolia network +export const ensureLineaSepoliaNetwork = async (signer?: ethers.Signer): 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..."); + + // 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; + } +}; + +// ERC20 ABI for token operations +export 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)" +]; + +// Message for signing to generate identity +export const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials"; \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/WalletContext.tsx b/examples/keystore-management/src/contexts/wallet/WalletContext.tsx similarity index 100% rename from examples/keystore-management/src/contexts/WalletContext.tsx rename to examples/keystore-management/src/contexts/wallet/WalletContext.tsx diff --git a/examples/keystore-management/src/contexts/wallet/index.ts b/examples/keystore-management/src/contexts/wallet/index.ts new file mode 100644 index 0000000..7419613 --- /dev/null +++ b/examples/keystore-management/src/contexts/wallet/index.ts @@ -0,0 +1 @@ +export { WalletProvider, useWallet } from './WalletContext'; \ No newline at end of file