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 (
-
-
+
);
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:
-
-
+
+
+ RLN Implementation
+
+
handleToggle('standard')}
- className={`px-4 py-2 text-sm font-medium rounded-l-lg ${
- implementation === 'standard'
- ? 'bg-blue-600 text-white'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600'
- }`}
- >
- Standard
-
- handleToggle('light')}
- className={`px-4 py-2 text-sm font-medium rounded-r-lg ${
+ onClick={() => setImplementation('light')}
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
implementation === 'light'
- ? 'bg-blue-600 text-white'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600'
+ ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
}`}
>
Light
+ setImplementation('standard')}
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
+ implementation === 'standard'
+ ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
+ }`}
+ >
+ Standard
+
-
- {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) && (
-
- {isInitializing ? "Initializing..." : "Initialize RLN"}
-
- )}
-
- {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
-
- ) : (
- <>
-
-
- {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.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 (
+
+
+ {isLoading && (
+
+
+
+
+
+
+ )}
+ {isInitialized && isStarted && (
+
+
+
+
+
+ )}
+
+ {getButtonText()}
+
+
+ {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 */}
+
+
+ Import Keystore
+
+
+
+ {/* RLN Status */}
+ {!isInitialized || !isStarted ? (
+
+
+ ⚠️ Please initialize RLN before managing credentials
+
+
+ ) : null}
+
+ {/* Stored Credentials */}
+
+
+ Stored Credentials
+
+
+ {hasStoredCredentials ? (
+
+ {storedCredentialsHashes.map((hash) => (
+
+
+
+
+ {hash}
+
+
+ setSelectedCredential(hash === selectedCredential ? null : hash)}
+ className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
+ >
+ Export
+
+ handleRemoveCredential(hash)}
+ className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
+ >
+ Remove
+
+
+
+
+ {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"
+ />
+ handleExportCredential(hash)}
+ className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
+ >
+ Export Credential
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+
+ 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
+
+ ) : (
+
+ )}
+
+ {/* 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 (
+
+
+ {tabs.map((tab) => (
+ onTabChange(tab.id)}
+ className={`
+ py-4 px-1 border-b-2 font-medium text-sm
+ ${
+ activeTab === tab.id
+ ? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
+ }
+ `}
+ aria-current={activeTab === tab.id ? 'page' : undefined}
+ >
+ {tab.label}
+
+ ))}
+
+
+ );
+}
\ 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 (
+
+
+ Connect Wallet
+
+
+ );
+ }
+
return (
-
+
{error && (
-
+
)}
- {isConnected ? (
-
-
-
-
-
Address:
-
- {address}
-
+
setIsOpen(!isOpen)}
+ className={`flex items-center gap-2 px-3 h-8 rounded transition-colors ${
+ isOpen ? 'bg-gray-800' : 'hover:bg-gray-800'
+ }`}
+ >
+
+
+ {address}
+
+
+ {balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}
+
+
+
+
+
+
+
+ {isOpen && (
+
+
+
+
+ Address:
+ {address}
-
-
Network:
-
+
+ Network:
+
{getNetworkName(chainId)}
-
-
Balance:
-
+
+ Balance:
+
{balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}
+
+
+
+ {isUnsupportedNetwork && (
+
+ Switch to Linea Sepolia
+
+ )}
Disconnect
-
- {isUnsupportedNetwork && (
-
-
- Switch to Linea Sepolia
-
-
- )}
-
- ) : (
-
-
- Connect Wallet
-
)}
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 && (
+
+ )}
+ {globalError && (
+
+ {globalError}
+ setGlobalError(null)}
+ className="ml-3 text-red-700 hover:text-red-900"
+ >
+ ✕
+
+
+ )}
+
+ );
+}
+
+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