chore: merge

This commit is contained in:
Danish Arora 2025-07-15 14:45:06 +05:30
commit 58bc81b6d1
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
12 changed files with 2358 additions and 254 deletions

View File

@ -58,14 +58,6 @@ If you encounter an "ERC20: insufficient allowance" error, it means the token ap
1. Waku Testnet Tokens: 0x185A0015aC462a0aECb81beCc0497b649a64B9ea
2. RLN Registration Contract: 0xB9cd878C90E49F797B4431fBF4fb333108CB90e6
## TODO
- [ ] add info about using with nwaku/nwaku-compose/waku-simulator
- [x] fix rate limit fetch
- [ ] fix membership management methods
- [ ] define epoch / quanity epoch
- [x] alias for individual credentials
- [x] remove export keystore method (if >1 credentials in keystore)
## CI/CD
PRs should be made for `develop` branch and `master` should be [rebased](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) on `develop` once changes are verified.

1445
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@waku/rln": "0.1.6-0cec760.0",
"@waku/rln": "0.1.7-987c6cd.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.6.3",
@ -30,6 +30,7 @@
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.1.0"
},

View File

@ -4,12 +4,14 @@ import React from 'react';
import { Layout } from '../components/Layout';
import { MembershipRegistration } from '../components/Tabs/MembershipTab/MembershipRegistration';
import { KeystoreManagement } from '../components/Tabs/KeystoreTab/KeystoreManagement';
import RunNodeTab from '../components/Tabs/RunNodeTab/RunNodeTab';
export default function Home() {
return (
<Layout>
<MembershipRegistration tabId="membership" />
<KeystoreManagement tabId="keystore" />
<RunNodeTab tabId="runNode" />
</Layout>
);
}

View File

@ -13,6 +13,10 @@ const tabs = [
id: 'keystore',
label: 'Keystore Management',
},
{
id: 'runNode',
label: 'Run a Node',
},
];
export function Layout({ children }: { children: React.ReactNode }) {

View File

@ -5,6 +5,7 @@ import { ethers } from 'ethers';
import { MembershipState } from '@waku/rln';
import { useRLN } from '../contexts/rln/RLNContext';
import { toast } from 'sonner';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './ui/tooltip';
interface MembershipDetailsProps {
membershipInfo: {
@ -177,7 +178,21 @@ export function MembershipDetails({ membershipInfo, copyToClipboard, hash }: Mem
</div>
<div>
<span className="text-muted-foreground text-xs">Rate Limit:</span>
<div className="text-accent">{membershipInfo.rateLimit} msg/epoch</div>
<div className="text-accent flex items-center gap-1">
{membershipInfo.rateLimit} msg/epoch
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-1 cursor-pointer align-middle inline-flex items-center justify-center">
<span className="w-4 h-4 rounded-full border border-muted-foreground text-muted-foreground flex items-center justify-center text-xs font-bold" style={{ fontFamily: 'monospace' }}>i</span>
</span>
</TooltipTrigger>
<TooltipContent>
1 epoch = 10 minutes
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
{/* Contract Info */}

View File

@ -36,7 +36,7 @@ export function RLNStatusIndicator() {
const getStatusText = () => {
if (error) return 'Error';
if (isLoading) return 'Initializing RLN...';
if (!isConnected) return 'Connect Wallet';
if (!isConnected) return 'Wallet Not Connected';
if (chainId !== 59141) return 'Switch to Linea Sepolia';
if (isInitialized && isStarted) return 'RLN Active';
return 'RLN Inactive';

View File

@ -6,21 +6,22 @@ import { KeystoreEntity } from '@waku/rln';
import { useRLN } from '../../../contexts/rln/RLNContext';
import { useWallet } from '../../../contexts/wallet';
import { TerminalWindow } from '../../ui/terminal-window';
import { Slider } from '../../ui/slider';
import { ToggleGroup, ToggleGroupItem } from '../../ui/toggle-group';
import { Button } from '../../ui/button';
import { membershipRegistration, type ContentSegment } from '../../../content/index';
import { toast } from 'sonner';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../../ui/tooltip';
interface MembershipRegistrationProps {
tabId?: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function MembershipRegistration({ tabId: _tabId }: MembershipRegistrationProps) {
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, isLoading } = useRLN();
const { registerMembership, isInitialized, isStarted, error, isLoading, getPriceForRateLimit } = useRLN();
const { isConnected, chainId } = useWallet();
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
// Replace slider state with discrete options
const [rateLimit, setRateLimit] = useState<number>(300); // Default to Standard
const [isRegistering, setIsRegistering] = useState(false);
const [saveToKeystore, setSaveToKeystore] = useState(true);
const [keystorePassword, setKeystorePassword] = useState('');
@ -35,6 +36,33 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration
const isLineaSepolia = chainId === 59141;
const [price, setPrice] = useState<string>('');
const [priceLoading, setPriceLoading] = useState(false);
const [priceError, setPriceError] = useState<string | null>(null);
useEffect(() => {
if (isLoading || !isInitialized || !isStarted ) return;
let cancelled = false;
setPrice('');
setPriceError(null);
setPriceLoading(true);
(async () => {
try {
const result = await getPriceForRateLimit(rateLimit);
if (!cancelled) {
setPrice(result.price.toString());
}
} catch {
if (!cancelled) {
setPriceError('Could not fetch price');
}
} finally {
if (!cancelled) setPriceLoading(false);
}
})();
return () => { cancelled = true; };
}, [rateLimit, getPriceForRateLimit, isLoading, isInitialized, isStarted]);
useEffect(() => {
if (error) {
toast.error(error);
@ -172,22 +200,51 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration
<div>
<label
htmlFor="rateLimit"
className="block text-sm font-mono text-muted-foreground mb-2"
className="block text-sm font-mono text-muted-foreground mb-2 flex items-center gap-1"
>
{membershipRegistration.form.rateLimitLabel}
Rate Limit (messages per epoch)
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-1 cursor-pointer align-middle inline-flex items-center justify-center">
{/* Unicode info icon, styled */}
<span className="w-4 h-4 rounded-full border border-muted-foreground text-muted-foreground flex items-center justify-center text-xs font-bold" style={{ fontFamily: 'monospace' }}>i</span>
</span>
</TooltipTrigger>
<TooltipContent>
1 epoch = 10 minutes
</TooltipContent>
</Tooltip>
</TooltipProvider>
</label>
<div className="flex items-center space-x-4 py-2">
<Slider
id="rateLimit"
min={rateMinLimit}
max={rateMaxLimit}
value={[rateLimit]}
onValueChange={(value) => setRateLimit(value[0])}
<ToggleGroup
type="single"
value={String(rateLimit)}
onValueChange={(value) => {
if (value === '300' || value === '600') setRateLimit(Number(value));
}}
className="w-full"
/>
<span className="text-sm text-muted-foreground w-12 font-mono">
{rateLimit}
</span>
>
<ToggleGroupItem value="300" className="flex-1 flex flex-col items-center">
<span>Standard (300)</span>
<span className="text-xs text-muted-foreground">lower token spend.</span>
</ToggleGroupItem>
<ToggleGroupItem value="600" className="flex-1 flex flex-col items-center">
<span>Max (600)</span>
<span className="text-xs text-muted-foreground">higher token spend. more messages.</span>
</ToggleGroupItem>
</ToggleGroup>
</div>
{/* Show calculated token spend for selected rate limit */}
<div className="text-xs text-muted-foreground font-mono mt-1">
{priceLoading ? (
<>Token spend required: <span className="italic">Loading...</span></>
) : priceError ? (
<>Token spend required: <span className="text-destructive">{priceError}</span></>
) : (
<>Token spend required: <span>{price}</span> WTT</>
)}
</div>
</div>

View File

@ -0,0 +1,225 @@
import React, { useState } from 'react';
import { useKeystore } from '../../../contexts/keystore/KeystoreContext';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { TerminalWindow } from '@/components/ui/terminal-window';
import { toast } from '@/components/ui/toast';
import { Copy } from 'lucide-react';
interface RunNodeTabProps {
tabId?: string;
}
function CodeBlock({ code }: { code: string }) {
const handleCopy = () => {
navigator.clipboard.writeText(code);
toast({ message: 'Copied to clipboard!', type: 'success' });
};
return (
<div className="relative my-2 flex items-center group">
<pre className="bg-terminal-background border border-terminal-border rounded px-3 py-2 text-xs font-mono overflow-x-auto w-full pr-12">
{code}
</pre>
<Button
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-60 group-hover:opacity-100"
onClick={handleCopy}
title="Copy to clipboard"
type="button"
tabIndex={0}
>
<Copy className="h-4 w-4" />
</Button>
</div>
);
}
export default function RunNodeTab({ tabId: _tabId }: RunNodeTabProps) {
const {
hasStoredCredentials,
storedCredentialsHashes,
credentialAliases,
exportCredential,
} = useKeystore();
const [exportPassword, setExportPassword] = useState('');
const [selectedCredential, setSelectedCredential] = useState<string | null>(null);
const [exportError, setExportError] = useState<string | null>(null);
const handleExport = async (hash: string) => {
setExportError(null);
try {
if (!exportPassword) {
setExportError('Please enter your keystore password to export');
return;
}
const keystore = await exportCredential(hash, exportPassword);
// Download as file
const blob = new Blob([JSON.stringify(keystore)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'keystore.json';
a.click();
URL.revokeObjectURL(url);
setExportPassword('');
setSelectedCredential(null);
} catch (err: unknown) {
if (err instanceof Error) {
setExportError(err.message);
} else {
setExportError('Failed to export credential');
}
}
};
return (
<div className="space-y-8 p-4">
<TerminalWindow title="Run a Node" className="mb-8">
<h2 className="text-lg font-mono font-medium mb-6">How to Run a Waku Node</h2>
<ol className="space-y-8 list-decimal ml-6">
{/* Step 1: Export Keystore with UI */}
<li className="space-y-2">
<div className="font-mono text-base font-semibold text-primary mb-1">Export Your Keystore</div>
<div className="text-sm text-foreground/90 mb-1">
Select your RLN keystore from the list below and click <b>Export Keystore</b>.<br />
This will download a file (e.g., <code>keystore.json</code>) to your computer.
</div>
{/* Keystore Export UI */}
{!hasStoredCredentials ? (
<div className="p-3 border border-warning-DEFAULT/20 bg-warning-DEFAULT/5 rounded text-warning-DEFAULT">
No keystores found. Please register a membership and generate a keystore first.
</div>
) : (
<div className="space-y-4">
{storedCredentialsHashes.map((hash: string) => {
const alias = credentialAliases[hash] || `${hash.substring(0, 6)}...${hash.substring(hash.length - 4)}`;
return (
<div key={hash} className="p-4 border rounded bg-terminal-background/30 flex flex-col md:flex-row md:items-center md:justify-between gap-3">
<span className="font-mono text-sm text-foreground">{alias}</span>
{selectedCredential === hash ? (
<div className="flex items-center gap-2">
<Input
type="password"
value={exportPassword}
onChange={(e) => setExportPassword(e.target.value)}
placeholder="Enter password to export"
className="h-8 text-sm"
/>
<Button
variant="terminal"
size="sm"
onClick={() => handleExport(hash)}
>
Export
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => { setSelectedCredential(null); setExportPassword(''); }}
>
Cancel
</Button>
</div>
) : (
<Button
variant="terminal"
size="sm"
onClick={() => setSelectedCredential(hash)}
>
Export Keystore
</Button>
)}
</div>
);
})}
{exportError && <div className="text-red-600 text-sm mt-2">{exportError}</div>}
</div>
)}
</li>
{/* Step 2: Set Up nwaku-compose */}
<li className="space-y-2">
<div className="font-mono text-base font-semibold text-primary mb-1">Set Up nwaku-compose</div>
<div className="text-sm text-foreground/90 mb-1">
If you havent already, clone the nwaku-compose repository:
</div>
<CodeBlock code={`git clone https://github.com/waku-org/nwaku-compose.git\ncd nwaku-compose`} />
<div className="mt-2 p-2 border-l-4 border-info-DEFAULT bg-info-DEFAULT/10 text-info-DEFAULT text-xs font-mono">
Make sure you have Docker and docker-compose installed.
</div>
</li>
{/* Step 3: Place Your Keystore */}
<li className="space-y-2">
<div className="font-mono text-base font-semibold text-primary mb-1">Place Your Keystore</div>
<div className="text-sm text-foreground/90 mb-1">
Copy your exported <code>keystore.json</code> file into the <code>keystore/</code> directory inside your <code>nwaku-compose</code> folder.<br />
Replace any existing file if prompted.
</div>
</li>
{/* Step 4: Configure Environment */}
<li className="space-y-2">
<div className="font-mono text-base font-semibold text-primary mb-1">Configure Environment</div>
<div className="text-sm text-foreground/90 mb-1">
Copy <code>.env.example</code> to <code>.env</code>:
</div>
<CodeBlock code={`cp .env.example .env`} />
<div className="text-sm text-foreground/90 mt-2">
Open <code>.env</code> in your editor and fill in the required values:
<ul className="list-disc ml-6 text-sm mt-1">
<li><b>Ethereum Sepolia HTTP endpoint</b> (e.g., from Infura)</li>
<li><b>Ethereum Sepolia account</b> (with a small balance)</li>
<li><b>Password</b> (the one you used to encrypt your keystore)</li>
</ul>
</div>
</li>
{/* Step 5: (Optional) Set Database Parameters */}
<li className="space-y-2">
<div className="font-mono text-base font-semibold text-primary mb-1">(Optional) Set Database Parameters</div>
<div className="text-sm text-foreground/90 mb-1">
To set storage size, run:
</div>
<CodeBlock code={`./set_storage_retention.sh`} />
<div className="text-sm text-foreground/90 mt-2">
or manually set <code>STORAGE_SIZE</code> in <code>.env</code>.<br />
To set Postgres memory, run:
</div>
<CodeBlock code={`./set_postgres_shm.sh`} />
<div className="text-sm text-foreground/90 mt-2">
or set <code>POSTGRES_SHM</code> in <code>.env</code>.
</div>
</li>
{/* Step 6: Start Your Node */}
<li className="space-y-2">
<div className="font-mono text-base font-semibold text-primary mb-1">Start Your Node</div>
<div className="text-sm text-foreground/90 mb-1">
Start all services:
</div>
<CodeBlock code={`docker-compose up -d`} />
<div className="mt-2 p-2 border-l-4 border-info-DEFAULT bg-info-DEFAULT/10 text-info-DEFAULT text-xs font-mono">
Your node will load your RLN membership from the keystore.
</div>
</li>
{/* Step 7: Interact with Your Node */}
<li className="space-y-2">
<div className="font-mono text-base font-semibold text-primary mb-1">Interact with Your Node</div>
<div className="text-sm text-foreground/90 mb-1">
<div>Visit <b>localhost:4000</b> for the frontend chat.</div>
<div>Visit <b>localhost:3000</b> for node metrics.</div>
<div>Use the REST API as described in the nwaku-compose README.</div>
</div>
</li>
</ol>
<div className="mt-8 p-3 border border-warning-DEFAULT/40 bg-warning-DEFAULT/10 rounded text-warning-DEFAULT text-sm font-mono">
<b>Note:</b> You do <u>NOT</u> need to run the <code>register_rln.sh</code> scriptyour RLN membership is already registered and stored in your exported keystore.
</div>
</TerminalWindow>
</div>
);
}

View File

@ -33,6 +33,7 @@ interface RLNContextType {
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>;
isLoading: boolean;
getPriceForRateLimit: (rateLimit: number) => Promise<{ price: string }>;
}
const RLNContext = createContext<RLNContextType | undefined>(undefined);
@ -465,6 +466,20 @@ export function RLNProvider({ children }: { children: ReactNode }) {
}
};
const getPriceForRateLimit = async (rateLimit: number): Promise<{ price: string }> => {
try {
if (!rln || !rln.contract || !isStarted) {
throw new Error('RLN not initialized or contract not available');
}
const result = await rln.contract.getPriceForRateLimit(rateLimit);
const formatted = ethers.utils.formatUnits(result.price, 18);
return { price: formatted };
} catch (err) {
console.error('Error getting price for rate limit:', err);
throw err;
}
};
return (
<RLNContext.Provider
value={{
@ -483,7 +498,8 @@ export function RLNProvider({ children }: { children: ReactNode }) {
getCurrentRateLimit,
getRateLimitsBounds,
saveCredentialsToKeystore: saveToKeystore,
isLoading
isLoading,
getPriceForRateLimit
}}
>
{children}

View File

@ -14,4 +14,4 @@ export function cn(...inputs: ClassValue[]) {
*/
export function hslToVar(hsl: string): string {
return `hsl(var(${hsl}))`;
}
}

797
yarn.lock

File diff suppressed because it is too large Load Diff