feat: setup keystore management

This commit is contained in:
Danish Arora 2025-03-27 13:48:38 +05:30
parent d60962dcb0
commit ba2f640eea
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
9 changed files with 488 additions and 43 deletions

View File

@ -4,6 +4,7 @@ import "./globals.css";
import { WalletProvider } from "../contexts/WalletContext";
import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2";
import { RLNImplementationProvider } from "../contexts/RLNImplementationContext";
import { KeystoreProvider } from "../contexts/KeystoreContext";
import { Header } from "../components/Header";
const geistSans = Geist({
@ -33,14 +34,16 @@ export default function RootLayout({
>
<WalletProvider>
<RLNImplementationProvider>
<RLNUnifiedProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow">
{children}
</main>
</div>
</RLNUnifiedProvider>
<KeystoreProvider>
<RLNUnifiedProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow">
{children}
</main>
</div>
</RLNUnifiedProvider>
</KeystoreProvider>
</RLNImplementationProvider>
</WalletProvider>
</body>

View File

@ -1,6 +1,7 @@
import RLNMembershipRegistration from '../components/RLNMembershipRegistration';
import { WalletInfo } from '../components/WalletInfo';
import { RLNImplementationToggle } from '../components/RLNImplementationToggle';
import KeystoreManager from '../components/KeystoreManager';
export default function Home() {
return (
@ -31,6 +32,16 @@ export default function Home() {
</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>

View File

@ -0,0 +1,123 @@
"use client";
import { useState } from 'react';
import { useKeystore } from '../contexts/KeystoreContext';
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>
);
}

View File

@ -3,7 +3,7 @@
import { useState } from 'react';
import { useRLN } from '../contexts/RLNUnifiedContext2';
import { useWallet } from '../contexts/WalletContext';
import { DecryptedCredentials } from '@waku/rln';
import { KeystoreEntity } from '@waku/rln';
export default function RLNMembershipRegistration() {
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN();
@ -12,12 +12,15 @@ export default function RLNMembershipRegistration() {
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?: DecryptedCredentials;
credentials?: KeystoreEntity;
keystoreHash?: string;
}>({});
const isLineaSepolia = chainId === 59141;
@ -56,6 +59,15 @@ export default function RLNMembershipRegistration() {
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({});
@ -65,11 +77,20 @@ export default function RLNMembershipRegistration() {
warning: 'Please check your wallet to sign the registration message.'
});
const result = await registerMembership(rateLimit);
// 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,
@ -189,6 +210,50 @@ export default function RLNMembershipRegistration() {
</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>
@ -199,9 +264,9 @@ export default function RLNMembershipRegistration() {
<button
type="submit"
disabled={isRegistering || !isInitialized || !isStarted}
disabled={isRegistering || !isInitialized || !isStarted || (saveToKeystore && keystorePassword.length < 8)}
className={`w-full py-2 px-4 rounded-md text-white font-medium
${isRegistering || !isInitialized || !isStarted
${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'
}`}
@ -289,6 +354,13 @@ export default function RLNMembershipRegistration() {
</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>
)}

View File

@ -0,0 +1,160 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Keystore, KeystoreEntity } from '@waku/rln';
// Define types for the context
interface KeystoreContextType {
keystore: Keystore | null;
isInitialized: boolean;
error: string | null;
hasStoredCredentials: boolean;
storedCredentialsHashes: string[];
saveCredentials: (credentials: KeystoreEntity, password: string) => Promise<string>;
loadCredential: (hash: string, password: string) => Promise<KeystoreEntity | undefined>;
exportKeystore: () => string;
importKeystore: (keystoreJson: string) => boolean;
removeCredential: (hash: string) => void;
}
// Create the context
const KeystoreContext = createContext<KeystoreContextType | undefined>(undefined);
// Provider component
export function KeystoreProvider({ children }: { children: ReactNode }) {
const [keystore, setKeystore] = useState<Keystore | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [error, setError] = useState<string | null>(null);
const [storedCredentialsHashes, setStoredCredentialsHashes] = useState<string[]>([]);
// Initialize keystore
useEffect(() => {
try {
const storedKeystore = localStorage.getItem('waku-rln-keystore');
let keystoreInstance: Keystore;
if (storedKeystore) {
const loaded = Keystore.fromString(storedKeystore);
if (loaded) {
keystoreInstance = loaded;
} else {
keystoreInstance = Keystore.create();
}
} else {
keystoreInstance = Keystore.create();
}
setKeystore(keystoreInstance);
setStoredCredentialsHashes(keystoreInstance.keys());
setIsInitialized(true);
} catch (err) {
console.error("Error initializing keystore:", err);
setError(err instanceof Error ? err.message : "Failed to initialize keystore");
}
}, []);
// Save keystore to localStorage whenever it changes
useEffect(() => {
if (keystore && isInitialized) {
try {
localStorage.setItem('waku-rln-keystore', keystore.toString());
} catch (err) {
console.warn("Could not save keystore to localStorage:", err);
}
}
}, [keystore, isInitialized]);
const saveCredentials = async (credentials: KeystoreEntity, password: string): Promise<string> => {
if (!keystore) {
throw new Error("Keystore not initialized");
}
try {
const hash = await keystore.addCredential(credentials, password);
localStorage.setItem('waku-rln-keystore', keystore.toString());
setStoredCredentialsHashes(keystore.keys());
return hash;
} catch (err) {
console.error("Error saving credentials:", err);
throw err;
}
};
const loadCredential = async (hash: string, password: string): Promise<KeystoreEntity | undefined> => {
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;
}
};
const exportKeystore = (): string => {
if (!keystore) {
throw new Error("Keystore not initialized");
}
return keystore.toString();
};
const importKeystore = (keystoreJson: string): boolean => {
try {
const imported = Keystore.fromString(keystoreJson);
if (imported) {
setKeystore(imported);
setStoredCredentialsHashes(imported.keys());
localStorage.setItem('waku-rln-keystore', keystoreJson);
return true;
}
return false;
} catch (err) {
console.error("Error importing keystore:", err);
setError(err instanceof Error ? err.message : "Failed to import keystore");
return false;
}
};
const removeCredential = (hash: string): void => {
if (!keystore) {
throw new Error("Keystore not initialized");
}
keystore.removeCredential(hash);
setStoredCredentialsHashes(keystore.keys());
localStorage.setItem('waku-rln-keystore', keystore.toString());
};
const contextValue: KeystoreContextType = {
keystore,
isInitialized,
error,
hasStoredCredentials: storedCredentialsHashes.length > 0,
storedCredentialsHashes,
saveCredentials,
loadCredential,
exportKeystore,
importKeystore,
removeCredential
};
return (
<KeystoreContext.Provider value={contextValue}>
{children}
</KeystoreContext.Provider>
);
}
export function useKeystore() {
const context = useContext(KeystoreContext);
if (context === undefined) {
throw new Error('useKeystore must be used within a KeystoreProvider');
}
return context;
}

View File

@ -1,6 +1,6 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln';
import { useWallet } from './WalletContext';
import { ethers } from 'ethers';
@ -88,7 +88,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
}
};
const initializeRLN = async () => {
const initializeRLN = useCallback(async () => {
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
try {
@ -117,11 +117,6 @@ export function RLNProvider({ children }: { children: ReactNode }) {
if (isConnected && signer && rln && !isStarted) {
console.log("Starting RLN with signer...");
try {
// Initialize with localKeystore if available (just for reference in localStorage)
const localKeystore = localStorage.getItem("rln-keystore") || "";
console.log("Local keystore available:", !!localKeystore);
// Start RLN with signer
await rln.start({ signer });
setIsStarted(true);
@ -153,7 +148,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
console.error('Error in initializeRLN:', err);
setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
}
};
}, [isConnected, signer, rln, isStarted]);
const registerMembership = async (rateLimit: number) => {
console.log("registerMembership called with rate limit:", rateLimit);

View File

@ -6,6 +6,7 @@ import { UnifiedRLNInstance } from './RLNFactory';
import { useRLNImplementation } from './RLNImplementationContext';
import { createRLNImplementation } from './RLNFactory';
import { ethers } from 'ethers';
import { useKeystore } from './KeystoreContext';
// Constants for RLN membership registration
const ERC20_ABI = [
@ -27,11 +28,17 @@ interface RLNContextType {
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: KeystoreEntity }>;
registerMembership: (rateLimit: number, saveOptions?: { password: string }) => Promise<{
success: boolean;
error?: string;
credentials?: KeystoreEntity;
keystoreHash?: string;
}>;
rateMinLimit: number;
rateMaxLimit: number;
getCurrentRateLimit: () => Promise<number | null>;
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>;
}
// Create the context
@ -50,6 +57,8 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
const [isConnected, setIsConnected] = useState(false);
const [rateMinLimit, setRateMinLimit] = useState<number>(0);
const [rateMaxLimit, setRateMaxLimit] = useState<number>(0);
const { saveCredentials: saveToKeystore } = useKeystore();
// Listen for wallet connection
useEffect(() => {
@ -176,18 +185,12 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
// Start RLN if wallet is connected
if (isConnected && signer && rln && !isStarted) {
console.log("Starting RLN with signer...");
try {
// Initialize with localKeystore if available (just for reference in localStorage)
const localKeystore = localStorage.getItem("rln-keystore") || "";
console.log("Local keystore available:", !!localKeystore);
// Start RLN with signer
try {
await rln.start({ signer });
setIsStarted(true);
console.log("RLN started successfully, isStarted set to true");
// Fetch rate limits after RLN is started
try {
const minLimit = await rln.contract.getMinRateLimit();
const maxLimit = await rln.contract.getMaxRateLimit();
@ -264,7 +267,23 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
}
}
const registerMembership = async (rateLimit: number) => {
// Save credentials to keystore
const saveCredentialsToKeystore = async (credentials: KeystoreEntity, password: string): Promise<string> => {
try {
return await saveToKeystore(credentials, password);
} catch (err) {
console.error("Error saving credentials to keystore:", err);
throw err;
}
};
// 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;
}> => {
console.log("registerMembership called with rate limit:", rateLimit);
if (!rln || !isStarted) {
@ -369,16 +388,21 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
console.log("Membership registered successfully");
// Store credentials in localStorage for reference
try {
localStorage.setItem("rln-keystore", JSON.stringify(credentials));
} catch (storageErr) {
console.warn("Could not store credentials in localStorage:", storageErr);
// 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: credentials
credentials,
keystoreHash
};
} catch (registerErr) {
console.error("Error registering membership:", registerErr);
@ -407,7 +431,8 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
getCurrentRateLimit,
getRateLimitsBounds,
rateMinLimit,
rateMaxLimit
rateMaxLimit,
saveCredentialsToKeystore,
};
return (

View File

@ -109,15 +109,9 @@ export function RLNProvider({ 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 {
// Initialize with localKeystore if available (just for reference in localStorage)
const localKeystore = localStorage.getItem("rln-keystore") || "";
console.log("Local keystore available:", !!localKeystore);
// Start RLN with signer
await rln.start({ signer });
setIsStarted(true);

View File

@ -0,0 +1,62 @@
/**
* Utility functions for handling keystore file operations
*/
/**
* Save a keystore JSON string to a file
* @param keystoreJson The keystore JSON as a string
* @param filename Optional filename (defaults to 'waku-rln-keystore.json')
*/
export const saveKeystoreToFile = (keystoreJson: string, filename: string = 'waku-rln-keystore.json'): void => {
const blob = new Blob([keystoreJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
document.body.removeChild(link);
};
/**
* Read a keystore file and return its content as a string
* @returns Promise resolving to the file content as a string
*/
export const readKeystoreFromFile = (): Promise<string> => {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.onchange = (event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
}
const reader = new FileReader();
reader.onload = () => {
const content = reader.result as string;
resolve(content);
};
reader.onerror = () => {
reject(new Error('Failed to read file'));
};
reader.readAsText(file);
};
input.click();
});
};