Skip to content

Commit 938ea37

Browse files
Merge pull request #2 from waku-org/feat/membership-management
feat: membership management
2 parents 692021e + b3ac4f7 commit 938ea37

File tree

11 files changed

+1146
-332
lines changed

11 files changed

+1146
-332
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ If you encounter an "ERC20: insufficient allowance" error, it means the token ap
4141
## TODO
4242
- [ ] add info about using with nwaku/nwaku-compose/waku-simulator
4343
- [x] fix rate limit fetch
44+
- [ ] fix membership management methods

package-lock.json

Lines changed: 503 additions & 220 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@
1212
"dependencies": {
1313
"@fontsource-variable/inter": "^5.2.5",
1414
"@fontsource-variable/jetbrains-mono": "^5.2.5",
15-
"@next/font": "^14.2.15",
1615
"@radix-ui/react-dropdown-menu": "^2.1.6",
1716
"@radix-ui/react-slider": "^1.2.3",
1817
"@radix-ui/react-slot": "^1.1.2",
1918
"@radix-ui/react-tabs": "^1.1.3",
2019
"@radix-ui/react-toggle": "^1.1.2",
2120
"@radix-ui/react-toggle-group": "^1.1.2",
2221
"@radix-ui/react-tooltip": "^1.1.8",
23-
"@waku/rln": "0.1.5-9901863.0",
22+
"@waku/rln": "0.1.5-35b50c3.0",
2423
"class-variance-authority": "^0.7.1",
2524
"clsx": "^2.1.1",
2625
"framer-motion": "^12.6.3",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
import { Button } from './ui/button';
3+
import { Copy } from 'lucide-react';
4+
import { DecryptedCredentials } from '@waku/rln';
5+
6+
interface CredentialDetailsProps {
7+
decryptedInfo: DecryptedCredentials;
8+
copyToClipboard: (text: string) => void;
9+
}
10+
11+
export function CredentialDetails({ decryptedInfo, copyToClipboard }: CredentialDetailsProps) {
12+
return (
13+
<div className="mt-3 space-y-2 border-t border-terminal-border/40 pt-3 animate-in fade-in-50 duration-300">
14+
<div className="flex items-center mb-2">
15+
<span className="text-primary font-mono font-medium mr-2">{">"}</span>
16+
<h3 className="text-sm font-mono font-semibold text-primary">
17+
Credential Details
18+
</h3>
19+
</div>
20+
<div className="space-y-2 text-xs font-mono">
21+
<div className="grid grid-cols-1 gap-2">
22+
<div className="flex flex-col">
23+
<span className="text-muted-foreground">ID Commitment:</span>
24+
<div className="flex items-center mt-1">
25+
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDCommitment}</span>
26+
<Button
27+
variant="ghost"
28+
size="sm"
29+
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
30+
onClick={() => copyToClipboard(decryptedInfo.identity.IDCommitment.toString())}
31+
>
32+
<Copy className="h-3 w-3" />
33+
</Button>
34+
</div>
35+
</div>
36+
<div className="flex flex-col">
37+
<span className="text-muted-foreground">ID Commitment BigInt:</span>
38+
<div className="flex items-center mt-1">
39+
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDCommitmentBigInt}</span>
40+
<Button
41+
variant="ghost"
42+
size="sm"
43+
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
44+
onClick={() => copyToClipboard(decryptedInfo.identity.IDCommitmentBigInt.toString())}
45+
>
46+
<Copy className="h-3 w-3" />
47+
</Button>
48+
</div>
49+
</div>
50+
<div className="flex flex-col border-t border-terminal-border/20 pt-2">
51+
<span className="text-muted-foreground">ID Nullifier:</span>
52+
<div className="flex items-center mt-1">
53+
<span className="break-all text-accent truncate">{decryptedInfo.identity.IDNullifier}</span>
54+
<Button
55+
variant="ghost"
56+
size="sm"
57+
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
58+
onClick={() => copyToClipboard(decryptedInfo.identity.IDNullifier.toString())}
59+
>
60+
<Copy className="h-3 w-3" />
61+
</Button>
62+
</div>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
);
68+
}
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import React, { useState } from 'react';
2+
import { Button } from './ui/button';
3+
import { Copy, Clock, Trash2, Wallet } from 'lucide-react';
4+
import { ethers } from 'ethers';
5+
import { MembershipState } from '@waku/rln';
6+
import { useRLN } from '../contexts/rln/RLNContext';
7+
import { toast } from 'sonner';
8+
9+
interface MembershipDetailsProps {
10+
membershipInfo: {
11+
address: string;
12+
chainId: string;
13+
treeIndex: number;
14+
rateLimit: number;
15+
idCommitment: string;
16+
startBlock: number;
17+
endBlock: number;
18+
state: MembershipState;
19+
depositAmount: ethers.BigNumber;
20+
activeDuration: number;
21+
gracePeriodDuration: number;
22+
holder: string;
23+
token: string;
24+
};
25+
copyToClipboard: (text: string) => void;
26+
hash: string;
27+
}
28+
29+
export function MembershipDetails({ membershipInfo, copyToClipboard, hash }: MembershipDetailsProps) {
30+
const { extendMembership, eraseMembership, withdrawDeposit } = useRLN();
31+
const [isLoading, setIsLoading] = useState<{[key: string]: boolean}>({});
32+
const [password, setPassword] = useState('');
33+
const [showPasswordInput, setShowPasswordInput] = useState(false);
34+
const [actionType, setActionType] = useState<'extend' | 'erase' | 'withdraw' | null>(null);
35+
36+
const handleAction = async (type: 'extend' | 'erase' | 'withdraw') => {
37+
if (!password) {
38+
setActionType(type);
39+
setShowPasswordInput(true);
40+
return;
41+
}
42+
43+
setIsLoading(prev => ({ ...prev, [type]: true }));
44+
try {
45+
let result;
46+
switch (type) {
47+
case 'extend':
48+
result = await extendMembership(hash, password);
49+
break;
50+
case 'erase':
51+
result = await eraseMembership(hash, password);
52+
break;
53+
case 'withdraw':
54+
result = await withdrawDeposit(hash, password);
55+
break;
56+
}
57+
58+
if (result.success) {
59+
toast.success(`Successfully ${type}ed membership`);
60+
setPassword('');
61+
setShowPasswordInput(false);
62+
setActionType(null);
63+
} else {
64+
toast.error(result.error || `Failed to ${type} membership`);
65+
}
66+
} catch (err) {
67+
toast.error(err instanceof Error ? err.message : `Failed to ${type} membership`);
68+
} finally {
69+
setIsLoading(prev => ({ ...prev, [type]: false }));
70+
}
71+
};
72+
73+
// Check if membership is in grace period
74+
const isInGracePeriod = membershipInfo.state === MembershipState.GracePeriod;
75+
76+
// Check if membership is erased and awaiting withdrawal
77+
const canWithdraw = membershipInfo.state === MembershipState.ErasedAwaitsWithdrawal;
78+
79+
// Check if membership can be erased (Active or GracePeriod)
80+
const canErase = membershipInfo.state === MembershipState.Active || membershipInfo.state === MembershipState.GracePeriod;
81+
82+
return (
83+
<div className="mt-3 space-y-2 border-t border-terminal-border/40 pt-3 animate-in fade-in-50 duration-300">
84+
<div className="flex items-center justify-between mb-2">
85+
<div className="flex items-center">
86+
<span className="text-primary font-mono font-medium mr-2">{">"}</span>
87+
<h3 className="text-sm font-mono font-semibold text-primary">
88+
Membership Details
89+
</h3>
90+
</div>
91+
<div className="flex items-center space-x-2">
92+
{isInGracePeriod && (
93+
<Button
94+
variant="outline"
95+
size="sm"
96+
className="text-warning-DEFAULT hover:text-warning-DEFAULT hover:border-warning-DEFAULT flex items-center gap-1"
97+
onClick={() => handleAction('extend')}
98+
disabled={isLoading.extend}
99+
>
100+
<Clock className="w-3 h-3" />
101+
<span>{isLoading.extend ? 'Extending...' : 'Extend'}</span>
102+
</Button>
103+
)}
104+
{canErase && (
105+
<Button
106+
variant="outline"
107+
size="sm"
108+
className="text-destructive hover:text-destructive hover:border-destructive flex items-center gap-1"
109+
onClick={() => handleAction('erase')}
110+
disabled={isLoading.erase}
111+
>
112+
<Trash2 className="w-3 h-3" />
113+
<span>{isLoading.erase ? 'Erasing...' : 'Erase'}</span>
114+
</Button>
115+
)}
116+
{canWithdraw && (
117+
<Button
118+
variant="outline"
119+
size="sm"
120+
className="text-accent hover:text-accent hover:border-accent flex items-center gap-1"
121+
onClick={() => handleAction('withdraw')}
122+
disabled={isLoading.withdraw}
123+
>
124+
<Wallet className="w-3 h-3" />
125+
<span>{isLoading.withdraw ? 'Withdrawing...' : 'Withdraw'}</span>
126+
</Button>
127+
)}
128+
</div>
129+
</div>
130+
131+
{showPasswordInput && (
132+
<div className="mb-4 space-y-2 border-b border-terminal-border pb-4">
133+
<input
134+
type="password"
135+
value={password}
136+
onChange={(e) => setPassword(e.target.value)}
137+
placeholder="Enter keystore password"
138+
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"
139+
/>
140+
<div className="flex space-x-2">
141+
<Button
142+
variant="default"
143+
size="sm"
144+
onClick={() => handleAction(actionType!)}
145+
disabled={!password || isLoading[actionType!]}
146+
>
147+
Confirm
148+
</Button>
149+
<Button
150+
variant="ghost"
151+
size="sm"
152+
onClick={() => {
153+
setShowPasswordInput(false);
154+
setPassword('');
155+
setActionType(null);
156+
}}
157+
>
158+
Cancel
159+
</Button>
160+
</div>
161+
</div>
162+
)}
163+
164+
<div className="space-y-2 text-xs font-mono">
165+
<div className="grid grid-cols-2 gap-4">
166+
{/* Membership State */}
167+
<div>
168+
<span className="text-muted-foreground text-xs">State:</span>
169+
<div className="text-accent">{membershipInfo.state || 'N/A'}</div>
170+
</div>
171+
172+
{/* Basic Info */}
173+
<div>
174+
<span className="text-muted-foreground text-xs">Chain ID:</span>
175+
<div className="text-accent">{membershipInfo.chainId}</div>
176+
</div>
177+
<div>
178+
<span className="text-muted-foreground text-xs">Rate Limit:</span>
179+
<div className="text-accent">{membershipInfo.rateLimit} msg/epoch</div>
180+
</div>
181+
182+
{/* Contract Info */}
183+
<div>
184+
<span className="text-muted-foreground text-xs">Contract Address:</span>
185+
<div className="text-accent truncate hover:text-clip flex items-center">
186+
{membershipInfo.address}
187+
<Button
188+
variant="ghost"
189+
size="sm"
190+
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
191+
onClick={() => membershipInfo.address && copyToClipboard(membershipInfo.address)}
192+
>
193+
<Copy className="h-3 w-3" />
194+
</Button>
195+
</div>
196+
</div>
197+
198+
{/* Member Details */}
199+
<div>
200+
<span className="text-muted-foreground text-xs">Member Index:</span>
201+
<div className="text-accent">{membershipInfo.treeIndex || 'N/A'}</div>
202+
</div>
203+
<div>
204+
<span className="text-muted-foreground text-xs">ID Commitment:</span>
205+
<div className="text-accent truncate hover:text-clip flex items-center">
206+
{membershipInfo.idCommitment || 'N/A'}
207+
{membershipInfo.idCommitment && (
208+
<Button
209+
variant="ghost"
210+
size="sm"
211+
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
212+
onClick={() => membershipInfo.idCommitment && copyToClipboard(membershipInfo.idCommitment)}
213+
>
214+
<Copy className="h-3 w-3" />
215+
</Button>
216+
)}
217+
</div>
218+
</div>
219+
220+
{/* Block Information */}
221+
<div>
222+
<span className="text-muted-foreground text-xs">Start Block:</span>
223+
<div className="text-accent">{membershipInfo.startBlock || 'N/A'}</div>
224+
</div>
225+
<div>
226+
<span className="text-muted-foreground text-xs">End Block:</span>
227+
<div className="text-accent">{membershipInfo.endBlock || 'N/A'}</div>
228+
</div>
229+
230+
{/* Duration Information */}
231+
<div>
232+
<span className="text-muted-foreground text-xs">Active Duration:</span>
233+
<div className="text-accent">{membershipInfo.activeDuration ? `${membershipInfo.activeDuration} blocks` : 'N/A'}</div>
234+
</div>
235+
<div>
236+
<span className="text-muted-foreground text-xs">Grace Period:</span>
237+
<div className="text-accent">{membershipInfo.gracePeriodDuration ? `${membershipInfo.gracePeriodDuration} blocks` : 'N/A'}</div>
238+
</div>
239+
240+
{/* Token Information */}
241+
<div>
242+
<span className="text-muted-foreground text-xs">Token Address:</span>
243+
<div className="text-accent truncate hover:text-clip flex items-center">
244+
{membershipInfo.token || 'N/A'}
245+
{membershipInfo.token && (
246+
<Button
247+
variant="ghost"
248+
size="sm"
249+
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
250+
onClick={() => membershipInfo.token && copyToClipboard(membershipInfo.token)}
251+
>
252+
<Copy className="h-3 w-3" />
253+
</Button>
254+
)}
255+
</div>
256+
</div>
257+
<div>
258+
<span className="text-muted-foreground text-xs">Deposit Amount:</span>
259+
<div className="text-accent">
260+
{membershipInfo.depositAmount ? `${ethers.utils.formatEther(membershipInfo.depositAmount)} ETH` : 'N/A'}
261+
</div>
262+
</div>
263+
264+
{/* Holder Information */}
265+
<div className="col-span-2">
266+
<span className="text-muted-foreground text-xs">Holder Address:</span>
267+
<div className="text-accent truncate hover:text-clip flex items-center">
268+
{membershipInfo.holder || 'N/A'}
269+
{membershipInfo.holder && (
270+
<Button
271+
variant="ghost"
272+
size="sm"
273+
className="h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
274+
onClick={() => membershipInfo.holder && copyToClipboard(membershipInfo.holder)}
275+
>
276+
<Copy className="h-3 w-3" />
277+
</Button>
278+
)}
279+
</div>
280+
</div>
281+
</div>
282+
</div>
283+
</div>
284+
);
285+
}

0 commit comments

Comments
 (0)