mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-03 06:13:08 +00:00
chore: cleanup and improvements
This commit is contained in:
parent
36a8ca0b0c
commit
c33ebfb874
56
examples/keystore-management/package-lock.json
generated
56
examples/keystore-management/package-lock.json
generated
@ -8,7 +8,7 @@
|
||||
"name": "waku-keystore-management",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@waku/rln": "0.1.5-6198efb.0",
|
||||
"@waku/rln": "0.1.5-ad0e277.0",
|
||||
"next": "15.1.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
@ -2309,16 +2309,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/core": {
|
||||
"version": "0.0.35-6198efb.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.35-6198efb.0.tgz",
|
||||
"integrity": "sha512-9ta28XPCs9mOT9DmaewxHuZyWVME5bqRsF8Hardrd4QaHVLEaE5nlqi1nF7PoxdwF+l+XmZ2x/gqwgDee6qPwg==",
|
||||
"version": "0.0.35-ad0e277.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.35-ad0e277.0.tgz",
|
||||
"integrity": "sha512-IfgWE/Kc8jpcmO6PGsLzySHAWwoDQtIY9gsxWtoqPnll1cE9ylxfGVh0je4o58E5F0XGw+wm9TdD149hq30cJQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@libp2p/ping": "2.0.1",
|
||||
"@waku/enr": "0.0.29-6198efb.0",
|
||||
"@waku/interfaces": "0.0.30-6198efb.0",
|
||||
"@waku/proto": "0.0.10-6198efb.0",
|
||||
"@waku/utils": "0.0.23-6198efb.0",
|
||||
"@waku/enr": "0.0.29-ad0e277.0",
|
||||
"@waku/interfaces": "0.0.30-ad0e277.0",
|
||||
"@waku/proto": "0.0.10-ad0e277.0",
|
||||
"@waku/utils": "0.0.23-ad0e277.0",
|
||||
"debug": "^4.3.4",
|
||||
"it-all": "^3.0.4",
|
||||
"it-length-prefixed": "^9.0.4",
|
||||
@ -2356,9 +2356,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/enr": {
|
||||
"version": "0.0.29-6198efb.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.29-6198efb.0.tgz",
|
||||
"integrity": "sha512-vInEV3d2LV1fakhXIACYn4PBZwYLZNpKYqDMU1YJQ3yoQv6nHl0emeGz7GviyXh+2qyLFjEKzesqhLht41kP/A==",
|
||||
"version": "0.0.29-ad0e277.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.29-ad0e277.0.tgz",
|
||||
"integrity": "sha512-3IoFVU3XX7rEge3VFMl5r3wc0XVnZTvheRuFYSm0HgPrMls23PmAd7IdFLOMq5e9CGWAbWx0fdT3StzIdN2PbQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ethersproject/rlp": "^5.7.0",
|
||||
@ -2366,7 +2366,7 @@
|
||||
"@libp2p/peer-id": "^5.0.1",
|
||||
"@multiformats/multiaddr": "^12.0.0",
|
||||
"@noble/secp256k1": "^1.7.1",
|
||||
"@waku/utils": "0.0.23-6198efb.0",
|
||||
"@waku/utils": "0.0.23-ad0e277.0",
|
||||
"debug": "^4.3.4",
|
||||
"js-sha3": "^0.9.2"
|
||||
},
|
||||
@ -2383,21 +2383,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/interfaces": {
|
||||
"version": "0.0.30-6198efb.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.30-6198efb.0.tgz",
|
||||
"integrity": "sha512-K+DIqbSFct0/1RYg2QdpviXOps9mDo04quW7jvlUvKP9qVBDvDUBlZrvfw+kIHr77g0DzyYLganuLv/Ocd13rw==",
|
||||
"version": "0.0.30-ad0e277.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.30-ad0e277.0.tgz",
|
||||
"integrity": "sha512-Xg0vupz9y+PGNKVsJ3AodpcxWLkGafLPser4H/0SYvwiBKR0doEs08wfRdXY83WID1GZPVkA3jcIm0rAnPZWSw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@waku/proto": "0.0.10-6198efb.0"
|
||||
"@waku/proto": "0.0.10-ad0e277.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/proto": {
|
||||
"version": "0.0.10-6198efb.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.10-6198efb.0.tgz",
|
||||
"integrity": "sha512-PQktYd1wgQ9Ihdyf+u950ugJL3LUfLh0kEvEHlSLhD9fOsipaegPBR7EsK78cecpZP1hBxuOZwjr5coO3byWlg==",
|
||||
"version": "0.0.10-ad0e277.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.10-ad0e277.0.tgz",
|
||||
"integrity": "sha512-F4RKTcdX3v+E3/TxdINPWpsQEjst1VXlslJdNXC1LXPRqd87S7r2e47PXafM7TGBdOitIIPw8Xf4MxbPFGa1Gg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"protons-runtime": "^5.4.0"
|
||||
@ -2407,15 +2407,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/rln": {
|
||||
"version": "0.1.5-6198efb.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.5-6198efb.0.tgz",
|
||||
"integrity": "sha512-v/x79fKH6M1E4qjpRWdGErSGt6p7LBLc9tt7G/ZFWJY8afBHbtOH3JrJ0ZqDc4oZsQbRRMGVLJ6WbJIDkQtoYA==",
|
||||
"version": "0.1.5-ad0e277.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.5-ad0e277.0.tgz",
|
||||
"integrity": "sha512-2QxvEhZoZjsP0J8wg6BAv7GTvDMeOzD7a46WR6M3Gld39TNf+2hQMzED5I91QxjcoAvMMbphD8FiH4X5tqzM5g==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chainsafe/bls-keystore": "3.0.0",
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"@waku/core": "0.0.35-6198efb.0",
|
||||
"@waku/utils": "0.0.23-6198efb.0",
|
||||
"@waku/core": "0.0.35-ad0e277.0",
|
||||
"@waku/utils": "0.0.23-ad0e277.0",
|
||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
||||
"chai": "^5.1.2",
|
||||
"chai-as-promised": "^8.0.1",
|
||||
@ -2432,13 +2432,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/utils": {
|
||||
"version": "0.0.23-6198efb.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.23-6198efb.0.tgz",
|
||||
"integrity": "sha512-K7RNG8ngHYpyVwIHeXaIAi52FO1fJM/h4gurxII331segXrnSXSYtnv3jwO3HDnoTMX+o21ddM0VfyzxilM3NQ==",
|
||||
"version": "0.0.23-ad0e277.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.23-ad0e277.0.tgz",
|
||||
"integrity": "sha512-r3nef/L4fZx2GA7byUAOFLSjhVXuH22FFgmEp7+/jzbAjKAcVIaCfG2M09SnNDbxc5uViKeEE6ODIfw+k+2rZw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.3.2",
|
||||
"@waku/interfaces": "0.0.30-6198efb.0",
|
||||
"@waku/interfaces": "0.0.30-ad0e277.0",
|
||||
"chai": "^4.3.10",
|
||||
"debug": "^4.3.4",
|
||||
"uint8arrays": "^5.0.1"
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/rln": "0.1.5-6198efb.0",
|
||||
"@waku/rln": "0.1.5-ad0e277.0",
|
||||
"next": "15.1.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
@ -18,4 +18,4 @@ body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Inter } from 'next/font/google'
|
||||
import "./globals.css";
|
||||
import { WalletProvider } from "../contexts/WalletContext";
|
||||
import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2";
|
||||
import { RLNImplementationProvider } from "../contexts/RLNImplementationContext";
|
||||
import { KeystoreProvider } from "../contexts/KeystoreContext";
|
||||
import { WalletProvider, RLNImplementationProvider, KeystoreProvider, RLNProvider } from "../contexts/index";
|
||||
import { Header } from "../components/Header";
|
||||
import { AppStateProvider } from "../contexts/AppStateContext";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
@ -30,23 +23,25 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${inter.variable} antialiased`}
|
||||
>
|
||||
<WalletProvider>
|
||||
<RLNImplementationProvider>
|
||||
<KeystoreProvider>
|
||||
<RLNUnifiedProvider>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</RLNUnifiedProvider>
|
||||
</KeystoreProvider>
|
||||
</RLNImplementationProvider>
|
||||
</WalletProvider>
|
||||
<AppStateProvider>
|
||||
<WalletProvider>
|
||||
<RLNImplementationProvider>
|
||||
<KeystoreProvider>
|
||||
<RLNProvider>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</RLNProvider>
|
||||
</KeystoreProvider>
|
||||
</RLNImplementationProvider>
|
||||
</WalletProvider>
|
||||
</AppStateProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,50 +1,15 @@
|
||||
import RLNMembershipRegistration from '../components/RLNMembershipRegistration';
|
||||
import { WalletInfo } from '../components/WalletInfo';
|
||||
import { RLNImplementationToggle } from '../components/RLNImplementationToggle';
|
||||
import KeystoreManager from '../components/KeystoreManager';
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Layout } from '../components/Layout';
|
||||
import { MembershipRegistration } from '../components/Tabs/MembershipTab/MembershipRegistration';
|
||||
import { KeystoreManagement } from '../components/Tabs/KeystoreTab/KeystoreManagement';
|
||||
|
||||
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 mb-6">Waku Keystore Management</h2>
|
||||
|
||||
<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 */}
|
||||
<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>
|
||||
|
||||
{/* Keystore Management Section */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Keystore Management</h3>
|
||||
<p className="mb-4 text-gray-700 dark:text-gray-300">
|
||||
Export your keystore credentials to use them with your Waku node or import existing credentials.
|
||||
Keep your keystores safe as they contain your membership information.
|
||||
</p>
|
||||
<KeystoreManager />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Layout>
|
||||
<MembershipRegistration />
|
||||
<KeystoreManagement />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,14 +5,12 @@ import { WalletInfo } from './WalletInfo';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-10 bg-white dark:bg-gray-900 shadow-sm">
|
||||
<div className="container mx-auto px-4 py-3 flex justify-between items-center">
|
||||
<header className="bg-gray-900 border-b border-gray-800">
|
||||
<div className="container mx-auto px-6 h-16 flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Waku Keystore Management</h1>
|
||||
</div>
|
||||
<div className="w-80">
|
||||
<WalletInfo />
|
||||
<h1 className="text-xl font-medium text-white">Waku Keystore Management</h1>
|
||||
</div>
|
||||
<WalletInfo />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
51
examples/keystore-management/src/components/Layout.tsx
Normal file
51
examples/keystore-management/src/components/Layout.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import React, { Children, isValidElement } from 'react';
|
||||
import { TabNavigation, TabItem } from './Tabs/TabNavigation';
|
||||
import { useAppState } from '../contexts/AppStateContext';
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
id: 'membership',
|
||||
label: 'Membership Registration',
|
||||
},
|
||||
{
|
||||
id: 'keystore',
|
||||
label: 'Keystore Management',
|
||||
},
|
||||
];
|
||||
|
||||
const componentToTabId: Record<string, string> = {
|
||||
MembershipRegistration: 'membership',
|
||||
KeystoreManagement: 'keystore',
|
||||
};
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { activeTab, setActiveTab } = useAppState();
|
||||
const childrenArray = Children.toArray(children);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||
<main className="container mx-auto px-4 py-6">
|
||||
<TabNavigation
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
<div className="mt-6">
|
||||
{childrenArray.map((child) => {
|
||||
if (isValidElement(child) && typeof child.type === 'function') {
|
||||
const componentName = child.type.name;
|
||||
const tabId = componentToTabId[componentName];
|
||||
|
||||
if (tabId === activeTab) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,50 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useRLNImplementation, RLNImplementationType } from '../contexts/RLNImplementationContext';
|
||||
import React from 'react';
|
||||
import { useRLNImplementation } from '../contexts/rln';
|
||||
|
||||
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">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
RLN Implementation
|
||||
</label>
|
||||
<div className="flex space-x-4">
|
||||
<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 ${
|
||||
onClick={() => setImplementation('light')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
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'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setImplementation('standard')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
implementation === 'standard'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
Standard
|
||||
</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>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{implementation === 'light'
|
||||
? 'Light implementation, without Zerokit. Instant initalisation.'
|
||||
: 'Standard implementation, with Zerokit. Initialisation takes 10-15 seconds for WASM module'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,398 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRLN } from '../contexts/RLNUnifiedContext2';
|
||||
import { useWallet } from '../contexts/WalletContext';
|
||||
import { KeystoreEntity } 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 [saveToKeystore, setSaveToKeystore] = useState(true);
|
||||
const [keystorePassword, setKeystorePassword] = useState('');
|
||||
const [registrationResult, setRegistrationResult] = useState<{
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
txHash?: string;
|
||||
warning?: string;
|
||||
credentials?: KeystoreEntity;
|
||||
keystoreHash?: string;
|
||||
}>({});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Validate keystore password if saving to keystore
|
||||
if (saveToKeystore && keystorePassword.length < 8) {
|
||||
setRegistrationResult({
|
||||
success: false,
|
||||
error: 'Keystore password must be at least 8 characters long'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRegistering(true);
|
||||
setRegistrationResult({});
|
||||
|
||||
try {
|
||||
setRegistrationResult({
|
||||
success: undefined,
|
||||
warning: 'Please check your wallet to sign the registration message.'
|
||||
});
|
||||
|
||||
// Pass save options if saving to keystore
|
||||
const saveOptions = saveToKeystore ? { password: keystorePassword } : undefined;
|
||||
|
||||
const result = await registerMembership(rateLimit, saveOptions);
|
||||
|
||||
setRegistrationResult({
|
||||
...result,
|
||||
credentials: result.credentials
|
||||
});
|
||||
|
||||
// Clear password field after successful registration
|
||||
if (result.success) {
|
||||
setKeystorePassword('');
|
||||
}
|
||||
} catch (error) {
|
||||
setRegistrationResult({
|
||||
success: false,
|
||||
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>
|
||||
|
||||
{/* Keystore Options */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="saveToKeystore"
|
||||
checked={saveToKeystore}
|
||||
onChange={(e) => setSaveToKeystore(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
htmlFor="saveToKeystore"
|
||||
className="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Save credentials to keystore
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{saveToKeystore && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="keystorePassword"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Keystore Password (min 8 characters)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="keystorePassword"
|
||||
autoComplete='password'
|
||||
value={keystorePassword}
|
||||
onChange={(e) => setKeystorePassword(e.target.value)}
|
||||
placeholder="Enter password to encrypt your keystore"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
minLength={8}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
The password will be used to encrypt your RLN credentials in the keystore.
|
||||
You will need this password to decrypt your credentials later.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{address && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 p-3 border border-gray-200 dark:border-gray-700 rounded-md">
|
||||
<p className="font-medium mb-1">Registration Details:</p>
|
||||
<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 || (saveToKeystore && keystorePassword.length < 8)}
|
||||
className={`w-full py-2 px-4 rounded-md text-white font-medium
|
||||
${isRegistering || !isInitialized || !isStarted || (saveToKeystore && keystorePassword.length < 8)
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
<p className="mb-1">
|
||||
<span className="font-semibold">Rate Limit:</span> {registrationResult.credentials.membership.rateLimit}
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
{registrationResult.keystoreHash && (
|
||||
<p className="text-sm mt-2">
|
||||
<span className="font-medium">Credentials saved to keystore!</span>
|
||||
<br />
|
||||
Hash: {registrationResult.keystoreHash.slice(0, 10)}...{registrationResult.keystoreHash.slice(-8)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<p>Current Rate Limit: {rateLimit}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { useRLN } from '../contexts/rln';
|
||||
|
||||
export function RLNInitButton() {
|
||||
const { initializeRLN, isInitialized, isStarted, error, isLoading } = useRLN();
|
||||
|
||||
const handleInitialize = async () => {
|
||||
try {
|
||||
await initializeRLN();
|
||||
} catch (err) {
|
||||
console.error('Error initializing RLN:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonText = () => {
|
||||
if (isLoading) return 'Initializing...';
|
||||
if (isInitialized && isStarted) return 'RLN Initialized';
|
||||
return 'Initialize RLN';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<button
|
||||
onClick={handleInitialize}
|
||||
disabled={isLoading || (isInitialized && isStarted)}
|
||||
className={`
|
||||
px-4 py-2 rounded-lg font-medium transition-colors relative
|
||||
${
|
||||
isLoading
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed dark:bg-gray-700 dark:text-gray-400'
|
||||
: isInitialized && isStarted
|
||||
? 'bg-green-600 text-white cursor-default dark:bg-green-500'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isLoading && (
|
||||
<span className="absolute left-2 top-1/2 -translate-y-1/2">
|
||||
<svg className="animate-spin h-5 w-5 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
{isInitialized && isStarted && (
|
||||
<span className="absolute left-2 top-1/2 -translate-y-1/2">
|
||||
<svg className="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
<span className={(isLoading || (isInitialized && isStarted)) ? 'pl-7' : ''}>
|
||||
{getButtonText()}
|
||||
</span>
|
||||
</button>
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useKeystore } from '../../../contexts/keystore';
|
||||
import { useAppState } from '../../../contexts/AppStateContext';
|
||||
import { useRLN } from '../../../contexts/rln';
|
||||
import { saveKeystoreToFile, readKeystoreFromFile } from '../../../utils/fileUtils';
|
||||
|
||||
export function KeystoreManagement() {
|
||||
const {
|
||||
hasStoredCredentials,
|
||||
storedCredentialsHashes,
|
||||
error,
|
||||
exportCredential,
|
||||
importKeystore,
|
||||
removeCredential
|
||||
} = useKeystore();
|
||||
const { setGlobalError } = useAppState();
|
||||
const { isInitialized, isStarted } = useRLN();
|
||||
const [exportPassword, setExportPassword] = useState<string>('');
|
||||
const [selectedCredential, setSelectedCredential] = useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (error) {
|
||||
setGlobalError(error);
|
||||
}
|
||||
}, [error, setGlobalError]);
|
||||
|
||||
const handleExportCredential = async (hash: string) => {
|
||||
try {
|
||||
if (!exportPassword) {
|
||||
setGlobalError('Please enter your keystore password to export');
|
||||
return;
|
||||
}
|
||||
const keystoreJson = await exportCredential(hash, exportPassword);
|
||||
saveKeystoreToFile(keystoreJson, `waku-rln-credential-${hash.slice(0, 8)}.json`);
|
||||
setExportPassword('');
|
||||
setSelectedCredential(null);
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to export credential');
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
try {
|
||||
const keystoreJson = await readKeystoreFromFile();
|
||||
const success = importKeystore(keystoreJson);
|
||||
if (!success) {
|
||||
setGlobalError('Failed to import keystore');
|
||||
}
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to import keystore');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCredential = (hash: string) => {
|
||||
try {
|
||||
removeCredential(hash);
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to remove credential');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
Keystore Management
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Import Action */}
|
||||
<div>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
Import Keystore
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* RLN Status */}
|
||||
{!isInitialized || !isStarted ? (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900 p-4 rounded-lg">
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||
⚠️ Please initialize RLN before managing credentials
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Stored Credentials */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">
|
||||
Stored Credentials
|
||||
</h3>
|
||||
|
||||
{hasStoredCredentials ? (
|
||||
<div className="space-y-4">
|
||||
{storedCredentialsHashes.map((hash) => (
|
||||
<div
|
||||
key={hash}
|
||||
className="p-4 rounded-lg border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<code className="text-sm text-gray-600 dark:text-gray-400 break-all">
|
||||
{hash}
|
||||
</code>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setSelectedCredential(hash === selectedCredential ? null : hash)}
|
||||
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemoveCredential(hash)}
|
||||
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedCredential === hash && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<input
|
||||
type="password"
|
||||
value={exportPassword}
|
||||
onChange={(e) => setExportPassword(e.target.value)}
|
||||
placeholder="Enter keystore password"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleExportCredential(hash)}
|
||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
Export Credential
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No credentials stored
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RLNImplementationToggle } from '../../RLNImplementationToggle';
|
||||
import { KeystoreEntity } from '@waku/rln';
|
||||
import { useAppState } from '@/contexts/AppStateContext';
|
||||
import { useRLN } from '@/contexts/rln/RLNContext';
|
||||
import { useWallet } from '@/contexts/wallet';
|
||||
import { RLNInitButton } from '@/components/RLNinitButton';
|
||||
|
||||
export function MembershipRegistration() {
|
||||
const { setGlobalError } = useAppState();
|
||||
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error } = useRLN();
|
||||
const { isConnected, chainId } = useWallet();
|
||||
|
||||
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
const [saveToKeystore, setSaveToKeystore] = useState(true);
|
||||
const [keystorePassword, setKeystorePassword] = useState('');
|
||||
const [registrationResult, setRegistrationResult] = useState<{
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
txHash?: string;
|
||||
warning?: string;
|
||||
credentials?: KeystoreEntity;
|
||||
keystoreHash?: string;
|
||||
}>({});
|
||||
|
||||
const isLineaSepolia = chainId === 59141;
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setGlobalError(error);
|
||||
}
|
||||
}, [error, setGlobalError]);
|
||||
|
||||
const handleRateLimitChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value);
|
||||
setRateLimit(isNaN(value) ? rateMinLimit : value);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Validate keystore password if saving to keystore
|
||||
if (saveToKeystore && keystorePassword.length < 8) {
|
||||
setRegistrationResult({
|
||||
success: false,
|
||||
error: 'Keystore password must be at least 8 characters long'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRegistering(true);
|
||||
setRegistrationResult({});
|
||||
|
||||
try {
|
||||
setRegistrationResult({
|
||||
success: undefined,
|
||||
warning: 'Please check your wallet to sign the registration message.'
|
||||
});
|
||||
|
||||
// Pass save options if saving to keystore
|
||||
const saveOptions = saveToKeystore ? { password: keystorePassword } : undefined;
|
||||
|
||||
const result = await registerMembership(rateLimit, saveOptions);
|
||||
|
||||
setRegistrationResult({
|
||||
...result,
|
||||
credentials: result.credentials
|
||||
});
|
||||
|
||||
// Clear password field after successful registration
|
||||
if (result.success) {
|
||||
setKeystorePassword('');
|
||||
}
|
||||
} catch (error) {
|
||||
setRegistrationResult({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Registration failed'
|
||||
});
|
||||
} finally {
|
||||
setIsRegistering(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
RLN Membership Registration
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
|
||||
<RLNImplementationToggle />
|
||||
</div>
|
||||
|
||||
{/* Network Warning */}
|
||||
{isConnected && !isLineaSepolia && (
|
||||
<div className="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="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>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<RLNInitButton />
|
||||
</div>
|
||||
|
||||
{!isConnected ? (
|
||||
<div className="text-amber-600 dark:text-amber-400">
|
||||
Please connect your wallet to register a membership
|
||||
</div>
|
||||
) : !isInitialized || !isStarted ? (
|
||||
<div className="text-amber-600 dark:text-amber-400">
|
||||
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 space-x-4">
|
||||
<input
|
||||
type="range"
|
||||
id="rateLimit"
|
||||
name="rateLimit"
|
||||
min={rateMinLimit}
|
||||
max={rateMaxLimit}
|
||||
value={rateLimit}
|
||||
onChange={handleRateLimitChange}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 w-12">
|
||||
{rateLimit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="saveToKeystore"
|
||||
checked={saveToKeystore}
|
||||
onChange={(e) => setSaveToKeystore(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
||||
/>
|
||||
<label
|
||||
htmlFor="saveToKeystore"
|
||||
className="ml-2 text-sm text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Save credentials to keystore
|
||||
</label>
|
||||
</div>
|
||||
{saveToKeystore && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="keystorePassword"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Keystore Password (min 8 characters)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="keystorePassword"
|
||||
value={keystorePassword}
|
||||
onChange={(e) => setKeystorePassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
placeholder="Enter password to encrypt credentials"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isRegistering || !isLineaSepolia || (saveToKeystore && !keystorePassword)}
|
||||
className={`w-full px-4 py-2 text-sm font-medium rounded-md ${
|
||||
isRegistering || !isLineaSepolia || (saveToKeystore && !keystorePassword)
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
{isRegistering ? 'Registering...' : 'Register Membership'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Registration Result */}
|
||||
{registrationResult.warning && (
|
||||
<div className="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg">
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-400">
|
||||
{registrationResult.warning}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{registrationResult.error && (
|
||||
<div className="mt-4 p-4 bg-red-50 dark:bg-red-900 rounded-lg">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">
|
||||
{registrationResult.error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{registrationResult.success && (
|
||||
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900 rounded-lg">
|
||||
<p className="text-sm text-green-700 dark:text-green-400 mb-2">
|
||||
✓ Membership registered successfully!
|
||||
</p>
|
||||
{registrationResult.txHash && (
|
||||
<p className="text-xs text-green-600 dark:text-green-500">
|
||||
Transaction Hash: {registrationResult.txHash}
|
||||
</p>
|
||||
)}
|
||||
{registrationResult.keystoreHash && (
|
||||
<p className="text-xs text-green-600 dark:text-green-500">
|
||||
Credentials saved to keystore with hash: {registrationResult.keystoreHash}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export type TabItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
interface TabNavigationProps {
|
||||
tabs: TabItem[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
}
|
||||
|
||||
export function TabNavigation({ tabs, activeTab, onTabChange }: TabNavigationProps) {
|
||||
return (
|
||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||
<nav className="flex space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
className={`
|
||||
py-4 px-1 border-b-2 font-medium text-sm
|
||||
${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}
|
||||
`}
|
||||
aria-current={activeTab === tab.id ? 'page' : undefined}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { useWallet } from '../contexts/WalletContext';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useWallet } from '../contexts/index';
|
||||
|
||||
function getNetworkName(chainId: number | null): string {
|
||||
if (!chainId) return 'Unknown';
|
||||
@ -23,6 +23,20 @@ interface ProviderRpcError extends Error {
|
||||
|
||||
export function WalletInfo() {
|
||||
const { isConnected, address, balance, chainId, connectWallet, disconnectWallet, error } = useWallet();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Function to switch to Linea Sepolia network
|
||||
const switchToLineaSepolia = async () => {
|
||||
@ -32,13 +46,11 @@ export function WalletInfo() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to switch to Linea Sepolia
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: LINEA_SEPOLIA_CHAIN_ID }],
|
||||
});
|
||||
} catch (err) {
|
||||
// If the error code is 4902, the chain hasn't been added to MetaMask
|
||||
const providerError = err as ProviderRpcError;
|
||||
if (providerError.code === 4902) {
|
||||
try {
|
||||
@ -70,64 +82,90 @@ export function WalletInfo() {
|
||||
// Check if user is on unsupported network
|
||||
const isUnsupportedNetwork = isConnected && chainId !== 59141;
|
||||
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={connectWallet}
|
||||
className="h-7 px-3 text-xs bg-blue-500/20 text-blue-300 rounded hover:bg-blue-500/30 transition-colors"
|
||||
>
|
||||
Connect Wallet
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm dark:bg-gray-800 p-3">
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{error && (
|
||||
<div className="mb-2 p-2 text-xs bg-red-100 text-red-700 rounded-md dark:bg-red-900 dark:text-red-100">
|
||||
<div className="absolute right-0 -top-2 transform -translate-y-full mb-2 px-2 py-1 text-xs bg-red-500/20 text-red-300 rounded whitespace-nowrap">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isConnected ? (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="mr-3">
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">Address:</span>
|
||||
<span className="font-mono text-xs text-gray-900 dark:text-white truncate max-w-[120px]">
|
||||
{address}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`flex items-center gap-2 px-3 h-8 rounded transition-colors ${
|
||||
isOpen ? 'bg-gray-800' : 'hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-gray-300 truncate max-w-[140px]">
|
||||
{address}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-gray-400">
|
||||
{balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-72 rounded-lg bg-gray-800 shadow-lg border border-gray-700 overflow-hidden">
|
||||
<div className="p-3">
|
||||
<div className="flex flex-col gap-2 text-gray-300">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-gray-400">Address:</span>
|
||||
<span className="font-mono text-xs truncate">{address}</span>
|
||||
</div>
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">Network:</span>
|
||||
<span className={`font-mono text-xs ${isUnsupportedNetwork ? 'text-orange-600 dark:text-orange-400' : 'text-gray-900 dark:text-white'}`}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-gray-400">Network:</span>
|
||||
<span className={`font-mono text-xs ${isUnsupportedNetwork ? 'text-orange-400' : ''}`}>
|
||||
{getNetworkName(chainId)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">Balance:</span>
|
||||
<span className="font-mono text-xs text-gray-900 dark:text-white">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-gray-400">Balance:</span>
|
||||
<span className="font-mono text-xs">
|
||||
{balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 p-2 flex flex-col gap-2">
|
||||
{isUnsupportedNetwork && (
|
||||
<button
|
||||
onClick={switchToLineaSepolia}
|
||||
className="w-full h-7 px-3 text-xs bg-orange-500/20 text-orange-300 rounded hover:bg-orange-500/30 transition-colors"
|
||||
>
|
||||
Switch to Linea Sepolia
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={disconnectWallet}
|
||||
className="px-2 py-1 text-xs bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||
className="w-full h-7 px-3 text-xs bg-red-500/20 text-red-300 rounded hover:bg-red-500/30 transition-colors"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isUnsupportedNetwork && (
|
||||
<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>
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
onClick={connectWallet}
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Connect Wallet
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
interface AppState {
|
||||
isLoading: boolean;
|
||||
globalError: string | null;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
setGlobalError: (error: string | null) => void;
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
const AppStateContext = createContext<AppState | undefined>(undefined);
|
||||
|
||||
export function AppStateProvider({ children }: { children: ReactNode }) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [globalError, setGlobalError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>('membership');
|
||||
|
||||
const value = {
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
globalError,
|
||||
setGlobalError,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
};
|
||||
|
||||
return (
|
||||
<AppStateContext.Provider value={value}>
|
||||
{children}
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 flex items-center space-x-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{globalError && (
|
||||
<div className="fixed bottom-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded z-50 flex items-center">
|
||||
<span>{globalError}</span>
|
||||
<button
|
||||
onClick={() => setGlobalError(null)}
|
||||
className="ml-3 text-red-700 hover:text-red-900"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</AppStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAppState() {
|
||||
const context = useContext(AppStateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAppState must be used within an AppStateProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { RLNProvider as StandardRLNProvider } from './RLNZerokitContext';
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
"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 hook
|
||||
const standardModule = await import('./RLNZerokitContext');
|
||||
const { useRLN: useStandardRLN } = standardModule;
|
||||
|
||||
// Create a temporary component to access the context
|
||||
function TempComponent() {
|
||||
const context = useStandardRLN();
|
||||
setContextValue(context);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render the component within the provider
|
||||
const { RLNProvider } = standardModule;
|
||||
return <RLNProvider><TempComponent /></RLNProvider>;
|
||||
} else {
|
||||
// Import the light RLN hook
|
||||
const lightModule = await import('./RLNLightContext');
|
||||
const { useRLN: useLightRLN } = lightModule;
|
||||
|
||||
// Create a temporary component to access the context
|
||||
function TempComponent() {
|
||||
const context = useLightRLN();
|
||||
setContextValue(context);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render the component within the provider
|
||||
const { RLNProvider } = lightModule;
|
||||
return <RLNProvider><TempComponent /></RLNProvider>;
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
15
examples/keystore-management/src/contexts/index.ts
Normal file
15
examples/keystore-management/src/contexts/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Re-export wallet context
|
||||
export { WalletProvider, useWallet } from './wallet';
|
||||
|
||||
// Re-export keystore context
|
||||
export { KeystoreProvider, useKeystore } from './keystore';
|
||||
|
||||
// Re-export RLN contexts
|
||||
export {
|
||||
RLNImplementationProvider,
|
||||
useRLNImplementation,
|
||||
type RLNImplementationType,
|
||||
RLNProvider,
|
||||
type UnifiedRLNInstance,
|
||||
useRLN
|
||||
} from './rln';
|
||||
@ -11,8 +11,7 @@ interface KeystoreContextType {
|
||||
hasStoredCredentials: boolean;
|
||||
storedCredentialsHashes: string[];
|
||||
saveCredentials: (credentials: KeystoreEntity, password: string) => Promise<string>;
|
||||
loadCredential: (hash: string, password: string) => Promise<KeystoreEntity | undefined>;
|
||||
exportKeystore: () => string;
|
||||
exportCredential: (hash: string, password: string) => Promise<string>;
|
||||
importKeystore: (keystoreJson: string) => boolean;
|
||||
removeCredential: (hash: string) => void;
|
||||
}
|
||||
@ -83,25 +82,24 @@ export function KeystoreProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCredential = async (hash: string, password: string): Promise<KeystoreEntity | undefined> => {
|
||||
const exportCredential = async (hash: string, password: string): Promise<string> => {
|
||||
if (!keystore) {
|
||||
throw new Error("Keystore not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
return await keystore.readCredential(hash, password);
|
||||
} catch (err) {
|
||||
console.error("Error loading credential:", err);
|
||||
throw err;
|
||||
// Create a new keystore instance for the single credential
|
||||
const singleCredentialKeystore = Keystore.create();
|
||||
|
||||
// Get the credential from the main keystore
|
||||
const credential = await keystore.readCredential(hash, password);
|
||||
if (!credential) {
|
||||
throw new Error("Credential not found");
|
||||
}
|
||||
};
|
||||
|
||||
const exportKeystore = (): string => {
|
||||
if (!keystore) {
|
||||
throw new Error("Keystore not initialized");
|
||||
}
|
||||
|
||||
return keystore.toString();
|
||||
|
||||
// Add the credential to the new keystore
|
||||
await singleCredentialKeystore.addCredential(credential, password);
|
||||
console.log("Single credential keystore:", singleCredentialKeystore.toString());
|
||||
return singleCredentialKeystore.toString();
|
||||
};
|
||||
|
||||
const importKeystore = (keystoreJson: string): boolean => {
|
||||
@ -138,8 +136,7 @@ export function KeystoreProvider({ children }: { children: ReactNode }) {
|
||||
hasStoredCredentials: storedCredentialsHashes.length > 0,
|
||||
storedCredentialsHashes,
|
||||
saveCredentials,
|
||||
loadCredential,
|
||||
exportKeystore,
|
||||
exportCredential,
|
||||
importKeystore,
|
||||
removeCredential
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export { KeystoreProvider, useKeystore } from './KeystoreContext';
|
||||
@ -1,27 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
||||
import { KeystoreEntity } from '@waku/rln';
|
||||
import { UnifiedRLNInstance } from './RLNFactory';
|
||||
import { createRLNImplementation, UnifiedRLNInstance } from './implementations';
|
||||
import { useRLNImplementation } from './RLNImplementationContext';
|
||||
import { createRLNImplementation } from './RLNFactory';
|
||||
import { ethers } from 'ethers';
|
||||
import { useKeystore } from './KeystoreContext';
|
||||
import { useKeystore } from '../keystore';
|
||||
import { ERC20_ABI, LINEA_SEPOLIA_CONFIG, ensureLineaSepoliaNetwork } from './utils/network';
|
||||
|
||||
// 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;
|
||||
@ -39,18 +25,18 @@ interface RLNContextType {
|
||||
getCurrentRateLimit: () => Promise<number | null>;
|
||||
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
|
||||
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
// Create the context
|
||||
const RLNUnifiedContext = createContext<RLNContextType | undefined>(undefined);
|
||||
const RLNContext = createContext<RLNContextType | undefined>(undefined);
|
||||
|
||||
// Create the provider component
|
||||
export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
export function RLNProvider({ 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 [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Get the signer from window.ethereum
|
||||
const [signer, setSigner] = useState<ethers.Signer | null>(null);
|
||||
@ -109,64 +95,17 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
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 () => {
|
||||
const initializeRLN = useCallback(async () => {
|
||||
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
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);
|
||||
@ -182,7 +121,6 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
console.log("RLN instance already exists, skipping creation");
|
||||
}
|
||||
|
||||
// Start RLN if wallet is connected
|
||||
if (isConnected && signer && rln && !isStarted) {
|
||||
console.log("Starting RLN with signer...");
|
||||
try {
|
||||
@ -215,8 +153,18 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
} catch (err) {
|
||||
console.error('Error in initializeRLN:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [isConnected, signer, implementation, rln, isStarted]);
|
||||
|
||||
// Auto-initialize effect for Light implementation
|
||||
useEffect(() => {
|
||||
if (implementation === 'light' && isConnected && signer && !isInitialized && !isStarted && !isLoading) {
|
||||
console.log('Auto-initializing Light RLN implementation...');
|
||||
initializeRLN();
|
||||
}
|
||||
}, [implementation, isConnected, signer, isInitialized, isStarted, isLoading, initializeRLN]);
|
||||
|
||||
const getCurrentRateLimit = async (): Promise<number | null> => {
|
||||
try {
|
||||
@ -251,23 +199,21 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
setRateMinLimit(minLimit);
|
||||
setRateMaxLimit(maxLimit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rateMinLimit: minLimit,
|
||||
rateMaxLimit: maxLimit
|
||||
return {
|
||||
success: true,
|
||||
rateMinLimit: minLimit,
|
||||
rateMaxLimit: maxLimit
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting rate limits bounds:", error);
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
rateMinLimit: 0,
|
||||
rateMaxLimit: 0,
|
||||
error: 'Failed to get rate limits bounds'
|
||||
rateMinLimit: rateMinLimit,
|
||||
rateMaxLimit: rateMaxLimit,
|
||||
error: err instanceof Error ? err.message : 'Failed to get rate limits'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Save credentials to keystore
|
||||
const saveCredentialsToKeystore = async (credentials: KeystoreEntity, password: string): Promise<string> => {
|
||||
try {
|
||||
return await saveToKeystore(credentials, password);
|
||||
@ -277,13 +223,7 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Update registerMembership to optionally save credentials to keystore
|
||||
const registerMembership = async (rateLimit: number, saveOptions?: { password: string }): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
credentials?: KeystoreEntity;
|
||||
keystoreHash?: string;
|
||||
}> => {
|
||||
const registerMembership = async (rateLimit: number, saveOptions?: { password: string }) => {
|
||||
console.log("registerMembership called with rate limit:", rateLimit);
|
||||
|
||||
if (!rln || !isStarted) {
|
||||
@ -295,13 +235,6 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("im here")
|
||||
const rateMinLimit = await rln.contract.getMinRateLimit();
|
||||
const rateMaxLimit = await rln.contract.getMaxRateLimit();
|
||||
console.log({
|
||||
rateMinLimit,
|
||||
rateMaxLimit
|
||||
})
|
||||
// Validate rate limit
|
||||
if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) {
|
||||
return {
|
||||
@ -309,11 +242,10 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}`
|
||||
};
|
||||
}
|
||||
rln.contract.setRateLimit(rateLimit);
|
||||
console.log("Rate limit set to:", rateLimit);
|
||||
await rln.contract.setRateLimit(rateLimit);
|
||||
|
||||
// Ensure we're on the correct network
|
||||
const isOnLineaSepolia = await ensureLineaSepoliaNetwork();
|
||||
const isOnLineaSepolia = await ensureLineaSepoliaNetwork(signer);
|
||||
if (!isOnLineaSepolia) {
|
||||
console.warn("Could not switch to Linea Sepolia network. Registration may fail.");
|
||||
}
|
||||
@ -343,111 +275,98 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// 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...");
|
||||
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, 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." };
|
||||
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");
|
||||
}
|
||||
|
||||
// Register membership
|
||||
console.log("Registering membership with rate limit:", rateLimit);
|
||||
// Generate signature for identity
|
||||
const timestamp = Date.now();
|
||||
const message = `Sign this message to generate your RLN credentials ${timestamp}`;
|
||||
const signature = await signer.signMessage(message);
|
||||
|
||||
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");
|
||||
// Register membership
|
||||
console.log("Registering membership...");
|
||||
const credentials = await rln.registerMembership({
|
||||
signature: signature
|
||||
});
|
||||
console.log("Credentials:", credentials);
|
||||
|
||||
// If we have save options, save to keystore
|
||||
let keystoreHash: string | undefined;
|
||||
if (saveOptions && saveOptions.password && credentials) {
|
||||
try {
|
||||
const credentialsEntity = credentials as KeystoreEntity;
|
||||
keystoreHash = await saveCredentialsToKeystore(credentialsEntity, saveOptions.password);
|
||||
console.log("Credentials saved to keystore with hash:", keystoreHash);
|
||||
} catch (saveErr) {
|
||||
console.error("Error saving credentials to keystore:", saveErr);
|
||||
// Continue without failing the overall registration
|
||||
}
|
||||
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");
|
||||
|
||||
// If saveOptions provided, save to keystore
|
||||
let keystoreHash: string | undefined;
|
||||
if (saveOptions?.password) {
|
||||
try {
|
||||
keystoreHash = await saveCredentialsToKeystore(credentials, saveOptions.password);
|
||||
console.log("Credentials saved to keystore with hash:", keystoreHash);
|
||||
} catch (keystoreErr) {
|
||||
console.warn("Could not save credentials to keystore:", keystoreErr);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
credentials,
|
||||
keystoreHash
|
||||
};
|
||||
} 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"
|
||||
success: true,
|
||||
credentials: credentials as KeystoreEntity,
|
||||
keystoreHash
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Error registering membership:", err);
|
||||
|
||||
let errorMsg = "Failed to register membership";
|
||||
if (err instanceof Error) {
|
||||
errorMsg = err.message;
|
||||
}
|
||||
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
};
|
||||
|
||||
// Create the context value
|
||||
const contextValue: RLNContextType = {
|
||||
rln,
|
||||
isInitialized,
|
||||
isStarted,
|
||||
error,
|
||||
initializeRLN,
|
||||
registerMembership,
|
||||
getCurrentRateLimit,
|
||||
getRateLimitsBounds,
|
||||
rateMinLimit,
|
||||
rateMaxLimit,
|
||||
saveCredentialsToKeystore,
|
||||
};
|
||||
|
||||
return (
|
||||
<RLNUnifiedContext.Provider value={contextValue}>
|
||||
<RLNContext.Provider
|
||||
value={{
|
||||
rln,
|
||||
isInitialized,
|
||||
isStarted,
|
||||
error,
|
||||
initializeRLN,
|
||||
registerMembership,
|
||||
rateMinLimit,
|
||||
rateMaxLimit,
|
||||
getCurrentRateLimit,
|
||||
getRateLimitsBounds,
|
||||
saveCredentialsToKeystore: saveToKeystore,
|
||||
isLoading
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RLNUnifiedContext.Provider>
|
||||
</RLNContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Create a hook to use the context
|
||||
export function useRLN() {
|
||||
const context = useContext(RLNUnifiedContext);
|
||||
const context = useContext(RLNContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useRLN must be used within a RLNUnifiedProvider');
|
||||
throw new Error('useRLN must be used within a RLNProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
}
|
||||
@ -2,21 +2,17 @@
|
||||
|
||||
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');
|
||||
const [implementation, setImplementation] = useState<RLNImplementationType>('light');
|
||||
|
||||
return (
|
||||
<RLNImplementationContext.Provider value={{ implementation, setImplementation }}>
|
||||
@ -25,11 +21,10 @@ export function RLNImplementationProvider({ children }: { children: ReactNode })
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
import { createRLN, MembershipInfo, RLNLightInstance } from '@waku/rln';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
// Define a unified interface that both implementations must support
|
||||
export interface UnifiedRLNInstance {
|
||||
contract: {
|
||||
address: string;
|
||||
@ -22,17 +21,13 @@ export interface UnifiedRLNInstance {
|
||||
registerMembership: (idCommitment: string, rateLimit?: number) => Promise<ethers.ContractTransaction>;
|
||||
};
|
||||
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> {
|
||||
export async function createRLNImplementation(type: 'standard' | 'light' = '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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export { RLNProvider as StandardRLNProvider, useRLN as useStandardRLN } from './standard';
|
||||
export { RLNProvider as LightRLNProvider, useRLN as useLightRLN } from './light';
|
||||
export { createRLNImplementation, type UnifiedRLNInstance } from './factory';
|
||||
@ -1,23 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
||||
import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln';
|
||||
import { useWallet } from './WalletContext';
|
||||
import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln';
|
||||
import { useWallet } from '../../wallet';
|
||||
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'
|
||||
};
|
||||
import { ensureLineaSepoliaNetwork, ERC20_ABI, SIGNATURE_MESSAGE } from '../utils/network';
|
||||
|
||||
interface RLNContextType {
|
||||
rln: RLNLightInstance | RLNInstance | null;
|
||||
@ -41,53 +28,6 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
const [rateMinLimit, setRateMinLimit] = useState(0);
|
||||
const [rateMaxLimit, setRateMaxLimit] = useState(0);
|
||||
|
||||
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 = useCallback(async () => {
|
||||
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
|
||||
|
||||
@ -172,7 +112,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
await rln.contract?.setRateLimit(rateLimit);
|
||||
|
||||
// Ensure we're on the correct network
|
||||
const isOnLineaSepolia = await ensureLineaSepoliaNetwork();
|
||||
const isOnLineaSepolia = await ensureLineaSepoliaNetwork(signer);
|
||||
if (!isOnLineaSepolia) {
|
||||
console.warn("Could not switch to Linea Sepolia network. Registration may fail.");
|
||||
}
|
||||
@ -185,7 +125,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
const contractAddress = rln.contract.address;
|
||||
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
|
||||
const tokenAddress = '0x185A0015aC462a0aECb81beCc0497b649a64B9ea'; // Linea Sepolia token address
|
||||
|
||||
// Create token contract instance
|
||||
const tokenContract = new ethers.Contract(
|
||||
@ -258,20 +198,8 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
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");
|
||||
}
|
||||
}, [initializeRLN, isConnected, signer]);
|
||||
|
||||
// Debug log for state changes
|
||||
useEffect(() => {
|
||||
console.log("RLN Context state:", {
|
||||
isInitialized,
|
||||
isStarted,
|
||||
hasRln: !!rln,
|
||||
error
|
||||
});
|
||||
}, [isInitialized, isStarted, rln, error]);
|
||||
}, [isConnected, signer, initializeRLN]);
|
||||
|
||||
return (
|
||||
<RLNContext.Provider
|
||||
@ -294,7 +222,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
export function useRLN() {
|
||||
const context = useContext(RLNContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useRLN must be used within an RLNProvider');
|
||||
throw new Error('useRLN must be used within a RLNProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -1,19 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import { createRLN, DecryptedCredentials, LINEA_CONTRACT, RLNInstance } from '@waku/rln';
|
||||
import { useWallet } from './WalletContext';
|
||||
import { useWallet } from '../../wallet';
|
||||
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)"
|
||||
];
|
||||
|
||||
|
||||
import { ensureLineaSepoliaNetwork, ERC20_ABI, SIGNATURE_MESSAGE } from '../utils/network';
|
||||
|
||||
interface RLNContextType {
|
||||
rln: RLNInstance | null;
|
||||
@ -37,53 +28,6 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
const [rateMinLimit, setRateMinLimit] = useState(0);
|
||||
const [rateMaxLimit, setRateMaxLimit] = useState(0);
|
||||
|
||||
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_CONTRACT.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_CONTRACT.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);
|
||||
|
||||
@ -163,7 +107,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
rln.contract?.setRateLimit(rateLimit);
|
||||
|
||||
// Ensure we're on the correct network
|
||||
const isOnLineaSepolia = await ensureLineaSepoliaNetwork();
|
||||
const isOnLineaSepolia = await ensureLineaSepoliaNetwork(signer);
|
||||
if (!isOnLineaSepolia) {
|
||||
console.warn("Could not switch to Linea Sepolia network. Registration may fail.");
|
||||
}
|
||||
@ -243,27 +187,6 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
// 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={{
|
||||
@ -285,7 +208,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
export function useRLN() {
|
||||
const context = useContext(RLNContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useRLN must be used within an RLNProvider');
|
||||
throw new Error('useRLN must be used within a RLNProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
3
examples/keystore-management/src/contexts/rln/index.ts
Normal file
3
examples/keystore-management/src/contexts/rln/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { RLNProvider, useRLN } from './RLNContext';
|
||||
export { RLNImplementationProvider, useRLNImplementation, type RLNImplementationType } from './RLNImplementationContext';
|
||||
export type { UnifiedRLNInstance } from './implementations';
|
||||
@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
// Linea Sepolia configuration
|
||||
export const LINEA_SEPOLIA_CONFIG = {
|
||||
chainId: 59141,
|
||||
tokenAddress: '0x185A0015aC462a0aECb81beCc0497b649a64B9ea'
|
||||
};
|
||||
|
||||
// Type for Ethereum provider in window
|
||||
export interface EthereumProvider {
|
||||
request: (args: {
|
||||
method: string;
|
||||
params?: unknown[]
|
||||
}) => Promise<unknown>;
|
||||
}
|
||||
|
||||
// Function to ensure the wallet is connected to Linea Sepolia network
|
||||
export const ensureLineaSepoliaNetwork = async (signer?: ethers.Signer): 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...");
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
// ERC20 ABI for token operations
|
||||
export 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)"
|
||||
];
|
||||
|
||||
// Message for signing to generate identity
|
||||
export const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials";
|
||||
@ -0,0 +1 @@
|
||||
export { WalletProvider, useWallet } from './WalletContext';
|
||||
Loading…
x
Reference in New Issue
Block a user