mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-15 04:03:11 +00:00
feat: UI refactor
This commit is contained in:
parent
56a81109ab
commit
cb5f76021e
@ -3,6 +3,7 @@ import { Inter } from 'next/font/google'
|
||||
import "./globals.css";
|
||||
import { WalletProvider, RLNImplementationProvider, KeystoreProvider, RLNProvider } from "../contexts/index";
|
||||
import { Header } from "../components/Header";
|
||||
import { AppStateProvider } from "../contexts/AppStateContext";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
@ -24,20 +25,22 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${inter.variable} antialiased`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
|
||||
@ -1,54 +1,15 @@
|
||||
import RLNMembershipRegistration from '../components/RLNMembershipRegistration';
|
||||
import { WalletInfo } from '../components/WalletInfo';
|
||||
import { RLNImplementationToggle } from '../components/RLNImplementationToggle';
|
||||
import KeystoreManager from '../components/KeystoreManager';
|
||||
import { RLNInitButton } from '../components/RLNInitButton';
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Layout } from '../components/Layout';
|
||||
import { MembershipRegistration } from '../components/Tabs/MembershipTab/MembershipRegistration';
|
||||
import { KeystoreManagement } from '../components/Tabs/KeystoreTab/KeystoreManagement';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<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>
|
||||
<div className="space-y-4">
|
||||
<RLNImplementationToggle />
|
||||
<RLNInitButton />
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -1,123 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useKeystore } from '../contexts/index';
|
||||
import { saveKeystoreToFile, readKeystoreFromFile } from '../utils/fileUtils';
|
||||
|
||||
export default function KeystoreManager() {
|
||||
const {
|
||||
isInitialized: isKeystoreInitialized,
|
||||
hasStoredCredentials,
|
||||
storedCredentialsHashes,
|
||||
exportKeystore,
|
||||
importKeystore
|
||||
} = useKeystore();
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
const handleExport = () => {
|
||||
try {
|
||||
const keystoreJson = exportKeystore();
|
||||
saveKeystoreToFile(keystoreJson);
|
||||
setSuccessMessage('Keystore exported successfully');
|
||||
setTimeout(() => setSuccessMessage(null), 3000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to export keystore');
|
||||
setTimeout(() => setError(null), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
try {
|
||||
const keystoreJson = await readKeystoreFromFile();
|
||||
const success = importKeystore(keystoreJson);
|
||||
|
||||
if (success) {
|
||||
setSuccessMessage('Keystore imported successfully');
|
||||
} else {
|
||||
setError('Failed to import keystore');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setSuccessMessage(null);
|
||||
setError(null);
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to import keystore');
|
||||
setTimeout(() => setError(null), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isKeystoreInitialized) {
|
||||
return (
|
||||
<div className="p-4 bg-gray-100 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-700 dark:text-gray-300">Initializing keystore...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Keystore Management</h2>
|
||||
|
||||
{/* Status */}
|
||||
<div className="mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<span className="font-semibold">Status:</span> {hasStoredCredentials ? 'Credentials found' : 'No credentials stored'}
|
||||
</p>
|
||||
{hasStoredCredentials && (
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1">
|
||||
<span className="font-semibold">Stored credentials:</span> {storedCredentialsHashes.length}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900 rounded-lg">
|
||||
<p className="text-sm text-red-700 dark:text-red-300">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{successMessage && (
|
||||
<div className="mb-4 p-3 bg-green-50 dark:bg-green-900 rounded-lg">
|
||||
<p className="text-sm text-green-700 dark:text-green-300">{successMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import/Export Buttons */}
|
||||
<div className="space-y-4">
|
||||
{/* Export Keystore */}
|
||||
<div>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={!hasStoredCredentials}
|
||||
className={`w-full py-2 px-4 rounded ${
|
||||
!hasStoredCredentials
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-gray-600 dark:text-gray-400'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800 dark:bg-blue-700 dark:hover:bg-blue-800'
|
||||
}`}
|
||||
>
|
||||
Export Keystore
|
||||
</button>
|
||||
{!hasStoredCredentials && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
No credentials to export
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Import Keystore */}
|
||||
<div>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
className="w-full py-2 px-4 bg-green-600 text-white rounded hover:bg-green-700 active:bg-green-800 dark:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
Import Keystore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
examples/keystore-management/src/components/Layout.tsx
Normal file
51
examples/keystore-management/src/components/Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,50 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useRLNImplementation, type RLNImplementationType } from '../contexts/index';
|
||||
import React from 'react';
|
||||
import { useRLNImplementation } from '../contexts/rln';
|
||||
|
||||
export function RLNImplementationToggle() {
|
||||
const { implementation, setImplementation } = useRLNImplementation();
|
||||
|
||||
const handleToggle = (newImplementation: RLNImplementationType) => {
|
||||
setImplementation(newImplementation);
|
||||
};
|
||||
|
||||
return (
|
||||
<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 ${
|
||||
onClick={() => setImplementation('standard')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
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'
|
||||
? '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>
|
||||
<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>
|
||||
</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 === 'standard'
|
||||
? 'Standard implementation with full security features'
|
||||
: 'Light implementation with optimized performance'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,398 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useWallet } from '../contexts/index';
|
||||
import { KeystoreEntity } from '@waku/rln';
|
||||
import { useRLN } from '../contexts/rln';
|
||||
|
||||
export default function RLNMembershipRegistration() {
|
||||
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN();
|
||||
const { isConnected, address, chainId } = useWallet();
|
||||
|
||||
const [rateLimit, setRateLimit] = useState<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'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's signatures produce values that aren'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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,252 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useKeystore } from '../../../contexts/keystore';
|
||||
import { useAppState } from '../../../contexts/AppStateContext';
|
||||
import { useRLN } from '../../../contexts/rln';
|
||||
import { saveKeystoreToFile, readKeystoreFromFile } from '../../../utils/fileUtils';
|
||||
import { KeystoreEntity } from '@waku/rln';
|
||||
|
||||
export function KeystoreManagement() {
|
||||
const {
|
||||
hasStoredCredentials,
|
||||
storedCredentialsHashes,
|
||||
error,
|
||||
exportKeystore,
|
||||
importKeystore,
|
||||
removeCredential,
|
||||
loadCredential
|
||||
} = useKeystore();
|
||||
const { setGlobalError } = useAppState();
|
||||
const { isInitialized, isStarted } = useRLN();
|
||||
|
||||
const [selectedHash, setSelectedHash] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState('');
|
||||
const [loadingCredential, setLoadingCredential] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [loadedCredential, setLoadedCredential] = useState<KeystoreEntity | null>(null);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setGlobalError(error);
|
||||
}
|
||||
}, [error, setGlobalError]);
|
||||
|
||||
const handleExport = () => {
|
||||
try {
|
||||
const keystoreJson = exportKeystore();
|
||||
saveKeystoreToFile(keystoreJson);
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to export keystore');
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
try {
|
||||
const keystoreJson = await readKeystoreFromFile();
|
||||
const success = importKeystore(keystoreJson);
|
||||
if (!success) {
|
||||
setGlobalError('Failed to import keystore');
|
||||
}
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to import keystore');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCredential = (hash: string) => {
|
||||
try {
|
||||
removeCredential(hash);
|
||||
if (selectedHash === hash) {
|
||||
setSelectedHash(null);
|
||||
setPassword('');
|
||||
setLoadedCredential(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to remove credential');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadCredential = async (hash: string) => {
|
||||
if (!password) {
|
||||
setLoadError('Please enter a password');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInitialized || !isStarted) {
|
||||
setLoadError('Please initialize RLN first');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingCredential(true);
|
||||
setLoadError(null);
|
||||
setShowSuccess(false);
|
||||
|
||||
try {
|
||||
const credential = await loadCredential(hash, password);
|
||||
if (credential) {
|
||||
setLoadedCredential(credential);
|
||||
setLoadError(null);
|
||||
setPassword('');
|
||||
setShowSuccess(true);
|
||||
// Auto-hide success message after 3 seconds
|
||||
setTimeout(() => setShowSuccess(false), 3000);
|
||||
} else {
|
||||
setLoadError('Failed to load credential');
|
||||
setLoadedCredential(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setLoadError(err instanceof Error ? err.message : 'Failed to load credential');
|
||||
setLoadedCredential(null);
|
||||
} finally {
|
||||
setLoadingCredential(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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/Export Actions */}
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="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 Keystore
|
||||
</button>
|
||||
<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 loading credentials
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Loaded Credential Info */}
|
||||
{loadedCredential && (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Currently Loaded Credential
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Hash:</span>{' '}
|
||||
<code className="break-all">{selectedHash}</code>
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Status:</span>{' '}
|
||||
<span className="text-green-600 dark:text-green-400">Active</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Message */}
|
||||
{showSuccess && (
|
||||
<div className="bg-green-50 dark:bg-green-900 p-4 rounded-lg">
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
✓ Credential loaded successfully
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 ${
|
||||
selectedHash === hash
|
||||
? 'border-blue-500 dark:border-blue-400'
|
||||
: 'border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<code className="text-sm text-gray-600 dark:text-gray-400 break-all">
|
||||
{hash}
|
||||
</code>
|
||||
{selectedHash === hash && (
|
||||
<div className="pt-3 space-y-3">
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter password to load credential"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
{loadError && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
{loadError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (selectedHash === hash) {
|
||||
setSelectedHash(null);
|
||||
setPassword('');
|
||||
setLoadedCredential(null);
|
||||
} else {
|
||||
setSelectedHash(hash);
|
||||
setPassword('');
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800"
|
||||
>
|
||||
{selectedHash === hash ? 'Cancel' : 'Load'}
|
||||
</button>
|
||||
{selectedHash === hash && (
|
||||
<button
|
||||
onClick={() => handleLoadCredential(hash)}
|
||||
disabled={loadingCredential || !isInitialized || !isStarted}
|
||||
className="px-3 py-1 text-sm bg-green-100 text-green-700 rounded-md hover:bg-green-200 dark:bg-green-900 dark:text-green-300 dark:hover:bg-green-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loadingCredential ? 'Loading...' : 'Confirm'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleRemoveCredential(hash)}
|
||||
className="px-3 py-1 text-sm bg-red-100 text-red-700 rounded-md hover:bg-red-200 dark:bg-red-900 dark:text-red-300 dark:hover:bg-red-800"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No credentials stored yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,269 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RLNInitButton } from '../../RLNInitButton';
|
||||
import { RLNImplementationToggle } from '../../RLNImplementationToggle';
|
||||
import { useAppState } from '../../../contexts/AppStateContext';
|
||||
import { useRLN } from '../../../contexts/rln';
|
||||
import { useWallet } from '../../../contexts/wallet';
|
||||
import { KeystoreEntity } from '@waku/rln';
|
||||
|
||||
export function MembershipRegistration() {
|
||||
const { setGlobalError } = useAppState();
|
||||
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error } = useRLN();
|
||||
const { isConnected, chainId } = useWallet();
|
||||
|
||||
const [rateLimit, setRateLimit] = useState<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 />
|
||||
{isInitialized && isStarted && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
✓ RLN Initialized
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useWallet } from '../contexts/index';
|
||||
|
||||
function getNetworkName(chainId: number | null): string {
|
||||
@ -23,6 +23,20 @@ interface ProviderRpcError extends Error {
|
||||
|
||||
export function WalletInfo() {
|
||||
const { isConnected, address, balance, chainId, connectWallet, disconnectWallet, error } = useWallet();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user