Skip to content

Commit 8e34075

Browse files
committed
audited
1 parent b0c51aa commit 8e34075

12 files changed

Lines changed: 360 additions & 150 deletions

File tree

.claude/settings.local.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
"Bash(npm install:*)",
1313
"Bash(npx next:*)",
1414
"Bash(node -e \"console.log\\(require\\(''next/package.json''\\).version\\)\")",
15-
"Bash(ls out/*.html)"
15+
"Bash(ls out/*.html)",
16+
"Bash(find /mnt/e/GitHub/fula-chain -type f -name *.sol)",
17+
"Bash(find /mnt/e/GitHub/rewards-program/src -type f \\\\\\(-name *.ts -o -name *.tsx \\\\\\))",
18+
"Bash(find /mnt/e/GitHub/rewards-program/src -type f \\\\\\(-name *.tsx -o -name *.ts \\\\\\))",
19+
"Bash(xargs ls:*)",
20+
"Bash(npm run:*)"
1621
]
1722
}
1823
}

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Contract addresses — set these after deploying the contracts
2+
NEXT_PUBLIC_REWARDS_PROGRAM_ADDRESS=0x0000000000000000000000000000000000000000
3+
NEXT_PUBLIC_STAKING_POOL_ADDRESS=0x0000000000000000000000000000000000000000
4+
NEXT_PUBLIC_FULA_TOKEN_ADDRESS=0x9e12735d77c72c5C3670636D428f2F3815d8A4cB
5+
6+
# WalletConnect project ID — register at https://cloud.walletconnect.com
7+
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=YOUR_PROJECT_ID
8+
9+
# Chain: "base" | "baseSepolia" | "hardhat"
10+
NEXT_PUBLIC_DEFAULT_CHAIN=base

src/app/balance/page.tsx

Lines changed: 78 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ import {
66
TableContainer, TableHead, TableRow, TextField, Button, Alert,
77
CircularProgress,
88
} from "@mui/material";
9-
import { useAccount, useReadContract } from "wagmi";
9+
import { useAccount, useReadContract, useReadContracts } from "wagmi";
1010
import { useSearchParams } from "next/navigation";
1111
import { zeroAddress } from "viem";
1212
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels } from "@/config/contracts";
13-
import { toBytes12, fromBytes12, fromBytes8, shortenAddress, formatFula, formatDate } from "@/lib/utils";
13+
import { toBytes12, fromBytes12, fromBytes8, shortenAddress, formatFula, formatDate, formatContractError } from "@/lib/utils";
1414
import { useProgramCount, useProgram, useTimeLocks, useDirectChildren, useTransferToParent, useWithdraw, useApproveToken, useAddTokens } from "@/hooks/useRewardsProgram";
1515
import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer";
1616

17-
/* ── Per-program membership row ── */
17+
/* -- Per-program membership row -- */
1818

19-
function MemberProgramRow({ memberID, programId, isOwner }: { memberID: string; programId: number; isOwner: boolean }) {
19+
function MemberProgramRow({ memberID, programId }: { memberID: string; programId: number }) {
2020
const memberIDBytes = toBytes12(memberID);
2121

2222
const { data: member } = useReadContract({
@@ -37,7 +37,6 @@ function MemberProgramRow({ memberID, programId, isOwner }: { memberID: string;
3737
query: { enabled: !!member?.wallet && member.wallet !== zeroAddress },
3838
});
3939

40-
// Don't render if member doesn't exist in this program
4140
if (!member || !member.wallet || member.wallet === zeroAddress) return null;
4241

4342
return (
@@ -57,9 +56,9 @@ function MemberProgramRow({ memberID, programId, isOwner }: { memberID: string;
5756
);
5857
}
5958

60-
/* ── Actions panel (only for wallet owner) ── */
59+
/* -- Actions panel (only for wallet owner) -- */
6160

62-
function OwnerActions({ memberID, memberWallet }: { memberID: string; memberWallet: string }) {
61+
function OwnerActions({ memberWallet }: { memberWallet: string }) {
6362
const { address } = useAccount();
6463
const isOwner = address?.toLowerCase() === memberWallet.toLowerCase();
6564

@@ -98,7 +97,7 @@ function OwnerActions({ memberID, memberWallet }: { memberID: string; memberWall
9897
fullWidth size="small" type="number" />
9998
<Box sx={{ display: "flex", gap: 1, mt: 1 }}>
10099
<Button size="small" variant="outlined" onClick={() => approve(depositAmount)}
101-
disabled={isApproving || isAppConf || !depositAmount}>
100+
disabled={isApproving || isAppConf || !depositAmount || !disclaimer}>
102101
{isApproving || isAppConf ? <CircularProgress size={16} /> : "Approve"}
103102
</Button>
104103
<Button size="small" variant="contained" onClick={() => addTokens(pid, depositAmount)}
@@ -108,7 +107,7 @@ function OwnerActions({ memberID, memberWallet }: { memberID: string; memberWall
108107
</Box>
109108
{approveSuccess && <Alert severity="info" sx={{ mt: 1 }}>Approved. Now click Deposit.</Alert>}
110109
{depositSuccess && <Alert severity="success" sx={{ mt: 1 }}>Deposited!</Alert>}
111-
{depositError && <Alert severity="error" sx={{ mt: 1 }}>{depositError.message}</Alert>}
110+
{depositError && <Alert severity="error" sx={{ mt: 1 }}>{formatContractError(depositError)}</Alert>}
112111
</Grid>
113112

114113
{/* Transfer to Parent */}
@@ -124,7 +123,7 @@ function OwnerActions({ memberID, memberWallet }: { memberID: string; memberWall
124123
{isTransBack || isTransBackConf ? <CircularProgress size={16} /> : "Transfer"}
125124
</Button>
126125
{transBackSuccess && <Alert severity="success" sx={{ mt: 1 }}>Transferred!</Alert>}
127-
{transBackError && <Alert severity="error" sx={{ mt: 1 }}>{transBackError.message}</Alert>}
126+
{transBackError && <Alert severity="error" sx={{ mt: 1 }}>{formatContractError(transBackError)}</Alert>}
128127
</Grid>
129128

130129
{/* Withdraw */}
@@ -138,7 +137,7 @@ function OwnerActions({ memberID, memberWallet }: { memberID: string; memberWall
138137
{isWithdrawing || isWithConf ? <CircularProgress size={16} /> : "Withdraw"}
139138
</Button>
140139
{withdrawSuccess && <Alert severity="success" sx={{ mt: 1 }}>Withdrawn!</Alert>}
141-
{withdrawError && <Alert severity="error" sx={{ mt: 1 }}>{withdrawError.message}</Alert>}
140+
{withdrawError && <Alert severity="error" sx={{ mt: 1 }}>{formatContractError(withdrawError)}</Alert>}
142141
</Grid>
143142
</Grid>
144143

@@ -147,7 +146,7 @@ function OwnerActions({ memberID, memberWallet }: { memberID: string; memberWall
147146
);
148147
}
149148

150-
/* ── Time Locks detail ── */
149+
/* -- Time Locks detail -- */
151150

152151
function TimeLockDetails({ programId, wallet }: { programId: number; wallet: `0x${string}` }) {
153152
const { data: timeLocks } = useTimeLocks(programId, wallet);
@@ -165,7 +164,7 @@ function TimeLockDetails({ programId, wallet }: { programId: number; wallet: `0x
165164
);
166165
}
167166

168-
/* ── Sub-member row ── */
167+
/* -- Sub-member row -- */
169168

170169
function SubMemberRow({ programId, wallet }: { programId: number; wallet: `0x${string}` }) {
171170
const { data: member } = useReadContract({
@@ -199,7 +198,7 @@ function SubMemberRow({ programId, wallet }: { programId: number; wallet: `0x${s
199198
);
200199
}
201200

202-
/* ── Sub-members for a program ── */
201+
/* -- Sub-members for a program -- */
203202

204203
function SubMembersSection({ programId, wallet }: { programId: number; wallet: `0x${string}` }) {
205204
const { data: children } = useDirectChildren(programId, wallet);
@@ -236,7 +235,7 @@ function SubMembersSection({ programId, wallet }: { programId: number; wallet: `
236235
);
237236
}
238237

239-
/* ── Main balance view ── */
238+
/* -- Main balance view -- */
240239

241240
function BalanceContent() {
242241
const searchParams = useSearchParams();
@@ -247,37 +246,33 @@ function BalanceContent() {
247246
const { data: programCount } = useProgramCount();
248247
const count = Number(programCount || 0);
249248

250-
// Look up the member in program 1 to get their wallet (for ownership check)
249+
// Dynamic wallet discovery: multicall getMemberByID across all programs
251250
const memberIDBytes = toBytes12(searchID);
252-
const { data: firstMatch } = useReadContract({
251+
const contracts = Array.from({ length: count }, (_, i) => ({
253252
address: CONTRACTS.rewardsProgram,
254253
abi: REWARDS_PROGRAM_ABI,
255-
functionName: "getMemberByID",
256-
args: [memberIDBytes, 1],
257-
query: { enabled: !!searchID && count > 0 },
258-
});
254+
functionName: "getMemberByID" as const,
255+
args: [memberIDBytes, i + 1] as const,
256+
}));
259257

260-
// Try to find the wallet by iterating if program 1 didn't have it
261-
// We'll check up to the first few programs for the wallet address
262-
const { data: match2 } = useReadContract({
263-
address: CONTRACTS.rewardsProgram,
264-
abi: REWARDS_PROGRAM_ABI,
265-
functionName: "getMemberByID",
266-
args: [memberIDBytes, 2],
267-
query: { enabled: !!searchID && count >= 2 && (!firstMatch?.wallet || firstMatch.wallet === zeroAddress) },
268-
});
269-
const { data: match3 } = useReadContract({
270-
address: CONTRACTS.rewardsProgram,
271-
abi: REWARDS_PROGRAM_ABI,
272-
functionName: "getMemberByID",
273-
args: [memberIDBytes, 3],
274-
query: { enabled: !!searchID && count >= 3 && (!firstMatch?.wallet || firstMatch.wallet === zeroAddress) && (!match2?.wallet || match2.wallet === zeroAddress) },
258+
const { data: multicallResults } = useReadContracts({
259+
contracts: contracts.length > 0 ? contracts : undefined,
260+
query: { enabled: !!searchID && count > 0 },
275261
});
276262

277-
const memberWallet =
278-
(firstMatch?.wallet && firstMatch.wallet !== zeroAddress ? firstMatch.wallet :
279-
match2?.wallet && match2.wallet !== zeroAddress ? match2.wallet :
280-
match3?.wallet && match3.wallet !== zeroAddress ? match3.wallet : "") as string;
263+
// Find the first result that has a valid wallet
264+
let memberWallet = "";
265+
if (multicallResults) {
266+
for (const result of multicallResults) {
267+
if (result.status === "success" && result.result) {
268+
const member = result.result as { wallet: string };
269+
if (member.wallet && member.wallet !== zeroAddress) {
270+
memberWallet = member.wallet;
271+
break;
272+
}
273+
}
274+
}
275+
}
281276

282277
const handleSearch = () => {
283278
setSearchID(memberID);
@@ -313,45 +308,51 @@ function BalanceContent() {
313308
</Paper>
314309
)}
315310

316-
<Paper sx={{ p: 2 }}>
317-
<Typography variant="h6" gutterBottom>Programs & Balances for &quot;{searchID}&quot;</Typography>
318-
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: "block" }}>
319-
Balance columns: Withdrawable / Permanently Locked / Time-Locked (FULA)
320-
</Typography>
321-
<TableContainer>
322-
<Table size="small">
323-
<TableHead>
324-
<TableRow>
325-
<TableCell>Program</TableCell>
326-
<TableCell>Code</TableCell>
327-
<TableCell>Role</TableCell>
328-
<TableCell>Parent</TableCell>
329-
<TableCell>Withdrawable</TableCell>
330-
<TableCell>Locked</TableCell>
331-
<TableCell>Time-Locked</TableCell>
332-
<TableCell>Status</TableCell>
333-
</TableRow>
334-
</TableHead>
335-
<TableBody>
336-
{Array.from({ length: count }, (_, i) => (
337-
<MemberProgramRow key={i + 1} memberID={searchID} programId={i + 1} isOwner={false} />
338-
))}
339-
</TableBody>
340-
</Table>
341-
</TableContainer>
342-
</Paper>
343-
344-
{/* Sub-members per program */}
345-
{memberWallet && (
346-
<Paper sx={{ p: 2, mt: 3 }}>
347-
<Typography variant="h6" gutterBottom>Sub-Members</Typography>
348-
{Array.from({ length: count }, (_, i) => (
349-
<SubMembersSection key={i + 1} programId={i + 1} wallet={memberWallet as `0x${string}`} />
350-
))}
351-
</Paper>
311+
{!memberWallet && (
312+
<Alert severity="warning" sx={{ mb: 3 }}>No member found with ID &quot;{searchID}&quot; in any program.</Alert>
352313
)}
353314

354-
{memberWallet && <OwnerActions memberID={searchID} memberWallet={memberWallet} />}
315+
{memberWallet && (
316+
<>
317+
<Paper sx={{ p: 2 }}>
318+
<Typography variant="h6" gutterBottom>Programs & Balances for &quot;{searchID}&quot;</Typography>
319+
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: "block" }}>
320+
Balance columns: Withdrawable / Permanently Locked / Time-Locked (FULA)
321+
</Typography>
322+
<TableContainer>
323+
<Table size="small">
324+
<TableHead>
325+
<TableRow>
326+
<TableCell>Program</TableCell>
327+
<TableCell>Code</TableCell>
328+
<TableCell>Role</TableCell>
329+
<TableCell>Parent</TableCell>
330+
<TableCell>Withdrawable</TableCell>
331+
<TableCell>Locked</TableCell>
332+
<TableCell>Time-Locked</TableCell>
333+
<TableCell>Status</TableCell>
334+
</TableRow>
335+
</TableHead>
336+
<TableBody>
337+
{Array.from({ length: count }, (_, i) => (
338+
<MemberProgramRow key={i + 1} memberID={searchID} programId={i + 1} />
339+
))}
340+
</TableBody>
341+
</Table>
342+
</TableContainer>
343+
</Paper>
344+
345+
{/* Sub-members per program */}
346+
<Paper sx={{ p: 2, mt: 3 }}>
347+
<Typography variant="h6" gutterBottom>Sub-Members</Typography>
348+
{Array.from({ length: count }, (_, i) => (
349+
<SubMembersSection key={i + 1} programId={i + 1} wallet={memberWallet as `0x${string}`} />
350+
))}
351+
</Paper>
352+
353+
<OwnerActions memberWallet={memberWallet} />
354+
</>
355+
)}
355356
</>
356357
)}
357358

src/app/error.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"use client";
2+
3+
import { Typography, Box, Button, Paper } from "@mui/material";
4+
5+
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
6+
return (
7+
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "60vh" }}>
8+
<Paper sx={{ p: 4, maxWidth: 500, textAlign: "center" }}>
9+
<Typography variant="h5" gutterBottom>Something went wrong</Typography>
10+
<Typography color="text.secondary" sx={{ mb: 3 }}>
11+
An unexpected error occurred. Please try again or refresh the page.
12+
</Typography>
13+
<Button variant="contained" onClick={reset}>Try Again</Button>
14+
</Paper>
15+
</Box>
16+
);
17+
}

src/app/layout.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type { Metadata } from "next";
22
import { Providers } from "./providers";
3-
import { Header } from "@/components/layout/Header";
4-
import { Navigation } from "@/components/layout/Navigation";
5-
import { Box, Typography } from "@mui/material";
3+
import { LayoutShell } from "@/components/layout/LayoutShell";
64

75
export const metadata: Metadata = {
86
title: "Rewards Program Portal",
@@ -14,20 +12,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
1412
<html lang="en">
1513
<body>
1614
<Providers>
17-
<Box sx={{ display: "flex", minHeight: "100vh" }}>
18-
<Navigation />
19-
<Box sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
20-
<Header />
21-
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
22-
{children}
23-
</Box>
24-
<Box component="footer" sx={{ p: 2, borderTop: 1, borderColor: "divider" }}>
25-
<Typography variant="caption" color="text.secondary" sx={{ display: "block", textAlign: "center" }}>
26-
All data on this portal is stored on-chain and is publicly visible and verifiable. Do not enter personal or protected information. By using this portal, you acknowledge that all transactions and records are permanently recorded on the blockchain.
27-
</Typography>
28-
</Box>
29-
</Box>
30-
</Box>
15+
<LayoutShell>{children}</LayoutShell>
3116
</Providers>
3217
</body>
3318
</html>

0 commit comments

Comments
 (0)