mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-25 09:03:09 +00:00
feat(keystore-management): membership registration and credentials generation (#115)
This commit is contained in:
parent
cdd74f9f56
commit
21692759ce
@ -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
|
||||
|
||||
977
examples/keystore-management/package-lock.json
generated
977
examples/keystore-management/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'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's signatures produce values that aren'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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
) : (
|
||||
|
||||
297
examples/keystore-management/src/contexts/RLNContext.tsx
Normal file
297
examples/keystore-management/src/contexts/RLNContext.tsx
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user