feat(keystore-management): membership registration and credentials generation (#115)

This commit is contained in:
Danish Arora 2025-03-10 02:20:30 +05:30 committed by GitHub
parent cdd74f9f56
commit 21692759ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1147 additions and 571 deletions

View File

@ -4,14 +4,15 @@ A simple Next.js application to manage Waku RLN keystores.
## Overview
This application provides an interface for managing keystores for Waku's rate-limiting nullifier (RLN) functionality. It integrates with MetaMask for wallet connectivity and demonstrates how to work with the Waku RLN library.
This application provides an interface for managing keystores for Waku's rate-limiting nullifier (RLN) functionality. It integrates with MetaMask for wallet connectivity.
## Features
- Connect to MetaMask wallet
- View wallet information including address, network, and balance
- Support for Sepolia testnet
- Support for Linea Sepolia testnet only
- Keystore management functionality
- Token approval for RLN membership registration
## Getting Started
@ -19,27 +20,36 @@ This application provides an interface for managing keystores for Waku's rate-li
```bash
npm install
# or
yarn
```
2. Run the development server:
```bash
npm run dev
# or
yarn dev
```
3. Open [http://localhost:3000](http://localhost:3000) with your browser.
4. Connect your MetaMask wallet (Sepolia testnet is supported).
4. Connect your MetaMask wallet (Linea Sepolia testnet is required).
## Technologies
## Linea Sepolia Network
This application is configured to use ONLY the Linea Sepolia testnet. If you don't have Linea Sepolia configured in your MetaMask, the application will help you add it with the following details:
- **Network Name**: Linea Sepolia Testnet
- **RPC URL**: https://rpc.sepolia.linea.build
- **Chain ID**: 59141
- **Currency Symbol**: ETH
- **Block Explorer URL**: https://sepolia.lineascan.build
You can get Linea Sepolia testnet ETH from the [Linea Faucet](https://faucet.goerli.linea.build/).
## RLN Membership Registration
When registering for RLN membership, you'll need to complete two transactions:
1. **Token Approval**: First, you'll need to approve the RLN contract to spend tokens on your behalf. This is a one-time approval.
2. **Membership Registration**: After approval, the actual membership registration transaction will be submitted.
If you encounter an "ERC20: insufficient allowance" error, it means the token approval transaction was not completed successfully. Please try again and make sure to approve the token spending in your wallet.
- Next.js
- React
- TypeScript
- TailwindCSS
- Waku RLN library
- Ethers.js

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { WalletProvider } from "../contexts/WalletContext";
import { RLNProvider } from "../contexts/RLNContext";
import { Header } from "../components/Header";
const geistSans = Geist({
@ -30,12 +31,14 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<WalletProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow">
{children}
</main>
</div>
<RLNProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow">
{children}
</main>
</div>
</RLNProvider>
</WalletProvider>
</body>
</html>

View File

@ -1,10 +1,30 @@
import RLNMembershipRegistration from '../components/RLNMembershipRegistration';
import { WalletInfo } from '../components/WalletInfo';
export default function Home() {
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-md dark:bg-gray-800 p-6">
<h2 className="text-2xl font-bold text-center text-gray-900 dark:text-white">Waku Keystore Management</h2>
{/* Your keystore management content will go here */}
<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">
{/* Wallet Information Section */}
<div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Wallet Connection</h3>
<WalletInfo />
</div>
{/* RLN Membership Registration Section */}
<div>
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">RLN Membership</h3>
<p className="mb-4 text-gray-700 dark:text-gray-300">
Register a new RLN membership to participate in Waku RLN Relay without exposing your private key on your node.
Set your desired rate limit for messages per epoch.
</p>
<RLNMembershipRegistration />
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,321 @@
"use client";
import { useState } from 'react';
import { useRLN } from '../contexts/RLNContext';
import { useWallet } from '../contexts/WalletContext';
import { DecryptedCredentials } from '@waku/rln';
export default function RLNMembershipRegistration() {
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN();
const { isConnected, address, chainId } = useWallet();
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
const [isRegistering, setIsRegistering] = useState(false);
const [isInitializing, setIsInitializing] = useState(false);
const [registrationResult, setRegistrationResult] = useState<{
success?: boolean;
error?: string;
txHash?: string;
warning?: string;
credentials?: DecryptedCredentials;
}>({});
const isLineaSepolia = chainId === 59141;
const handleRateLimitChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value);
setRateLimit(isNaN(value) ? rateMinLimit : value);
};
const handleInitializeRLN = async () => {
setIsInitializing(true);
try {
await initializeRLN();
} catch (err) {
console.error("Error initializing RLN:", err);
} finally {
setIsInitializing(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isConnected) {
setRegistrationResult({ success: false, error: 'Please connect your wallet first' });
return;
}
if (!isInitialized || !isStarted) {
setRegistrationResult({ success: false, error: 'RLN is not initialized' });
return;
}
if (!isLineaSepolia) {
setRegistrationResult({ success: false, error: 'Please switch to Linea Sepolia network' });
return;
}
setIsRegistering(true);
setRegistrationResult({});
try {
setRegistrationResult({
success: undefined,
warning: 'Please check your wallet to sign the registration message.'
});
const result = await registerMembership(rateLimit);
setRegistrationResult({
...result,
credentials: result.credentials
});
} catch (error) {
setRegistrationResult({
success: false,
error: error instanceof Error ? error.message : 'Registration failed'
});
} finally {
setIsRegistering(false);
}
};
return (
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
RLN Membership Registration
</h2>
{/* Network Warning */}
{isConnected && !isLineaSepolia && (
<div className="mb-4 bg-orange-50 dark:bg-orange-900 p-4 rounded-lg">
<p className="text-sm text-orange-700 dark:text-orange-400">
<strong>Warning:</strong> You are not connected to Linea Sepolia network. Please switch networks to register.
</p>
</div>
)}
{/* Informational Box */}
<div className="mb-6 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
<h3 className="text-md font-semibold text-blue-800 dark:text-blue-300 mb-2">
About RLN Membership on Linea Sepolia
</h3>
<p className="text-sm text-blue-700 dark:text-blue-400 mb-2">
RLN (Rate Limiting Nullifier) membership allows you to participate in Waku RLN Relay with rate limiting protection,
without exposing your private keys on your node.
</p>
<p className="text-sm text-blue-700 dark:text-blue-400 mb-2">
This application is configured to use the <strong>Linea Sepolia</strong> testnet for RLN registrations.
</p>
<p className="text-sm text-blue-700 dark:text-blue-400">
When you register, your wallet will sign a message that will be used to generate a cryptographic identity
for your membership. This allows your node to prove it has permission to send messages without revealing your identity.
</p>
</div>
{/* Initialization Status */}
<div className="mb-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
RLN Status:
<span className={isInitialized && isStarted ? "text-green-600 ml-2" : "text-amber-600 ml-2"}>
{isInitialized && isStarted ? "Ready" : "Not Initialized"}
</span>
</p>
</div>
{isConnected && (!isInitialized || !isStarted) && (
<button
onClick={handleInitializeRLN}
disabled={isInitializing || !isLineaSepolia}
className={`px-3 py-1 text-xs rounded-md transition-colors ${
isInitializing
? "bg-gray-400 text-gray-700 cursor-not-allowed"
: isLineaSepolia
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-400 text-gray-700 cursor-not-allowed"
}`}
>
{isInitializing ? "Initializing..." : "Initialize RLN"}
</button>
)}
</div>
{error && (
<p className="text-xs text-red-600 mt-1">{error}</p>
)}
</div>
{isInitialized && !isStarted && (
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-400">
<strong>Note:</strong> RLN is partially initialized. You can still proceed with registration, but some advanced features might be limited.
</p>
</div>
)}
{!isConnected ? (
<div className="text-amber-600 dark:text-amber-400 mb-4">
Please connect your wallet to register a membership
</div>
) : !isInitialized || !isStarted ? (
<div className="text-amber-600 dark:text-amber-400 mb-4">
Please initialize RLN before registering a membership
</div>
) : (
<>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="rateLimit"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Rate Limit (messages per epoch)
</label>
<div className="flex items-center">
<input
type="range"
id="rateLimit"
name="rateLimit"
min={rateMinLimit}
max={rateMaxLimit}
value={rateLimit}
onChange={handleRateLimitChange}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
<span className="ml-3 w-12 text-gray-700 dark:text-gray-300">{rateLimit}</span>
</div>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Select a rate limit between {rateMinLimit} and {rateMaxLimit}
</p>
</div>
{address && (
<div className="text-sm text-gray-600 dark:text-gray-400 p-3 border border-gray-200 dark:border-gray-700 rounded-md">
<p className="font-medium mb-1">Registration Details:</p>
<p>Connected Address: {address.slice(0, 8)}...{address.slice(-6)}</p>
<p className="mt-1">When you register, your wallet will sign a secure message containing a random nonce. This signature will be used to generate your RLN credentials without exposing your private key.</p>
</div>
)}
<button
type="submit"
disabled={isRegistering || !isInitialized || !isStarted}
className={`w-full py-2 px-4 rounded-md text-white font-medium
${isRegistering || !isInitialized || !isStarted
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
}`}
>
{isRegistering ? 'Registering...' : 'Register Membership'}
</button>
</form>
{registrationResult.warning && registrationResult.success === undefined && (
<div className="mt-4 p-3 bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded">
<p className="font-medium">Important:</p>
<p className="text-sm mt-1">{registrationResult.warning}</p>
<div className="text-sm mt-1">
You&apos;ll need to sign a message with your wallet to complete the registration.
</div>
</div>
)}
{registrationResult.success === true && (
<div className="mt-4 p-3 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
<p className="font-medium">Registration submitted!</p>
{registrationResult.txHash && (
<div>
<p className="text-sm mt-1 break-all">
{registrationResult.txHash}
</p>
{registrationResult.txHash.startsWith('0x') && (
<p className="mt-2">
<a
href={`https://sepolia.lineascan.build/tx/${registrationResult.txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline"
>
View on Linea Sepolia Explorer
</a>
</p>
)}
</div>
)}
{registrationResult.warning && (
<p className="text-sm mt-2 text-yellow-600 dark:text-yellow-300">
<strong>Note:</strong> {registrationResult.warning}
</p>
)}
<p className="text-sm mt-2">
Your RLN membership is now registered and can be used with your Waku node.
</p>
{registrationResult.credentials && (
<div className="mt-3 p-3 bg-gray-100 dark:bg-gray-800 rounded-md">
<p className="font-medium mb-2">Your RLN Credentials:</p>
<div className="text-xs font-mono overflow-auto">
<h4 className="font-semibold mt-2 mb-1">Identity:</h4>
<p className="mb-1">
<span className="font-semibold">ID Commitment:</span> {Buffer.from(registrationResult.credentials.identity.IDCommitment).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Secret Hash:</span> {Buffer.from(registrationResult.credentials.identity.IDSecretHash).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Nullifier:</span> {Buffer.from(registrationResult.credentials.identity.IDNullifier).toString('hex')}
</p>
<p className="mb-3">
<span className="font-semibold">ID Trapdoor:</span> {Buffer.from(registrationResult.credentials.identity.IDTrapdoor).toString('hex')}
</p>
<h4 className="font-semibold mt-3 mb-1">Membership:</h4>
<p className="mb-1">
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
</p>
<p className="mb-1">
<span className="font-semibold">Contract Address:</span> {registrationResult.credentials.membership.address}
</p>
<p className="mb-1">
<span className="font-semibold">Tree Index:</span> {registrationResult.credentials.membership.treeIndex}
</p>
</div>
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
These credentials are your proof of membership. Store them securely.
</p>
</div>
)}
</div>
)}
{registrationResult.success === false && (
<div className="mt-4 p-3 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded">
<p className="font-medium">Registration failed</p>
<p className="text-sm mt-1">{registrationResult.error}</p>
{registrationResult.error?.includes("field") && (
<div className="mt-2 text-sm">
<p>
This is a mathematical constraint in the zero-knowledge proof system.
Your wallet&apos;s signatures produce values that aren&apos;t compatible with the RLN cryptographic system.
</p>
<p className="mt-2 font-medium">Recommended solution:</p>
<p>Please try using a different wallet address for registration. Different wallet addresses
generate different signatures, and some are more compatible with the RLN cryptographic system.</p>
</div>
)}
</div>
)}
</>
)}
{/* Debug Info (For Development) */}
<div className="mt-6 p-3 border border-gray-200 dark:border-gray-700 rounded text-xs">
<p className="font-semibold">Debug Info:</p>
<p>Wallet Connected: {isConnected ? "Yes" : "No"}</p>
<p>RLN Initialized: {isInitialized ? "Yes" : "No"}</p>
<p>RLN Started: {isStarted ? "Yes" : "No"}</p>
<p>Min Rate: {rateMinLimit}, Max Rate: {rateMaxLimit}</p>
</div>
</div>
);
}

View File

@ -7,13 +7,13 @@ function getNetworkName(chainId: number | null): string {
if (!chainId) return 'Unknown';
switch (chainId) {
case 11155111: return 'Sepolia Testnet (Supported)';
case 59141: return 'Linea Sepolia (Supported)';
default: return `Unsupported Network (Chain ID: ${chainId})`;
}
}
// Sepolia Chain ID
const SEPOLIA_CHAIN_ID = '0xaa36a7'; // 11155111 in hex
// Linea Sepolia Chain ID
const LINEA_SEPOLIA_CHAIN_ID = '0xe705'; // 59141 in hex
// Define interface for provider errors
interface ProviderRpcError extends Error {
@ -24,18 +24,18 @@ interface ProviderRpcError extends Error {
export function WalletInfo() {
const { isConnected, address, balance, chainId, connectWallet, disconnectWallet, error } = useWallet();
// Function to switch to Sepolia network
const switchToSepolia = async () => {
// Function to switch to Linea Sepolia network
const switchToLineaSepolia = async () => {
if (!window.ethereum) {
console.error("MetaMask not installed");
return;
}
try {
// Try to switch to Sepolia
// Try to switch to Linea Sepolia
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: SEPOLIA_CHAIN_ID }],
params: [{ chainId: LINEA_SEPOLIA_CHAIN_ID }],
});
} catch (err) {
// If the error code is 4902, the chain hasn't been added to MetaMask
@ -46,29 +46,29 @@ export function WalletInfo() {
method: 'wallet_addEthereumChain',
params: [
{
chainId: SEPOLIA_CHAIN_ID,
chainName: 'Sepolia Testnet',
chainId: LINEA_SEPOLIA_CHAIN_ID,
chainName: 'Linea Sepolia Testnet',
nativeCurrency: {
name: 'Sepolia ETH',
name: 'Linea Sepolia ETH',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: ['https://sepolia.infura.io/v3/'],
blockExplorerUrls: ['https://sepolia.etherscan.io'],
rpcUrls: ['https://linea-sepolia.infura.io/v3/', 'https://rpc.sepolia.linea.build'],
blockExplorerUrls: ['https://sepolia.lineascan.build'],
},
],
});
} catch (addError) {
console.error("Error adding Sepolia chain", addError);
console.error("Error adding Linea Sepolia chain", addError);
}
} else {
console.error("Error switching to Sepolia chain", providerError);
console.error("Error switching to Linea Sepolia chain", providerError);
}
}
};
// Check if user is on unsupported network
const isUnsupportedNetwork = isConnected && chainId !== 11155111;
const isUnsupportedNetwork = isConnected && chainId !== 59141;
return (
<div className="bg-white rounded-lg shadow-sm dark:bg-gray-800 p-3">
@ -110,12 +110,14 @@ export function WalletInfo() {
</div>
{isUnsupportedNetwork && (
<button
onClick={switchToSepolia}
className="w-full mt-1 px-2 py-1 text-xs bg-orange-500 text-white rounded-md hover:bg-orange-600 transition-colors"
>
Switch to Sepolia
</button>
<div className="flex flex-col gap-2 mt-1">
<button
onClick={switchToLineaSepolia}
className="w-full px-2 py-1 text-xs bg-orange-500 text-white rounded-md hover:bg-orange-600 transition-colors"
>
Switch to Linea Sepolia
</button>
</div>
)}
</div>
) : (

View File

@ -0,0 +1,297 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createRLN, DecryptedCredentials, RLNInstance } 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: 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<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 = await createRLN();
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;
}