mirror of
https://github.com/logos-messaging/rln.waku.org.git
synced 2026-01-02 14:13:09 +00:00
Merge pull request #2 from waku-org/feat/membership-management
feat: membership management
This commit is contained in:
commit
938ea376df
@ -41,3 +41,4 @@ If you encounter an "ERC20: insufficient allowance" error, it means the token ap
|
||||
## TODO
|
||||
- [ ] add info about using with nwaku/nwaku-compose/waku-simulator
|
||||
- [x] fix rate limit fetch
|
||||
- [ ] fix membership management methods
|
||||
723
package-lock.json
generated
723
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.5",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.5",
|
||||
"@next/font": "^14.2.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
@ -20,7 +19,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.5-9901863.0",
|
||||
"@waku/rln": "0.1.5-35b50c3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.6.3",
|
||||
|
||||
68
src/components/CredentialDetails.tsx
Normal file
68
src/components/CredentialDetails.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { DecryptedCredentials } from '@waku/rln';
|
||||
|
||||
interface CredentialDetailsProps {
|
||||
decryptedInfo: DecryptedCredentials;
|
||||
copyToClipboard: (text: string) => void;
|
||||
}
|
||||
|
||||
export function CredentialDetails({ decryptedInfo, copyToClipboard }: CredentialDetailsProps) {
|
||||
return (
|
||||
<div className="mt-3 space-y-2 border-t border-terminal-border/40 pt-3 animate-in fade-in-50 duration-300">
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="text-primary font-mono font-medium mr-2">{">"}</span>
|
||||
<h3 className="text-sm font-mono font-semibold text-primary">
|
||||
Credential Details
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-2 text-xs font-mono">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-muted-foreground">ID Commitment:</span>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDCommitment}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => copyToClipboard(decryptedInfo.identity.IDCommitment.toString())}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-muted-foreground">ID Commitment BigInt:</span>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDCommitmentBigInt}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => copyToClipboard(decryptedInfo.identity.IDCommitmentBigInt.toString())}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col border-t border-terminal-border/20 pt-2">
|
||||
<span className="text-muted-foreground">ID Nullifier:</span>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDNullifier}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => copyToClipboard(decryptedInfo.identity.IDNullifier.toString())}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
285
src/components/MembershipDetails.tsx
Normal file
285
src/components/MembershipDetails.tsx
Normal file
@ -0,0 +1,285 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Copy, Clock, Trash2, Wallet } from 'lucide-react';
|
||||
import { ethers } from 'ethers';
|
||||
import { MembershipState } from '@waku/rln';
|
||||
import { useRLN } from '../contexts/rln/RLNContext';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface MembershipDetailsProps {
|
||||
membershipInfo: {
|
||||
address: string;
|
||||
chainId: string;
|
||||
treeIndex: number;
|
||||
rateLimit: number;
|
||||
idCommitment: string;
|
||||
startBlock: number;
|
||||
endBlock: number;
|
||||
state: MembershipState;
|
||||
depositAmount: ethers.BigNumber;
|
||||
activeDuration: number;
|
||||
gracePeriodDuration: number;
|
||||
holder: string;
|
||||
token: string;
|
||||
};
|
||||
copyToClipboard: (text: string) => void;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export function MembershipDetails({ membershipInfo, copyToClipboard, hash }: MembershipDetailsProps) {
|
||||
const { extendMembership, eraseMembership, withdrawDeposit } = useRLN();
|
||||
const [isLoading, setIsLoading] = useState<{[key: string]: boolean}>({});
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPasswordInput, setShowPasswordInput] = useState(false);
|
||||
const [actionType, setActionType] = useState<'extend' | 'erase' | 'withdraw' | null>(null);
|
||||
|
||||
const handleAction = async (type: 'extend' | 'erase' | 'withdraw') => {
|
||||
if (!password) {
|
||||
setActionType(type);
|
||||
setShowPasswordInput(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(prev => ({ ...prev, [type]: true }));
|
||||
try {
|
||||
let result;
|
||||
switch (type) {
|
||||
case 'extend':
|
||||
result = await extendMembership(hash, password);
|
||||
break;
|
||||
case 'erase':
|
||||
result = await eraseMembership(hash, password);
|
||||
break;
|
||||
case 'withdraw':
|
||||
result = await withdrawDeposit(hash, password);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast.success(`Successfully ${type}ed membership`);
|
||||
setPassword('');
|
||||
setShowPasswordInput(false);
|
||||
setActionType(null);
|
||||
} else {
|
||||
toast.error(result.error || `Failed to ${type} membership`);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : `Failed to ${type} membership`);
|
||||
} finally {
|
||||
setIsLoading(prev => ({ ...prev, [type]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// Check if membership is in grace period
|
||||
const isInGracePeriod = membershipInfo.state === MembershipState.GracePeriod;
|
||||
|
||||
// Check if membership is erased and awaiting withdrawal
|
||||
const canWithdraw = membershipInfo.state === MembershipState.ErasedAwaitsWithdrawal;
|
||||
|
||||
// Check if membership can be erased (Active or GracePeriod)
|
||||
const canErase = membershipInfo.state === MembershipState.Active || membershipInfo.state === MembershipState.GracePeriod;
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-2 border-t border-terminal-border/40 pt-3 animate-in fade-in-50 duration-300">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
<span className="text-primary font-mono font-medium mr-2">{">"}</span>
|
||||
<h3 className="text-sm font-mono font-semibold text-primary">
|
||||
Membership Details
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{isInGracePeriod && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-warning-DEFAULT hover:text-warning-DEFAULT hover:border-warning-DEFAULT flex items-center gap-1"
|
||||
onClick={() => handleAction('extend')}
|
||||
disabled={isLoading.extend}
|
||||
>
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{isLoading.extend ? 'Extending...' : 'Extend'}</span>
|
||||
</Button>
|
||||
)}
|
||||
{canErase && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive hover:border-destructive flex items-center gap-1"
|
||||
onClick={() => handleAction('erase')}
|
||||
disabled={isLoading.erase}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
<span>{isLoading.erase ? 'Erasing...' : 'Erase'}</span>
|
||||
</Button>
|
||||
)}
|
||||
{canWithdraw && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-accent hover:text-accent hover:border-accent flex items-center gap-1"
|
||||
onClick={() => handleAction('withdraw')}
|
||||
disabled={isLoading.withdraw}
|
||||
>
|
||||
<Wallet className="w-3 h-3" />
|
||||
<span>{isLoading.withdraw ? 'Withdrawing...' : 'Withdraw'}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showPasswordInput && (
|
||||
<div className="mb-4 space-y-2 border-b border-terminal-border pb-4">
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter keystore password"
|
||||
className="w-full px-3 py-2 border border-terminal-border rounded-md bg-terminal-background text-foreground font-mono focus:ring-1 focus:ring-accent focus:border-accent text-sm"
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => handleAction(actionType!)}
|
||||
disabled={!password || isLoading[actionType!]}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowPasswordInput(false);
|
||||
setPassword('');
|
||||
setActionType(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 text-xs font-mono">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Membership State */}
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">State:</span>
|
||||
<div className="text-accent">{membershipInfo.state || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
{/* Basic Info */}
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Chain ID:</span>
|
||||
<div className="text-accent">{membershipInfo.chainId}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Rate Limit:</span>
|
||||
<div className="text-accent">{membershipInfo.rateLimit} msg/epoch</div>
|
||||
</div>
|
||||
|
||||
{/* Contract Info */}
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Contract Address:</span>
|
||||
<div className="text-accent truncate hover:text-clip flex items-center">
|
||||
{membershipInfo.address}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => membershipInfo.address && copyToClipboard(membershipInfo.address)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Member Details */}
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Member Index:</span>
|
||||
<div className="text-accent">{membershipInfo.treeIndex || 'N/A'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">ID Commitment:</span>
|
||||
<div className="text-accent truncate hover:text-clip flex items-center">
|
||||
{membershipInfo.idCommitment || 'N/A'}
|
||||
{membershipInfo.idCommitment && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => membershipInfo.idCommitment && copyToClipboard(membershipInfo.idCommitment)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Block Information */}
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Start Block:</span>
|
||||
<div className="text-accent">{membershipInfo.startBlock || 'N/A'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">End Block:</span>
|
||||
<div className="text-accent">{membershipInfo.endBlock || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
{/* Duration Information */}
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Active Duration:</span>
|
||||
<div className="text-accent">{membershipInfo.activeDuration ? `${membershipInfo.activeDuration} blocks` : 'N/A'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Grace Period:</span>
|
||||
<div className="text-accent">{membershipInfo.gracePeriodDuration ? `${membershipInfo.gracePeriodDuration} blocks` : 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
{/* Token Information */}
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Token Address:</span>
|
||||
<div className="text-accent truncate hover:text-clip flex items-center">
|
||||
{membershipInfo.token || 'N/A'}
|
||||
{membershipInfo.token && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => membershipInfo.token && copyToClipboard(membershipInfo.token)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Deposit Amount:</span>
|
||||
<div className="text-accent">
|
||||
{membershipInfo.depositAmount ? `${ethers.utils.formatEther(membershipInfo.depositAmount)} ETH` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Holder Information */}
|
||||
<div className="col-span-2">
|
||||
<span className="text-muted-foreground text-xs">Holder Address:</span>
|
||||
<div className="text-accent truncate hover:text-clip flex items-center">
|
||||
{membershipInfo.holder || 'N/A'}
|
||||
{membershipInfo.holder && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => membershipInfo.holder && copyToClipboard(membershipInfo.holder)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/components/RLNStatusIndicator.tsx
Normal file
55
src/components/RLNStatusIndicator.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { useRLN } from '../contexts';
|
||||
import { useRLNImplementation } from '../contexts';
|
||||
import { useWallet } from '../contexts';
|
||||
|
||||
export function RLNStatusIndicator() {
|
||||
const { isInitialized, isStarted, isLoading, error } = useRLN();
|
||||
const { implementation } = useRLNImplementation();
|
||||
const { isConnected, chainId } = useWallet();
|
||||
|
||||
// Debug logging
|
||||
console.log('RLN Status:', {
|
||||
isConnected,
|
||||
chainId,
|
||||
isInitialized,
|
||||
isStarted,
|
||||
isLoading,
|
||||
error,
|
||||
implementation
|
||||
});
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (error) return 'bg-red-500 shadow-[0_0_8px_0_rgba(239,68,68,0.6)]';
|
||||
if (isLoading) return 'bg-yellow-500 animate-pulse shadow-[0_0_8px_0_rgba(234,179,8,0.6)]';
|
||||
if (isInitialized && isStarted) return 'bg-green-500 shadow-[0_0_8px_0_rgba(34,197,94,0.6)]';
|
||||
return 'bg-gray-500';
|
||||
};
|
||||
|
||||
const getTextColor = () => {
|
||||
if (error) return 'text-red-500';
|
||||
if (isLoading) return 'text-yellow-500';
|
||||
if (isInitialized && isStarted) return 'text-green-500';
|
||||
return 'text-gray-500';
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
if (error) return 'Error';
|
||||
if (isLoading) return `Initializing ${implementation} RLN...`;
|
||||
if (!isConnected) return 'Connect Wallet';
|
||||
if (chainId !== 59141) return 'Switch to Linea Sepolia';
|
||||
if (isInitialized && isStarted) return `RLN Active`;
|
||||
return 'RLN Inactive';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full border border-terminal-border bg-terminal-background">
|
||||
<div className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${getStatusColor()}`} />
|
||||
<span className={`text-xs font-mono font-medium transition-all duration-300 ${getTextColor()} capitalize`}>
|
||||
{getStatusText()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,15 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useKeystore } from '../../../contexts/keystore';
|
||||
import { useKeystore } from '../../../contexts/keystore/KeystoreContext';
|
||||
import { useRLN } from '../../../contexts/rln/RLNContext';
|
||||
import { readKeystoreFromFile, saveKeystoreCredentialToFile } from '../../../utils/keystore';
|
||||
import { DecryptedCredentials } from '@waku/rln';
|
||||
import { DecryptedCredentials, MembershipInfo, MembershipState } from '@waku/rln';
|
||||
import { useAppState } from '../../../contexts/AppStateContext';
|
||||
import { TerminalWindow } from '../../ui/terminal-window';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Copy, Eye, Download, Trash2, ArrowDownToLine } from 'lucide-react';
|
||||
import { KeystoreExporter } from '../../KeystoreExporter';
|
||||
import { keystoreManagement, type ContentSegment } from '../../../content/index';
|
||||
import { ethers } from 'ethers';
|
||||
import { toast } from 'sonner';
|
||||
import { CredentialDetails } from '@/components/CredentialDetails';
|
||||
import { MembershipDetails } from '@/components/MembershipDetails';
|
||||
|
||||
interface ExtendedMembershipInfo extends Omit<MembershipInfo, 'state'> {
|
||||
address: string;
|
||||
chainId: string;
|
||||
treeIndex: number;
|
||||
rateLimit: number;
|
||||
idCommitment: string;
|
||||
startBlock: number;
|
||||
endBlock: number;
|
||||
state: MembershipState;
|
||||
depositAmount: ethers.BigNumber;
|
||||
activeDuration: number;
|
||||
gracePeriodDuration: number;
|
||||
holder: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function KeystoreManagement() {
|
||||
const {
|
||||
@ -21,7 +42,11 @@ export function KeystoreManagement() {
|
||||
removeCredential,
|
||||
getDecryptedCredential
|
||||
} = useKeystore();
|
||||
const { setGlobalError } = useAppState();
|
||||
|
||||
const {
|
||||
getMembershipInfo
|
||||
} = useRLN();
|
||||
|
||||
const [exportPassword, setExportPassword] = useState<string>('');
|
||||
const [selectedCredential, setSelectedCredential] = useState<string | null>(null);
|
||||
const [viewPassword, setViewPassword] = useState<string>('');
|
||||
@ -29,17 +54,18 @@ export function KeystoreManagement() {
|
||||
const [decryptedInfo, setDecryptedInfo] = useState<DecryptedCredentials | null>(null);
|
||||
const [isDecrypting, setIsDecrypting] = useState(false);
|
||||
const [copiedHash, setCopiedHash] = useState<string | null>(null);
|
||||
const [membershipInfo, setMembershipInfo] = useState<ExtendedMembershipInfo | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (error) {
|
||||
setGlobalError(error);
|
||||
toast.error(error);
|
||||
}
|
||||
}, [error, setGlobalError]);
|
||||
}, [error]);
|
||||
|
||||
const handleExportKeystoreCredential = async (hash: string) => {
|
||||
try {
|
||||
if (!exportPassword) {
|
||||
setGlobalError('Please enter your keystore password to export');
|
||||
toast.error('Please enter your keystore password to export');
|
||||
return;
|
||||
}
|
||||
const keystore = await exportCredential(hash, exportPassword);
|
||||
@ -47,7 +73,7 @@ export function KeystoreManagement() {
|
||||
setExportPassword('');
|
||||
setSelectedCredential(null);
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to export credential');
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to export credential');
|
||||
}
|
||||
};
|
||||
|
||||
@ -56,10 +82,10 @@ export function KeystoreManagement() {
|
||||
const keystore = await readKeystoreFromFile();
|
||||
const success = importKeystore(keystore);
|
||||
if (!success) {
|
||||
setGlobalError('Failed to import keystore');
|
||||
toast.error('Failed to import keystore');
|
||||
}
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to import keystore');
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to import keystore');
|
||||
}
|
||||
};
|
||||
|
||||
@ -67,17 +93,18 @@ export function KeystoreManagement() {
|
||||
try {
|
||||
removeCredential(hash);
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to remove credential');
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to remove credential');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewCredential = async (hash: string) => {
|
||||
if (!viewPassword) {
|
||||
setGlobalError('Please enter your keystore password to view credential');
|
||||
toast.error('Please enter your keystore password to view credential');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDecrypting(true);
|
||||
setMembershipInfo(null);
|
||||
|
||||
try {
|
||||
const credential = await getDecryptedCredential(hash, viewPassword);
|
||||
@ -85,12 +112,14 @@ export function KeystoreManagement() {
|
||||
|
||||
if (credential) {
|
||||
setDecryptedInfo(credential);
|
||||
const info = await getMembershipInfo(hash, viewPassword);
|
||||
setMembershipInfo(info as ExtendedMembershipInfo);
|
||||
} else {
|
||||
setGlobalError('Could not decrypt credential. Please check your password and try again.');
|
||||
toast.error('Could not decrypt credential. Please check your password and try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
setIsDecrypting(false);
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to decrypt credential');
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to decrypt credential');
|
||||
}
|
||||
};
|
||||
|
||||
@ -293,66 +322,29 @@ export function KeystoreManagement() {
|
||||
|
||||
{/* Decrypted Information Display */}
|
||||
{decryptedInfo && (
|
||||
<div className="mt-3 space-y-2 border-t border-terminal-border/40 pt-3 animate-in fade-in-50 duration-300">
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="text-primary font-mono font-medium mr-2">{">"}</span>
|
||||
<h3 className="text-sm font-mono font-semibold text-primary">
|
||||
Credential Details
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-2 text-xs font-mono">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-muted-foreground">ID Commitment:</span>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDCommitment}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => copyToClipboard(decryptedInfo.identity.IDCommitment.toString())}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col border-t border-terminal-border/20 pt-2">
|
||||
<span className="text-muted-foreground">ID Nullifier:</span>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDNullifier}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
|
||||
onClick={() => copyToClipboard(decryptedInfo.identity.IDNullifier.toString())}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col border-t border-terminal-border/20 pt-2">
|
||||
<span className="text-muted-foreground">Membership Details:</span>
|
||||
<div className="grid grid-cols-2 gap-4 mt-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Chain ID:</span>
|
||||
<div className="text-accent">{decryptedInfo.membership.chainId}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">Rate Limit:</span>
|
||||
<div className="text-accent">{decryptedInfo.membership.rateLimit}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<CredentialDetails
|
||||
decryptedInfo={decryptedInfo}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
|
||||
{membershipInfo && (
|
||||
<div className="flex flex-col border-t border-terminal-border/20 pt-2">
|
||||
<MembershipDetails
|
||||
membershipInfo={membershipInfo}
|
||||
copyToClipboard={copyToClipboard}
|
||||
hash={hash}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setDecryptedInfo(null)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mt-2 text-xs text-muted-foreground hover:text-accent"
|
||||
>
|
||||
Hide Details
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setDecryptedInfo(null)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mt-2 text-xs text-muted-foreground hover:text-accent"
|
||||
>
|
||||
Hide Details
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -2,20 +2,22 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RLNImplementationToggle } from '../../RLNImplementationToggle';
|
||||
import { RLNStatusIndicator } from '../../RLNStatusIndicator';
|
||||
import { KeystoreEntity } from '@waku/rln';
|
||||
import { useAppState } from '../../../contexts/AppStateContext';
|
||||
import { useRLN } from '../../../contexts/rln/RLNContext';
|
||||
import { useWallet } from '../../../contexts/wallet';
|
||||
import { useRLNImplementation } from '../../../contexts';
|
||||
import { RLNInitButton } from '../../RLNinitButton';
|
||||
import { TerminalWindow } from '../../ui/terminal-window';
|
||||
import { Slider } from '../../ui/slider';
|
||||
import { Button } from '../../ui/button';
|
||||
import { membershipRegistration, type ContentSegment } from '../../../content/index';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function MembershipRegistration() {
|
||||
const { setGlobalError } = useAppState();
|
||||
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error } = useRLN();
|
||||
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, isLoading } = useRLN();
|
||||
const { isConnected, chainId } = useWallet();
|
||||
const { implementation } = useRLNImplementation();
|
||||
|
||||
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
@ -34,9 +36,9 @@ export function MembershipRegistration() {
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setGlobalError(error);
|
||||
toast.error(error);
|
||||
}
|
||||
}, [error, setGlobalError]);
|
||||
}, [error]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@ -101,9 +103,12 @@ export function MembershipRegistration() {
|
||||
return (
|
||||
<div className="space-y-6 max-w-full">
|
||||
<TerminalWindow className="w-full">
|
||||
<h2 className="text-lg font-mono font-medium text-primary mb-4 cursor-blink">
|
||||
{membershipRegistration.title}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-mono font-medium text-primary cursor-blink">
|
||||
{membershipRegistration.title}
|
||||
</h2>
|
||||
<RLNStatusIndicator />
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-terminal-border pb-6">
|
||||
<RLNImplementationToggle />
|
||||
@ -155,9 +160,11 @@ export function MembershipRegistration() {
|
||||
</div>
|
||||
|
||||
<div className="border-t border-terminal-border pt-6 mt-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RLNInitButton />
|
||||
</div>
|
||||
{implementation === 'standard' && !isInitialized && !isStarted && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<RLNInitButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isConnected ? (
|
||||
<div className="text-warning-DEFAULT font-mono text-sm mt-4 flex items-center">
|
||||
@ -167,7 +174,7 @@ export function MembershipRegistration() {
|
||||
) : !isInitialized || !isStarted ? (
|
||||
<div className="text-warning-DEFAULT font-mono text-sm mt-4 flex items-center">
|
||||
<span className="mr-2">ℹ️</span>
|
||||
{membershipRegistration.initializePrompt}
|
||||
{implementation === 'light' && isLoading ? 'Initializing Light RLN...' : membershipRegistration.initializePrompt}
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
|
||||
|
||||
@ -4,9 +4,7 @@ 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;
|
||||
}
|
||||
@ -15,14 +13,11 @@ 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,
|
||||
};
|
||||
@ -38,17 +33,6 @@ export function AppStateProvider({ children }: { children: ReactNode }) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ interface KeystoreContextType {
|
||||
error: string | null;
|
||||
hasStoredCredentials: boolean;
|
||||
storedCredentialsHashes: string[];
|
||||
decryptedCredentials: KeystoreEntity | null;
|
||||
hideCredentials: () => void;
|
||||
saveCredentials: (credentials: KeystoreEntity, password: string) => Promise<string>;
|
||||
exportCredential: (hash: string, password: string) => Promise<Keystore>;
|
||||
exportEntireKeystore: (password: string) => Promise<void>;
|
||||
@ -26,6 +28,7 @@ export function KeystoreProvider({ children }: { children: ReactNode }) {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [storedCredentialsHashes, setStoredCredentialsHashes] = useState<string[]>([]);
|
||||
const [decryptedCredentials, setDecryptedCredentials] = useState<KeystoreEntity | null>(null);
|
||||
|
||||
// Initialize keystore
|
||||
useEffect(() => {
|
||||
@ -91,6 +94,9 @@ export function KeystoreProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
// Get the credential from the keystore
|
||||
const credential = await keystore.readCredential(hash, password);
|
||||
if (credential) {
|
||||
setDecryptedCredentials(credential);
|
||||
}
|
||||
return credential || null;
|
||||
} catch (err) {
|
||||
console.error("Error reading credential:", err);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
||||
import { KeystoreEntity, RLNCredentialsManager } from '@waku/rln';
|
||||
import { KeystoreEntity, MembershipInfo, RLNCredentialsManager } from '@waku/rln';
|
||||
import { createRLNImplementation } from './implementations';
|
||||
import { useRLNImplementation } from './RLNImplementationContext';
|
||||
import { ethers } from 'ethers';
|
||||
@ -20,6 +20,15 @@ interface RLNContextType {
|
||||
credentials?: KeystoreEntity;
|
||||
keystoreHash?: string;
|
||||
}>;
|
||||
extendMembership: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
||||
eraseMembership: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
||||
withdrawDeposit: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
||||
getMembershipInfo: (hash: string, password: string) => Promise<MembershipInfo & {
|
||||
address: string;
|
||||
chainId: string;
|
||||
treeIndex: number;
|
||||
rateLimit: number;
|
||||
}>;
|
||||
rateMinLimit: number;
|
||||
rateMaxLimit: number;
|
||||
getCurrentRateLimit: () => Promise<number | null>;
|
||||
@ -44,8 +53,8 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
const [rateMinLimit, setRateMinLimit] = useState<number>(0);
|
||||
const [rateMaxLimit, setRateMaxLimit] = useState<number>(0);
|
||||
|
||||
const { saveCredentials: saveToKeystore } = useKeystore();
|
||||
|
||||
const { saveCredentials: saveToKeystore, getDecryptedCredential } = useKeystore();
|
||||
|
||||
// Listen for wallet connection
|
||||
useEffect(() => {
|
||||
const checkWallet = async () => {
|
||||
@ -89,6 +98,12 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Reset RLN state when implementation changes
|
||||
useEffect(() => {
|
||||
console.log('Implementation changed, resetting state:', {
|
||||
oldRln: !!rln,
|
||||
oldIsInitialized: isInitialized,
|
||||
oldIsStarted: isStarted,
|
||||
newImplementation: implementation
|
||||
});
|
||||
setRln(null);
|
||||
setIsInitialized(false);
|
||||
setIsStarted(false);
|
||||
@ -102,11 +117,13 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
if (!rln) {
|
||||
let rlnInstance = rln;
|
||||
|
||||
if (!rlnInstance) {
|
||||
console.log(`Creating RLN ${implementation} instance...`);
|
||||
|
||||
try {
|
||||
const rlnInstance = await createRLNImplementation(implementation);
|
||||
rlnInstance = await createRLNImplementation(implementation);
|
||||
|
||||
console.log("RLN instance created successfully:", !!rlnInstance);
|
||||
setRln(rlnInstance);
|
||||
@ -121,17 +138,17 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
console.log("RLN instance already exists, skipping creation");
|
||||
}
|
||||
|
||||
if (isConnected && signer && rln && !isStarted) {
|
||||
if (isConnected && signer && rlnInstance && !isStarted) {
|
||||
console.log("Starting RLN with signer...");
|
||||
try {
|
||||
await rln.start({ signer });
|
||||
await rlnInstance.start({ signer });
|
||||
|
||||
setIsStarted(true);
|
||||
console.log("RLN started successfully, isStarted set to true");
|
||||
|
||||
try {
|
||||
const minLimit = await rln.contract?.getMinRateLimit();
|
||||
const maxLimit = await rln.contract?.getMaxRateLimit();
|
||||
const minLimit = await rlnInstance.contract?.getMinRateLimit();
|
||||
const maxLimit = await rlnInstance.contract?.getMaxRateLimit();
|
||||
if (minLimit !== undefined && maxLimit !== undefined) {
|
||||
setRateMinLimit(minLimit);
|
||||
setRateMaxLimit(maxLimit);
|
||||
@ -150,7 +167,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
console.log("Skipping RLN start because:", {
|
||||
isConnected,
|
||||
hasSigner: !!signer,
|
||||
hasRln: !!rln,
|
||||
hasRln: !!rlnInstance,
|
||||
isAlreadyStarted: isStarted
|
||||
});
|
||||
}
|
||||
@ -164,6 +181,14 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Auto-initialize effect for Light implementation
|
||||
useEffect(() => {
|
||||
console.log('Auto-init check:', {
|
||||
implementation,
|
||||
isConnected,
|
||||
hasSigner: !!signer,
|
||||
isInitialized,
|
||||
isStarted,
|
||||
isLoading
|
||||
});
|
||||
if (implementation === 'light' && isConnected && signer && !isInitialized && !isStarted && !isLoading) {
|
||||
console.log('Auto-initializing Light RLN implementation...');
|
||||
initializeRLN();
|
||||
@ -348,6 +373,111 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getMembershipInfo = async (hash: string, password: string) => {
|
||||
if (!rln || !rln.contract) {
|
||||
throw new Error('RLN not initialized or contract not available');
|
||||
}
|
||||
|
||||
const credential = await getDecryptedCredential(hash, password);
|
||||
if (!credential) {
|
||||
throw new Error('Could not decrypt credential');
|
||||
}
|
||||
|
||||
try {
|
||||
const membershipInfo = await rln.contract.getMembershipInfo(credential.identity.IDCommitmentBigInt);
|
||||
if (!membershipInfo) {
|
||||
throw new Error('Could not fetch membership info');
|
||||
}
|
||||
return {
|
||||
...membershipInfo,
|
||||
address: rln.contract.address,
|
||||
chainId: LINEA_SEPOLIA_CONFIG.chainId.toString(),
|
||||
treeIndex: Number(membershipInfo.index.toString()),
|
||||
rateLimit: Number(membershipInfo.rateLimit.toString())
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const extendMembership = async (hash: string, password: string) => {
|
||||
try {
|
||||
if (!rln || !rln.contract) {
|
||||
throw new Error('RLN not initialized or contract not available');
|
||||
}
|
||||
|
||||
const credential = await getDecryptedCredential(hash, password);
|
||||
if (!credential) {
|
||||
throw new Error('Could not decrypt credential');
|
||||
}
|
||||
|
||||
await rln.contract.extendMembership(credential.identity.IDCommitmentBigInt);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error extending membership:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to extend membership'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const eraseMembership = async (hash: string, password: string) => {
|
||||
try {
|
||||
if (!rln || !rln.contract) {
|
||||
throw new Error('RLN not initialized or contract not available');
|
||||
}
|
||||
|
||||
const credential = await getDecryptedCredential(hash, password);
|
||||
if (!credential) {
|
||||
throw new Error('Could not decrypt credential');
|
||||
}
|
||||
|
||||
|
||||
await rln.contract.eraseMembership(credential.identity.IDCommitmentBigInt);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error erasing membership:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to erase membership'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const withdrawDeposit = async (hash: string, password: string) => {
|
||||
try {
|
||||
if (!rln || !rln.contract) {
|
||||
throw new Error('RLN not initialized or contract not available');
|
||||
}
|
||||
|
||||
const credential = await getDecryptedCredential(hash, password);
|
||||
if (!credential) {
|
||||
throw new Error('Could not decrypt credential');
|
||||
}
|
||||
|
||||
// Get token address from config
|
||||
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
|
||||
const userAddress = await signer?.getAddress();
|
||||
|
||||
if (!userAddress) {
|
||||
throw new Error('No signer available');
|
||||
}
|
||||
|
||||
// Call withdraw with token address and holder
|
||||
await rln.contract.withdraw(tokenAddress, userAddress);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error withdrawing deposit:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to withdraw deposit'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RLNContext.Provider
|
||||
value={{
|
||||
@ -357,6 +487,10 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
||||
error,
|
||||
initializeRLN,
|
||||
registerMembership,
|
||||
extendMembership,
|
||||
eraseMembership,
|
||||
withdrawDeposit,
|
||||
getMembershipInfo,
|
||||
rateMinLimit,
|
||||
rateMaxLimit,
|
||||
getCurrentRateLimit,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user