mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-03 06:13:08 +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 { 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
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 { 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>
|
||||
)}
|
||||
|
||||
|
||||
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";
|
||||
|
||||
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);
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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);
|
||||
|
||||
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