Skip to content

Commit d47ad71

Browse files
committed
enhanced reporting extensively to use subgraph
1 parent eee1f09 commit d47ad71

25 files changed

+8622
-171
lines changed

.claude/settings.local.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,28 @@
4343
"Read(//mnt/e/GitHub/fula-chain/**)",
4444
"Read(//mnt/e/GitHub/**)",
4545
"Bash(HARDHAT_CONTRACT_SIZER=false npx hardhat test test/governance/integration/RewardsProgram.test.ts --no-compile)",
46-
"Bash(wc:*)"
46+
"Bash(wc:*)",
47+
"Bash(awk '{print $9, $5}')",
48+
"Bash(curl -s \"https://docs.alchemy.com/reference/alchemy-getassettransfers\")",
49+
"Bash(curl -s \"https://docs.moralis.io/web3-data-api/evm/reference/get-contract-events\")",
50+
"Bash(curl -s \"https://docs.drpc.org/\")",
51+
"Bash(curl -s \"https://docs.basescan.org/api-endpoints/logs\")",
52+
"WebSearch",
53+
"WebFetch(domain:docs.alchemy.com)",
54+
"WebFetch(domain:www.alchemy.com)",
55+
"WebFetch(domain:docs.moralis.io)",
56+
"WebFetch(domain:docs.moralis.com)",
57+
"WebFetch(domain:docs.cdp.coinbase.com)",
58+
"WebFetch(domain:drpc.org)",
59+
"WebFetch(domain:docs.etherscan.io)",
60+
"WebFetch(domain:moralis.com)",
61+
"WebFetch(domain:onfinality.io)",
62+
"WebFetch(domain:www.coinbase.com)",
63+
"WebFetch(domain:info.etherscan.com)",
64+
"WebFetch(domain:thegraph.com)",
65+
"WebFetch(domain:github.com)",
66+
"WebFetch(domain:www.npmjs.com)",
67+
"WebFetch(domain:docs.thegraph.academy)"
4768
]
4869
}
4970
}

.env.production

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Production contract addresses (Base mainnet)
2-
NEXT_PUBLIC_REWARDS_PROGRAM_ADDRESS=0xc4a9c629310D71de8B8Cd290F0455A81CC67399c
3-
NEXT_PUBLIC_STAKING_POOL_ADDRESS=0x91Ab1543a4E56d20cF6fEFEEE16DFd3ec9956189
2+
NEXT_PUBLIC_REWARDS_PROGRAM_ADDRESS=0x3BE7914Bf3eCfee640f00988397e5e86598b4565
3+
NEXT_PUBLIC_STAKING_POOL_ADDRESS=0xEB7179B0EF5C7B469F37789Ed71776790B0e2a80
44
NEXT_PUBLIC_FULA_TOKEN_ADDRESS=0x9e12735d77c72c5C3670636D428f2F3815d8A4cB
55

66
# WalletConnect
@@ -11,3 +11,10 @@ NEXT_PUBLIC_DEPLOYMENT_BLOCK=44106300
1111

1212
# Chain
1313
NEXT_PUBLIC_DEFAULT_CHAIN=base
14+
15+
# The Graph — Gateway production endpoint (domain-restricted API key)
16+
NEXT_PUBLIC_GRAPH_API_KEY=ec5e3bd1dadadda195c193b7a285ca44
17+
NEXT_PUBLIC_SUBGRAPH_ID=EbvumWYgwdYZsH4pAZkJo2XacNnnK9yj4fwGXmWCj576
18+
19+
# Gap threshold: use The Graph for gaps larger than this many blocks (default ~22h on Base)
20+
NEXT_PUBLIC_BLOCK_GAP_THRESHOLD=39996

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ yarn-error.log*
3434
# typescript
3535
*.tsbuildinfo
3636
next-env.d.ts
37+
/subgraph/node_modules

src/app/balance/page.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useAccount, useReadContract, useReadContracts } from "wagmi";
1111
import { useSearchParams } from "next/navigation";
1212
import { zeroAddress, encodePacked, keccak256, getAddress } from "viem";
1313
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels, MemberTypeLabels } from "@/config/contracts";
14-
import { toBytes12, fromBytes12, fromBytes8, shortenAddress, formatFula, formatContractError, fromBytes16 } from "@/lib/utils";
14+
import { toBytes12, fromBytes12, fromBytes8, shortenAddress, formatFula, formatContractError, fromBytes16, ipfsLogoUrl } from "@/lib/utils";
1515

1616
/** Compute the virtual storage key for a walletless member (mirrors _virtualAddr in contract) */
1717
function virtualAddr(memberID: string, programId: number): `0x${string}` {
@@ -21,7 +21,7 @@ function virtualAddr(memberID: string, programId: number): `0x${string}` {
2121
return getAddress("0x" + hash.slice(-40)) as `0x${string}`;
2222
}
2323
import { QRCodeSVG } from "qrcode.react";
24-
import { useProgramCount, useProgram, useTransferToParent, useWithdraw, useDepositTokens, useRewardTypes, useTransferLimit, useClaimMember } from "@/hooks/useRewardsProgram";
24+
import { useProgramCount, useProgram, useTransferToParent, useWithdraw, useDepositTokens, useRewardTypes, useTransferLimit, useClaimMember, useProgramLogo } from "@/hooks/useRewardsProgram";
2525
import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer";
2626
import { QRCodeDisplay } from "@/components/common/QRCodeDisplay";
2727
import { QRScannerButton } from "@/components/common/QRScannerButton";
@@ -399,6 +399,10 @@ function BalanceContent() {
399399
setSearchID(m);
400400
};
401401

402+
const claimProgramId = claimParam ? parseInt(claimParam) : 0;
403+
const { data: logoCID } = useProgramLogo(claimProgramId);
404+
const logoUrl = logoCID ? ipfsLogoUrl(logoCID as string) : "";
405+
402406
const [redeemQrUrl, setRedeemQrUrl] = useState("");
403407
useEffect(() => {
404408
if (typeof window !== "undefined" && searchID && codeParam && claimParam && memberExists) {
@@ -414,10 +418,30 @@ function BalanceContent() {
414418

415419
{redeemQrUrl && (
416420
<Paper sx={{ p: 2, mb: 3, textAlign: "center" }}>
421+
{logoUrl && (
422+
<Box
423+
component="img"
424+
src={logoUrl}
425+
alt="Program logo"
426+
sx={{ width: 48, height: 48, borderRadius: 1, objectFit: "contain", mb: 1 }}
427+
/>
428+
)}
417429
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
418430
Scan to redeem rewards
419431
</Typography>
420-
<QRCodeSVG value={redeemQrUrl} size={isMobile ? 160 : 200} level="M" />
432+
<QRCodeSVG
433+
value={redeemQrUrl}
434+
size={isMobile ? 220 : 280}
435+
level={logoUrl ? "H" : "M"}
436+
{...(logoUrl ? {
437+
imageSettings: {
438+
src: logoUrl,
439+
height: isMobile ? 55 : 70,
440+
width: isMobile ? 55 : 70,
441+
excavate: true,
442+
}
443+
} : {})}
444+
/>
421445
<Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}>
422446
{searchID} &middot; Program {claimParam}
423447
</Typography>

src/app/programs/page.tsx

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import BlockIcon from "@mui/icons-material/Block";
1515
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
1616
import DeleteIcon from "@mui/icons-material/Delete";
1717
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
18+
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
1819
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
1920
import { useAccount, useReadContract } from "wagmi";
2021
import { useSearchParams } from "next/navigation";
@@ -29,10 +30,10 @@ import {
2930
useAddRewardType, useRemoveRewardType, useRewardTypes,
3031
useAddSubType, useRemoveSubType, useSubTypes,
3132
useDepositTokens, useTransferToSubMember, useTransferToParent, useWithdraw,
32-
useTokenBalance,
33+
useTokenBalance, useProgramLogo, useSetProgramLogo,
3334
} from "@/hooks/useRewardsProgram";
3435
import { MemberRoleLabels, MemberRoleEnum, MemberTypeLabels, CONTRACTS, REWARDS_PROGRAM_ABI } from "@/config/contracts";
35-
import { fromBytes8, fromBytes12, fromBytes16, toBytes12, shortenAddress, formatFula, isValidAddress, formatContractError } from "@/lib/utils";
36+
import { fromBytes8, fromBytes12, fromBytes16, toBytes12, shortenAddress, formatFula, isValidAddress, formatContractError, ipfsLogoUrl, parseCID } from "@/lib/utils";
3637
import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer";
3738
import { QRCodeDisplay } from "@/components/common/QRCodeDisplay";
3839

@@ -290,6 +291,13 @@ function ProgramDetail({ programId }: { programId: number }) {
290291
const { deactivateProgram, isPending: isPendingDP, isConfirming: isConfirmingDP, isSuccess: isSuccessDP, error: errorDP } = useDeactivateProgram();
291292
const [openDeactivate, setOpenDeactivate] = useState(false);
292293

294+
// Program Logo
295+
const { data: logoCID } = useProgramLogo(programId);
296+
const logoUrl = logoCID ? ipfsLogoUrl(logoCID as string) : "";
297+
const { setProgramLogo, isPending: isPendingLogo, isConfirming: isConfirmingLogo, isSuccess: isSuccessLogo, error: errorLogo } = useSetProgramLogo();
298+
const [openLogo, setOpenLogo] = useState(false);
299+
const [logoCIDInput, setLogoCIDInput] = useState("");
300+
293301
// Show generated editCode
294302
const [showEditCodeDialog, setShowEditCodeDialog] = useState(false);
295303
const [displayEditCode, setDisplayEditCode] = useState("");
@@ -414,6 +422,14 @@ function ProgramDetail({ programId }: { programId: number }) {
414422
}
415423
}, [isSuccessDP, refetchProgram]);
416424

425+
// After logo set
426+
useEffect(() => {
427+
if (isSuccessLogo) {
428+
const t = setTimeout(() => setOpenLogo(false), 1200);
429+
return () => clearTimeout(t);
430+
}
431+
}, [isSuccessLogo]);
432+
417433
const handleAssignPA = () => {
418434
const wallet = (paWallet || "0x0000000000000000000000000000000000000000") as `0x${string}`;
419435
let hash: `0x${string}` = "0x0000000000000000000000000000000000000000000000000000000000000000";
@@ -458,6 +474,20 @@ function ProgramDetail({ programId }: { programId: number }) {
458474
<IconButton component={Link} href="/programs" sx={{ mt: 0.5 }} aria-label="Back to programs">
459475
<ArrowBackIcon />
460476
</IconButton>
477+
{logoUrl && (
478+
<Box
479+
component="img"
480+
src={logoUrl}
481+
alt={`${program.name} logo`}
482+
sx={{
483+
width: { xs: 56, sm: 72 },
484+
height: { xs: 56, sm: 72 },
485+
borderRadius: 2,
486+
objectFit: "contain",
487+
flexShrink: 0,
488+
}}
489+
/>
490+
)}
461491
<Box>
462492
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
463493
<Typography variant="h5">{program.name}</Typography>
@@ -486,6 +516,12 @@ function ProgramDetail({ programId }: { programId: number }) {
486516
</Tooltip>
487517
</>
488518
)}
519+
{canManageProgram && program.active && (
520+
<Button size="small" variant="outlined"
521+
onClick={() => { setLogoCIDInput((logoCID as string) || ""); setOpenLogo(true); }}>
522+
{logoUrl ? "Change Logo" : "Set Logo"}
523+
</Button>
524+
)}
489525
{canSetTransferLimit && program.active && (
490526
<Button size="small" variant="outlined"
491527
onClick={() => { setTlValue(String(transferLimit && Number(transferLimit) > 0 ? transferLimit : "")); setOpenTL(true); }}>
@@ -879,6 +915,60 @@ function ProgramDetail({ programId }: { programId: number }) {
879915
</DialogActions>
880916
</Dialog>
881917

918+
{/* Set Logo Dialog */}
919+
<Dialog open={openLogo} onClose={() => setOpenLogo(false)} maxWidth="sm" fullWidth>
920+
<DialogTitle>Set Program Logo</DialogTitle>
921+
<DialogContent>
922+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
923+
Enter an IPFS CID for the program logo image (recommended 256x256px or larger, square).
924+
</Typography>
925+
<TextField
926+
label="IPFS CID or Gateway URL"
927+
value={logoCIDInput}
928+
onChange={(e) => setLogoCIDInput(parseCID(e.target.value))}
929+
fullWidth
930+
margin="normal"
931+
placeholder="bafkr4i... or https://ipfs.cloud.fx.land/gateway/bafkr4i..."
932+
inputProps={{ maxLength: 256 }}
933+
InputProps={{
934+
endAdornment: (
935+
<Tooltip title="Paste from clipboard">
936+
<IconButton size="small" onClick={async () => {
937+
try {
938+
const text = await navigator.clipboard.readText();
939+
if (text) setLogoCIDInput(parseCID(text));
940+
} catch { /* clipboard permission denied */ }
941+
}}>
942+
<ContentPasteIcon fontSize="small" />
943+
</IconButton>
944+
</Tooltip>
945+
),
946+
}}
947+
/>
948+
{logoCIDInput && (
949+
<Box sx={{ mt: 2, textAlign: "center" }}>
950+
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 1 }}>Preview:</Typography>
951+
<Box
952+
component="img"
953+
src={ipfsLogoUrl(logoCIDInput)}
954+
alt="Logo preview"
955+
sx={{ maxWidth: 128, maxHeight: 128, borderRadius: 2 }}
956+
onError={(e: React.SyntheticEvent<HTMLImageElement>) => { e.currentTarget.style.display = "none"; }}
957+
/>
958+
</Box>
959+
)}
960+
{errorLogo && <Alert severity="error" sx={{ mt: 2 }}>{formatContractError(errorLogo)}</Alert>}
961+
{isSuccessLogo && <Alert severity="success" sx={{ mt: 2 }}>Logo updated!</Alert>}
962+
</DialogContent>
963+
<DialogActions>
964+
<Button onClick={() => setOpenLogo(false)}>Cancel</Button>
965+
<Button variant="contained" onClick={() => setProgramLogo(programId, logoCIDInput)}
966+
disabled={isPendingLogo || isConfirmingLogo || !logoCIDInput}>
967+
{isPendingLogo || isConfirmingLogo ? <CircularProgress size={20} /> : "Save"}
968+
</Button>
969+
</DialogActions>
970+
</Dialog>
971+
882972
{/* Edit Code Display Dialog (shown after walletless member creation) */}
883973
<Dialog open={showEditCodeDialog} onClose={() => setShowEditCodeDialog(false)} maxWidth="sm" fullWidth>
884974
<DialogTitle>Edit Code Generated</DialogTitle>

0 commit comments

Comments
 (0)