chore: show membership management methods

This commit is contained in:
Danish Arora 2025-04-08 02:51:41 +05:30
parent eadc45f57e
commit 8a2b4a4b1f
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
5 changed files with 192 additions and 70 deletions

56
package-lock.json generated
View File

@ -18,7 +18,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-731214b.0",
"@waku/rln": "0.1.5-35b50c3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.6.3",
@ -3384,16 +3384,16 @@
]
},
"node_modules/@waku/core": {
"version": "0.0.35-731214b.0",
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.35-731214b.0.tgz",
"integrity": "sha512-yfNKTZHjjYtGw6/SEuXwzeyAhfhCyBPk5TPBuqgfpnXa9mQZmggBQpK7cHwiX0qYYLQG0sM6SL1qqGnqxJGKwA==",
"version": "0.0.35-35b50c3.0",
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.35-35b50c3.0.tgz",
"integrity": "sha512-seYdY5sj/5fq7pGLYHHLaXCW9ZFtxcYJPbFiNm+2ZY+WhriUPtOT2ZXQ3SN99fUzfAUa5nZbLt+QHwaMuMqbIw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@libp2p/ping": "2.0.1",
"@waku/enr": "0.0.29-731214b.0",
"@waku/interfaces": "0.0.30-731214b.0",
"@waku/proto": "0.0.10-731214b.0",
"@waku/utils": "0.0.23-731214b.0",
"@waku/enr": "0.0.29-35b50c3.0",
"@waku/interfaces": "0.0.30-35b50c3.0",
"@waku/proto": "0.0.10-35b50c3.0",
"@waku/utils": "0.0.23-35b50c3.0",
"debug": "^4.3.4",
"it-all": "^3.0.4",
"it-length-prefixed": "^9.0.4",
@ -3431,9 +3431,9 @@
}
},
"node_modules/@waku/enr": {
"version": "0.0.29-731214b.0",
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.29-731214b.0.tgz",
"integrity": "sha512-A/rULGlnwC7Dgz21WNsFGQX2Ff5NRQHmrpkY3vnZZmXWngRfm/8QaoZd0USsI3w/K8VMW+9YxTwuKynK4bKLEg==",
"version": "0.0.29-35b50c3.0",
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.29-35b50c3.0.tgz",
"integrity": "sha512-vCtiU+OAFiBFsPWOe+/7fDzngJJeLTjSAwbF37GEaX8yj5eSdxuhgccaHevPiWiRMDPfw9hBfh/C3N+9TYtl/w==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@ethersproject/rlp": "^5.7.0",
@ -3441,7 +3441,7 @@
"@libp2p/peer-id": "^5.0.1",
"@multiformats/multiaddr": "^12.0.0",
"@noble/secp256k1": "^1.7.1",
"@waku/utils": "0.0.23-731214b.0",
"@waku/utils": "0.0.23-35b50c3.0",
"debug": "^4.3.4",
"js-sha3": "^0.9.2"
},
@ -3458,21 +3458,21 @@
}
},
"node_modules/@waku/interfaces": {
"version": "0.0.30-731214b.0",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.30-731214b.0.tgz",
"integrity": "sha512-9TnlIAKHmAaT+C9fd/UE7KN8XU3Do/Ma1az8Q2C584heCin3vVPHYSU6KggA3czBpwq8lA34DecjdpRwvpd8Fg==",
"version": "0.0.30-35b50c3.0",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.30-35b50c3.0.tgz",
"integrity": "sha512-rjLNYuNDU8SgsiT6pJF0qp871CHIS2Y3tNq8fXWz4eY7nIZajcJQdOoacG1SdqsswyTpSjk8eP4ERDAYwVjbAg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@waku/proto": "0.0.10-731214b.0"
"@waku/proto": "0.0.10-35b50c3.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@waku/proto": {
"version": "0.0.10-731214b.0",
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.10-731214b.0.tgz",
"integrity": "sha512-/fwe8Hy6CefvD+t0D5aBuSBIRy70/rNzglNQAnePzSDwooE7ox9Hpe++O/mquKLyR49+x8+B3VrScgMy5u6aFQ==",
"version": "0.0.10-35b50c3.0",
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.10-35b50c3.0.tgz",
"integrity": "sha512-ErSJYWgbVbpDfu5hcXkZBoz/vd6X3H98NEzNHSPjoj0J1RxvbXV7fPExPXh7FjAK8cePS0xi2tOBQDIbMDOkOw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"protons-runtime": "^5.4.0"
@ -3482,15 +3482,15 @@
}
},
"node_modules/@waku/rln": {
"version": "0.1.5-731214b.0",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.5-731214b.0.tgz",
"integrity": "sha512-n7FIsfNN26z+u7xVKoJyVxZvu4N3ZOIP69ptHfW3U5XrkEI8CC9rP1ajgriFOF4LLXR9fNJB9D5aGLOh9vgWhQ==",
"version": "0.1.5-35b50c3.0",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.5-35b50c3.0.tgz",
"integrity": "sha512-DCg7LIOT7HD0pc7jRBK98k8qitaGONYGOuTFLlYn4pgm3UHce/N90t9ATMyz7YUVObj1D6Z6hfjkwMU8LcZpTw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@chainsafe/bls-keystore": "3.0.0",
"@noble/hashes": "^1.2.0",
"@waku/core": "0.0.35-731214b.0",
"@waku/utils": "0.0.23-731214b.0",
"@waku/core": "0.0.35-35b50c3.0",
"@waku/utils": "0.0.23-35b50c3.0",
"@waku/zerokit-rln-wasm": "^0.0.13",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
@ -3507,13 +3507,13 @@
}
},
"node_modules/@waku/utils": {
"version": "0.0.23-731214b.0",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.23-731214b.0.tgz",
"integrity": "sha512-XPxWwkKscYBiDk70jPwet6YR/xH+I0tfdmaj/meWODz03/+taNtTvaKSispnEIBcQ6loLNcM+5B1E72eG7et5g==",
"version": "0.0.23-35b50c3.0",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.23-35b50c3.0.tgz",
"integrity": "sha512-0ter2UjLimJ+btXaSD/+NM7P1uzcikBIem5fXZ05RwgOwHrzirlhSORXwmGSjSSf6sGmM612/zrVffzDh83WEw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@noble/hashes": "^1.3.2",
"@waku/interfaces": "0.0.30-731214b.0",
"@waku/interfaces": "0.0.30-35b50c3.0",
"chai": "^4.3.10",
"debug": "^4.3.4",
"uint8arrays": "^5.0.1"

View File

@ -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-731214b.0",
"@waku/rln": "0.1.5-35b50c3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.6.3",

View File

@ -1,8 +1,10 @@
import React from 'react';
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Copy } from 'lucide-react';
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: {
@ -21,17 +23,144 @@ interface MembershipDetailsProps {
token: string;
};
copyToClipboard: (text: string) => void;
hash: string;
}
export function MembershipDetails({ membershipInfo, copyToClipboard }: MembershipDetailsProps) {
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 mb-2">
<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 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 */}

View File

@ -334,6 +334,7 @@ export function KeystoreManagement() {
<MembershipDetails
membershipInfo={membershipInfo}
copyToClipboard={copyToClipboard}
hash={hash}
/>
</div>
)}

View File

@ -54,7 +54,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
const [rateMaxLimit, setRateMaxLimit] = useState<number>(0);
const { saveCredentials: saveToKeystore, getDecryptedCredential } = useKeystore();
// Listen for wallet connection
useEffect(() => {
const checkWallet = async () => {
@ -359,33 +359,31 @@ 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');
}
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');
}
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;
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) => {
@ -399,10 +397,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
throw new Error('Could not decrypt credential');
}
// Convert IDCommitment to hex string
const idCommitmentHex = ethers.utils.hexlify(credential.identity.IDCommitment);
await rln.contract.extendMembership(idCommitmentHex);
await rln.contract.extendMembership(credential.identity.IDCommitmentBigInt);
return { success: true };
} catch (err) {
console.error('Error extending membership:', err);
@ -424,10 +419,8 @@ export function RLNProvider({ children }: { children: ReactNode }) {
throw new Error('Could not decrypt credential');
}
// Convert IDCommitment to hex string
const idCommitmentHex = ethers.utils.hexlify(credential.identity.IDCommitment);
await rln.contract.eraseMembership(idCommitmentHex);
await rln.contract.eraseMembership(credential.identity.IDCommitmentBigInt);
return { success: true };
} catch (err) {
console.error('Error erasing membership:', err);