feat: toggle between zerokit and light implementaitons

This commit is contained in:
Danish Arora 2025-03-10 02:12:57 +05:30
parent 21692759ce
commit 67b8723904
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
12 changed files with 930 additions and 39 deletions

View File

@ -8,7 +8,7 @@
"name": "waku-keystore-management", "name": "waku-keystore-management",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@waku/rln": "0.0.2-a3e7f15.0", "@waku/rln": "0.0.2-5c50ed7.0",
"next": "15.1.7", "next": "15.1.7",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
@ -2265,16 +2265,16 @@
} }
}, },
"node_modules/@waku/core": { "node_modules/@waku/core": {
"version": "0.0.34-a3e7f15.0", "version": "0.0.34-5c50ed7.0",
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.34-a3e7f15.0.tgz", "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.34-5c50ed7.0.tgz",
"integrity": "sha512-KWx7Epz7yAZKt9SJdUPjbZYY90312omLMXzBV9C3I8H94Q7u8C3dQep4wZsTBB+VGnVsKLYEyVmdorGZIgcubg==", "integrity": "sha512-GWYRzLZTfWgwXrK/KCWU3bbsYiE7pCyjGHKOfTDnUX9Z0LEx9fs5nDF7LyK41uefrZ0c6zrth3TH+Rkawm5aHw==",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@libp2p/ping": "2.0.1", "@libp2p/ping": "2.0.1",
"@waku/enr": "0.0.28-a3e7f15.0", "@waku/enr": "0.0.28-5c50ed7.0",
"@waku/interfaces": "0.0.29-a3e7f15.0", "@waku/interfaces": "0.0.29-5c50ed7.0",
"@waku/proto": "0.0.9-a3e7f15.0", "@waku/proto": "0.0.9-5c50ed7.0",
"@waku/utils": "0.0.22-a3e7f15.0", "@waku/utils": "0.0.22-5c50ed7.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"it-all": "^3.0.4", "it-all": "^3.0.4",
"it-length-prefixed": "^9.0.4", "it-length-prefixed": "^9.0.4",
@ -2312,9 +2312,9 @@
} }
}, },
"node_modules/@waku/enr": { "node_modules/@waku/enr": {
"version": "0.0.28-a3e7f15.0", "version": "0.0.28-5c50ed7.0",
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.28-a3e7f15.0.tgz", "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.28-5c50ed7.0.tgz",
"integrity": "sha512-IdVrw04Bi/DJd7caYtYbIGnb26hZJdGMsB0hBaqwto+kgAjp0gzOBB+NSutiBIPsVH0ED4fr1FHjW/pIRjrCfA==", "integrity": "sha512-Ms4FSw/fvkC63yHlikXCSRNUbxcAph4Os4uveVEnevV96B9k5GIoy5VzkLQbimWEdplgNBTiPzwI47O2AhvDrw==",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@ethersproject/rlp": "^5.7.0", "@ethersproject/rlp": "^5.7.0",
@ -2322,7 +2322,7 @@
"@libp2p/peer-id": "^5.0.1", "@libp2p/peer-id": "^5.0.1",
"@multiformats/multiaddr": "^12.0.0", "@multiformats/multiaddr": "^12.0.0",
"@noble/secp256k1": "^1.7.1", "@noble/secp256k1": "^1.7.1",
"@waku/utils": "0.0.22-a3e7f15.0", "@waku/utils": "0.0.22-5c50ed7.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"js-sha3": "^0.9.2" "js-sha3": "^0.9.2"
}, },
@ -2339,21 +2339,21 @@
} }
}, },
"node_modules/@waku/interfaces": { "node_modules/@waku/interfaces": {
"version": "0.0.29-a3e7f15.0", "version": "0.0.29-5c50ed7.0",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.29-a3e7f15.0.tgz", "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.29-5c50ed7.0.tgz",
"integrity": "sha512-RgtxDuLDmzrQBq17fcFRVSpQoeiZocbNTXIhW39+zh5HhggYRu0694W1D2c3toGMTODggEyCfPdFLfC24cMfuQ==", "integrity": "sha512-yfu9+SYLHpA7z9U1VuYlbhQVb5hBfUnqvAT7cLSbBKW4S13+gHaalZ3SFpZy8hYTqiIA5MTvXysWlSfLv6zIyw==",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@waku/proto": "0.0.9-a3e7f15.0" "@waku/proto": "0.0.9-5c50ed7.0"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@waku/proto": { "node_modules/@waku/proto": {
"version": "0.0.9-a3e7f15.0", "version": "0.0.9-5c50ed7.0",
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.9-a3e7f15.0.tgz", "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.9-5c50ed7.0.tgz",
"integrity": "sha512-nHUDg6QQ1OG1lEcx5o4HawkKWZl8eUd0gU7amk7reAJ64Y0I2UkblQt+FzUM3v8RinZnFUCweRKy1j+/ygiPPw==", "integrity": "sha512-3QfqbglgWjotrlzrNgtbiXDnxOOP68Bew2OZMhDW8bF7bUdBsq1Y8eih6eJPEcQ/8EP53OQ+gJ0FFv8BTaKLrw==",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"protons-runtime": "^5.4.0" "protons-runtime": "^5.4.0"
@ -2363,14 +2363,15 @@
} }
}, },
"node_modules/@waku/rln": { "node_modules/@waku/rln": {
"version": "0.0.2-a3e7f15.0", "version": "0.0.2-5c50ed7.0",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.0.2-a3e7f15.0.tgz", "resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.0.2-5c50ed7.0.tgz",
"integrity": "sha512-IK0CBJ16XBnODSc1uIaVmCMejSqQbVLEIzP6HdvrGH+9bJuyUzBLL7hkceAlMNPsOwZXb/fj0VR3DvzW1x9rKQ==", "integrity": "sha512-8C1OevAJMgZn7FWjqsQIMS5PCgAKlr9Dcs4nyXCvTaHBptkFWzVfhVa7hdz2EpcOtlYD1HqsvDeT17S15m1lmA==",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@chainsafe/bls-keystore": "3.0.0", "@chainsafe/bls-keystore": "3.0.0",
"@waku/core": "0.0.34-a3e7f15.0", "@noble/hashes": "^1.2.0",
"@waku/utils": "0.0.22-a3e7f15.0", "@waku/core": "0.0.34-5c50ed7.0",
"@waku/utils": "0.0.22-5c50ed7.0",
"@waku/zerokit-rln-wasm": "^0.0.13", "@waku/zerokit-rln-wasm": "^0.0.13",
"ethereum-cryptography": "^3.1.0", "ethereum-cryptography": "^3.1.0",
"ethers": "^5.7.2", "ethers": "^5.7.2",
@ -2382,13 +2383,13 @@
} }
}, },
"node_modules/@waku/utils": { "node_modules/@waku/utils": {
"version": "0.0.22-a3e7f15.0", "version": "0.0.22-5c50ed7.0",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.22-a3e7f15.0.tgz", "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.22-5c50ed7.0.tgz",
"integrity": "sha512-N3JiVD5mzJklheZAyVgMrE/RqrFQjUQ13b24c1XTLGgqrwXAVoLxPK47EQQK/hAt6OxX1ql2f4wHMYLP3mu1Tw==", "integrity": "sha512-hD+XO7lZ86OL9zFEzZvBy6pmgZlmNw7g1xeoui3FOauv6+zL///tarjLrmkPWh+eLJHdalVvTF9D/zkUWbK9SA==",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@noble/hashes": "^1.3.2", "@noble/hashes": "^1.3.2",
"@waku/interfaces": "0.0.29-a3e7f15.0", "@waku/interfaces": "0.0.29-5c50ed7.0",
"chai": "^4.3.10", "chai": "^4.3.10",
"debug": "^4.3.4", "debug": "^4.3.4",
"uint8arrays": "^5.0.1" "uint8arrays": "^5.0.1"

View File

@ -10,7 +10,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@waku/rln": "0.0.2-a3e7f15.0", "@waku/rln": "0.0.2-5c50ed7.0",
"next": "15.1.7", "next": "15.1.7",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"

View File

@ -2,7 +2,8 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { WalletProvider } from "../contexts/WalletContext"; import { WalletProvider } from "../contexts/WalletContext";
import { RLNProvider } from "../contexts/RLNContext"; import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2";
import { RLNImplementationProvider } from "../contexts/RLNImplementationContext";
import { Header } from "../components/Header"; import { Header } from "../components/Header";
const geistSans = Geist({ const geistSans = Geist({
@ -31,14 +32,16 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
<WalletProvider> <WalletProvider>
<RLNProvider> <RLNImplementationProvider>
<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>
</RLNProvider> </div>
</RLNUnifiedProvider>
</RLNImplementationProvider>
</WalletProvider> </WalletProvider>
</body> </body>
</html> </html>

View File

@ -1,5 +1,6 @@
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';
export default function Home() { export default function Home() {
return ( return (
@ -9,6 +10,12 @@ export default function Home() {
<h2 className="text-2xl font-bold text-center text-gray-900 dark:text-white mb-6">Waku Keystore Management</h2> <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"> <div className="space-y-8">
{/* RLN Implementation Toggle */}
<div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">RLN Implementation</h3>
<RLNImplementationToggle />
</div>
{/* Wallet Information Section */} {/* Wallet Information Section */}
<div> <div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Wallet Connection</h3> <h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Wallet Connection</h3>

View File

@ -0,0 +1,50 @@
"use client";
import { useRLNImplementation, RLNImplementationType } from '../contexts/RLNImplementationContext';
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">
<button
type="button"
onClick={() => handleToggle('standard')}
className={`px-4 py-2 text-sm font-medium rounded-l-lg ${
implementation === 'standard'
? 'bg-blue-600 text-white'
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600'
}`}
>
Standard
</button>
<button
type="button"
onClick={() => handleToggle('light')}
className={`px-4 py-2 text-sm font-medium rounded-r-lg ${
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'
}`}
>
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>
</div>
);
}

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useState } from 'react'; import { useState } from 'react';
import { useRLN } from '../contexts/RLNContext'; import { useRLN } from '../contexts/RLNUnifiedContext2';
import { useWallet } from '../contexts/WalletContext'; import { useWallet } from '../contexts/WalletContext';
import { DecryptedCredentials } from '@waku/rln'; import { DecryptedCredentials } from '@waku/rln';

View File

@ -0,0 +1,26 @@
"use client";
import { createRLN, RLNLightInstance } from '@waku/rln';
import { ethers } from 'ethers';
// Define a unified interface that both implementations must support
export interface UnifiedRLNInstance {
contract: {
address: string;
membershipFee?: () => Promise<ethers.BigNumber>;
};
start: (options: { signer: ethers.Signer }) => Promise<void>;
// Both implementations use registerMembership but with different parameters
registerMembership: (options: { signature: string }) => Promise<Record<string, unknown>>;
}
// Define a factory function that creates the appropriate RLN implementation
export async function createRLNImplementation(type: 'standard' | 'light'): Promise<UnifiedRLNInstance> {
if (type === 'standard') {
// Create and return the standard RLN implementation
return await createRLN() as unknown as UnifiedRLNInstance;
} else {
// Create and return the light RLN implementation
return new RLNLightInstance() as unknown as UnifiedRLNInstance;
}
}

View File

@ -0,0 +1,35 @@
"use client";
import { createContext, useContext, useState, ReactNode } from 'react';
// Define the implementation types
export type RLNImplementationType = 'standard' | 'light';
// Define the context type
interface RLNImplementationContextType {
implementation: RLNImplementationType;
setImplementation: (implementation: RLNImplementationType) => void;
}
// Create the context
const RLNImplementationContext = createContext<RLNImplementationContextType | undefined>(undefined);
// Create the provider component
export function RLNImplementationProvider({ children }: { children: ReactNode }) {
const [implementation, setImplementation] = useState<RLNImplementationType>('standard');
return (
<RLNImplementationContext.Provider value={{ implementation, setImplementation }}>
{children}
</RLNImplementationContext.Provider>
);
}
// Create a hook to use the context
export function useRLNImplementation() {
const context = useContext(RLNImplementationContext);
if (context === undefined) {
throw new Error('useRLNImplementation must be used within a RLNImplementationProvider');
}
return context;
}

View File

@ -0,0 +1,297 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln';
import { useWallet } from './WalletContext';
import { ethers } from 'ethers';
// Constants
const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials";
const ERC20_ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function balanceOf(address account) view returns (uint256)"
];
// Linea Sepolia configuration
const LINEA_SEPOLIA_CONFIG = {
chainId: 59141,
tokenAddress: '0x185A0015aC462a0aECb81beCc0497b649a64B9ea'
};
interface RLNContextType {
rln: RLNLightInstance | RLNInstance | null;
isInitialized: boolean;
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>;
rateMinLimit: number;
rateMaxLimit: number;
}
const RLNContext = createContext<RLNContextType | undefined>(undefined);
export function RLNProvider({ children }: { children: ReactNode }) {
const { isConnected, signer } = useWallet();
const [rln, setRln] = useState<RLNLightInstance | RLNInstance | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isStarted, setIsStarted] = useState(false);
const [error, setError] = useState<string | null>(null);
const [rateMinLimit, setRateMinLimit] = useState(20);
const [rateMaxLimit, setRateMaxLimit] = useState(600);
const ensureLineaSepoliaNetwork = async (): Promise<boolean> => {
try {
console.log("Current network: unknown", await signer?.getChainId());
// Check if already on Linea Sepolia
if (await signer?.getChainId() === LINEA_SEPOLIA_CONFIG.chainId) {
console.log("Already on Linea Sepolia network");
return true;
}
// If not on Linea Sepolia, try to switch
console.log("Not on Linea Sepolia, attempting to switch...");
interface EthereumProvider {
request: (args: {
method: string;
params?: unknown[]
}) => Promise<unknown>;
}
// Get the provider from window.ethereum
const provider = window.ethereum as EthereumProvider | undefined;
if (!provider) {
console.warn("No Ethereum provider found");
return false;
}
try {
// Request network switch
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${LINEA_SEPOLIA_CONFIG.chainId.toString(16)}` }],
});
console.log("Successfully switched to Linea Sepolia");
return true;
} catch (switchError: unknown) {
console.error("Error switching network:", switchError);
return false;
}
} catch (err) {
console.error("Error checking or switching network:", err);
return false;
}
};
const initializeRLN = async () => {
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
try {
setError(null);
if (!rln) {
console.log("Creating RLN instance...");
try {
const rlnInstance = new RLNLightInstance();
console.log("RLN instance created successfully:", !!rlnInstance);
setRln(rlnInstance);
setIsInitialized(true);
console.log("isInitialized set to true");
// Update rate limits to match contract requirements
setRateMinLimit(20); // Contract minimum (RATE_LIMIT_PARAMS.MIN_RATE)
setRateMaxLimit(600); // Contract maximum (RATE_LIMIT_PARAMS.MAX_RATE)
} catch (createErr) {
console.error("Error creating RLN instance:", createErr);
throw createErr;
}
} else {
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);
console.log("RLN started successfully, isStarted set to true");
} catch (startErr) {
console.error("Error starting RLN:", startErr);
throw startErr;
}
} else {
console.log("Skipping RLN start because:", {
isConnected,
hasSigner: !!signer,
hasRln: !!rln,
isAlreadyStarted: isStarted
});
}
} catch (err) {
console.error('Error in initializeRLN:', err);
setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
}
};
const registerMembership = async (rateLimit: number) => {
console.log("registerMembership called with rate limit:", rateLimit);
if (!rln || !isStarted) {
return { success: false, error: 'RLN not initialized or not started' };
}
if (!signer) {
return { success: false, error: 'No signer available' };
}
try {
// Validate rate limit
if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) {
return {
success: false,
error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}`
};
}
// Ensure we're on the correct network
const isOnLineaSepolia = await ensureLineaSepoliaNetwork();
if (!isOnLineaSepolia) {
console.warn("Could not switch to Linea Sepolia network. Registration may fail.");
}
// Get user address and contract address
const userAddress = await signer.getAddress();
if (!rln.contract || !rln.contract.address) {
return { success: false, error: "RLN contract address not available. Cannot proceed with registration." };
}
const contractAddress = rln.contract.address;
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
// Create token contract instance
const tokenContract = new ethers.Contract(
tokenAddress,
ERC20_ABI,
signer
);
// Check token balance
const tokenBalance = await tokenContract.balanceOf(userAddress);
if (tokenBalance.isZero()) {
return { success: false, error: "You need tokens to register a membership. Your token balance is zero." };
}
// Check and approve token allowance if needed
const currentAllowance = await tokenContract.allowance(userAddress, contractAddress);
if (currentAllowance.eq(0)) {
console.log("Requesting token approval...");
// Approve a large amount (max uint256)
const maxUint256 = ethers.constants.MaxUint256;
try {
const approveTx = await tokenContract.approve(contractAddress, maxUint256);
console.log("Approval transaction submitted:", approveTx.hash);
// Wait for the transaction to be mined
await approveTx.wait(1);
console.log("Token approval confirmed");
} catch (approvalErr) {
console.error("Error during token approval:", approvalErr);
return {
success: false,
error: `Failed to approve token: ${approvalErr instanceof Error ? approvalErr.message : String(approvalErr)}`
};
}
} else {
console.log("Token allowance already sufficient");
}
// Generate signature for identity
const message = `${SIGNATURE_MESSAGE} ${Date.now()}`;
const signature = await signer.signMessage(message);
const _credentials = await rln.registerMembership({signature: signature});
if (!_credentials) {
throw new Error("Failed to register membership: No credentials returned");
}
if (!_credentials.identity) {
throw new Error("Failed to register membership: Missing identity information");
}
if (!_credentials.membership) {
throw new Error("Failed to register membership: Missing membership information");
}
return { success: true, credentials: _credentials };
} catch (err) {
let errorMsg = "Failed to register membership";
if (err instanceof Error) {
errorMsg = err.message;
}
return { success: false, error: errorMsg };
}
};
// Initialize RLN when wallet connects
useEffect(() => {
console.log("Wallet connection state changed:", { isConnected, hasSigner: !!signer });
if (isConnected && signer) {
console.log("Wallet connected, attempting to initialize RLN");
initializeRLN();
} else {
console.log("Wallet not connected or no signer available, skipping RLN initialization");
}
}, [isConnected, signer]);
// Debug log for state changes
useEffect(() => {
console.log("RLN Context state:", {
isInitialized,
isStarted,
hasRln: !!rln,
error
});
}, [isInitialized, isStarted, rln, error]);
return (
<RLNContext.Provider
value={{
rln,
isInitialized,
isStarted,
error,
initializeRLN,
registerMembership,
rateMinLimit,
rateMaxLimit
}}
>
{children}
</RLNContext.Provider>
);
}
export function useRLN() {
const context = useContext(RLNContext);
if (context === undefined) {
throw new Error('useRLN must be used within an RLNProvider');
}
return context;
}

View File

@ -0,0 +1,23 @@
"use client";
import { ReactNode } from 'react';
import { RLNProvider as StandardRLNProvider } from './RLNContext';
import { RLNProvider as LightRLNProvider } from './RLNLightContext';
import { useRLNImplementation } from './RLNImplementationContext';
// Create a unified provider that conditionally renders the appropriate provider
export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
const { implementation } = useRLNImplementation();
// Render the appropriate provider based on the implementation
return (
<>
{implementation === 'standard' ? (
<StandardRLNProvider>{children}</StandardRLNProvider>
) : (
<LightRLNProvider>{children}</LightRLNProvider>
)}
</>
);
}

View File

@ -0,0 +1,358 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { KeystoreEntity } from '@waku/rln';
import { UnifiedRLNInstance } from './RLNFactory';
import { useRLNImplementation } from './RLNImplementationContext';
import { createRLNImplementation } from './RLNFactory';
import { ethers } from 'ethers';
// Constants for RLN membership registration
const ERC20_ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function balanceOf(address account) view returns (uint256)"
];
// Linea Sepolia configuration
const LINEA_SEPOLIA_CONFIG = {
chainId: 59141,
tokenAddress: '0x185A0015aC462a0aECb81beCc0497b649a64B9ea'
};
// Define the context type
interface RLNContextType {
rln: UnifiedRLNInstance | null;
isInitialized: boolean;
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: KeystoreEntity }>;
rateMinLimit: number;
rateMaxLimit: number;
}
// Create the context
const RLNUnifiedContext = createContext<RLNContextType | undefined>(undefined);
// Create the provider component
export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
const { implementation } = useRLNImplementation();
const [rln, setRln] = useState<UnifiedRLNInstance | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isStarted, setIsStarted] = useState(false);
const [error, setError] = useState<string | null>(null);
const [rateMinLimit, setRateMinLimit] = useState(20);
const [rateMaxLimit, setRateMaxLimit] = useState(600);
// Get the signer from window.ethereum
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [isConnected, setIsConnected] = useState(false);
// Listen for wallet connection
useEffect(() => {
const checkWallet = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.listAccounts();
if (accounts.length > 0) {
const signer = provider.getSigner();
setSigner(signer);
setIsConnected(true);
return;
}
}
setSigner(null);
setIsConnected(false);
} catch (err) {
console.error("Error checking wallet:", err);
setSigner(null);
setIsConnected(false);
}
};
checkWallet();
// Listen for account changes
if (window.ethereum) {
window.ethereum.on('accountsChanged', checkWallet);
window.ethereum.on('chainChanged', checkWallet);
}
return () => {
if (window.ethereum) {
window.ethereum.removeListener('accountsChanged', checkWallet);
window.ethereum.removeListener('chainChanged', checkWallet);
}
};
}, []);
// Reset RLN state when implementation changes
useEffect(() => {
setRln(null);
setIsInitialized(false);
setIsStarted(false);
setError(null);
}, [implementation]);
const ensureLineaSepoliaNetwork = async (): Promise<boolean> => {
try {
console.log("Current network: unknown", await signer?.getChainId());
// Check if already on Linea Sepolia
if (await signer?.getChainId() === LINEA_SEPOLIA_CONFIG.chainId) {
console.log("Already on Linea Sepolia network");
return true;
}
// If not on Linea Sepolia, try to switch
console.log("Not on Linea Sepolia, attempting to switch...");
interface EthereumProvider {
request: (args: {
method: string;
params?: unknown[]
}) => Promise<unknown>;
}
// Get the provider from window.ethereum
const provider = window.ethereum as EthereumProvider | undefined;
if (!provider) {
console.warn("No Ethereum provider found");
return false;
}
try {
// Request network switch
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${LINEA_SEPOLIA_CONFIG.chainId.toString(16)}` }],
});
console.log("Successfully switched to Linea Sepolia");
return true;
} catch (switchError: unknown) {
console.error("Error switching network:", switchError);
return false;
}
} catch (err) {
console.error("Error checking or switching network:", err);
return false;
}
};
const initializeRLN = async () => {
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
try {
setError(null);
if (!rln) {
console.log(`Creating RLN ${implementation} instance...`);
try {
// Use our factory to create the appropriate implementation
const rlnInstance = await createRLNImplementation(implementation);
console.log("RLN instance created successfully:", !!rlnInstance);
setRln(rlnInstance);
setIsInitialized(true);
console.log("isInitialized set to true");
// Update rate limits to match contract requirements
setRateMinLimit(20); // Contract minimum (RATE_LIMIT_PARAMS.MIN_RATE)
setRateMaxLimit(600); // Contract maximum (RATE_LIMIT_PARAMS.MAX_RATE)
} catch (createErr) {
console.error("Error creating RLN instance:", createErr);
throw createErr;
}
} else {
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);
console.log("RLN started successfully, isStarted set to true");
} catch (startErr) {
console.error("Error starting RLN:", startErr);
throw startErr;
}
} else {
console.log("Skipping RLN start because:", {
isConnected,
hasSigner: !!signer,
hasRln: !!rln,
isAlreadyStarted: isStarted
});
}
} catch (err) {
console.error('Error in initializeRLN:', err);
setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
}
};
const registerMembership = async (rateLimit: number) => {
console.log("registerMembership called with rate limit:", rateLimit);
if (!rln || !isStarted) {
return { success: false, error: 'RLN not initialized or not started' };
}
if (!signer) {
return { success: false, error: 'No signer available' };
}
try {
// Validate rate limit
if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) {
return {
success: false,
error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}`
};
}
// Ensure we're on the correct network
const isOnLineaSepolia = await ensureLineaSepoliaNetwork();
if (!isOnLineaSepolia) {
console.warn("Could not switch to Linea Sepolia network. Registration may fail.");
}
// Get user address and contract address
const userAddress = await signer.getAddress();
if (!rln.contract || !rln.contract.address) {
return { success: false, error: "RLN contract address not available. Cannot proceed with registration." };
}
const contractAddress = rln.contract.address;
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
// Create token contract instance
const tokenContract = new ethers.Contract(
tokenAddress,
ERC20_ABI,
signer
);
// Check token balance
const tokenBalance = await tokenContract.balanceOf(userAddress);
if (tokenBalance.isZero()) {
return { success: false, error: "You need tokens to register a membership. Your token balance is zero." };
}
// Check and approve token allowance if needed
const currentAllowance = await tokenContract.allowance(userAddress, contractAddress);
// Get membership fee - implementation may differ between standard and light
const membershipFee = await rln.contract.membershipFee?.() || ethers.utils.parseEther("0.01");
if (currentAllowance.lt(membershipFee)) {
console.log("Approving token allowance...");
try {
const approveTx = await tokenContract.approve(contractAddress, membershipFee);
await approveTx.wait();
console.log("Token allowance approved");
} catch (approveErr) {
console.error("Error approving token allowance:", approveErr);
return { success: false, error: "Failed to approve token allowance for membership registration." };
}
} else {
console.log("Token allowance already sufficient");
}
// Register membership
console.log("Registering membership with rate limit:", rateLimit);
try {
// Both implementations use registerMembership with a signature
// Generate signature for identity
const message = `Sign this message to generate your RLN credentials ${Date.now()}`;
const signature = await signer.signMessage(message);
// Call registerMembership with the signature
const credentials = await rln.registerMembership({
signature: signature
}) as unknown as KeystoreEntity;
// Validate credentials
if (!credentials) {
throw new Error("Failed to register membership: No credentials returned");
}
if (!credentials.identity) {
throw new Error("Failed to register membership: Missing identity information");
}
if (!credentials.membership) {
throw new Error("Failed to register membership: Missing membership information");
}
console.log("Membership registered successfully");
// 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);
}
return {
success: true,
credentials: credentials
};
} catch (registerErr) {
console.error("Error registering membership:", registerErr);
return {
success: false,
error: registerErr instanceof Error ? registerErr.message : "Failed to register membership"
};
}
} catch (err) {
console.error("Error in registerMembership:", err);
return {
success: false,
error: err instanceof Error ? err.message : "An unknown error occurred during registration"
};
}
};
// Create the context value
const contextValue: RLNContextType = {
rln,
isInitialized,
isStarted,
error,
initializeRLN,
registerMembership,
rateMinLimit,
rateMaxLimit
};
return (
<RLNUnifiedContext.Provider value={contextValue}>
{children}
</RLNUnifiedContext.Provider>
);
}
// Create a hook to use the context
export function useRLN() {
const context = useContext(RLNUnifiedContext);
if (context === undefined) {
throw new Error('useRLN must be used within a RLNUnifiedProvider');
}
return context;
}

View File

@ -0,0 +1,91 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln';
import { useRLNImplementation } from './RLNImplementationContext';
// Define a dummy context for when neither implementation is available
interface RLNContextType {
rln: RLNInstance | RLNLightInstance | null;
isInitialized: boolean;
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>;
rateMinLimit: number;
rateMaxLimit: number;
}
// Create a dummy context with default values
const dummyRLNContext: RLNContextType = {
rln: null,
isInitialized: false,
isStarted: false,
error: 'RLN context not initialized',
initializeRLN: async () => { throw new Error('RLN context not initialized'); },
registerMembership: async () => ({ success: false, error: 'RLN context not initialized' }),
rateMinLimit: 20,
rateMaxLimit: 600
};
// Create a context to store the selected RLN implementation
const UnifiedRLNContext = createContext<RLNContextType>(dummyRLNContext);
// Create a provider component that will fetch the appropriate implementation
export function UnifiedRLNProvider({ children }: { children: ReactNode }) {
const { implementation } = useRLNImplementation();
const [contextValue, setContextValue] = useState<RLNContextType>(dummyRLNContext);
useEffect(() => {
// This effect will run when the implementation changes
// We'll dynamically import the appropriate context module
const fetchContext = async () => {
try {
if (implementation === 'standard') {
// Import the standard RLN context
const standardModule = await import('./RLNContext');
const standardRLNContext = standardModule.RLNContext;
if (standardRLNContext) {
// Access the context value
const contextConsumer = standardRLNContext.Consumer;
contextConsumer(value => {
if (value) {
setContextValue(value);
}
});
}
} else {
// Import the light RLN context
const lightModule = await import('./RLNLightContext');
const lightRLNContext = lightModule.RLNContext;
if (lightRLNContext) {
// Access the context value
const contextConsumer = lightRLNContext.Consumer;
contextConsumer(value => {
if (value) {
setContextValue(value);
}
});
}
}
} catch (error) {
console.error('Error loading RLN context:', error);
}
};
fetchContext();
}, [implementation]);
return (
<UnifiedRLNContext.Provider value={contextValue}>
{children}
</UnifiedRLNContext.Provider>
);
}
// Create a hook to use the unified RLN context
export function useRLN() {
return useContext(UnifiedRLNContext);
}