diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
new file mode 100644
index 0000000..5a5a9be
--- /dev/null
+++ b/CONTRIBUTION.md
@@ -0,0 +1,30 @@
+Contributing to The Guild Genesis
+
+Thank you for your interest in contributing! Please follow these steps before working on an issue or feature:
+
+1. Join our Discord: https://discord.gg/pg4UgaTr
+2. Create your profile on https://theguild.dev
+3. Edit your Guild profile to include your Discord username (so we can verify your identity and attribute rewards).
+4. Discuss the terms for fulfilling the ticket with Antoine (scope, deliverables, timeline).
+
+Rewards and recognition
+
+- Rule of thumb: 10 points per hour, 80 points per full day.
+- Contribution tokens have no monetary value. They represent reputation and will grant voting rights in the Guild in the future.
+- Your contributions will also be recognized with badges on https://theguild.dev.
+
+Process
+
+- Pick an open issue and comment that you’d like to work on it, or propose a new one.
+- Align requirements with Antoine in Discord before starting.
+- Submit a PR referencing the issue. Include clear commit messages and a brief changelog in the PR description.
+- Be responsive to review feedback; we’ll merge once acceptance criteria are met.
+
+Development quick links
+
+- Frontend (Astro + React): `frontend/`
+- Backend (Rust + Axum + SQLx): `backend/`
+- Smart Contracts (Foundry): `the-guild-smart-contracts/`
+
+If anything is unclear, ping us in Discord and we’ll help you get unblocked quickly.
+
diff --git a/README.md b/README.md
index 345bd35..1bbdf82 100644
--- a/README.md
+++ b/README.md
@@ -154,6 +154,8 @@ event BadgeCreated(bytes32 indexed name, bytes32 description, address indexed cr
This is a community-driven project. Join our [Discord](https://discord.gg/pg4UgaTr) to discuss features, propose changes, and contribute to the codebase.
+See detailed steps in [CONTRIBUTION.md](CONTRIBUTION.md).
+
## License
See [LICENSE](LICENSE) file for details.
diff --git a/frontend/src/components/ActivityTokenBalance.tsx b/frontend/src/components/ActivityTokenBalance.tsx
new file mode 100644
index 0000000..633fe8d
--- /dev/null
+++ b/frontend/src/components/ActivityTokenBalance.tsx
@@ -0,0 +1,10 @@
+import { useAccount } from "wagmi";
+import AddressTokenBalance from "@/components/AddressTokenBalance";
+
+export function ActivityTokenBalance() {
+ const { address, isConnected } = useAccount();
+ if (!isConnected || !address) return null;
+ return ;
+}
+
+export default ActivityTokenBalance;
diff --git a/frontend/src/components/AddressTokenBalance.tsx b/frontend/src/components/AddressTokenBalance.tsx
new file mode 100644
index 0000000..afa106e
--- /dev/null
+++ b/frontend/src/components/AddressTokenBalance.tsx
@@ -0,0 +1,38 @@
+import { Coins, HelpCircle } from "lucide-react";
+import { formatUnits } from "viem";
+import useGetActivityTokenBalance from "@/hooks/attestations/use-get-activity-token-balance";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+export function AddressTokenBalance({ address }: { address: `0x${string}` }) {
+ const balanceQuery = useGetActivityTokenBalance(address);
+
+ const raw = balanceQuery.data?.raw;
+ const decimals = balanceQuery.data?.decimals ?? 18;
+ const formatted = raw === undefined ? undefined : formatUnits(raw, decimals);
+
+ return (
+
+
+
{balanceQuery.isLoading ? "…" : (formatted ?? "0")} TGA
+
+
+
+
+
+
+
+
+ The Guild Attestation token rewards activity on the DApp.
+
+
+
+
+ );
+}
+
+export default AddressTokenBalance;
diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx
index 9336fc2..d5ade67 100644
--- a/frontend/src/components/AppSidebar.tsx
+++ b/frontend/src/components/AppSidebar.tsx
@@ -1,4 +1,4 @@
-import { Smile, BadgeCheck, Home } from "lucide-react";
+import { Smile, BadgeCheck, Home, BookOpen } from "lucide-react";
import {
Sidebar,
@@ -18,6 +18,11 @@ const items = [
url: "/",
icon: Home,
},
+ {
+ title: "Getting Started",
+ url: "/getting-started",
+ icon: BookOpen,
+ },
{
title: "Profiles",
url: "/profiles",
diff --git a/frontend/src/components/AppWrapper.tsx b/frontend/src/components/AppWrapper.tsx
index 8455957..77c6e11 100644
--- a/frontend/src/components/AppWrapper.tsx
+++ b/frontend/src/components/AppWrapper.tsx
@@ -11,6 +11,7 @@ import {
SidebarInset,
} from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/AppSidebar";
+import { ActivityTokenBalance } from "@/components/ActivityTokenBalance";
const queryClient = new QueryClient();
@@ -37,6 +38,7 @@ export function AppWrapper({ children }: Web3ProviderProps) {
diff --git a/frontend/src/components/pages/GettingStartedPage.tsx b/frontend/src/components/pages/GettingStartedPage.tsx
new file mode 100644
index 0000000..f5d3af1
--- /dev/null
+++ b/frontend/src/components/pages/GettingStartedPage.tsx
@@ -0,0 +1,113 @@
+import { AppWrapper } from "@/components/AppWrapper";
+import React from "react";
+
+export default function GettingStartedPage() {
+ return (
+
+
+
+
+
+
+
+ Install and set up MetaMask
+
+
+ Install the MetaMask browser extension or mobile app and create a
+ wallet. Then add the Polygon Amoy test network in MetaMask (Chain
+ ID 80002).
+
+
+
+
+
+
+
+
+ Get Amoy testnet MATIC (gas)
+
+
+ You need a small amount of Amoy MATIC to make on‑chain actions.
+ Ask in our Discord and a member will send you some testnet funds.
+
+
+ Request Amoy gas on Discord
+
+
+
+
+
+
+
Create your Guild profile
+
+ Head to the Profiles page, connect your wallet, and create your
+ profile to start building your on‑chain reputation.
+
+
+ Go to Profiles
+
+
+
+
+
+
+
Add a badge
+
+ Propose and create a new skill badge in the Badges page. Badges
+ represent skills or contributions recognized by the community.
+
+
+ Go to Badges
+
+
+
+
+
+
+
Give a badge to someone
+
+ After collaborating, issue a badge attestation to a peer to
+ acknowledge their work. This helps build credible, portable
+ developer profiles.
+
+
+ Find a profile to attest
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/pages/HomePage.tsx b/frontend/src/components/pages/HomePage.tsx
index 69fd1e1..3fde3f7 100644
--- a/frontend/src/components/pages/HomePage.tsx
+++ b/frontend/src/components/pages/HomePage.tsx
@@ -62,6 +62,32 @@ export default function HomePage() {
can earn your first attestation.
+
+
);
}
diff --git a/frontend/src/components/profiles/action-buttons/AddAttestationDialog.tsx b/frontend/src/components/profiles/action-buttons/AddAttestationDialog.tsx
index e5a2b4c..6365c1e 100644
--- a/frontend/src/components/profiles/action-buttons/AddAttestationDialog.tsx
+++ b/frontend/src/components/profiles/action-buttons/AddAttestationDialog.tsx
@@ -31,6 +31,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useGetAttestations } from "@/hooks/attestations/use-get-attestations";
+import { useGetActivityTokenBalance } from "@/hooks/attestations/use-get-activity-token-balance";
const formSchema = z.object({
badgeName: z.string().min(1, { message: "Badge name is required." }),
@@ -56,7 +57,8 @@ export function AddAttestationDialog({
reset,
} = useCreateAttestation();
const { data: badges, isLoading: badgesLoading } = useGetBadges();
- const { refetch } = useGetAttestations();
+ const { refetch: refetchAttestations } = useGetAttestations();
+ const { refetch: refetchActivityTokenBalance } = useGetActivityTokenBalance();
const form = useForm({
resolver: zodResolver(formSchema),
@@ -73,12 +75,19 @@ export function AddAttestationDialog({
useEffect(() => {
if (isConfirmed) {
- refetch();
+ refetchAttestations();
+ refetchActivityTokenBalance();
setOpen(false);
form.reset();
reset();
}
- }, [isConfirmed, refetch, form, reset]);
+ }, [
+ isConfirmed,
+ refetchAttestations,
+ refetchActivityTokenBalance,
+ form,
+ reset,
+ ]);
return (
diff --git a/frontend/src/components/profiles/list/ProfileCard.tsx b/frontend/src/components/profiles/list/ProfileCard.tsx
index 5a1c475..eae0318 100644
--- a/frontend/src/components/profiles/list/ProfileCard.tsx
+++ b/frontend/src/components/profiles/list/ProfileCard.tsx
@@ -11,6 +11,7 @@ import { EditProfileDialog } from "../action-buttons/EditProfileDialog";
import { useAccount } from "wagmi";
import DeleteProfileDialog from "../action-buttons/DeleteProfileDialog";
import { AddAttestationDialog } from "../action-buttons/AddAttestationDialog";
+import AddressTokenBalance from "@/components/AddressTokenBalance";
interface ProfileCardProps {
address: string;
@@ -79,6 +80,7 @@ export function ProfileCard({
{displayAddress}
+
{isOwner && (
{
const list = attestationsQuery.data ?? [];
@@ -24,6 +26,15 @@ export function ProfileAttestations({ address }: { address: string }) {
}));
}, [attestationsQuery.data, address]);
+ const profileNameByAddress = useMemo(() => {
+ const map = new Map();
+ const list = profilesQuery.data ?? [];
+ for (const p of list) {
+ if (p.address) map.set(p.address.toLowerCase(), p.name || "");
+ }
+ return map;
+ }, [profilesQuery.data]);
+
const grouped = useMemo(() => {
const map = new Map<
string,
@@ -77,7 +88,15 @@ export function ProfileAttestations({ address }: { address: string }) {
{it.justification}
- Issued by {it.issuer}
+ Issued by{" "}
+
+ {profileNameByAddress.get(
+ it.issuer.toLowerCase()
+ ) || it.issuer}
+
diff --git a/frontend/src/components/profiles/profile-page/ProfileHeader.tsx b/frontend/src/components/profiles/profile-page/ProfileHeader.tsx
index a4cfdc9..cd0bc07 100644
--- a/frontend/src/components/profiles/profile-page/ProfileHeader.tsx
+++ b/frontend/src/components/profiles/profile-page/ProfileHeader.tsx
@@ -2,6 +2,7 @@ import { User } from "lucide-react";
import { useAccount } from "wagmi";
import { useMemo } from "react";
import { useGetProfiles } from "@/hooks/profiles/use-get-profiles";
+import AddressTokenBalance from "@/components/AddressTokenBalance";
export function ProfileHeader({ address }: { address: string }) {
const profilesQuery = useGetProfiles();
@@ -44,6 +45,7 @@ export function ProfileHeader({ address }: { address: string }) {
{displayAddress ? (
{displayAddress}
) : null}
+
);
diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx
index 3bec14b..3530569 100644
--- a/frontend/src/components/ui/tooltip.tsx
+++ b/frontend/src/components/ui/tooltip.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils/index";
function TooltipProvider({
delayDuration = 0,
diff --git a/frontend/src/hooks/attestations/use-create-attestation.ts b/frontend/src/hooks/attestations/use-create-attestation.ts
index 69f5912..79a23c5 100644
--- a/frontend/src/hooks/attestations/use-create-attestation.ts
+++ b/frontend/src/hooks/attestations/use-create-attestation.ts
@@ -1,5 +1,11 @@
-import { useMemo } from "react";
-import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
+import { useMemo, useRef } from "react";
+import {
+ useWriteContract,
+ useWaitForTransactionReceipt,
+ useConfig,
+ useAccount,
+} from "wagmi";
+import { simulateContract } from "@wagmi/core";
import { encodeAbiParameters } from "viem";
import { easAbi } from "@/lib/abis/easAbi";
import {
@@ -31,6 +37,8 @@ function encodeBadgeData(
}
export function useCreateAttestation() {
+ const config = useConfig();
+ const { address: account, chainId } = useAccount();
const {
writeContractAsync,
data: hash,
@@ -38,6 +46,7 @@ export function useCreateAttestation() {
error,
reset,
} = useWriteContract();
+ const isBusyRef = useRef(false);
const createAttestation = useMemo(() => {
return async (
@@ -45,14 +54,20 @@ export function useCreateAttestation() {
badgeName: string,
justification: string
) => {
+ if (isBusyRef.current || isPending) {
+ throw new Error(
+ "Previous attestation is still pending. Please wait..."
+ );
+ }
+ isBusyRef.current = true;
// Convert strings to bytes32
const badgeNameBytes = stringToBytes32(badgeName);
const justificationBytes = stringToBytes32(justification);
// Encode data according to schema
const encodedData = encodeBadgeData(badgeNameBytes, justificationBytes);
- // Call EAS.attest via wagmi
- const uid = await writeContractAsync({
+ // 1) Simulate with exact sender/chain to catch reverts and get prepared request
+ const simulation = await simulateContract(config, {
abi: easAbi,
address: EAS_CONTRACT_ADDRESS,
functionName: "attest",
@@ -70,12 +85,26 @@ export function useCreateAttestation() {
},
},
],
- // value: 0n, // no ETH
+ account: account as `0x${string}` | undefined,
+ chainId,
});
- return uid;
+ // 2) Bump gas params slightly and send
+ const bump = (v?: bigint) =>
+ typeof v === "bigint" ? (v * 12n) / 10n : v;
+ const req: any = { ...simulation.request };
+ req.gas = bump(req.gas);
+ req.maxFeePerGas = bump(req.maxFeePerGas);
+ req.maxPriorityFeePerGas = bump(req.maxPriorityFeePerGas);
+
+ try {
+ const txHash = await writeContractAsync(req);
+ return txHash;
+ } finally {
+ isBusyRef.current = false;
+ }
};
- }, [writeContractAsync]);
+ }, [account, chainId, config, isPending, writeContractAsync]);
const wait = useWaitForTransactionReceipt({
hash: hash as `0x${string}` | undefined,
diff --git a/frontend/src/hooks/attestations/use-get-activity-token-balance.ts b/frontend/src/hooks/attestations/use-get-activity-token-balance.ts
new file mode 100644
index 0000000..9bb1e78
--- /dev/null
+++ b/frontend/src/hooks/attestations/use-get-activity-token-balance.ts
@@ -0,0 +1,40 @@
+import { useMemo } from "react";
+import { useReadContract } from "wagmi";
+import { erc20Abi } from "@/lib/abis/erc20Abi";
+import { ACTIVITY_TOKEN_ADDRESS } from "@/lib/constants/blockchainConstants";
+
+export function useGetActivityTokenBalance(address?: `0x${string}`) {
+ const token = ACTIVITY_TOKEN_ADDRESS;
+
+ const balanceQuery = useReadContract({
+ abi: erc20Abi,
+ address: token,
+ functionName: "balanceOf",
+ args: address ? [address] : undefined,
+ query: { enabled: Boolean(token && address) },
+ });
+
+ const decimalsQuery = useReadContract({
+ abi: erc20Abi,
+ address: token,
+ functionName: "decimals",
+ query: { enabled: Boolean(token) },
+ });
+
+ const value = useMemo(() => {
+ const raw = balanceQuery.data as bigint | undefined;
+ const decimals = Number((decimalsQuery.data as number | undefined) ?? 18);
+ if (raw === undefined) return undefined;
+ return { raw, decimals };
+ }, [balanceQuery.data, decimalsQuery.data]);
+
+ const isLoading = balanceQuery.isLoading || decimalsQuery.isLoading;
+ const error =
+ (balanceQuery.error as Error | null) ||
+ (decimalsQuery.error as Error | null) ||
+ null;
+
+ return { data: value, isLoading, error, refetch: balanceQuery.refetch };
+}
+
+export default useGetActivityTokenBalance;
diff --git a/frontend/src/lib/abis/erc20Abi.ts b/frontend/src/lib/abis/erc20Abi.ts
new file mode 100644
index 0000000..74058ae
--- /dev/null
+++ b/frontend/src/lib/abis/erc20Abi.ts
@@ -0,0 +1,16 @@
+export const erc20Abi = [
+ {
+ type: "function",
+ name: "balanceOf",
+ stateMutability: "view",
+ inputs: [{ name: "account", type: "address" }],
+ outputs: [{ name: "", type: "uint256" }],
+ },
+ {
+ type: "function",
+ name: "decimals",
+ stateMutability: "view",
+ inputs: [],
+ outputs: [{ name: "", type: "uint8" }],
+ },
+] as const;
diff --git a/frontend/src/lib/wagmi.ts b/frontend/src/lib/wagmi.ts
index e2b7afb..6fce4e3 100644
--- a/frontend/src/lib/wagmi.ts
+++ b/frontend/src/lib/wagmi.ts
@@ -1,4 +1,5 @@
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
+import { http } from "wagmi";
import { polygonAmoy } from "wagmi/chains";
const projectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID as
@@ -10,4 +11,8 @@ export const config = getDefaultConfig({
projectId: projectId ?? "",
chains: [polygonAmoy],
ssr: false,
+ syncConnectedChain: true,
+ transports: {
+ [polygonAmoy.id]: http(),
+ },
});
diff --git a/frontend/src/pages/getting-started.astro b/frontend/src/pages/getting-started.astro
new file mode 100644
index 0000000..85a6277
--- /dev/null
+++ b/frontend/src/pages/getting-started.astro
@@ -0,0 +1,16 @@
+---
+import Layout from "../layouts/Layout.astro";
+import GettingStartedPage from "@/components/pages/GettingStartedPage";
+import { AppWrapper } from "@/components/AppWrapper";
+---
+
+
+
+
+
+
+
diff --git a/frontend/src/test/ProfileCard.test.tsx b/frontend/src/test/ProfileCard.test.tsx
index a62c979..9d1804a 100644
--- a/frontend/src/test/ProfileCard.test.tsx
+++ b/frontend/src/test/ProfileCard.test.tsx
@@ -29,6 +29,7 @@ vi.mock("wagmi", () => ({
useWaitForTransactionReceipt: () => ({ data: vi.fn() }),
useReadContract: () => ({ data: undefined }),
useReadContracts: () => ({ data: undefined }),
+ useConfig: () => ({}),
}));
// Mock RainbowKit