diff --git a/frontend/src/components/badges/BadgesList.tsx b/frontend/src/components/badges/BadgesList.tsx
index cd3a7cf..9b784b0 100644
--- a/frontend/src/components/badges/BadgesList.tsx
+++ b/frontend/src/components/badges/BadgesList.tsx
@@ -1,5 +1,5 @@
import React, { useMemo, useState } from "react";
-import { BadgeCheck } from "lucide-react";
+import { BadgeCheck, ThumbsUp, ThumbsDown } from "lucide-react";
import {
Card,
@@ -8,7 +8,9 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
import { useGetBadges } from "@/hooks/badges/use-get-badges";
+import { useVoteBadge } from "@/hooks/badges/use-vote-badge";
import { HARD_CODED_BADGES } from "@/lib/constants/badgeConstants";
import type { Badge } from "@/lib/types/badges";
import { Search } from "lucide-react";
@@ -18,7 +20,8 @@ import { AiOutlineLoading3Quarters } from "react-icons/ai";
import ErrorDisplay from "@/components/displayError/index";
export function BadgesList(): React.ReactElement {
- const { data, isLoading, error } = useGetBadges();
+ const { data, isLoading, error, refetch } = useGetBadges();
+ const { vote, isPending } = useVoteBadge();
const [searchQuery, setSearchQuery] = useState("");
const list = (data && data.length > 0 ? data : HARD_CODED_BADGES) as Badge[];
@@ -28,6 +31,16 @@ export function BadgesList(): React.ReactElement {
return list.filter((b) => b.name.toLowerCase().includes(q));
}, [list, searchQuery]);
+ const handleVote = async (badgeName: string, isUpvote: boolean) => {
+ try {
+ await vote(badgeName, isUpvote);
+ // Refetch badges after voting
+ refetch();
+ } catch (error) {
+ console.error("Voting failed:", error);
+ }
+ };
+
if (isLoading) {
return (
@@ -69,7 +82,31 @@ export function BadgesList(): React.ReactElement {
{badge.description}
-
+
+
+
+ Score: {badge.voteScore || 0}
+
+
+
+
+
+
+
))}
diff --git a/frontend/src/hooks/badges/use-add-pointers.ts b/frontend/src/hooks/badges/use-add-pointers.ts
new file mode 100644
index 0000000..c4289b4
--- /dev/null
+++ b/frontend/src/hooks/badges/use-add-pointers.ts
@@ -0,0 +1,28 @@
+import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
+import { badgeRegistryAbi } from "@/lib/abis/badgeRegistryAbi";
+import { BADGE_REGISTRY_ADDRESS } from "@/lib/constants/blockchainConstants";
+
+export function useAddPointers() {
+ const { writeContract, data: hash, isPending, error } = useWriteContract();
+
+ const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
+ hash,
+ });
+
+ const addPointers = (fromBadge: string, toBadges: string[]) => {
+ writeContract({
+ abi: badgeRegistryAbi,
+ address: BADGE_REGISTRY_ADDRESS,
+ functionName: "addPointers",
+ args: [fromBadge as `0x${string}`, toBadges as `0x${string}`[]],
+ });
+ };
+
+ return {
+ addPointers,
+ isPending,
+ isConfirming,
+ isSuccess,
+ error,
+ };
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/badges/use-get-badge-details.ts b/frontend/src/hooks/badges/use-get-badge-details.ts
new file mode 100644
index 0000000..c9f847e
--- /dev/null
+++ b/frontend/src/hooks/badges/use-get-badge-details.ts
@@ -0,0 +1,53 @@
+import { useMemo } from "react";
+import { useReadContract } from "wagmi";
+import { badgeRegistryAbi } from "@/lib/abis/badgeRegistryAbi";
+import { BADGE_REGISTRY_ADDRESS } from "@/lib/constants/blockchainConstants";
+import type { Badge } from "@/lib/types/badges";
+import { bytes32ToString } from "@/lib/utils/blockchainUtils";
+
+export function useGetBadgeDetails(badgeName?: string) {
+ const address = BADGE_REGISTRY_ADDRESS;
+
+ const badgeQuery = useReadContract({
+ abi: badgeRegistryAbi,
+ address,
+ functionName: "getBadge",
+ args: badgeName ? [badgeName as `0x${string}`] : undefined,
+ query: {
+ enabled: Boolean(address) && Boolean(badgeName),
+ },
+ });
+
+ const pointersQuery = useReadContract({
+ abi: badgeRegistryAbi,
+ address,
+ functionName: "getPointers",
+ args: badgeName ? [badgeName as `0x${string}`] : undefined,
+ query: {
+ enabled: Boolean(address) && Boolean(badgeName),
+ },
+ });
+
+ const data: Badge | undefined = useMemo(() => {
+ const badgeResult = badgeQuery.data as
+ | [`0x${string}`, `0x${string}`, `0x${string}`, bigint]
+ | undefined;
+ const pointersResult = pointersQuery.data as `0x${string}`[] | undefined;
+
+ if (!badgeResult) return undefined;
+
+ const [nameBytes, descriptionBytes, creator, voteScore] = badgeResult;
+ return {
+ name: bytes32ToString(nameBytes),
+ description: bytes32ToString(descriptionBytes),
+ creator,
+ voteScore: Number(voteScore),
+ pointers: pointersResult?.map(bytes32ToString),
+ };
+ }, [badgeQuery.data, pointersQuery.data]);
+
+ const isLoading = badgeQuery.isLoading || pointersQuery.isLoading;
+ const error = badgeQuery.error || pointersQuery.error;
+
+ return { data, isLoading, error, refetch: badgeQuery.refetch };
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/badges/use-get-badges.ts b/frontend/src/hooks/badges/use-get-badges.ts
index 121fa4a..83c493b 100644
--- a/frontend/src/hooks/badges/use-get-badges.ts
+++ b/frontend/src/hooks/badges/use-get-badges.ts
@@ -47,12 +47,14 @@ export function useGetBadges(): {
const data: Badge[] | undefined = useMemo(() => {
const results = badgesQuery.data as
- | [`0x${string}`, `0x${string}`, `0x${string}`][]
+ | [`0x${string}`, `0x${string}`, `0x${string}`, bigint][]
| undefined;
if (!results) return undefined;
- return results.map(([nameBytes, descriptionBytes]) => ({
+ return results.map(([nameBytes, descriptionBytes, creator, voteScore]) => ({
name: bytes32ToString(nameBytes),
description: bytes32ToString(descriptionBytes),
+ creator,
+ voteScore: Number(voteScore),
}));
}, [badgesQuery.data]);
diff --git a/frontend/src/hooks/badges/use-vote-badge.ts b/frontend/src/hooks/badges/use-vote-badge.ts
new file mode 100644
index 0000000..de22ca8
--- /dev/null
+++ b/frontend/src/hooks/badges/use-vote-badge.ts
@@ -0,0 +1,28 @@
+import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
+import { badgeRegistryAbi } from "@/lib/abis/badgeRegistryAbi";
+import { BADGE_REGISTRY_ADDRESS } from "@/lib/constants/blockchainConstants";
+
+export function useVoteBadge() {
+ const { writeContract, data: hash, isPending, error } = useWriteContract();
+
+ const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
+ hash,
+ });
+
+ const vote = (badgeName: string, isUpvote: boolean) => {
+ writeContract({
+ abi: badgeRegistryAbi,
+ address: BADGE_REGISTRY_ADDRESS,
+ functionName: "vote",
+ args: [badgeName as `0x${string}`, isUpvote],
+ });
+ };
+
+ return {
+ vote,
+ isPending,
+ isConfirming,
+ isSuccess,
+ error,
+ };
+}
\ No newline at end of file
diff --git a/frontend/src/lib/abis/badgeRegistryAbi.ts b/frontend/src/lib/abis/badgeRegistryAbi.ts
index 6e79c27..3757097 100644
--- a/frontend/src/lib/abis/badgeRegistryAbi.ts
+++ b/frontend/src/lib/abis/badgeRegistryAbi.ts
@@ -16,8 +16,45 @@ export const badgeRegistryAbi = [
{ name: "", type: "bytes32" },
{ name: "", type: "bytes32" },
{ name: "", type: "address" },
+ { name: "", type: "int256" },
],
},
+ {
+ type: "function",
+ name: "getBadge",
+ stateMutability: "view",
+ inputs: [{ name: "name", type: "bytes32" }],
+ outputs: [
+ { name: "", type: "bytes32" },
+ { name: "", type: "bytes32" },
+ { name: "", type: "address" },
+ { name: "", type: "int256" },
+ ],
+ },
+ {
+ type: "function",
+ name: "getPointers",
+ stateMutability: "view",
+ inputs: [{ name: "name", type: "bytes32" }],
+ outputs: [{ name: "", type: "bytes32[]" }],
+ },
+ {
+ type: "function",
+ name: "getVoteScore",
+ stateMutability: "view",
+ inputs: [{ name: "name", type: "bytes32" }],
+ outputs: [{ name: "", type: "int256" }],
+ },
+ {
+ type: "function",
+ name: "hasVoted",
+ stateMutability: "view",
+ inputs: [
+ { name: "badgeName", type: "bytes32" },
+ { name: "voter", type: "address" },
+ ],
+ outputs: [{ name: "", type: "bool" }],
+ },
{
type: "function",
name: "createBadge",
@@ -28,4 +65,24 @@ export const badgeRegistryAbi = [
],
outputs: [],
},
+ {
+ type: "function",
+ name: "addPointers",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "fromBadge", type: "bytes32" },
+ { name: "toBadges", type: "bytes32[]" },
+ ],
+ outputs: [],
+ },
+ {
+ type: "function",
+ name: "vote",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "badgeName", type: "bytes32" },
+ { name: "isUpvote", type: "bool" },
+ ],
+ outputs: [],
+ },
] as const;
diff --git a/frontend/src/lib/constants/badgeConstants.ts b/frontend/src/lib/constants/badgeConstants.ts
index f0ec9de..4c89fae 100644
--- a/frontend/src/lib/constants/badgeConstants.ts
+++ b/frontend/src/lib/constants/badgeConstants.ts
@@ -4,23 +4,33 @@ export const HARD_CODED_BADGES: Badge[] = [
{
name: "Open Source Contributor",
description: "Contributed code to The Guild Genesis repositories.",
+ creator: "0x0000000000000000000000000000000000000000",
+ voteScore: 0,
},
{
name: "Smart Contract Deployer",
description:
"Successfully deployed a smart contract to testnet or mainnet.",
+ creator: "0x0000000000000000000000000000000000000000",
+ voteScore: 0,
},
{
name: "Community Mentor",
description: "Helped onboard and mentor new members of the community.",
+ creator: "0x0000000000000000000000000000000000000000",
+ voteScore: 0,
},
{
name: "Bug Hunter",
description:
"Reported and helped resolve significant issues or vulnerabilities.",
+ creator: "0x0000000000000000000000000000000000000000",
+ voteScore: 0,
},
{
name: "Docs Champion",
description: "Improved documentation or tutorials for the project.",
+ creator: "0x0000000000000000000000000000000000000000",
+ voteScore: 0,
},
];
diff --git a/frontend/src/lib/types/badges.d.ts b/frontend/src/lib/types/badges.d.ts
index 35b700f..e3ce187 100644
--- a/frontend/src/lib/types/badges.d.ts
+++ b/frontend/src/lib/types/badges.d.ts
@@ -1,4 +1,7 @@
export type Badge = {
name: string;
description: string;
+ creator: string;
+ voteScore: number;
+ pointers?: string[]; // optional for backward compatibility
};
diff --git a/the-guild-smart-contracts/src/TheGuildBadgeRegistry.sol b/the-guild-smart-contracts/src/TheGuildBadgeRegistry.sol
index 966f902..3071ecb 100644
--- a/the-guild-smart-contracts/src/TheGuildBadgeRegistry.sol
+++ b/the-guild-smart-contracts/src/TheGuildBadgeRegistry.sol
@@ -10,6 +10,9 @@ contract TheGuildBadgeRegistry {
bytes32 name;
bytes32 description;
address creator;
+ bytes32[] pointers; // badges this badge points to (hierarchy)
+ mapping(address => bool) hasVoted; // prevent double voting
+ int256 voteScore; // net upvotes - downvotes
}
/// @notice Emitted when a new badge is created.
@@ -34,26 +37,59 @@ contract TheGuildBadgeRegistry {
require(name != bytes32(0), "EMPTY_NAME");
require(!nameExists[name], "DUPLICATE_NAME");
- Badge memory badge = Badge({
- name: name,
- description: description,
- creator: msg.sender
- });
- nameToBadge[name] = badge;
+ Badge storage badge = nameToBadge[name];
+ badge.name = name;
+ badge.description = description;
+ badge.creator = msg.sender;
+ badge.voteScore = 0;
nameExists[name] = true;
badgeNames.push(name);
emit BadgeCreated(name, description, msg.sender);
}
- /// @notice Get a badge by its name.
- /// @dev Reverts if the badge does not exist.
- function getBadge(
- bytes32 name
- ) external view returns (bytes32, bytes32, address) {
+ /// @notice Add pointers from this badge to other badges (hierarchy).
+ /// @param fromBadge The badge that points to others.
+ /// @param toBadges Array of badge names this badge points to.
+ function addPointers(bytes32 fromBadge, bytes32[] calldata toBadges) external {
+ require(nameExists[fromBadge], "FROM_BADGE_NOT_FOUND");
+ Badge storage badge = nameToBadge[fromBadge];
+ require(badge.creator == msg.sender, "ONLY_CREATOR_CAN_ADD_POINTERS");
+
+ for (uint256 i = 0; i < toBadges.length; i++) {
+ require(nameExists[toBadges[i]], "TO_BADGE_NOT_FOUND");
+ badge.pointers.push(toBadges[i]);
+ }
+ }
+ /// @param badgeName The badge to vote on.
+ /// @param isUpvote True for upvote, false for downvote.
+ function vote(bytes32 badgeName, bool isUpvote) external {
+ require(nameExists[badgeName], "BADGE_NOT_FOUND");
+ Badge storage badge = nameToBadge[badgeName];
+ require(!badge.hasVoted[msg.sender], "ALREADY_VOTED");
+
+ badge.hasVoted[msg.sender] = true;
+ if (isUpvote) {
+ badge.voteScore += 1;
+ } else {
+ badge.voteScore -= 1;
+ }
+ }
+
+ /// @notice Get pointers for a badge.
+ /// @param name The badge name.
+ /// @return Array of badge names this badge points to.
+ function getPointers(bytes32 name) external view returns (bytes32[] memory) {
require(nameExists[name], "NOT_FOUND");
- Badge memory b = nameToBadge[name];
- return (b.name, b.description, b.creator);
+ return nameToBadge[name].pointers;
+ }
+
+ /// @notice Get vote score for a badge.
+ /// @param name The badge name.
+ /// @return The net vote score (upvotes - downvotes).
+ function getVoteScore(bytes32 name) external view returns (int256) {
+ require(nameExists[name], "NOT_FOUND");
+ return nameToBadge[name].voteScore;
}
/// @notice Get whether a badge name exists.
@@ -61,6 +97,16 @@ contract TheGuildBadgeRegistry {
return nameExists[name];
}
+ /// @notice Get a badge by its name.
+ /// @dev Reverts if the badge does not exist.
+ function getBadge(
+ bytes32 name
+ ) external view returns (bytes32, bytes32, address, int256) {
+ require(nameExists[name], "NOT_FOUND");
+ Badge storage b = nameToBadge[name];
+ return (b.name, b.description, b.creator, b.voteScore);
+ }
+
/// @notice Total number of badges created.
function totalBadges() external view returns (uint256) {
return badgeNames.length;
@@ -76,9 +122,9 @@ contract TheGuildBadgeRegistry {
/// @dev Reverts if index is out of bounds.
function getBadgeAt(
uint256 index
- ) external view returns (bytes32, bytes32, address) {
+ ) external view returns (bytes32, bytes32, address, int256) {
bytes32 name = badgeNames[index];
- Badge memory b = nameToBadge[name];
- return (b.name, b.description, b.creator);
+ Badge storage b = nameToBadge[name];
+ return (b.name, b.description, b.creator, b.voteScore);
}
}
diff --git a/the-guild-smart-contracts/test/TheGuildBadgeRegistry.t.sol b/the-guild-smart-contracts/test/TheGuildBadgeRegistry.t.sol
index a521c40..eef0748 100644
--- a/the-guild-smart-contracts/test/TheGuildBadgeRegistry.t.sol
+++ b/the-guild-smart-contracts/test/TheGuildBadgeRegistry.t.sol
@@ -24,12 +24,13 @@ contract TheGuildBadgeRegistryTest is Test {
registry.createBadge(name, description);
- (bytes32 rName, bytes32 rDesc, address creator) = registry.getBadge(
+ (bytes32 rName, bytes32 rDesc, address creator, int256 voteScore) = registry.getBadge(
name
);
assertEq(rName, name, "name mismatch");
assertEq(rDesc, description, "description mismatch");
assertEq(creator, address(this), "creator mismatch");
+ assertEq(voteScore, 0, "initial vote score should be 0");
assertTrue(registry.exists(name), "should exist");
assertEq(registry.totalBadges(), 1);
assertEq(registry.badgeNameAt(0), name);
@@ -48,8 +49,69 @@ contract TheGuildBadgeRegistryTest is Test {
registry.getBadge(bytes32("MISSING"));
}
- function test_CreateBadge_RevertOnEmptyName() public {
- vm.expectRevert(bytes("EMPTY_NAME"));
- registry.createBadge(bytes32(0), bytes32("desc"));
+ function test_Vote_Upvote_IncreasesScore() public {
+ bytes32 name = bytes32("VOTE_TEST");
+ registry.createBadge(name, bytes32("test"));
+
+ registry.vote(name, true);
+
+ (, , , int256 score) = registry.getBadge(name);
+ assertEq(score, 1);
+ }
+
+ function test_Vote_Downvote_DecreasesScore() public {
+ bytes32 name = bytes32("VOTE_TEST_DOWN");
+ registry.createBadge(name, bytes32("test"));
+
+ registry.vote(name, false);
+
+ (, , , int256 score) = registry.getBadge(name);
+ assertEq(score, -1);
+ }
+
+ function test_Vote_CannotVoteTwice() public {
+ bytes32 name = bytes32("VOTE_TWICE");
+ registry.createBadge(name, bytes32("test"));
+
+ registry.vote(name, true);
+ vm.expectRevert(bytes("ALREADY_VOTED"));
+ registry.vote(name, true);
+ }
+
+ function test_AddPointers_Succeeds() public {
+ bytes32 fromName = bytes32("FROM_BADGE");
+ bytes32 toName1 = bytes32("TO_BADGE_1");
+ bytes32 toName2 = bytes32("TO_BADGE_2");
+
+ registry.createBadge(fromName, bytes32("from"));
+ registry.createBadge(toName1, bytes32("to1"));
+ registry.createBadge(toName2, bytes32("to2"));
+
+ bytes32[] memory pointers = new bytes32[](2);
+ pointers[0] = toName1;
+ pointers[1] = toName2;
+
+ registry.addPointers(fromName, pointers);
+
+ bytes32[] memory retrieved = registry.getPointers(fromName);
+ assertEq(retrieved.length, 2);
+ assertEq(retrieved[0], toName1);
+ assertEq(retrieved[1], toName2);
+ }
+
+ function test_AddPointers_OnlyCreator() public {
+ bytes32 fromName = bytes32("FROM_BADGE_CREATOR");
+ bytes32 toName = bytes32("TO_BADGE_CREATOR");
+
+ registry.createBadge(fromName, bytes32("from"));
+ registry.createBadge(toName, bytes32("to"));
+
+ // Try to add pointers as different user
+ vm.prank(address(1));
+ bytes32[] memory pointers = new bytes32[](1);
+ pointers[0] = toName;
+
+ vm.expectRevert(bytes("ONLY_CREATOR_CAN_ADD_POINTERS"));
+ registry.addPointers(fromName, pointers);
}
}