Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,13 @@ A community-driven badge registry where anyone can create badges with unique nam
- **Gas-efficient**: Simple storage patterns
- **Event-driven**: Emits events for badge creation

**Contract Interface:**
**Contract Interface (V2 - current):**
```solidity
// Create a new badge
function createBadge(bytes32 name, bytes32 description) external
function createBadge(bytes32 name, bytes calldata description) external

// Get badge information
function getBadge(bytes32 name) external view returns (bytes32, bytes32, address)
function getBadge(bytes32 name) external view returns (bytes32, bytes memory, address)

// Check if badge exists
function exists(bytes32 name) external view returns (bool)
Expand All @@ -197,13 +197,16 @@ function totalBadges() external view returns (uint256)

// Enumerate badges
function badgeNameAt(uint256 index) external view returns (bytes32)
function getBadgeAt(uint256 index) external view returns (bytes32, bytes memory, address)
```

**Events:**
```solidity
event BadgeCreated(bytes32 indexed name, bytes32 description, address indexed creator)
event BadgeCreated(bytes32 indexed name, bytes description, address indexed creator)
```

> **Note:** V1 contracts used `bytes32` for descriptions (max 32 chars). V2 uses `bytes` for unlimited length. The frontend is retrocompatible with both versions.

### TheGuildActivityToken (TGA)

An ERC20 token used to reward attestations. Ownable; the owner is the attestation resolver contract.
Expand Down
48 changes: 48 additions & 0 deletions frontend/docs/V2_CLEANUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# BadgeRegistry V2 Cleanup Guide

## Overview

This document outlines the cleanup steps required after full V2 deployment of BadgeRegistry contracts. The current codebase includes retro-compatibility logic to support both V1 and V2 contracts during the migration period. Once all registries are upgraded to V2, this temporary code can be removed.

## Files to Delete

- `frontend/src/lib/utils/abiDetection.ts` - Error-based ABI detection utilities
- `frontend/src/lib/badges/registryVersion.ts` - Version detection module

## Functions / Logic to Remove

- `detectBadgeRegistryVersion()` - Version detection function
- `isDecodeError()` - Decode error detection utility
- `isFunctionSelectorError()` - Function selector error detection utility
- `buildCreateBadgeArgs()` - Conditional argument builder for V1/V2 differences
- Any version-probe logic / error-based ABI inference

## Hook Simplifications

### use-create-badge.ts

- Remove version detection and conditional ABI selection
- Remove `detectBadgeRegistryVersion()` call
- Always use `badgeRegistryAbiV2` (remove conditional `finalAbiMode === "v2" ? badgeRegistryAbiV2 : badgeRegistryAbiV1`)
- Always use `bytes` description format: replace `buildCreateBadgeArgs()` with direct `stringToBytes(description)`
- Remove `badgeRegistryAbiV1` import
- Remove `detectBadgeRegistryVersion` import
- Remove `buildCreateBadgeArgs` import

### use-get-badges.ts

- Remove version probing / `abiMode` inference
- Remove `versionProbeQuery` query
- Remove `abiMode` useMemo logic
- Always use `badgeRegistryAbiV2` in `badgeContracts` (remove conditional ABI selection)
- Always decode description as `bytes` using `bytesToString()` (remove conditional `bytesToString` vs `bytes32ToString`)
- Remove `isDecodeError` import and usage
- Remove `badgeRegistryAbiV1` import

## Expected End State

- Only V2 ABI (`badgeRegistryAbiV2`) used throughout the codebase
- No fallback branches or conditional logic based on contract version
- No error-based ABI detection or version probing
- Simpler, more maintainable codebase with reduced complexity

12 changes: 11 additions & 1 deletion frontend/src/components/AppWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,17 @@ import { ActivityTokenBalance } from "@/components/ActivityTokenBalance";
import { Background } from "@/components/Background";
import { LoginButton } from "@/components/LoginButton";

const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // 1 minute - badges change infrequently
refetchOnWindowFocus: false, // Prevent refetch storms on alt-tab
refetchOnReconnect: false, // Prevent refetch storms on network reconnect
refetchOnMount: false, // Prevent refetch on component remount (cache is fresh)
retry: 1, // Single retry for transient errors
},
},
});

interface AppWrapperProps {
children: React.ReactNode;
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/badges/BadgesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ICONS: Record<string, string> = {
};

export function BadgesList(): React.ReactElement {
const { data, isLoading, error } = useGetBadges();
const { data, isLoading, error, refetch } = useGetBadges();
const [searchQuery, setSearchQuery] = useState("");
const list = (data && data.length > 0 ? data : HARD_CODED_BADGES) as Badge[];

Expand Down Expand Up @@ -62,7 +62,7 @@ export function BadgesList(): React.ReactElement {
/>
</div>

<CreateBadgeButton />
<CreateBadgeButton onBadgeCreated={refetch} />
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/badges/CreateBadgeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useGetBadges } from "@/hooks/badges/use-get-badges";

const formSchema = z.object({
name: z.string().min(1, { message: "Name is required." }).max(32),
Expand All @@ -32,11 +31,14 @@ const formSchema = z.object({

type FormValues = z.infer<typeof formSchema>;

export function CreateBadgeButton() {
interface CreateBadgeButtonProps {
onBadgeCreated?: () => void;
}

export function CreateBadgeButton({ onBadgeCreated }: CreateBadgeButtonProps) {
const [open, setOpen] = useState(false);
const { createBadge, isPending, error, reset, isConfirmed, isConfirming } =
useCreateBadge();
const { refetch } = useGetBadges();

const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
Expand All @@ -49,12 +51,12 @@ export function CreateBadgeButton() {

useEffect(() => {
if (isConfirmed) {
refetch();
onBadgeCreated?.();
setOpen(false);
form.reset();
reset();
}
}, [isConfirmed, refetch, form, reset]);
}, [isConfirmed, onBadgeCreated, form, reset]);

return (
<Dialog open={open} onOpenChange={setOpen}>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/displayError/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ type ErrorDisplayProps = {
};

export default function ErrorDisplay({ error }: ErrorDisplayProps) {
if (!error) return null;
if (!error) return null;

return (
<p className="text-2xl text-yellow-600 flex items-center gap-2">
Expand Down
25 changes: 3 additions & 22 deletions frontend/src/hooks/attestations/use-create-attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,7 @@ import {
EAS_CONTRACT_ADDRESS,
SCHEMA_ID,
} from "@/lib/constants/blockchainConstants";

function stringToBytes32(value: string): `0x${string}` {
const encoder = new TextEncoder();
const bytes = encoder.encode(value);
const out = new Uint8Array(32);
const len = Math.min(32, bytes.length);
for (let i = 0; i < len; i++) out[i] = bytes[i];
let hex = "0x";
for (let i = 0; i < out.length; i++)
hex += out[i].toString(16).padStart(2, "0");
return hex as `0x${string}`;
}

function stringToBytes(value: string): `0x${string}` {
const encoder = new TextEncoder();
const bytes = encoder.encode(value);
let hex = "0x";
for (let i = 0; i < bytes.length; i++)
hex += bytes[i].toString(16).padStart(2, "0");
return hex as `0x${string}`;
}
import { stringToBytes32, stringToBytes } from "@/lib/utils/blockchainUtils";

function encodeBadgeData(
badgeName: `0x${string}`,
Expand Down Expand Up @@ -69,7 +49,7 @@ export function useCreateAttestation() {
);
}
isBusyRef.current = true;
// Convert strings to bytes
// Convert strings to bytes32
const badgeNameBytes = stringToBytes32(badgeName);
const justificationBytes = stringToBytes(justification);

Expand Down Expand Up @@ -117,6 +97,7 @@ export function useCreateAttestation() {

const wait = useWaitForTransactionReceipt({
hash: hash as `0x${string}` | undefined,
confirmations: 6,
query: { enabled: Boolean(hash) },
});

Expand Down
58 changes: 49 additions & 9 deletions frontend/src/hooks/badges/use-create-badge.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,66 @@
import { useMemo } from "react";
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { useWriteContract, useWaitForTransactionReceipt, useAccount, useConfig } from "wagmi";
import { simulateContract, readContract } from "@wagmi/core";
import { BADGE_REGISTRY_ADDRESS } from "@/lib/constants/blockchainConstants";
import { badgeRegistryAbi } from "@/lib/abis/badgeRegistryAbi";
import { stringToBytes32 } from "@/lib/utils/blockchainUtils";
import {
badgeRegistryAbiV1,
badgeRegistryAbiV2,
} from "@/lib/abis/badgeRegistryAbi";
import { stringToBytes32, buildCreateBadgeArgs } from "@/lib/utils/blockchainUtils";
import { detectBadgeRegistryVersion } from "@/lib/badges/registryVersion";

export function useCreateBadge() {
const config = useConfig();
const { address: account, chainId } = useAccount();
const { writeContractAsync, isPending, error, data, reset } =
useWriteContract();

const createBadge = useMemo(() => {
return async (name: string, description: string) => {
if (!BADGE_REGISTRY_ADDRESS) throw new Error("Missing registry address");
if (!BADGE_REGISTRY_ADDRESS) {
throw new Error("Badge registry address not configured");
}
if (!account) {
throw new Error("No wallet connected");
}
if (!chainId) {
throw new Error("No chain ID available");
}

const nameBytes = stringToBytes32(name);
const descriptionBytes = stringToBytes32(description);
return writeContractAsync({
abi: badgeRegistryAbi,

// Determine ABI mode deterministically BEFORE sending transaction
// All version detection happens on-demand here to prevent refetch storm
// Fetch totalBadges on-demand
const totalBadgesResult = await readContract(config, {
abi: badgeRegistryAbiV2,
address: BADGE_REGISTRY_ADDRESS,
functionName: "totalBadges",
});
const currentCount = Number(totalBadgesResult ?? 0n);

// TODO(cleanup-after-v2): Remove V1 fallback logic after V2 full deployment. Always use V2 ABI. See docs/V2_CLEANUP.md.
const finalAbiMode = await detectBadgeRegistryVersion(
config,
account as `0x${string}`,
chainId,
currentCount
);

// Now that ABI mode is determined, simulate and send with correct ABI
const simulation = await simulateContract(config, {
abi: finalAbiMode === "v2" ? badgeRegistryAbiV2 : badgeRegistryAbiV1,
address: BADGE_REGISTRY_ADDRESS,
functionName: "createBadge",
args: [nameBytes, descriptionBytes],
args: buildCreateBadgeArgs(nameBytes, description, finalAbiMode),
account: account as `0x${string}` | undefined,
chainId,
});

// Single writeContractAsync call with correct ABI
return await writeContractAsync(simulation.request);
};
}, [writeContractAsync]);
}, [writeContractAsync, account, chainId, config]);

const wait = useWaitForTransactionReceipt({
hash: data as `0x${string}` | undefined,
Expand Down
Loading
Loading