diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f23eec..1b1bc09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main, develop ] + branches: [main, develop] pull_request: - branches: [ main, develop ] + branches: [main, develop] env: CARGO_TERM_COLOR: always @@ -14,42 +14,42 @@ jobs: frontend: name: Frontend Tests runs-on: ubuntu-latest - + steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install dependencies - run: | - cd frontend - npm ci - - - name: Run linter - run: | - cd frontend - npm run lint - - - name: Run tests - run: | - cd frontend - npm run test:run - - - name: Build frontend - run: | - cd frontend - npm run build + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: | + cd frontend + npm ci + + - name: Run linter + run: | + cd frontend + npm run lint + + - name: Run tests + run: | + cd frontend + npm run test:run + + - name: Build frontend + run: | + cd frontend + npm run build backend: name: Backend Tests runs-on: ubuntu-latest - + services: postgres: image: postgres:15 @@ -64,57 +64,99 @@ jobs: --health-retries 5 ports: - 5432:5432 - + steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: rustfmt, clippy - override: true - - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - backend/target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- - - - name: Install dependencies - run: | - cd backend - cargo build --verbose - - - name: Run clippy - run: | - cd backend - cargo clippy --all-targets --all-features -- -D warnings - - - name: Run rustfmt - run: | - cd backend - cargo fmt --all -- --check - - - name: Run tests - run: | - cd backend - cargo test --verbose - env: - DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test - TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy + override: true + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + backend/target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install dependencies + run: | + cd backend + cargo build --verbose + + - name: Run clippy + run: | + cd backend + cargo clippy --all-targets --all-features -- -D warnings + + - name: Run rustfmt + run: | + cd backend + cargo fmt --all -- --check + + - name: Run tests + run: | + cd backend + cargo test --verbose + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test + TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test + + smart-contracts: + name: Smart Contracts Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + components: forge, cast, anvil, chisel + + - name: Cache Foundry + uses: actions/cache@v3 + with: + path: | + ~/.foundry + the-guild-smart-contracts/cache + the-guild-smart-contracts/out + key: ${{ runner.os }}-foundry-${{ hashFiles('the-guild-smart-contracts/foundry.lock') }} + restore-keys: | + ${{ runner.os }}-foundry- + + - name: Build contracts + run: | + cd the-guild-smart-contracts + forge build + + - name: Run tests + run: | + cd the-guild-smart-contracts + forge test --verbosity + + - name: Run tests with gas reporting + run: | + cd the-guild-smart-contracts + forge test --gas-report integration: name: Integration Tests runs-on: ubuntu-latest - needs: [frontend, backend] - + needs: [frontend, backend, smart-contracts] + services: postgres: image: postgres:15 @@ -129,53 +171,52 @@ jobs: --health-retries 5 ports: - 5432:5432 - + steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Install dependencies - run: | - cd frontend && npm ci - cd ../backend && cargo build - - - name: Run integration tests - run: | - cd backend - cargo test --test integration_tests --verbose - env: - DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test - TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Install dependencies + run: | + cd frontend && npm ci + cd ../backend && cargo build + + - name: Run integration tests + run: | + cd backend + cargo test --test integration_tests --verbose + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test + TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/guild_genesis_test docker: name: Docker Build Test runs-on: ubuntu-latest - needs: [frontend, backend] - + needs: [frontend, backend, smart-contracts] + steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and test Docker images - run: | - docker-compose build - docker-compose up -d postgres - sleep 10 - docker-compose up --build --abort-on-container-exit backend frontend + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and test Docker images + run: | + docker-compose build + docker-compose up -d postgres + sleep 10 + docker-compose up --build --abort-on-container-exit backend frontend diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..40000dd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "the-guild-smart-contracts/lib/forge-std"] + path = the-guild-smart-contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index 6b13b2e..35dbea7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This is a monorepo containing: - **`frontend/`** - Astro + React frontend with Web3 integration - **`backend/`** - Rust backend with Axum, SQLx, and SIWE authentication +- **`the-guild-smart-contracts/`** - Foundry-based Solidity smart contracts for badge registry ## Tech Stack @@ -30,12 +31,17 @@ This is a monorepo containing: - **PostgreSQL** - Database - **SIWE** - Sign-In with Ethereum authentication +### Smart Contracts +- **Solidity** - Smart contract programming language +- **Foundry** - Fast, portable and modular toolkit for Ethereum application development + ## Quick Start ### Prerequisites - [Nix](https://nixos.org/download.html) with flakes enabled - [direnv](https://direnv.net/) (optional, for automatic environment loading) - [just](https://github.com/casey/just) (command runner) +- [Foundry](https://book.getfoundry.sh/getting-started/installation) (for smart contracts) ### Setup @@ -102,6 +108,30 @@ just db-reset just db-stop ``` +#### Smart Contracts Development + +```bash +# Navigate to smart contracts directory +cd the-guild-smart-contracts + +# Build contracts +forge build + +# Run tests +forge test + +# Run tests with verbose output +forge test -vv + +# Deploy to local network (Anvil) +anvil +# In another terminal: +forge script script/TheGuildBadgeRegistry.s.sol:TheGuildBadgeRegistryScript --rpc-url http://localhost:8545 --private-key --broadcast + +# Deploy to testnet/mainnet +forge script script/TheGuildBadgeRegistry.s.sol:TheGuildBadgeRegistryScript --rpc-url --private-key --broadcast +``` + ### Available Commands Run `just help` to see all available commands: @@ -113,6 +143,44 @@ Run `just help` to see all available commands: - **Code Quality:** `just lint`, `just format` - **Utilities:** `just clean`, `just help` +## Smart Contracts + +The `the-guild-smart-contracts/` directory contains our Solidity smart contracts built with Foundry. + +### TheGuildBadgeRegistry + +A community-driven badge registry where anyone can create badges with unique names and descriptions. + +**Key Features:** +- **Community-driven**: Anyone can create badges +- **Unique names**: No duplicate badge names allowed +- **Immutable**: No owner or upgrade mechanism +- **Gas-efficient**: Simple storage patterns +- **Event-driven**: Emits events for badge creation + +**Contract Interface:** +```solidity +// Create a new badge +function createBadge(bytes32 name, bytes32 description) external + +// Get badge information +function getBadge(bytes32 name) external view returns (bytes32, bytes32, address) + +// Check if badge exists +function exists(bytes32 name) external view returns (bool) + +// Get total number of badges +function totalBadges() external view returns (uint256) + +// Enumerate badges +function badgeNameAt(uint256 index) external view returns (bytes32) +``` + +**Events:** +```solidity +event BadgeCreated(bytes32 indexed name, bytes32 description, address indexed creator) +``` + ## Features ### V0 (Current) @@ -121,12 +189,12 @@ Run `just help` to see all available commands: - [x] Rust backend with Axum - [x] Web3 wallet integration - [x] Basic profile and badge system +- [x] Smart contracts for on-chain badges - [ ] SIWE authentication - [ ] Database models and migrations - [ ] API endpoints for profiles and badges ### V1+ (Future) -- [ ] Smart contracts for on-chain badges - [ ] Gasless transactions - [ ] Badge hierarchy and categories - [ ] Activity and contribution tokens @@ -154,7 +222,7 @@ This project uses Nix for development instead of Docker because: ## Contributing -This is a community-driven project. Join our Discord to discuss features, propose changes, and contribute to the codebase. +This is a community-driven project. Join our [Discord](https://discord.gg/pg4UgaTr) to discuss features, propose changes, and contribute to the codebase. ## License diff --git a/the-guild-smart-contracts/.github/workflows/test.yml b/the-guild-smart-contracts/.github/workflows/test.yml new file mode 100644 index 0000000..4481ec6 --- /dev/null +++ b/the-guild-smart-contracts/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/the-guild-smart-contracts/.gitignore b/the-guild-smart-contracts/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/the-guild-smart-contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/the-guild-smart-contracts/INTEGRATION.md b/the-guild-smart-contracts/INTEGRATION.md new file mode 100644 index 0000000..1207b25 --- /dev/null +++ b/the-guild-smart-contracts/INTEGRATION.md @@ -0,0 +1,206 @@ +# EAS Integration (TypeScript + wagmi) + +This guide shows how to create on-chain attestations of TheGuild badges from a frontend using the EAS SDK, following the schema described in `README.md`. + +- Schema: `bytes32 badgeName, bytes32 justification` +- Hardcoded schema id for now: set `SCHEMA_ID` to a placeholder and replace later + +References: +- EAS SDK: [Creating on-chain attestations](https://docs.attest.org/docs/developer-tools/eas-sdk#creating-onchain-attestations) +- EAS SDK + wagmi: [wagmi integration](https://docs.attest.org/docs/developer-tools/sdk-wagmi) + +## Install + +```bash +npm install @ethereum-attestation-service/eas-sdk wagmi viem ethers @tanstack/react-query +``` + +## Choose network and addresses + +```ts +// EAS contracts: +// Mainnet: 0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587 +// Sepolia: 0xC2679fBD37d54388Ce493F1DB75320D236e1815e +export const EAS_CONTRACT_ADDRESS = '0xC2679fBD37d54388Ce493F1DB75320D236e1815e'; // sepolia default for dev + +// Replace with your real schema id once registered in EAS +export const SCHEMA_ID = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; +``` + +## Encoding helper for the schema + +```ts +import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'; + +export const schemaEncoder = new SchemaEncoder( + 'bytes32 badgeName, bytes32 justification' +); + +export function encodeBadgeData( + badgeNameBytes32: `0x${string}`, + justificationBytes32: `0x${string}` +) { + return schemaEncoder.encodeData([ + { name: 'badgeName', value: badgeNameBytes32, type: 'bytes32' }, + { name: 'justification', value: justificationBytes32, type: 'bytes32' }, + ]); +} +``` + +## Converting strings to bytes32 + +For UI inputs (badge name and justification), hash the string to `bytes32` for a fixed length value. + +```ts +import { keccak256, encodePacked } from 'viem'; + +export function stringToBytes32(value: string): `0x${string}` { + return keccak256(encodePacked(['string'], [value])); +} +``` + +## Hook: create a badge attestation (wagmi + ethers + EAS SDK) + +This uses wagmi for connection state, ethers Signer for EAS SDK, and the EAS `attest` flow. + +```ts +import { useAccount } from 'wagmi'; +import { EAS } from '@ethereum-attestation-service/eas-sdk'; +import { BrowserProvider } from 'ethers'; +import { EAS_CONTRACT_ADDRESS, SCHEMA_ID } from './easConfig'; +import { encodeBadgeData, stringToBytes32 } from './easEncoding'; + +export function useCreateBadgeAttestation() { + const { isConnected } = useAccount(); + + const createAttestation = async ( + recipient: `0x${string}`, + badgeName: string, + justification: string + ) => { + if (!isConnected) throw new Error('Wallet not connected'); + + // 1) Get signer via ethers from injected provider (wagmi has already connected the wallet) + const provider = new BrowserProvider((window as any).ethereum); + const signer = await provider.getSigner(); + + // 2) Init EAS with signer + const eas = new EAS(EAS_CONTRACT_ADDRESS); + eas.connect(signer); + + // 3) Encode data + const encodedData = encodeBadgeData( + stringToBytes32(badgeName), + stringToBytes32(justification) + ); + + // 4) Attest + const tx = await eas.attest({ + schema: SCHEMA_ID, + data: { + recipient, + expirationTime: 0n, // no expiration + revocable: true, + refUID: '0x0000000000000000000000000000000000000000000000000000000000000000', + data: encodedData, + value: 0n, + }, + }); + + // 5) Wait for receipt and get the new attestation UID + const receipt = await tx.wait(); + return receipt.uid; + }; + + return { createAttestation }; +} +``` + +Notes: +- We use `ethers` Signer because EAS SDK expects an ethers-compatible signer/provider in its official examples. +- wagmi manages connection; the injected provider from the connected wallet is available via `window.ethereum`. + +## Example component + +```tsx +import { useState } from 'react'; +import { useAccount, useConnect } from 'wagmi'; +import { useCreateBadgeAttestation } from './useCreateBadgeAttestation'; + +export function BadgeAttestationForm() { + const { isConnected } = useAccount(); + const { connect, connectors } = useConnect(); + const { createAttestation } = useCreateBadgeAttestation(); + + const [recipient, setRecipient] = useState('' as `0x${string}`); + const [badgeName, setBadgeName] = useState(''); + const [justification, setJustification] = useState(''); + const [txUid, setTxUid] = useState(null); + const [loading, setLoading] = useState(false); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!isConnected) return connect({ connector: connectors[0] }); + try { + setLoading(true); + const uid = await createAttestation(recipient, badgeName, justification); + setTxUid(uid); + } catch (err) { + console.error(err); + alert('Failed to create attestation'); + } finally { + setLoading(false); + } + }; + + return ( +
+ setRecipient(e.target.value as `0x${string}`)} + /> + setBadgeName(e.target.value)} + /> + setJustification(e.target.value)} + /> + + {txUid &&

Created attestation UID: {txUid}

} +
+ ); +} +``` + +## Reading an attestation + +```ts +import { EAS } from '@ethereum-attestation-service/eas-sdk'; +import { BrowserProvider } from 'ethers'; +import { EAS_CONTRACT_ADDRESS } from './easConfig'; + +export async function getAttestation(uid: `0x${string}`) { + const provider = new BrowserProvider((window as any).ethereum); + const eas = new EAS(EAS_CONTRACT_ADDRESS); + eas.connect(await provider.getSigner()); + return eas.getAttestation(uid); +} +``` + +## Tips + +- Replace `SCHEMA_ID` with the actual schema id you register in EAS. +- Keep the schema definition exactly as specified: `bytes32 badgeName, bytes32 justification`. +- If you prefer using viem’s clients directly, you can still obtain an `ethers` Signer (as shown) solely for interacting with EAS SDK per its docs. + +Links: [EAS SDK attestations](https://docs.attest.org/docs/developer-tools/eas-sdk#creating-onchain-attestations), [EAS + wagmi](https://docs.attest.org/docs/developer-tools/sdk-wagmi) + + diff --git a/the-guild-smart-contracts/README.md b/the-guild-smart-contracts/README.md new file mode 100644 index 0000000..9e55465 --- /dev/null +++ b/the-guild-smart-contracts/README.md @@ -0,0 +1,70 @@ +## TheGuild Smart Contracts + +The idea is to be able to create badges and then send them to other users (denominated by their address). + +### Badges +Anybody can create a badge. The idea is to let the community input whatever they want and see what they come up with. Badges should be reused and have a description and a unique name. + +We will create a smart contract TheGuildBadgeRegistry that will have a list of badges with unique non-duplicate names. + +### Attestations +Then, we let users create an attestation of a badge to another user. The attestation can contain an optional justification (link to a project, or text explanation). + +To do this, we can just use EAS' already deployed contracts. First we will register our schema (using their sdk or their UI): "bytes32 badgeName, bytes32 justification". Then, in the front end, we can use their sdk to create attestation from one user to another, referencing our schema id, the unique badge name, and a justification. We can also use EAS Resolver contract to prevent duplicate badges and reward attestations with Activity Token. + +### Integration +For detailed frontend integration instructions, see [INTEGRATION.md](./INTEGRATION.md). + +## Foundry Usage + +https://book.getfoundry.sh/ + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/the-guild-smart-contracts/foundry.lock b/the-guild-smart-contracts/foundry.lock new file mode 100644 index 0000000..5643642 --- /dev/null +++ b/the-guild-smart-contracts/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.10.0", + "rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824" + } + } +} \ No newline at end of file diff --git a/the-guild-smart-contracts/foundry.toml b/the-guild-smart-contracts/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/the-guild-smart-contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/the-guild-smart-contracts/lib/forge-std b/the-guild-smart-contracts/lib/forge-std new file mode 160000 index 0000000..8bbcf6e --- /dev/null +++ b/the-guild-smart-contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8bbcf6e3f8f62f419e5429a0bd89331c85c37824 diff --git a/the-guild-smart-contracts/script/TheGuildBadgeRegistry.s.sol b/the-guild-smart-contracts/script/TheGuildBadgeRegistry.s.sol new file mode 100644 index 0000000..aea97ac --- /dev/null +++ b/the-guild-smart-contracts/script/TheGuildBadgeRegistry.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol"; + +contract TheGuildBadgeRegistryScript is Script { + function run() public { + vm.startBroadcast(); + new TheGuildBadgeRegistry(); + vm.stopBroadcast(); + } +} diff --git a/the-guild-smart-contracts/src/TheGuildBadgeRegistry.sol b/the-guild-smart-contracts/src/TheGuildBadgeRegistry.sol new file mode 100644 index 0000000..0e2345f --- /dev/null +++ b/the-guild-smart-contracts/src/TheGuildBadgeRegistry.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/// @title TheGuildBadgeRegistry +/// @notice Minimal registry of community-created badges. Anyone can create a badge. +/// Badge names are unique and cannot be duplicated. +contract TheGuildBadgeRegistry { + /// @notice Representation of a badge. + struct Badge { + bytes32 name; + bytes32 description; + address creator; + } + + /// @notice Emitted when a new badge is created. + event BadgeCreated( + bytes32 indexed name, + bytes32 description, + address indexed creator + ); + + // name => badge + mapping(bytes32 => Badge) private nameToBadge; + // track existence to prevent duplicates + mapping(bytes32 => bool) private nameExists; + + // enumerate badge names + bytes32[] private badgeNames; + + /// @notice Create a new badge with a unique name. + /// @param name The unique badge name (bytes32). + /// @param description The badge description (bytes32). + function createBadge(bytes32 name, bytes32 description) external { + 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; + 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) { + require(nameExists[name], "NOT_FOUND"); + Badge memory b = nameToBadge[name]; + return (b.name, b.description, b.creator); + } + + /// @notice Get whether a badge name exists. + function exists(bytes32 name) external view returns (bool) { + return nameExists[name]; + } + + /// @notice Total number of badges created. + function totalBadges() external view returns (uint256) { + return badgeNames.length; + } + + /// @notice Get badge name at a specific index for enumeration. + /// @dev Reverts if index is out of bounds. + function badgeNameAt(uint256 index) external view returns (bytes32) { + return badgeNames[index]; + } +} diff --git a/the-guild-smart-contracts/test/TheGuildBadgeRegistry.t.sol b/the-guild-smart-contracts/test/TheGuildBadgeRegistry.t.sol new file mode 100644 index 0000000..a521c40 --- /dev/null +++ b/the-guild-smart-contracts/test/TheGuildBadgeRegistry.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol"; + +contract TheGuildBadgeRegistryTest is Test { + TheGuildBadgeRegistry private registry; + + function setUp() public { + registry = new TheGuildBadgeRegistry(); + } + + function test_CreateBadge_SucceedsAndEmitsEvent() public { + bytes32 name = bytes32("BADGE_ALPHA"); + bytes32 description = bytes32("First badge"); + + vm.expectEmit(true, false, false, true); + emit TheGuildBadgeRegistry.BadgeCreated( + name, + description, + address(this) + ); + + registry.createBadge(name, description); + + (bytes32 rName, bytes32 rDesc, address creator) = registry.getBadge( + name + ); + assertEq(rName, name, "name mismatch"); + assertEq(rDesc, description, "description mismatch"); + assertEq(creator, address(this), "creator mismatch"); + assertTrue(registry.exists(name), "should exist"); + assertEq(registry.totalBadges(), 1); + assertEq(registry.badgeNameAt(0), name); + } + + function test_CreateBadge_RevertOnDuplicate() public { + bytes32 name = bytes32("BADGE_DUP"); + registry.createBadge(name, bytes32("desc")); + + vm.expectRevert(bytes("DUPLICATE_NAME")); + registry.createBadge(name, bytes32("another")); + } + + function test_GetBadge_RevertOnMissing() public { + vm.expectRevert(bytes("NOT_FOUND")); + registry.getBadge(bytes32("MISSING")); + } + + function test_CreateBadge_RevertOnEmptyName() public { + vm.expectRevert(bytes("EMPTY_NAME")); + registry.createBadge(bytes32(0), bytes32("desc")); + } +}