chore: cleanup and improvements

This commit is contained in:
Danish Arora 2025-04-03 20:47:02 +05:30
parent 36a8ca0b0c
commit c33ebfb874
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
30 changed files with 1040 additions and 1078 deletions

View File

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

View File

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

View File

@ -18,4 +18,4 @@ body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
}

View File

@ -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 (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${inter.variable} antialiased`}
>
<WalletProvider>
<RLNImplementationProvider>
<KeystoreProvider>
<RLNUnifiedProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow">
{children}
</main>
</div>
</RLNUnifiedProvider>
</KeystoreProvider>
</RLNImplementationProvider>
</WalletProvider>
<AppStateProvider>
<WalletProvider>
<RLNImplementationProvider>
<KeystoreProvider>
<RLNProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow">
{children}
</main>
</div>
</RLNProvider>
</KeystoreProvider>
</RLNImplementationProvider>
</WalletProvider>
</AppStateProvider>
</body>
</html>
);
}
}

View File

@ -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 (
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-md dark:bg-gray-800 p-6">
<h2 className="text-2xl font-bold text-center text-gray-900 dark:text-white mb-6">Waku Keystore Management</h2>
<div className="space-y-8">
{/* RLN Implementation Toggle */}
<div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">RLN Implementation</h3>
<RLNImplementationToggle />
</div>
{/* Wallet Information Section */}
<div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Wallet Connection</h3>
<WalletInfo />
</div>
{/* RLN Membership Registration Section */}
<div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">RLN Membership</h3>
<p className="mb-4 text-gray-700 dark:text-gray-300">
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.
</p>
<RLNMembershipRegistration />
</div>
{/* Keystore Management Section */}
<div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Keystore Management</h3>
<p className="mb-4 text-gray-700 dark:text-gray-300">
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.
</p>
<KeystoreManager />
</div>
</div>
</div>
</div>
</div>
<Layout>
<MembershipRegistration />
<KeystoreManagement />
</Layout>
);
}
}

View File

@ -5,14 +5,12 @@ import { WalletInfo } from './WalletInfo';
export function Header() {
return (
<header className="sticky top-0 z-10 bg-white dark:bg-gray-900 shadow-sm">
<div className="container mx-auto px-4 py-3 flex justify-between items-center">
<header className="bg-gray-900 border-b border-gray-800">
<div className="container mx-auto px-6 h-16 flex justify-between items-center">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Waku Keystore Management</h1>
</div>
<div className="w-80">
<WalletInfo />
<h1 className="text-xl font-medium text-white">Waku Keystore Management</h1>
</div>
<WalletInfo />
</div>
</header>
);

View File

@ -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<string, string> = {
MembershipRegistration: 'membership',
KeystoreManagement: 'keystore',
};
export function Layout({ children }: { children: React.ReactNode }) {
const { activeTab, setActiveTab } = useAppState();
const childrenArray = Children.toArray(children);
return (
<div className="min-h-screen bg-white dark:bg-gray-900">
<main className="container mx-auto px-4 py-6">
<TabNavigation
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
<div className="mt-6">
{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;
})}
</div>
</main>
</div>
);
}

View File

@ -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 (
<div className="flex items-center space-x-4 p-3 bg-gray-100 dark:bg-gray-800 rounded-lg">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
RLN Implementation:
</span>
<div className="flex rounded-md shadow-sm" role="group">
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
RLN Implementation
</label>
<div className="flex space-x-4">
<button
type="button"
onClick={() => 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
</button>
<button
type="button"
onClick={() => 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
</button>
<button
onClick={() => 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
</button>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{implementation === 'standard' ? (
<span>Using full RLN implementation</span>
) : (
<span>Using lightweight RLN implementation</span>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{implementation === 'light'
? 'Light implementation, without Zerokit. Instant initalisation.'
: 'Standard implementation, with Zerokit. Initialisation takes 10-15 seconds for WASM module'
}
</p>
</div>
);
}
}

View File

@ -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<number>(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<HTMLInputElement>) => {
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 (
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
RLN Membership Registration
</h2>
{/* Network Warning */}
{isConnected && !isLineaSepolia && (
<div className="mb-4 bg-orange-50 dark:bg-orange-900 p-4 rounded-lg">
<p className="text-sm text-orange-700 dark:text-orange-400">
<strong>Warning:</strong> You are not connected to Linea Sepolia network. Please switch networks to register.
</p>
</div>
)}
{/* Informational Box */}
<div className="mb-6 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
<h3 className="text-md font-semibold text-blue-800 dark:text-blue-300 mb-2">
About RLN Membership on Linea Sepolia
</h3>
<p className="text-sm text-blue-700 dark:text-blue-400 mb-2">
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.
</p>
<p className="text-sm text-blue-700 dark:text-blue-400 mb-2">
This application is configured to use the <strong>Linea Sepolia</strong> testnet for RLN registrations.
</p>
<p className="text-sm text-blue-700 dark:text-blue-400">
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.
</p>
</div>
{/* Initialization Status */}
<div className="mb-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
RLN Status:
<span className={isInitialized && isStarted ? "text-green-600 ml-2" : "text-amber-600 ml-2"}>
{isInitialized && isStarted ? "Ready" : "Not Initialized"}
</span>
</p>
</div>
{isConnected && (!isInitialized || !isStarted) && (
<button
onClick={handleInitializeRLN}
disabled={isInitializing || !isLineaSepolia}
className={`px-3 py-1 text-xs rounded-md transition-colors ${
isInitializing
? "bg-gray-400 text-gray-700 cursor-not-allowed"
: isLineaSepolia
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-400 text-gray-700 cursor-not-allowed"
}`}
>
{isInitializing ? "Initializing..." : "Initialize RLN"}
</button>
)}
</div>
{error && (
<p className="text-xs text-red-600 mt-1">{error}</p>
)}
</div>
{isInitialized && !isStarted && (
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-400">
<strong>Note:</strong> RLN is partially initialized. You can still proceed with registration, but some advanced features might be limited.
</p>
</div>
)}
{!isConnected ? (
<div className="text-amber-600 dark:text-amber-400 mb-4">
Please connect your wallet to register a membership
</div>
) : !isInitialized || !isStarted ? (
<div className="text-amber-600 dark:text-amber-400 mb-4">
Please initialize RLN before registering a membership
</div>
) : (
<>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="rateLimit"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Rate Limit (messages per epoch)
</label>
<div className="flex items-center">
<input
type="range"
id="rateLimit"
name="rateLimit"
min={rateMinLimit}
max={rateMaxLimit}
value={rateLimit}
onChange={handleRateLimitChange}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
<span className="ml-3 w-12 text-gray-700 dark:text-gray-300">{rateLimit}</span>
</div>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Select a rate limit between {rateMinLimit} and {rateMaxLimit}
</p>
</div>
{/* Keystore Options */}
<div className="space-y-2">
<div className="flex items-center">
<input
type="checkbox"
id="saveToKeystore"
checked={saveToKeystore}
onChange={(e) => 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"
/>
<label
htmlFor="saveToKeystore"
className="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300"
>
Save credentials to keystore
</label>
</div>
{saveToKeystore && (
<div>
<label
htmlFor="keystorePassword"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Keystore Password (min 8 characters)
</label>
<input
type="password"
id="keystorePassword"
autoComplete='password'
value={keystorePassword}
onChange={(e) => 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}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
The password will be used to encrypt your RLN credentials in the keystore.
You will need this password to decrypt your credentials later.
</p>
</div>
)}
</div>
{address && (
<div className="text-sm text-gray-600 dark:text-gray-400 p-3 border border-gray-200 dark:border-gray-700 rounded-md">
<p className="font-medium mb-1">Registration Details:</p>
<p>Connected Address: {address.slice(0, 8)}...{address.slice(-6)}</p>
<p className="mt-1">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.</p>
</div>
)}
<button
type="submit"
disabled={isRegistering || !isInitialized || !isStarted || (saveToKeystore && keystorePassword.length < 8)}
className={`w-full py-2 px-4 rounded-md text-white font-medium
${isRegistering || !isInitialized || !isStarted || (saveToKeystore && keystorePassword.length < 8)
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
}`}
>
{isRegistering ? 'Registering...' : 'Register Membership'}
</button>
</form>
{registrationResult.warning && registrationResult.success === undefined && (
<div className="mt-4 p-3 bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded">
<p className="font-medium">Important:</p>
<p className="text-sm mt-1">{registrationResult.warning}</p>
<div className="text-sm mt-1">
You&apos;ll need to sign a message with your wallet to complete the registration.
</div>
</div>
)}
{registrationResult.success === true && (
<div className="mt-4 p-3 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
<p className="font-medium">Registration submitted!</p>
{registrationResult.txHash && (
<div>
<p className="text-sm mt-1 break-all">
{registrationResult.txHash}
</p>
{registrationResult.txHash.startsWith('0x') && (
<p className="mt-2">
<a
href={`https://sepolia.lineascan.build/tx/${registrationResult.txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline"
>
View on Linea Sepolia Explorer
</a>
</p>
)}
</div>
)}
{registrationResult.warning && (
<p className="text-sm mt-2 text-yellow-600 dark:text-yellow-300">
<strong>Note:</strong> {registrationResult.warning}
</p>
)}
<p className="text-sm mt-2">
Your RLN membership is now registered and can be used with your Waku node.
</p>
{registrationResult.credentials && (
<><div className="mt-3 p-3 bg-gray-100 dark:bg-gray-800 rounded-md">
<p className="font-medium mb-2">Your RLN Credentials:</p>
<div className="text-xs font-mono overflow-auto">
<h4 className="font-semibold mt-2 mb-1">Identity:</h4>
<p className="mb-1">
<span className="font-semibold">ID Commitment:</span> {Buffer.from(registrationResult.credentials.identity.IDCommitment).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Secret Hash:</span> {Buffer.from(registrationResult.credentials.identity.IDSecretHash).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Nullifier:</span> {Buffer.from(registrationResult.credentials.identity.IDNullifier).toString('hex')}
</p>
<p className="mb-3">
<span className="font-semibold">ID Trapdoor:</span> {Buffer.from(registrationResult.credentials.identity.IDTrapdoor).toString('hex')}
</p>
<h4 className="font-semibold mt-3 mb-1">Membership:</h4>
<p className="mb-1">
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
</p>
<p className="mb-1">
<span className="font-semibold">Contract Address:</span> {registrationResult.credentials.membership.address}
</p>
<p className="mb-1">
<span className="font-semibold">Tree Index:</span> {registrationResult.credentials.membership.treeIndex}
</p>
<p className="mb-1">
<span className="font-semibold">Rate Limit:</span> {registrationResult.credentials.membership.rateLimit}
</p>
</div>
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
These credentials are your proof of membership. Store them securely.
</p>
</div>
</>
)}
{registrationResult.keystoreHash && (
<p className="text-sm mt-2">
<span className="font-medium">Credentials saved to keystore!</span>
<br />
Hash: {registrationResult.keystoreHash.slice(0, 10)}...{registrationResult.keystoreHash.slice(-8)}
</p>
)}
</div>
)}
{registrationResult.success === false && (
<div className="mt-4 p-3 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded">
<p className="font-medium">Registration failed</p>
<p className="text-sm mt-1">{registrationResult.error}</p>
{registrationResult.error?.includes("field") && (
<div className="mt-2 text-sm">
<p>
This is a mathematical constraint in the zero-knowledge proof system.
Your wallet&apos;s signatures produce values that aren&apos;t compatible with the RLN cryptographic system.
</p>
<p className="mt-2 font-medium">Recommended solution:</p>
<p>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.</p>
</div>
)}
</div>
)}
</>
)}
{/* Debug Info (For Development) */}
<div className="mt-6 p-3 border border-gray-200 dark:border-gray-700 rounded text-xs">
<p className="font-semibold">Debug Info:</p>
<p>Wallet Connected: {isConnected ? "Yes" : "No"}</p>
<p>RLN Initialized: {isInitialized ? "Yes" : "No"}</p>
<p>RLN Started: {isStarted ? "Yes" : "No"}</p>
<p>Min Rate: {rateMinLimit}, Max Rate: {rateMaxLimit}</p>
<p>Current Rate Limit: {rateLimit}</p>
</div>
</div>
);
}

View File

@ -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 (
<div className="flex flex-col items-start gap-2">
<button
onClick={handleInitialize}
disabled={isLoading || (isInitialized && isStarted)}
className={`
px-4 py-2 rounded-lg font-medium transition-colors relative
${
isLoading
? 'bg-gray-200 text-gray-500 cursor-not-allowed dark:bg-gray-700 dark:text-gray-400'
: isInitialized && isStarted
? 'bg-green-600 text-white cursor-default dark:bg-green-500'
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600'
}
`}
>
{isLoading && (
<span className="absolute left-2 top-1/2 -translate-y-1/2">
<svg className="animate-spin h-5 w-5 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
)}
{isInitialized && isStarted && (
<span className="absolute left-2 top-1/2 -translate-y-1/2">
<svg className="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</span>
)}
<span className={(isLoading || (isInitialized && isStarted)) ? 'pl-7' : ''}>
{getButtonText()}
</span>
</button>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">
{error}
</p>
)}
</div>
);
}

View File

@ -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<string>('');
const [selectedCredential, setSelectedCredential] = useState<string | null>(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 (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Keystore Management
</h2>
<div className="space-y-6">
{/* Import Action */}
<div>
<button
onClick={handleImport}
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
>
Import Keystore
</button>
</div>
{/* RLN Status */}
{!isInitialized || !isStarted ? (
<div className="bg-yellow-50 dark:bg-yellow-900 p-4 rounded-lg">
<p className="text-sm text-yellow-700 dark:text-yellow-300">
Please initialize RLN before managing credentials
</p>
</div>
) : null}
{/* Stored Credentials */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">
Stored Credentials
</h3>
{hasStoredCredentials ? (
<div className="space-y-4">
{storedCredentialsHashes.map((hash) => (
<div
key={hash}
className="p-4 rounded-lg border border-gray-200 dark:border-gray-700"
>
<div className="flex flex-col space-y-3">
<div className="flex items-start justify-between">
<code className="text-sm text-gray-600 dark:text-gray-400 break-all">
{hash}
</code>
<div className="flex space-x-2">
<button
onClick={() => setSelectedCredential(hash === selectedCredential ? null : hash)}
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Export
</button>
<button
onClick={() => handleRemoveCredential(hash)}
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
Remove
</button>
</div>
</div>
{selectedCredential === hash && (
<div className="mt-2 space-y-2">
<input
type="password"
value={exportPassword}
onChange={(e) => 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"
/>
<button
onClick={() => 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
</button>
</div>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400">
No credentials stored
</p>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -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<number>(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<HTMLInputElement>) => {
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 (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
RLN Membership Registration
</h2>
<div className="space-y-6">
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
<RLNImplementationToggle />
</div>
{/* Network Warning */}
{isConnected && !isLineaSepolia && (
<div className="bg-orange-50 dark:bg-orange-900 p-4 rounded-lg">
<p className="text-sm text-orange-700 dark:text-orange-400">
<strong>Warning:</strong> You are not connected to Linea Sepolia network. Please switch networks to register.
</p>
</div>
)}
{/* Informational Box */}
<div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
<h3 className="text-md font-semibold text-blue-800 dark:text-blue-300 mb-2">
About RLN Membership on Linea Sepolia
</h3>
<p className="text-sm text-blue-700 dark:text-blue-400 mb-2">
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.
</p>
<p className="text-sm text-blue-700 dark:text-blue-400 mb-2">
This application is configured to use the <strong>Linea Sepolia</strong> testnet for RLN registrations.
</p>
<p className="text-sm text-blue-700 dark:text-blue-400">
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.
</p>
</div>
<div className="flex items-center space-x-2">
<RLNInitButton />
</div>
{!isConnected ? (
<div className="text-amber-600 dark:text-amber-400">
Please connect your wallet to register a membership
</div>
) : !isInitialized || !isStarted ? (
<div className="text-amber-600 dark:text-amber-400">
Please initialize RLN before registering a membership
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="rateLimit"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Rate Limit (messages per epoch)
</label>
<div className="flex items-center space-x-4">
<input
type="range"
id="rateLimit"
name="rateLimit"
min={rateMinLimit}
max={rateMaxLimit}
value={rateLimit}
onChange={handleRateLimitChange}
className="w-full"
/>
<span className="text-sm text-gray-600 dark:text-gray-400 w-12">
{rateLimit}
</span>
</div>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
id="saveToKeystore"
checked={saveToKeystore}
onChange={(e) => setSaveToKeystore(e.target.checked)}
className="h-4 w-4 text-blue-600 rounded border-gray-300"
/>
<label
htmlFor="saveToKeystore"
className="ml-2 text-sm text-gray-700 dark:text-gray-300"
>
Save credentials to keystore
</label>
</div>
{saveToKeystore && (
<div>
<label
htmlFor="keystorePassword"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Keystore Password (min 8 characters)
</label>
<input
type="password"
id="keystorePassword"
value={keystorePassword}
onChange={(e) => 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"
/>
</div>
)}
</div>
<button
type="submit"
disabled={isRegistering || !isLineaSepolia || (saveToKeystore && !keystorePassword)}
className={`w-full px-4 py-2 text-sm font-medium rounded-md ${
isRegistering || !isLineaSepolia || (saveToKeystore && !keystorePassword)
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isRegistering ? 'Registering...' : 'Register Membership'}
</button>
</form>
)}
{/* Registration Result */}
{registrationResult.warning && (
<div className="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg">
<p className="text-sm text-yellow-700 dark:text-yellow-400">
{registrationResult.warning}
</p>
</div>
)}
{registrationResult.error && (
<div className="mt-4 p-4 bg-red-50 dark:bg-red-900 rounded-lg">
<p className="text-sm text-red-700 dark:text-red-400">
{registrationResult.error}
</p>
</div>
)}
{registrationResult.success && (
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900 rounded-lg">
<p className="text-sm text-green-700 dark:text-green-400 mb-2">
Membership registered successfully!
</p>
{registrationResult.txHash && (
<p className="text-xs text-green-600 dark:text-green-500">
Transaction Hash: {registrationResult.txHash}
</p>
)}
{registrationResult.keystoreHash && (
<p className="text-xs text-green-600 dark:text-green-500">
Credentials saved to keystore with hash: {registrationResult.keystoreHash}
</p>
)}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -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 (
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => 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}
</button>
))}
</nav>
</div>
);
}

View File

@ -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<HTMLDivElement>(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 (
<div className="flex items-center">
<button
onClick={connectWallet}
className="h-7 px-3 text-xs bg-blue-500/20 text-blue-300 rounded hover:bg-blue-500/30 transition-colors"
>
Connect Wallet
</button>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-sm dark:bg-gray-800 p-3">
<div className="relative" ref={dropdownRef}>
{error && (
<div className="mb-2 p-2 text-xs bg-red-100 text-red-700 rounded-md dark:bg-red-900 dark:text-red-100">
<div className="absolute right-0 -top-2 transform -translate-y-full mb-2 px-2 py-1 text-xs bg-red-500/20 text-red-300 rounded whitespace-nowrap">
<p>{error}</p>
</div>
)}
{isConnected ? (
<div className="flex flex-col">
<div className="flex items-center justify-between mb-2">
<div className="mr-3">
<div className="flex items-center mb-1">
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">Address:</span>
<span className="font-mono text-xs text-gray-900 dark:text-white truncate max-w-[120px]">
{address}
</span>
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex items-center gap-2 px-3 h-8 rounded transition-colors ${
isOpen ? 'bg-gray-800' : 'hover:bg-gray-800'
}`}
>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-300 truncate max-w-[140px]">
{address}
</span>
<span className="font-mono text-xs text-gray-400">
{balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}
</span>
</div>
<svg
className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-72 rounded-lg bg-gray-800 shadow-lg border border-gray-700 overflow-hidden">
<div className="p-3">
<div className="flex flex-col gap-2 text-gray-300">
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-gray-400">Address:</span>
<span className="font-mono text-xs truncate">{address}</span>
</div>
<div className="flex items-center mb-1">
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">Network:</span>
<span className={`font-mono text-xs ${isUnsupportedNetwork ? 'text-orange-600 dark:text-orange-400' : 'text-gray-900 dark:text-white'}`}>
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-gray-400">Network:</span>
<span className={`font-mono text-xs ${isUnsupportedNetwork ? 'text-orange-400' : ''}`}>
{getNetworkName(chainId)}
</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">Balance:</span>
<span className="font-mono text-xs text-gray-900 dark:text-white">
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-gray-400">Balance:</span>
<span className="font-mono text-xs">
{balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}
</span>
</div>
</div>
</div>
<div className="border-t border-gray-700 p-2 flex flex-col gap-2">
{isUnsupportedNetwork && (
<button
onClick={switchToLineaSepolia}
className="w-full h-7 px-3 text-xs bg-orange-500/20 text-orange-300 rounded hover:bg-orange-500/30 transition-colors"
>
Switch to Linea Sepolia
</button>
)}
<button
onClick={disconnectWallet}
className="px-2 py-1 text-xs bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
className="w-full h-7 px-3 text-xs bg-red-500/20 text-red-300 rounded hover:bg-red-500/30 transition-colors"
>
Disconnect
</button>
</div>
{isUnsupportedNetwork && (
<div className="flex flex-col gap-2 mt-1">
<button
onClick={switchToLineaSepolia}
className="w-full px-2 py-1 text-xs bg-orange-500 text-white rounded-md hover:bg-orange-600 transition-colors"
>
Switch to Linea Sepolia
</button>
</div>
)}
</div>
) : (
<div className="flex items-center justify-center">
<button
onClick={connectWallet}
className="px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Connect Wallet
</button>
</div>
)}
</div>

View File

@ -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<AppState | undefined>(undefined);
export function AppStateProvider({ children }: { children: ReactNode }) {
const [isLoading, setIsLoading] = useState(false);
const [globalError, setGlobalError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<string>('membership');
const value = {
isLoading,
setIsLoading,
globalError,
setGlobalError,
activeTab,
setActiveTab,
};
return (
<AppStateContext.Provider value={value}>
{children}
{isLoading && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 flex items-center space-x-3">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500"></div>
<span className="text-gray-900 dark:text-white">Loading...</span>
</div>
</div>
)}
{globalError && (
<div className="fixed bottom-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded z-50 flex items-center">
<span>{globalError}</span>
<button
onClick={() => setGlobalError(null)}
className="ml-3 text-red-700 hover:text-red-900"
>
</button>
</div>
)}
</AppStateContext.Provider>
);
}
export function useAppState() {
const context = useContext(AppStateContext);
if (context === undefined) {
throw new Error('useAppState must be used within an AppStateProvider');
}
return context;
}

View File

@ -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' ? (
<StandardRLNProvider>{children}</StandardRLNProvider>
) : (
<LightRLNProvider>{children}</LightRLNProvider>
)}
</>
);
}

View File

@ -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<void>;
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<RLNContextType>(dummyRLNContext);
// Create a provider component that will fetch the appropriate implementation
export function UnifiedRLNProvider({ children }: { children: ReactNode }) {
const { implementation } = useRLNImplementation();
const [contextValue, setContextValue] = useState<RLNContextType>(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 <RLNProvider><TempComponent /></RLNProvider>;
} 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 <RLNProvider><TempComponent /></RLNProvider>;
}
} catch (error) {
console.error('Error loading RLN context:', error);
}
};
fetchContext();
}, [implementation]);
return (
<UnifiedRLNContext.Provider value={contextValue}>
{children}
</UnifiedRLNContext.Provider>
);
}
// Create a hook to use the unified RLN context
export function useRLN() {
return useContext(UnifiedRLNContext);
}

View File

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

View File

@ -11,8 +11,7 @@ interface KeystoreContextType {
hasStoredCredentials: boolean;
storedCredentialsHashes: string[];
saveCredentials: (credentials: KeystoreEntity, password: string) => Promise<string>;
loadCredential: (hash: string, password: string) => Promise<KeystoreEntity | undefined>;
exportKeystore: () => string;
exportCredential: (hash: string, password: string) => Promise<string>;
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<KeystoreEntity | undefined> => {
const exportCredential = async (hash: string, password: string): Promise<string> => {
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
};

View File

@ -0,0 +1 @@
export { KeystoreProvider, useKeystore } from './KeystoreContext';

View File

@ -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<number | null>;
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>;
isLoading: boolean;
}
// Create the context
const RLNUnifiedContext = createContext<RLNContextType | undefined>(undefined);
const RLNContext = createContext<RLNContextType | undefined>(undefined);
// Create the provider component
export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
export function RLNProvider({ children }: { children: ReactNode }) {
const { implementation } = useRLNImplementation();
const [rln, setRln] = useState<UnifiedRLNInstance | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isStarted, setIsStarted] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// Get the signer from window.ethereum
const [signer, setSigner] = useState<ethers.Signer | null>(null);
@ -109,64 +95,17 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
setError(null);
}, [implementation]);
const ensureLineaSepoliaNetwork = async (): Promise<boolean> => {
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<unknown>;
}
// 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<number | null> => {
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<string> => {
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 (
<RLNUnifiedContext.Provider value={contextValue}>
<RLNContext.Provider
value={{
rln,
isInitialized,
isStarted,
error,
initializeRLN,
registerMembership,
rateMinLimit,
rateMaxLimit,
getCurrentRateLimit,
getRateLimitsBounds,
saveCredentialsToKeystore: saveToKeystore,
isLoading
}}
>
{children}
</RLNUnifiedContext.Provider>
</RLNContext.Provider>
);
}
// 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;
}
}

View File

@ -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<RLNImplementationContextType | undefined>(undefined);
// Create the provider component
export function RLNImplementationProvider({ children }: { children: ReactNode }) {
const [implementation, setImplementation] = useState<RLNImplementationType>('standard');
const [implementation, setImplementation] = useState<RLNImplementationType>('light');
return (
<RLNImplementationContext.Provider value={{ implementation, setImplementation }}>
@ -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;
}
}

View File

@ -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<ethers.ContractTransaction>;
};
start: (options: { signer: ethers.Signer }) => Promise<void>;
// Both implementations use registerMembership but with different parameters
registerMembership: (options: { signature: string }) => Promise<Record<string, unknown>>;
}
// Define a factory function that creates the appropriate RLN implementation
export async function createRLNImplementation(type: 'standard' | 'light'): Promise<UnifiedRLNInstance> {
export async function createRLNImplementation(type: 'standard' | 'light' = 'light'): Promise<UnifiedRLNInstance> {
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;
}
}
}

View File

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

View File

@ -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<boolean> => {
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<unknown>;
}
// 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 (
<RLNContext.Provider
@ -294,7 +222,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
export function useRLN() {
const context = useContext(RLNContext);
if (context === undefined) {
throw new Error('useRLN must be used within an RLNProvider');
throw new Error('useRLN must be used within a RLNProvider');
}
return context;
}

View File

@ -1,19 +1,10 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createContext, useContext, useState, ReactNode } from 'react';
import { createRLN, DecryptedCredentials, LINEA_CONTRACT, RLNInstance } from '@waku/rln';
import { useWallet } from './WalletContext';
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)"
];
import { ensureLineaSepoliaNetwork, ERC20_ABI, SIGNATURE_MESSAGE } from '../utils/network';
interface RLNContextType {
rln: RLNInstance | null;
@ -37,53 +28,6 @@ export function RLNProvider({ children }: { children: ReactNode }) {
const [rateMinLimit, setRateMinLimit] = useState(0);
const [rateMaxLimit, setRateMaxLimit] = useState(0);
const ensureLineaSepoliaNetwork = async (): Promise<boolean> => {
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<unknown>;
}
// 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 (
<RLNContext.Provider
value={{
@ -285,7 +208,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
export function useRLN() {
const context = useContext(RLNContext);
if (context === undefined) {
throw new Error('useRLN must be used within an RLNProvider');
throw new Error('useRLN must be used within a RLNProvider');
}
return context;
}

View File

@ -0,0 +1,3 @@
export { RLNProvider, useRLN } from './RLNContext';
export { RLNImplementationProvider, useRLNImplementation, type RLNImplementationType } from './RLNImplementationContext';
export type { UnifiedRLNInstance } from './implementations';

View File

@ -0,0 +1,68 @@
"use client";
import { ethers } from 'ethers';
// Linea Sepolia configuration
export const LINEA_SEPOLIA_CONFIG = {
chainId: 59141,
tokenAddress: '0x185A0015aC462a0aECb81beCc0497b649a64B9ea'
};
// Type for Ethereum provider in window
export interface EthereumProvider {
request: (args: {
method: string;
params?: unknown[]
}) => Promise<unknown>;
}
// Function to ensure the wallet is connected to Linea Sepolia network
export const ensureLineaSepoliaNetwork = async (signer?: ethers.Signer): Promise<boolean> => {
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";

View File

@ -0,0 +1 @@
export { WalletProvider, useWallet } from './WalletContext';