From cb5f76021e0f4db71e55feb2ca551681536e7b9a Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 27 Mar 2025 14:49:19 +0530 Subject: [PATCH] feat: UI refactor --- .../keystore-management/src/app/layout.tsx | 31 +- examples/keystore-management/src/app/page.tsx | 59 +-- .../src/components/Header.tsx | 10 +- .../src/components/KeystoreManager.tsx | 123 ------ .../src/components/Layout.tsx | 51 +++ .../components/RLNImplementationToggle.tsx | 48 +-- .../components/RLNMembershipRegistration.tsx | 398 ------------------ .../Tabs/KeystoreTab/KeystoreManagement.tsx | 252 +++++++++++ .../MembershipTab/MembershipRegistration.tsx | 269 ++++++++++++ .../src/components/Tabs/TabNavigation.tsx | 40 ++ .../src/components/WalletInfo.tsx | 120 ++++-- .../src/contexts/AppStateContext.tsx | 62 +++ 12 files changed, 805 insertions(+), 658 deletions(-) delete mode 100644 examples/keystore-management/src/components/KeystoreManager.tsx 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/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 diff --git a/examples/keystore-management/src/app/layout.tsx b/examples/keystore-management/src/app/layout.tsx index c760e7e..7c919e0 100644 --- a/examples/keystore-management/src/app/layout.tsx +++ b/examples/keystore-management/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from 'next/font/google' import "./globals.css"; import { WalletProvider, RLNImplementationProvider, KeystoreProvider, RLNProvider } from "../contexts/index"; import { Header } from "../components/Header"; +import { AppStateProvider } from "../contexts/AppStateContext"; const inter = Inter({ variable: "--font-inter", @@ -24,20 +25,22 @@ export default function RootLayout({ - - - - -
-
-
- {children} -
-
-
-
-
-
+ + + + + +
+
+
+ {children} +
+
+
+
+
+
+
); diff --git a/examples/keystore-management/src/app/page.tsx b/examples/keystore-management/src/app/page.tsx index c9e49a2..46f6b0f 100644 --- a/examples/keystore-management/src/app/page.tsx +++ b/examples/keystore-management/src/app/page.tsx @@ -1,54 +1,15 @@ -import RLNMembershipRegistration from '../components/RLNMembershipRegistration'; -import { WalletInfo } from '../components/WalletInfo'; -import { RLNImplementationToggle } from '../components/RLNImplementationToggle'; -import KeystoreManager from '../components/KeystoreManager'; -import { RLNInitButton } from '../components/RLNInitButton'; +"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. -

- -
-
-
-
-
+ + + + ); } 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/KeystoreManager.tsx b/examples/keystore-management/src/components/KeystoreManager.tsx deleted file mode 100644 index 33db7de..0000000 --- a/examples/keystore-management/src/components/KeystoreManager.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { useKeystore } from '../contexts/index'; -import { saveKeystoreToFile, readKeystoreFromFile } from '../utils/fileUtils'; - -export default function KeystoreManager() { - const { - isInitialized: isKeystoreInitialized, - hasStoredCredentials, - storedCredentialsHashes, - exportKeystore, - importKeystore - } = useKeystore(); - - const [error, setError] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); - - const handleExport = () => { - try { - const keystoreJson = exportKeystore(); - saveKeystoreToFile(keystoreJson); - setSuccessMessage('Keystore exported successfully'); - setTimeout(() => setSuccessMessage(null), 3000); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to export keystore'); - setTimeout(() => setError(null), 3000); - } - }; - - const handleImport = async () => { - try { - const keystoreJson = await readKeystoreFromFile(); - const success = importKeystore(keystoreJson); - - if (success) { - setSuccessMessage('Keystore imported successfully'); - } else { - setError('Failed to import keystore'); - } - - setTimeout(() => { - setSuccessMessage(null); - setError(null); - }, 3000); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to import keystore'); - setTimeout(() => setError(null), 3000); - } - }; - - if (!isKeystoreInitialized) { - return ( -
-

Initializing keystore...

-
- ); - } - - return ( -
-

Keystore Management

- - {/* Status */} -
-

- Status: {hasStoredCredentials ? 'Credentials found' : 'No credentials stored'} -

- {hasStoredCredentials && ( -

- Stored credentials: {storedCredentialsHashes.length} -

- )} -
- - {/* Notifications */} - {error && ( -
-

{error}

-
- )} - - {successMessage && ( -
-

{successMessage}

-
- )} - - {/* Import/Export Buttons */} -
- {/* Export Keystore */} -
- - {!hasStoredCredentials && ( -

- No credentials to export -

- )} -
- - {/* Import Keystore */} -
- -
-
-
- ); -} \ No newline at end of file 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 6250093..d5aedf8 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, type RLNImplementationType } from '../contexts/index'; +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 === 'standard' + ? 'Standard implementation with full security features' + : 'Light implementation with optimized performance' + } +

); } diff --git a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx deleted file mode 100644 index 8a8ba82..0000000 --- a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx +++ /dev/null @@ -1,398 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { useWallet } from '../contexts/index'; -import { KeystoreEntity } from '@waku/rln'; -import { useRLN } from '../contexts/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/Tabs/KeystoreTab/KeystoreManagement.tsx b/examples/keystore-management/src/components/Tabs/KeystoreTab/KeystoreManagement.tsx new file mode 100644 index 0000000..026e1e6 --- /dev/null +++ b/examples/keystore-management/src/components/Tabs/KeystoreTab/KeystoreManagement.tsx @@ -0,0 +1,252 @@ +"use client"; + +import React, { useEffect, useState } from 'react'; +import { useKeystore } from '../../../contexts/keystore'; +import { useAppState } from '../../../contexts/AppStateContext'; +import { useRLN } from '../../../contexts/rln'; +import { saveKeystoreToFile, readKeystoreFromFile } from '../../../utils/fileUtils'; +import { KeystoreEntity } from '@waku/rln'; + +export function KeystoreManagement() { + const { + hasStoredCredentials, + storedCredentialsHashes, + error, + exportKeystore, + importKeystore, + removeCredential, + loadCredential + } = useKeystore(); + const { setGlobalError } = useAppState(); + const { isInitialized, isStarted } = useRLN(); + + const [selectedHash, setSelectedHash] = useState(null); + const [password, setPassword] = useState(''); + const [loadingCredential, setLoadingCredential] = useState(false); + const [loadError, setLoadError] = useState(null); + const [loadedCredential, setLoadedCredential] = useState(null); + const [showSuccess, setShowSuccess] = useState(false); + + useEffect(() => { + if (error) { + setGlobalError(error); + } + }, [error, setGlobalError]); + + const handleExport = () => { + try { + const keystoreJson = exportKeystore(); + saveKeystoreToFile(keystoreJson); + } catch (err) { + setGlobalError(err instanceof Error ? err.message : 'Failed to export keystore'); + } + }; + + 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); + if (selectedHash === hash) { + setSelectedHash(null); + setPassword(''); + setLoadedCredential(null); + } + } catch (err) { + setGlobalError(err instanceof Error ? err.message : 'Failed to remove credential'); + } + }; + + const handleLoadCredential = async (hash: string) => { + if (!password) { + setLoadError('Please enter a password'); + return; + } + + if (!isInitialized || !isStarted) { + setLoadError('Please initialize RLN first'); + return; + } + + setLoadingCredential(true); + setLoadError(null); + setShowSuccess(false); + + try { + const credential = await loadCredential(hash, password); + if (credential) { + setLoadedCredential(credential); + setLoadError(null); + setPassword(''); + setShowSuccess(true); + // Auto-hide success message after 3 seconds + setTimeout(() => setShowSuccess(false), 3000); + } else { + setLoadError('Failed to load credential'); + setLoadedCredential(null); + } + } catch (err) { + setLoadError(err instanceof Error ? err.message : 'Failed to load credential'); + setLoadedCredential(null); + } finally { + setLoadingCredential(false); + } + }; + + return ( +
+
+

+ Keystore Management +

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

+ ⚠️ Please initialize RLN before loading credentials +

+
+ ) : null} + + {/* Loaded Credential Info */} + {loadedCredential && ( +
+

+ Currently Loaded Credential +

+
+

+ Hash:{' '} + {selectedHash} +

+

+ Status:{' '} + Active +

+
+
+ )} + + {/* Success Message */} + {showSuccess && ( +
+

+ ✓ Credential loaded successfully +

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

+ Stored Credentials +

+ + {hasStoredCredentials ? ( +
+ {storedCredentialsHashes.map((hash) => ( +
+
+
+ + {hash} + + {selectedHash === hash && ( +
+ setPassword(e.target.value)} + placeholder="Enter password to load credential" + 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" + /> + {loadError && ( +

+ {loadError} +

+ )} +
+ )} +
+
+ + {selectedHash === hash && ( + + )} + +
+
+
+ ))} +
+ ) : ( +

+ No credentials stored yet. +

+ )} +
+
+
+
+ ); +} \ 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..00b2aaf --- /dev/null +++ b/examples/keystore-management/src/components/Tabs/MembershipTab/MembershipRegistration.tsx @@ -0,0 +1,269 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { RLNInitButton } from '../../RLNInitButton'; +import { RLNImplementationToggle } from '../../RLNImplementationToggle'; +import { useAppState } from '../../../contexts/AppStateContext'; +import { useRLN } from '../../../contexts/rln'; +import { useWallet } from '../../../contexts/wallet'; +import { KeystoreEntity } from '@waku/rln'; + +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. +

+
+ +
+ + {isInitialized && isStarted && ( + + ✓ RLN Initialized + + )} +
+ + {!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 aa9444e..2b1a443 100644 --- a/examples/keystore-management/src/components/WalletInfo.tsx +++ b/examples/keystore-management/src/components/WalletInfo.tsx @@ -1,6 +1,6 @@ "use client"; -import React from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useWallet } from '../contexts/index'; function getNetworkName(chainId: number | null): string { @@ -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