mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-05-28 13:49:49 +00:00
feat: setup keystore management
This commit is contained in:
parent
d60962dcb0
commit
ba2f640eea
@ -4,6 +4,7 @@ import "./globals.css";
|
|||||||
import { WalletProvider } from "../contexts/WalletContext";
|
import { WalletProvider } from "../contexts/WalletContext";
|
||||||
import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2";
|
import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2";
|
||||||
import { RLNImplementationProvider } from "../contexts/RLNImplementationContext";
|
import { RLNImplementationProvider } from "../contexts/RLNImplementationContext";
|
||||||
|
import { KeystoreProvider } from "../contexts/KeystoreContext";
|
||||||
import { Header } from "../components/Header";
|
import { Header } from "../components/Header";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@ -33,14 +34,16 @@ export default function RootLayout({
|
|||||||
>
|
>
|
||||||
<WalletProvider>
|
<WalletProvider>
|
||||||
<RLNImplementationProvider>
|
<RLNImplementationProvider>
|
||||||
<RLNUnifiedProvider>
|
<KeystoreProvider>
|
||||||
<div className="flex flex-col min-h-screen">
|
<RLNUnifiedProvider>
|
||||||
<Header />
|
<div className="flex flex-col min-h-screen">
|
||||||
<main className="flex-grow">
|
<Header />
|
||||||
{children}
|
<main className="flex-grow">
|
||||||
</main>
|
{children}
|
||||||
</div>
|
</main>
|
||||||
</RLNUnifiedProvider>
|
</div>
|
||||||
|
</RLNUnifiedProvider>
|
||||||
|
</KeystoreProvider>
|
||||||
</RLNImplementationProvider>
|
</RLNImplementationProvider>
|
||||||
</WalletProvider>
|
</WalletProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import RLNMembershipRegistration from '../components/RLNMembershipRegistration';
|
import RLNMembershipRegistration from '../components/RLNMembershipRegistration';
|
||||||
import { WalletInfo } from '../components/WalletInfo';
|
import { WalletInfo } from '../components/WalletInfo';
|
||||||
import { RLNImplementationToggle } from '../components/RLNImplementationToggle';
|
import { RLNImplementationToggle } from '../components/RLNImplementationToggle';
|
||||||
|
import KeystoreManager from '../components/KeystoreManager';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
@ -31,6 +32,16 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
<RLNMembershipRegistration />
|
<RLNMembershipRegistration />
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
123
examples/keystore-management/src/components/KeystoreManager.tsx
Normal file
123
examples/keystore-management/src/components/KeystoreManager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRLN } from '../contexts/RLNUnifiedContext2';
|
import { useRLN } from '../contexts/RLNUnifiedContext2';
|
||||||
import { useWallet } from '../contexts/WalletContext';
|
import { useWallet } from '../contexts/WalletContext';
|
||||||
import { DecryptedCredentials } from '@waku/rln';
|
import { KeystoreEntity } from '@waku/rln';
|
||||||
|
|
||||||
export default function RLNMembershipRegistration() {
|
export default function RLNMembershipRegistration() {
|
||||||
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN();
|
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN();
|
||||||
@ -12,12 +12,15 @@ export default function RLNMembershipRegistration() {
|
|||||||
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
|
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
|
||||||
const [isRegistering, setIsRegistering] = useState(false);
|
const [isRegistering, setIsRegistering] = useState(false);
|
||||||
const [isInitializing, setIsInitializing] = useState(false);
|
const [isInitializing, setIsInitializing] = useState(false);
|
||||||
|
const [saveToKeystore, setSaveToKeystore] = useState(true);
|
||||||
|
const [keystorePassword, setKeystorePassword] = useState('');
|
||||||
const [registrationResult, setRegistrationResult] = useState<{
|
const [registrationResult, setRegistrationResult] = useState<{
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
txHash?: string;
|
txHash?: string;
|
||||||
warning?: string;
|
warning?: string;
|
||||||
credentials?: DecryptedCredentials;
|
credentials?: KeystoreEntity;
|
||||||
|
keystoreHash?: string;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
const isLineaSepolia = chainId === 59141;
|
const isLineaSepolia = chainId === 59141;
|
||||||
@ -56,6 +59,15 @@ export default function RLNMembershipRegistration() {
|
|||||||
return;
|
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);
|
setIsRegistering(true);
|
||||||
setRegistrationResult({});
|
setRegistrationResult({});
|
||||||
|
|
||||||
@ -65,11 +77,20 @@ export default function RLNMembershipRegistration() {
|
|||||||
warning: 'Please check your wallet to sign the registration message.'
|
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({
|
setRegistrationResult({
|
||||||
...result,
|
...result,
|
||||||
credentials: result.credentials
|
credentials: result.credentials
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear password field after successful registration
|
||||||
|
if (result.success) {
|
||||||
|
setKeystorePassword('');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setRegistrationResult({
|
setRegistrationResult({
|
||||||
success: false,
|
success: false,
|
||||||
@ -189,6 +210,50 @@ export default function RLNMembershipRegistration() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 && (
|
{address && (
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400 p-3 border border-gray-200 dark:border-gray-700 rounded-md">
|
<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 className="font-medium mb-1">Registration Details:</p>
|
||||||
@ -199,9 +264,9 @@ export default function RLNMembershipRegistration() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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
|
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-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'
|
: '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>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
160
examples/keystore-management/src/contexts/KeystoreContext.tsx
Normal file
160
examples/keystore-management/src/contexts/KeystoreContext.tsx
Normal 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;
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"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 { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln';
|
||||||
import { useWallet } from './WalletContext';
|
import { useWallet } from './WalletContext';
|
||||||
import { ethers } from 'ethers';
|
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);
|
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -117,11 +117,6 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
|||||||
if (isConnected && signer && rln && !isStarted) {
|
if (isConnected && signer && rln && !isStarted) {
|
||||||
console.log("Starting RLN with signer...");
|
console.log("Starting RLN with signer...");
|
||||||
try {
|
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 });
|
await rln.start({ signer });
|
||||||
|
|
||||||
setIsStarted(true);
|
setIsStarted(true);
|
||||||
@ -153,7 +148,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
|||||||
console.error('Error in initializeRLN:', err);
|
console.error('Error in initializeRLN:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
|
setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
|
||||||
}
|
}
|
||||||
};
|
}, [isConnected, signer, rln, isStarted]);
|
||||||
|
|
||||||
const registerMembership = async (rateLimit: number) => {
|
const registerMembership = async (rateLimit: number) => {
|
||||||
console.log("registerMembership called with rate limit:", rateLimit);
|
console.log("registerMembership called with rate limit:", rateLimit);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { UnifiedRLNInstance } from './RLNFactory';
|
|||||||
import { useRLNImplementation } from './RLNImplementationContext';
|
import { useRLNImplementation } from './RLNImplementationContext';
|
||||||
import { createRLNImplementation } from './RLNFactory';
|
import { createRLNImplementation } from './RLNFactory';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
import { useKeystore } from './KeystoreContext';
|
||||||
|
|
||||||
// Constants for RLN membership registration
|
// Constants for RLN membership registration
|
||||||
const ERC20_ABI = [
|
const ERC20_ABI = [
|
||||||
@ -27,11 +28,17 @@ interface RLNContextType {
|
|||||||
isStarted: boolean;
|
isStarted: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
initializeRLN: () => Promise<void>;
|
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;
|
rateMinLimit: number;
|
||||||
rateMaxLimit: number;
|
rateMaxLimit: number;
|
||||||
getCurrentRateLimit: () => Promise<number | null>;
|
getCurrentRateLimit: () => Promise<number | null>;
|
||||||
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
|
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
|
||||||
|
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the context
|
// Create the context
|
||||||
@ -50,6 +57,8 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
|||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [rateMinLimit, setRateMinLimit] = useState<number>(0);
|
const [rateMinLimit, setRateMinLimit] = useState<number>(0);
|
||||||
const [rateMaxLimit, setRateMaxLimit] = useState<number>(0);
|
const [rateMaxLimit, setRateMaxLimit] = useState<number>(0);
|
||||||
|
|
||||||
|
const { saveCredentials: saveToKeystore } = useKeystore();
|
||||||
|
|
||||||
// Listen for wallet connection
|
// Listen for wallet connection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -176,18 +185,12 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
|||||||
// Start RLN if wallet is connected
|
// Start RLN if wallet is connected
|
||||||
if (isConnected && signer && rln && !isStarted) {
|
if (isConnected && signer && rln && !isStarted) {
|
||||||
console.log("Starting RLN with signer...");
|
console.log("Starting RLN with signer...");
|
||||||
try {
|
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 });
|
await rln.start({ signer });
|
||||||
|
|
||||||
setIsStarted(true);
|
setIsStarted(true);
|
||||||
console.log("RLN started successfully, isStarted set to true");
|
console.log("RLN started successfully, isStarted set to true");
|
||||||
|
|
||||||
// Fetch rate limits after RLN is started
|
|
||||||
try {
|
try {
|
||||||
const minLimit = await rln.contract.getMinRateLimit();
|
const minLimit = await rln.contract.getMinRateLimit();
|
||||||
const maxLimit = await rln.contract.getMaxRateLimit();
|
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);
|
console.log("registerMembership called with rate limit:", rateLimit);
|
||||||
|
|
||||||
if (!rln || !isStarted) {
|
if (!rln || !isStarted) {
|
||||||
@ -369,16 +388,21 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
console.log("Membership registered successfully");
|
console.log("Membership registered successfully");
|
||||||
|
|
||||||
// Store credentials in localStorage for reference
|
// If saveOptions provided, save to keystore
|
||||||
try {
|
let keystoreHash: string | undefined;
|
||||||
localStorage.setItem("rln-keystore", JSON.stringify(credentials));
|
if (saveOptions?.password) {
|
||||||
} catch (storageErr) {
|
try {
|
||||||
console.warn("Could not store credentials in localStorage:", storageErr);
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
credentials: credentials
|
credentials,
|
||||||
|
keystoreHash
|
||||||
};
|
};
|
||||||
} catch (registerErr) {
|
} catch (registerErr) {
|
||||||
console.error("Error registering membership:", registerErr);
|
console.error("Error registering membership:", registerErr);
|
||||||
@ -407,7 +431,8 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
|||||||
getCurrentRateLimit,
|
getCurrentRateLimit,
|
||||||
getRateLimitsBounds,
|
getRateLimitsBounds,
|
||||||
rateMinLimit,
|
rateMinLimit,
|
||||||
rateMaxLimit
|
rateMaxLimit,
|
||||||
|
saveCredentialsToKeystore,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -109,15 +109,9 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
|||||||
console.log("RLN instance already exists, skipping creation");
|
console.log("RLN instance already exists, skipping creation");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start RLN if wallet is connected
|
|
||||||
if (isConnected && signer && rln && !isStarted) {
|
if (isConnected && signer && rln && !isStarted) {
|
||||||
console.log("Starting RLN with signer...");
|
console.log("Starting RLN with signer...");
|
||||||
try {
|
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 });
|
await rln.start({ signer });
|
||||||
|
|
||||||
setIsStarted(true);
|
setIsStarted(true);
|
||||||
|
|||||||
62
examples/keystore-management/src/utils/fileUtils.ts
Normal file
62
examples/keystore-management/src/utils/fileUtils.ts
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user