From ef0ae094b47364e01b993e2ffb7c035affdb66a2 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Wed, 3 Dec 2025 15:57:56 +0100 Subject: [PATCH 1/3] update and run notebook to get list of attestations --- notebooks/01_github_issues.ipynb | 473 ++++++++++++++---- notebooks/env.example | 4 +- the-guild-smart-contracts/.env.example | 2 + .../attestations-2025-12-02.json | 139 +++++ ...nsCsv.s.sol => EmitAttestationsJson.s.sol} | 0 5 files changed, 521 insertions(+), 97 deletions(-) create mode 100644 the-guild-smart-contracts/attestations-2025-12-02.json rename the-guild-smart-contracts/script/{EmitAttestationsCsv.s.sol => EmitAttestationsJson.s.sol} (100%) diff --git a/notebooks/01_github_issues.ipynb b/notebooks/01_github_issues.ipynb index 9b77e78..f3534c3 100644 --- a/notebooks/01_github_issues.ipynb +++ b/notebooks/01_github_issues.ipynb @@ -24,14 +24,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Fetched 42 issues from TheSoftwareDevGuild/TheGuildGenesis\n" + "Fetched 43 issues from TheSoftwareDevGuild/TheGuildGenesis\n" ] }, { @@ -69,122 +69,122 @@ " \n", " \n", " 0\n", - " 126\n", - " Use jupyter notebook to analyze github issues\n", + " 142\n", + " Add contributor leaderboard\n", " open\n", - " 2025-10-30T13:17:36Z\n", - " 2025-10-30T13:17:36Z\n", - " joelamouche\n", + " 2025-11-24T15:04:07Z\n", + " 2025-11-24T15:04:42Z\n", " joelamouche\n", - " jupyter-notebook,python\n", + " \n", + " good first issue,front end,react,typescript,80pts\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 1\n", - " 125\n", - " Improve UX and design of theguild.dev\n", + " 141\n", + " Add a new leaderboard page, with link in the s...\n", " open\n", - " 2025-10-28T11:23:19Z\n", - " 2025-10-28T11:23:19Z\n", + " 2025-11-24T15:01:00Z\n", + " 2025-11-24T15:01:00Z\n", " joelamouche\n", " \n", - " good first issue,ux,design,hacktoberfest\n", + " good first issue,front end,react,typescript,20...\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 2\n", - " 118\n", - " Badge enhancements\n", + " 140\n", + " Create top badge owner leaderboard\n", " open\n", - " 2025-10-17T08:35:11Z\n", - " 2025-10-30T13:44:59Z\n", - " joelamouche\n", + " 2025-11-24T14:59:35Z\n", + " 2025-11-24T15:02:16Z\n", " joelamouche\n", - " enhancement,planning,hacktoberfest\n", + " \n", + " good first issue,front end,react,typescript,40pts\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 3\n", - " 110\n", - " Discord bot: relay git activity on a discord c...\n", + " 138\n", + " Public contributor leaderboard\n", " open\n", - " 2025-10-10T09:57:28Z\n", - " 2025-10-30T09:00:26Z\n", + " 2025-11-17T13:58:58Z\n", + " 2025-11-24T15:04:19Z\n", " joelamouche\n", - " \n", - " good first issue,nodejs,typescript,discord-bot...\n", + " joelamouche\n", + " enhancement,front end,planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 4\n", - " 106\n", - " Implement JWT for our profile api\n", + " 137\n", + " Figure out sharing new badges on X\n", " open\n", - " 2025-10-09T13:18:18Z\n", - " 2025-10-27T08:48:29Z\n", + " 2025-11-17T13:58:23Z\n", + " 2025-11-17T13:58:28Z\n", + " joelamouche\n", " joelamouche\n", - " tusharshah21\n", - " front end,rust,back-end,react,typescript,hackt...\n", + " planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 5\n", - " 105\n", - " Add backend endpoints to fetch attestations\n", + " 135\n", + " Add projects to the Backend\n", " open\n", - " 2025-10-09T12:31:06Z\n", - " 2025-10-09T12:31:06Z\n", + " 2025-11-17T12:38:30Z\n", + " 2025-11-27T12:15:32Z\n", " joelamouche\n", - " \n", - " rust,back-end,db\n", + " pheobeayo\n", + " good first issue,rust,back-end,160pts,db,hackt...\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 6\n", - " 104\n", - " Blockchain Indexer - Get blockchain data from ...\n", + " 134\n", + " Project Page\n", " open\n", - " 2025-10-09T12:20:02Z\n", - " 2025-10-09T12:33:22Z\n", + " 2025-11-17T12:38:11Z\n", + " 2025-11-27T10:58:18Z\n", + " joelamouche\n", " joelamouche\n", - " oscarwroche\n", " enhancement,planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 7\n", - " 103\n", - " Improve auth logic for API\n", + " 131\n", + " Add backend for distributions\n", " open\n", - " 2025-10-09T12:14:41Z\n", - " 2025-10-14T08:05:29Z\n", + " 2025-11-03T13:30:48Z\n", + " 2025-11-24T10:10:14Z\n", " joelamouche\n", - " oscarwroche\n", - " enhancement,planning\n", + " Chesblaw\n", + " good first issue,rust,back-end,typescript,db\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 8\n", - " 102\n", - " Add twitter handle to profiles in the backend\n", + " 125\n", + " Improve UX and design of theguild.dev\n", " open\n", - " 2025-10-08T13:18:45Z\n", - " 2025-10-27T08:43:18Z\n", + " 2025-10-28T11:23:19Z\n", + " 2025-10-28T11:23:19Z\n", " joelamouche\n", - " ayushhh101\n", - " good first issue,rust,back-end,db,hacktoberfest\n", + " \n", + " good first issue,ux,design,hacktoberfest\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 9\n", - " 101\n", - " Add twitter account to profiles\n", + " 118\n", + " Badge enhancements\n", " open\n", - " 2025-10-08T13:16:50Z\n", - " 2025-10-16T13:09:30Z\n", + " 2025-10-17T08:35:11Z\n", + " 2025-10-30T13:44:59Z\n", " joelamouche\n", - " \n", - " enhancement,planning\n", + " joelamouche\n", + " enhancement,planning,hacktoberfest\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", @@ -193,40 +193,40 @@ ], "text/plain": [ " number title state \\\n", - "0 126 Use jupyter notebook to analyze github issues open \n", - "1 125 Improve UX and design of theguild.dev open \n", - "2 118 Badge enhancements open \n", - "3 110 Discord bot: relay git activity on a discord c... open \n", - "4 106 Implement JWT for our profile api open \n", - "5 105 Add backend endpoints to fetch attestations open \n", - "6 104 Blockchain Indexer - Get blockchain data from ... open \n", - "7 103 Improve auth logic for API open \n", - "8 102 Add twitter handle to profiles in the backend open \n", - "9 101 Add twitter account to profiles open \n", + "0 142 Add contributor leaderboard open \n", + "1 141 Add a new leaderboard page, with link in the s... open \n", + "2 140 Create top badge owner leaderboard open \n", + "3 138 Public contributor leaderboard open \n", + "4 137 Figure out sharing new badges on X open \n", + "5 135 Add projects to the Backend open \n", + "6 134 Project Page open \n", + "7 131 Add backend for distributions open \n", + "8 125 Improve UX and design of theguild.dev open \n", + "9 118 Badge enhancements open \n", "\n", - " created_at updated_at user assignees \\\n", - "0 2025-10-30T13:17:36Z 2025-10-30T13:17:36Z joelamouche joelamouche \n", - "1 2025-10-28T11:23:19Z 2025-10-28T11:23:19Z joelamouche \n", - "2 2025-10-17T08:35:11Z 2025-10-30T13:44:59Z joelamouche joelamouche \n", - "3 2025-10-10T09:57:28Z 2025-10-30T09:00:26Z joelamouche \n", - "4 2025-10-09T13:18:18Z 2025-10-27T08:48:29Z joelamouche tusharshah21 \n", - "5 2025-10-09T12:31:06Z 2025-10-09T12:31:06Z joelamouche \n", - "6 2025-10-09T12:20:02Z 2025-10-09T12:33:22Z joelamouche oscarwroche \n", - "7 2025-10-09T12:14:41Z 2025-10-14T08:05:29Z joelamouche oscarwroche \n", - "8 2025-10-08T13:18:45Z 2025-10-27T08:43:18Z joelamouche ayushhh101 \n", - "9 2025-10-08T13:16:50Z 2025-10-16T13:09:30Z joelamouche \n", + " created_at updated_at user assignees \\\n", + "0 2025-11-24T15:04:07Z 2025-11-24T15:04:42Z joelamouche \n", + "1 2025-11-24T15:01:00Z 2025-11-24T15:01:00Z joelamouche \n", + "2 2025-11-24T14:59:35Z 2025-11-24T15:02:16Z joelamouche \n", + "3 2025-11-17T13:58:58Z 2025-11-24T15:04:19Z joelamouche joelamouche \n", + "4 2025-11-17T13:58:23Z 2025-11-17T13:58:28Z joelamouche joelamouche \n", + "5 2025-11-17T12:38:30Z 2025-11-27T12:15:32Z joelamouche pheobeayo \n", + "6 2025-11-17T12:38:11Z 2025-11-27T10:58:18Z joelamouche joelamouche \n", + "7 2025-11-03T13:30:48Z 2025-11-24T10:10:14Z joelamouche Chesblaw \n", + "8 2025-10-28T11:23:19Z 2025-10-28T11:23:19Z joelamouche \n", + "9 2025-10-17T08:35:11Z 2025-10-30T13:44:59Z joelamouche joelamouche \n", "\n", " labels \\\n", - "0 jupyter-notebook,python \n", - "1 good first issue,ux,design,hacktoberfest \n", - "2 enhancement,planning,hacktoberfest \n", - "3 good first issue,nodejs,typescript,discord-bot... \n", - "4 front end,rust,back-end,react,typescript,hackt... \n", - "5 rust,back-end,db \n", + "0 good first issue,front end,react,typescript,80pts \n", + "1 good first issue,front end,react,typescript,20... \n", + "2 good first issue,front end,react,typescript,40pts \n", + "3 enhancement,front end,planning \n", + "4 planning \n", + "5 good first issue,rust,back-end,160pts,db,hackt... \n", "6 enhancement,planning \n", - "7 enhancement,planning \n", - "8 good first issue,rust,back-end,db,hacktoberfest \n", - "9 enhancement,planning \n", + "7 good first issue,rust,back-end,typescript,db \n", + "8 good first issue,ux,design,hacktoberfest \n", + "9 enhancement,planning,hacktoberfest \n", "\n", " url \n", "0 https://github.com/TheSoftwareDevGuild/TheGuil... \n", @@ -241,7 +241,7 @@ "9 https://github.com/TheSoftwareDevGuild/TheGuil... " ] }, - "execution_count": 3, + "execution_count": 1, "metadata": {}, "output_type": "execute_result" } @@ -330,7 +330,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -347,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -356,9 +356,9 @@ "text": [ "{\n", " \"contributors\": 4,\n", - " \"tags\": 26,\n", - " \"total_contributions\": 940,\n", - " \"number_of_closed_tickets\": 34\n", + " \"tags\": 28,\n", + " \"total_contribution_tokens\": 1040,\n", + " \"number_of_closed_tickets\": 42\n", "}\n" ] } @@ -415,6 +415,287 @@ "print(json.dumps(stats, indent=4))\n", " " ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate Attestations from Issues\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### fetch all issues" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fetched 42 total issues (open + closed)\n", + "\n", + "============================================================\n", + "BADGES/LABELS THAT NEED TO BE CREATED (23):\n", + "============================================================\n", + " - back-end\n", + " - blockchain\n", + " - bug\n", + " - db\n", + " - design\n", + " - discord-bot\n", + " - documentation\n", + " - foundry\n", + " - front end\n", + " - good first issue\n", + " - hacktoberfest\n", + " - in progress\n", + " - jupyter-notebook\n", + " - nodejs\n", + " - onlydust-wave\n", + " - planning\n", + " - python\n", + " - react\n", + " - rust\n", + " - solidity\n", + " - typescript\n", + " - ux\n", + " - wagmi\n", + "============================================================\n", + "\n" + ] + } + ], + "source": [ + "import json\n", + "import re\n", + "from datetime import datetime\n", + "from collections import defaultdict\n", + "\n", + "# Fetch all issues (both open and closed) to get complete picture\n", + "all_issues = fetch_issues(state=\"closed\", per_page=100, max_pages=10)\n", + "print(f\"Fetched {len(all_issues)} total issues (open + closed)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract Labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Extract unique labels/badges from all issues\n", + "all_labels = set()\n", + "for issue in all_issues:\n", + " for label in issue.get(\"labels\", []):\n", + " label_name = label.get(\"name\", \"\")\n", + " # Filter out point labels (e.g., \"10pts\", \"5pts\") as they're not badges\n", + " if label_name and not re.match(r'^\\d+pts$', label_name):\n", + " all_labels.add(label_name)\n", + "\n", + "badges_to_create = sorted(list(all_labels))\n", + "print(f\"\\n{'='*60}\")\n", + "print(f\"BADGES/LABELS THAT NEED TO BE CREATED ({len(badges_to_create)}):\")\n", + "print(f\"{'='*60}\")\n", + "for badge in badges_to_create:\n", + " print(f\" - {badge}\")\n", + "print(f\"{'='*60}\\n\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get github handles from API" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://theguild-backend-e2df290d177e.herokuapp.com/\n", + "Loaded 1 GitHub username -> address mappings from backend API\n", + "\n", + "Warning: 7 assignees without address mapping:\n", + " - oscarwroche\n", + " - peteroche\n", + " - pheobeayo\n", + " - rainwaters11\n", + " - teddy1792\n", + " - tusharshah21\n", + " - yash-1104github\n", + "These will be skipped in attestations.\n", + "\n" + ] + } + ], + "source": [ + "# Optionally fetch profiles from backend API to map GitHub usernames to addresses\n", + "BACKEND_API_URL = os.getenv(\"BACKEND_API_URL\", \"http://localhost:3000\")\n", + "print(BACKEND_API_URL)\n", + "github_to_address = {}\n", + "\n", + "try:\n", + " profiles_resp = requests.get(f\"{BACKEND_API_URL}/profiles\", timeout=5)\n", + " if profiles_resp.status_code == 200:\n", + " profiles = profiles_resp.json()\n", + " for profile in profiles:\n", + " if profile.get(\"github_login\"):\n", + " github_to_address[profile[\"github_login\"].lower()] = profile[\"address\"]\n", + " print(f\"Loaded {len(github_to_address)} GitHub username -> address mappings from backend API\")\n", + " else:\n", + " print(f\"Backend API not available (status {profiles_resp.status_code}), will need manual address mapping\")\n", + "except Exception as e:\n", + " print(f\"Could not fetch profiles from backend API ({e}), will need manual address mapping\")\n", + "\n", + "# Show unmapped assignees\n", + "all_assignees = set()\n", + "for issue in all_issues:\n", + " for assignee in issue.get(\"assignees\", []):\n", + " username = assignee.get(\"login\", \"\").lower()\n", + " if username:\n", + " all_assignees.add(username)\n", + "\n", + "unmapped = [u for u in all_assignees if u not in github_to_address]\n", + "if unmapped:\n", + " print(f\"\\nWarning: {len(unmapped)} assignees without address mapping:\")\n", + " for u in sorted(unmapped):\n", + " print(f\" - {u}\")\n", + " print(\"These will be skipped in attestations.\\n\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generate all attestations" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated 27 attestations from 42 issues\n", + "Unique recipients: 1\n", + "Unique badges: 13\n" + ] + } + ], + "source": [ + "# Generate attestations from issues\n", + "# For each issue with assignees and labels, create attestations\n", + "attestations = []\n", + "\n", + "for issue in all_issues:\n", + " assignees = issue.get(\"assignees\", [])\n", + " labels = issue.get(\"labels\", [])\n", + " issue_number = issue.get(\"number\")\n", + " issue_title = issue.get(\"title\", \"\")\n", + " issue_url = issue.get(\"html_url\", \"\")\n", + " \n", + " # Skip if no assignees or no labels\n", + " if not assignees or not labels:\n", + " continue\n", + " \n", + " # Process each assignee\n", + " for assignee in assignees:\n", + " github_username = assignee.get(\"login\", \"\").lower()\n", + " if not github_username:\n", + " continue\n", + " \n", + " # Get Ethereum address for this GitHub user\n", + " recipient_address = github_to_address.get(github_username)\n", + " if not recipient_address:\n", + " continue # Skip if no address mapping\n", + " \n", + " # Create attestation for each label (badge) on this issue\n", + " for label in labels:\n", + " label_name = label.get(\"name\", \"\")\n", + " # Skip point labels\n", + " if not label_name or re.match(r'^\\d+pts$', label_name):\n", + " continue\n", + " \n", + " # Create justification based on issue\n", + " justification = f\"Awarded for contributions to issue #{issue_number}: {issue_title}\"\n", + " if issue_url:\n", + " justification += f\" ({issue_url})\"\n", + " \n", + " attestations.append({\n", + " \"recipient\": recipient_address,\n", + " \"badgeName\": label_name,\n", + " \"justification\": justification\n", + " })\n", + "\n", + "print(f\"Generated {len(attestations)} attestations from {len(all_issues)} issues\")\n", + "print(f\"Unique recipients: {len(set(a['recipient'] for a in attestations))}\")\n", + "print(f\"Unique badges: {len(set(a['badgeName'] for a in attestations))}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save attestations to file" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "✓ Saved 27 attestations to: ../the-guild-smart-contracts/attestations-2025-12-02.json\n", + " File size: 7751 bytes\n" + ] + } + ], + "source": [ + "# Save attestations to date-based file\n", + "from pathlib import Path\n", + "\n", + "output_dir = Path(\"../the-guild-smart-contracts\")\n", + "output_dir.mkdir(parents=True, exist_ok=True)\n", + "\n", + "# Create filename with current date\n", + "date_str = datetime.now().strftime(\"%Y-%m-%d\")\n", + "output_file = output_dir / f\"attestations-{date_str}.json\"\n", + "\n", + "output_data = {\n", + " \"attestations\": attestations\n", + "}\n", + "\n", + "with open(output_file, \"w\") as f:\n", + " json.dump(output_data, f, indent=2)\n", + "\n", + "print(f\"\\n✓ Saved {len(attestations)} attestations to: {output_file}\")\n", + "print(f\" File size: {output_file.stat().st_size} bytes\")\n" + ] } ], "metadata": { diff --git a/notebooks/env.example b/notebooks/env.example index 243a69c..eca5436 100644 --- a/notebooks/env.example +++ b/notebooks/env.example @@ -1,4 +1,6 @@ GITHUB_TOKEN= GITHUB_OWNER= GITHUB_REPO= -GITHUB_API_URL= \ No newline at end of file +GITHUB_API_URL= + +BACKEND_API_URL= \ No newline at end of file diff --git a/the-guild-smart-contracts/.env.example b/the-guild-smart-contracts/.env.example index 393a5c3..ce15e2b 100644 --- a/the-guild-smart-contracts/.env.example +++ b/the-guild-smart-contracts/.env.example @@ -8,6 +8,8 @@ CREATE2_SALT=1 # Generic fallback for any other network (used if chain isn't matched) EAS_ADDRESS= +BADGE_REGISTRY_ADDRESS= + # RPC URLS AMOY_RPC_URL=https://polygon-amoy.drpc.org BASE_SEPOLIA_URL=https://base-sepolia.therpc.io diff --git a/the-guild-smart-contracts/attestations-2025-12-02.json b/the-guild-smart-contracts/attestations-2025-12-02.json new file mode 100644 index 0000000..c38a03a --- /dev/null +++ b/the-guild-smart-contracts/attestations-2025-12-02.json @@ -0,0 +1,139 @@ +{ + "attestations": [ + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "jupyter-notebook", + "justification": "Awarded for contributions to issue #126: Use jupyter notebook to analyze github issues (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/126)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "python", + "justification": "Awarded for contributions to issue #126: Use jupyter notebook to analyze github issues (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/126)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "bug", + "justification": "Awarded for contributions to issue #119: Discord Link (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/119)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "documentation", + "justification": "Awarded for contributions to issue #119: Discord Link (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/119)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "documentation", + "justification": "Awarded for contributions to issue #91: Improve instructions to run things locally (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/91)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "documentation", + "justification": "Awarded for contributions to issue #72: Contribution token feature plan (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/72)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "planning", + "justification": "Awarded for contributions to issue #72: Contribution token feature plan (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/72)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "solidity", + "justification": "Awarded for contributions to issue #55: Could be a good idea to merge ActivityToken and BadgeRegistry contracts into one (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/55)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "foundry", + "justification": "Awarded for contributions to issue #55: Could be a good idea to merge ActivityToken and BadgeRegistry contracts into one (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/55)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "good first issue", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "front end", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "react", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "typescript", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "wagmi", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "documentation", + "justification": "Awarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "front end", + "justification": "Awarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "in progress", + "justification": "Awarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "solidity", + "justification": "Awarded for contributions to issue #48: Prevent duplicate attestations (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/48)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "foundry", + "justification": "Awarded for contributions to issue #48: Prevent duplicate attestations (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/48)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "bug", + "justification": "Awarded for contributions to issue #47: Submitting too many attestations in a row emits error (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/47)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "front end", + "justification": "Awarded for contributions to issue #47: Submitting too many attestations in a row emits error (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/47)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "documentation", + "justification": "Awarded for contributions to issue #45: Update contribution guide (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/45)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "front end", + "justification": "Awarded for contributions to issue #34: Api doesn't seem to work on mobile (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/34)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "good first issue", + "justification": "Awarded for contributions to issue #32: Issuer should link to profile (and use name) (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/32)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "front end", + "justification": "Awarded for contributions to issue #32: Issuer should link to profile (and use name) (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/32)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "solidity", + "justification": "Awarded for contributions to issue #29: Full contract test coverage (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/29)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "front end", + "justification": "Awarded for contributions to issue #22: Add basic project description in the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/22)" + } + ] +} \ No newline at end of file diff --git a/the-guild-smart-contracts/script/EmitAttestationsCsv.s.sol b/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol similarity index 100% rename from the-guild-smart-contracts/script/EmitAttestationsCsv.s.sol rename to the-guild-smart-contracts/script/EmitAttestationsJson.s.sol From 21a082aba89ecb6656262e3403116d8bf03b4b5f Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Wed, 3 Dec 2025 15:58:17 +0100 Subject: [PATCH 2/3] add a script to batch create badges --- the-guild-smart-contracts/badges.json | 8 + the-guild-smart-contracts/badges.json.example | 25 ++ .../script/CreateBadgesFromJson.s.sol | 283 ++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 the-guild-smart-contracts/badges.json create mode 100644 the-guild-smart-contracts/badges.json.example create mode 100644 the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol diff --git a/the-guild-smart-contracts/badges.json b/the-guild-smart-contracts/badges.json new file mode 100644 index 0000000..ca7d4fd --- /dev/null +++ b/the-guild-smart-contracts/badges.json @@ -0,0 +1,8 @@ +{ + "badges": [ + { + "name": "Python", + "description": "Know how to code in Python" + } + ] +} diff --git a/the-guild-smart-contracts/badges.json.example b/the-guild-smart-contracts/badges.json.example new file mode 100644 index 0000000..0797615 --- /dev/null +++ b/the-guild-smart-contracts/badges.json.example @@ -0,0 +1,25 @@ +{ + "badges": [ + { + "name": "Rust", + "description": "Know how to code in Rust" + }, + { + "name": "Solidity", + "description": "Know how to code in Solidity" + }, + { + "name": "TypeScript", + "description": "Know how to code in TypeScript" + }, + { + "name": "Python", + "description": "Know how to code in Python" + }, + { + "name": "JavaScript", + "description": "Know how to code in JavaScript" + } + ] +} + diff --git a/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol b/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol new file mode 100644 index 0000000..43471b8 --- /dev/null +++ b/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, stdJson} from "forge-std/Script.sol"; +import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol"; +import {console} from "forge-std/console.sol"; + +contract CreateBadgesFromJson is Script { + using stdJson for string; + + // Configuration constants + uint256 constant MAX_BADGES = 1000; // Reasonable limit for processing + + struct BadgeData { + bytes32 name; + bytes32 description; + } + + function run() public { + bool isDryRun = vm.envOr("DRY_RUN", false); + + console.log("=== TheGuild Create Badges from JSON Script ==="); + console.log("Dry run mode:", isDryRun ? "ENABLED" : "DISABLED"); + + // Get badge registry address + address badgeRegistryAddress = vm.envOr( + "BADGE_REGISTRY_ADDRESS", + address(0) + ); + require( + badgeRegistryAddress != address(0), + "BADGE_REGISTRY_ADDRESS must be set" + ); + console.log("Badge Registry Address:", badgeRegistryAddress); + + TheGuildBadgeRegistry badgeRegistry = TheGuildBadgeRegistry( + badgeRegistryAddress + ); + + // Read and validate JSON file + string memory jsonPath = vm.envOr( + "JSON_PATH", + string("badges.json") + ); + console.log("Reading JSON from:", jsonPath); + + string memory jsonData = vm.readFile(jsonPath); + BadgeData[] memory badges = parseAndValidateJson(jsonData); + + console.log( + string( + abi.encodePacked( + "Parsed ", + vm.toString(badges.length), + " badges from JSON" + ) + ) + ); + + if (isDryRun) { + console.log( + string( + abi.encodePacked( + "DRY RUN: Would create ", + vm.toString(badges.length), + " badges" + ) + ) + ); + for (uint256 i = 0; i < badges.length; i++) { + console.log( + string( + abi.encodePacked( + "Badge ", + vm.toString(i + 1), + ": ", + vm.toString(badges[i].name), + " - ", + vm.toString(badges[i].description) + ) + ) + ); + } + console.log("Dry run completed successfully!"); + return; + } + + // Execute badge creation + createBadges(badgeRegistry, badges); + } + + function parseAndValidateJson( + string memory jsonData + ) internal view returns (BadgeData[] memory) { + console.log("Parsing JSON badges array..."); + + // Count badges - parse the full array to get length + BadgeData[] memory tempBadges = new BadgeData[](MAX_BADGES); + uint256 count = 0; + + // Parse badges until we run out + for (uint256 i = 0; i < MAX_BADGES; i++) { + string memory basePath = string( + abi.encodePacked(".badges[", vm.toString(i), "]") + ); + + // Try to parse this badge - if it doesn't exist, parseJson will revert + bytes memory testPath = vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".name")) + ); + if (testPath.length == 0) break; // No more badges + + // Parse name with proper bytes32 conversion + string memory nameStr = abi.decode( + vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".name")) + ), + (string) + ); + bytes32 name; + bytes memory nameBytes = bytes(nameStr); + require(nameBytes.length <= 32, "name too long"); + assembly { + name := mload(add(nameBytes, 32)) + } + + // Parse description with proper bytes32 conversion + string memory descriptionStr = abi.decode( + vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".description")) + ), + (string) + ); + bytes32 description; + bytes memory descriptionBytes = bytes(descriptionStr); + require(descriptionBytes.length <= 32, "description too long"); + assembly { + description := mload(add(descriptionBytes, 32)) + } + + tempBadges[count] = BadgeData({name: name, description: description}); + + count++; + } + + // Copy to properly sized array + BadgeData[] memory badges = new BadgeData[](count); + for (uint256 i = 0; i < count; i++) { + badges[i] = tempBadges[i]; + } + + console.log("Found", badges.length, "badges in JSON"); + + require(badges.length > 0, "JSON must contain at least 1 badge"); + require( + badges.length <= MAX_BADGES, + "Too many badges, max 1000 allowed" + ); + + // Validate each badge + for (uint256 i = 0; i < badges.length; i++) { + require( + badges[i].name != bytes32(0), + string( + abi.encodePacked( + "Badge ", + vm.toString(i), + ": name cannot be empty" + ) + ) + ); + } + + return badges; + } + + function createBadges( + TheGuildBadgeRegistry badgeRegistry, + BadgeData[] memory badges + ) internal { + uint256 pk = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(pk); + + console.log("Starting badge creation..."); + + uint256 successCount = 0; + uint256 skipCount = 0; + + for (uint256 i = 0; i < badges.length; i++) { + BadgeData memory badge = badges[i]; + + // Check if badge already exists + bool exists = badgeRegistry.exists(badge.name); + if (exists) { + console.log( + string( + abi.encodePacked( + "Skipping badge ", + vm.toString(i + 1), + "/", + vm.toString(badges.length), + ": ", + vm.toString(badge.name), + " already exists" + ) + ) + ); + skipCount++; + continue; + } + + try badgeRegistry.createBadge(badge.name, badge.description) { + console.log( + string( + abi.encodePacked( + "Created badge ", + vm.toString(i + 1), + "/", + vm.toString(badges.length), + ": ", + vm.toString(badge.name), + " - ", + vm.toString(badge.description) + ) + ) + ); + successCount++; + } catch Error(string memory reason) { + console.log( + string( + abi.encodePacked( + "Badge ", + vm.toString(i + 1), + " failed: ", + reason + ) + ) + ); + revert( + string( + abi.encodePacked( + "Badge ", + vm.toString(i + 1), + " failed: ", + reason + ) + ) + ); + } catch { + console.log( + string( + abi.encodePacked( + "Badge ", + vm.toString(i + 1), + " failed with unknown error" + ) + ) + ); + revert( + string( + abi.encodePacked( + "Badge ", + vm.toString(i + 1), + " failed with unknown error" + ) + ) + ); + } + } + + vm.stopBroadcast(); + + console.log("=== Badge Creation Summary ==="); + console.log("Total badges processed:", badges.length); + console.log("Successfully created:", successCount); + console.log("Skipped (already exist):", skipCount); + console.log("Execution completed successfully!"); + } +} + From 7767fed7d365a71807964f0f5a1883a29f2484d4 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Thu, 4 Dec 2025 17:00:27 +0100 Subject: [PATCH 3/3] create badges and distrib --- notebooks/01_github_issues.ipynb | 397 ++++++++++++------ the-guild-smart-contracts/README.md | 96 ++++- .../attestations-2025-12-02.json | 139 ------ .../attestations-latest.json | 139 ++++++ the-guild-smart-contracts/badges-latest.json | 60 +++ the-guild-smart-contracts/badges.json | 8 - .../run_batch_attestations.sh | 36 +- the-guild-smart-contracts/run_batch_badges.sh | 79 ++++ .../script/CreateBadgesFromJson.s.sol | 8 +- .../script/EmitAttestationsJson.s.sol | 245 +++++++++-- .../test/EmitAttestationsCsv.t.sol | 47 ++- 11 files changed, 887 insertions(+), 367 deletions(-) delete mode 100644 the-guild-smart-contracts/attestations-2025-12-02.json create mode 100644 the-guild-smart-contracts/attestations-latest.json create mode 100644 the-guild-smart-contracts/badges-latest.json delete mode 100644 the-guild-smart-contracts/badges.json create mode 100755 the-guild-smart-contracts/run_batch_badges.sh diff --git a/notebooks/01_github_issues.ipynb b/notebooks/01_github_issues.ipynb index f3534c3..e6db440 100644 --- a/notebooks/01_github_issues.ipynb +++ b/notebooks/01_github_issues.ipynb @@ -24,14 +24,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Fetched 43 issues from TheSoftwareDevGuild/TheGuildGenesis\n" + "Fetched 47 issues from TheSoftwareDevGuild/TheGuildGenesis\n" ] }, { @@ -69,6 +69,54 @@ " \n", " \n", " 0\n", + " 146\n", + " Figure out how to admin delete profile\n", + " open\n", + " 2025-12-03T17:26:37Z\n", + " 2025-12-03T17:26:37Z\n", + " joelamouche\n", + " \n", + " good first issue,back-end,planning\n", + " https://github.com/TheSoftwareDevGuild/TheGuil...\n", + " \n", + " \n", + " 1\n", + " 145\n", + " I think we should allow duplicate attestations\n", + " open\n", + " 2025-12-03T15:18:42Z\n", + " 2025-12-03T15:18:42Z\n", + " joelamouche\n", + " \n", + " solidity,planning\n", + " https://github.com/TheSoftwareDevGuild/TheGuil...\n", + " \n", + " \n", + " 2\n", + " 144\n", + " Implement Upgradable pattern for TheGuildAttes...\n", + " open\n", + " 2025-12-03T10:28:34Z\n", + " 2025-12-03T10:28:55Z\n", + " joelamouche\n", + " \n", + " good first issue,solidity,foundry\n", + " https://github.com/TheSoftwareDevGuild/TheGuil...\n", + " \n", + " \n", + " 3\n", + " 143\n", + " TheGuildAttestationResolver should have admin ...\n", + " open\n", + " 2025-12-03T10:27:07Z\n", + " 2025-12-03T10:27:51Z\n", + " joelamouche\n", + " \n", + " good first issue,solidity,foundry\n", + " https://github.com/TheSoftwareDevGuild/TheGuil...\n", + " \n", + " \n", + " 4\n", " 142\n", " Add contributor leaderboard\n", " open\n", @@ -80,7 +128,7 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 1\n", + " 5\n", " 141\n", " Add a new leaderboard page, with link in the s...\n", " open\n", @@ -92,7 +140,7 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 2\n", + " 6\n", " 140\n", " Create top badge owner leaderboard\n", " open\n", @@ -104,7 +152,7 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 3\n", + " 7\n", " 138\n", " Public contributor leaderboard\n", " open\n", @@ -116,7 +164,7 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 4\n", + " 8\n", " 137\n", " Figure out sharing new badges on X\n", " open\n", @@ -128,7 +176,7 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 5\n", + " 9\n", " 135\n", " Add projects to the Backend\n", " open\n", @@ -139,94 +187,46 @@ " good first issue,rust,back-end,160pts,db,hackt...\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", - " \n", - " 6\n", - " 134\n", - " Project Page\n", - " open\n", - " 2025-11-17T12:38:11Z\n", - " 2025-11-27T10:58:18Z\n", - " joelamouche\n", - " joelamouche\n", - " enhancement,planning\n", - " https://github.com/TheSoftwareDevGuild/TheGuil...\n", - " \n", - " \n", - " 7\n", - " 131\n", - " Add backend for distributions\n", - " open\n", - " 2025-11-03T13:30:48Z\n", - " 2025-11-24T10:10:14Z\n", - " joelamouche\n", - " Chesblaw\n", - " good first issue,rust,back-end,typescript,db\n", - " https://github.com/TheSoftwareDevGuild/TheGuil...\n", - " \n", - " \n", - " 8\n", - " 125\n", - " Improve UX and design of theguild.dev\n", - " open\n", - " 2025-10-28T11:23:19Z\n", - " 2025-10-28T11:23:19Z\n", - " joelamouche\n", - " \n", - " good first issue,ux,design,hacktoberfest\n", - " https://github.com/TheSoftwareDevGuild/TheGuil...\n", - " \n", - " \n", - " 9\n", - " 118\n", - " Badge enhancements\n", - " open\n", - " 2025-10-17T08:35:11Z\n", - " 2025-10-30T13:44:59Z\n", - " joelamouche\n", - " joelamouche\n", - " enhancement,planning,hacktoberfest\n", - " https://github.com/TheSoftwareDevGuild/TheGuil...\n", - " \n", " \n", "\n", "" ], "text/plain": [ " number title state \\\n", - "0 142 Add contributor leaderboard open \n", - "1 141 Add a new leaderboard page, with link in the s... open \n", - "2 140 Create top badge owner leaderboard open \n", - "3 138 Public contributor leaderboard open \n", - "4 137 Figure out sharing new badges on X open \n", - "5 135 Add projects to the Backend open \n", - "6 134 Project Page open \n", - "7 131 Add backend for distributions open \n", - "8 125 Improve UX and design of theguild.dev open \n", - "9 118 Badge enhancements open \n", + "0 146 Figure out how to admin delete profile open \n", + "1 145 I think we should allow duplicate attestations open \n", + "2 144 Implement Upgradable pattern for TheGuildAttes... open \n", + "3 143 TheGuildAttestationResolver should have admin ... open \n", + "4 142 Add contributor leaderboard open \n", + "5 141 Add a new leaderboard page, with link in the s... open \n", + "6 140 Create top badge owner leaderboard open \n", + "7 138 Public contributor leaderboard open \n", + "8 137 Figure out sharing new badges on X open \n", + "9 135 Add projects to the Backend open \n", "\n", " created_at updated_at user assignees \\\n", - "0 2025-11-24T15:04:07Z 2025-11-24T15:04:42Z joelamouche \n", - "1 2025-11-24T15:01:00Z 2025-11-24T15:01:00Z joelamouche \n", - "2 2025-11-24T14:59:35Z 2025-11-24T15:02:16Z joelamouche \n", - "3 2025-11-17T13:58:58Z 2025-11-24T15:04:19Z joelamouche joelamouche \n", - "4 2025-11-17T13:58:23Z 2025-11-17T13:58:28Z joelamouche joelamouche \n", - "5 2025-11-17T12:38:30Z 2025-11-27T12:15:32Z joelamouche pheobeayo \n", - "6 2025-11-17T12:38:11Z 2025-11-27T10:58:18Z joelamouche joelamouche \n", - "7 2025-11-03T13:30:48Z 2025-11-24T10:10:14Z joelamouche Chesblaw \n", - "8 2025-10-28T11:23:19Z 2025-10-28T11:23:19Z joelamouche \n", - "9 2025-10-17T08:35:11Z 2025-10-30T13:44:59Z joelamouche joelamouche \n", + "0 2025-12-03T17:26:37Z 2025-12-03T17:26:37Z joelamouche \n", + "1 2025-12-03T15:18:42Z 2025-12-03T15:18:42Z joelamouche \n", + "2 2025-12-03T10:28:34Z 2025-12-03T10:28:55Z joelamouche \n", + "3 2025-12-03T10:27:07Z 2025-12-03T10:27:51Z joelamouche \n", + "4 2025-11-24T15:04:07Z 2025-11-24T15:04:42Z joelamouche \n", + "5 2025-11-24T15:01:00Z 2025-11-24T15:01:00Z joelamouche \n", + "6 2025-11-24T14:59:35Z 2025-11-24T15:02:16Z joelamouche \n", + "7 2025-11-17T13:58:58Z 2025-11-24T15:04:19Z joelamouche joelamouche \n", + "8 2025-11-17T13:58:23Z 2025-11-17T13:58:28Z joelamouche joelamouche \n", + "9 2025-11-17T12:38:30Z 2025-11-27T12:15:32Z joelamouche pheobeayo \n", "\n", " labels \\\n", - "0 good first issue,front end,react,typescript,80pts \n", - "1 good first issue,front end,react,typescript,20... \n", - "2 good first issue,front end,react,typescript,40pts \n", - "3 enhancement,front end,planning \n", - "4 planning \n", - "5 good first issue,rust,back-end,160pts,db,hackt... \n", - "6 enhancement,planning \n", - "7 good first issue,rust,back-end,typescript,db \n", - "8 good first issue,ux,design,hacktoberfest \n", - "9 enhancement,planning,hacktoberfest \n", + "0 good first issue,back-end,planning \n", + "1 solidity,planning \n", + "2 good first issue,solidity,foundry \n", + "3 good first issue,solidity,foundry \n", + "4 good first issue,front end,react,typescript,80pts \n", + "5 good first issue,front end,react,typescript,20... \n", + "6 good first issue,front end,react,typescript,40pts \n", + "7 enhancement,front end,planning \n", + "8 planning \n", + "9 good first issue,rust,back-end,160pts,db,hackt... \n", "\n", " url \n", "0 https://github.com/TheSoftwareDevGuild/TheGuil... \n", @@ -241,7 +241,7 @@ "9 https://github.com/TheSoftwareDevGuild/TheGuil... " ] }, - "execution_count": 1, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -330,7 +330,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -347,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -432,14 +432,44 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fetched 42 total issues (closed)\n" + ] + } + ], + "source": [ + "import json\n", + "import re\n", + "from datetime import datetime\n", + "from collections import defaultdict\n", + "\n", + "# Fetch all issues (only closed) to get complete picture\n", + "all_issues = fetch_issues(state=\"closed\", per_page=100, max_pages=10)\n", + "print(f\"Fetched {len(all_issues)} total issues (closed)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract Labels" + ] + }, + { + "cell_type": "code", + "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Fetched 42 total issues (open + closed)\n", "\n", "============================================================\n", "BADGES/LABELS THAT NEED TO BE CREATED (23):\n", @@ -472,29 +502,6 @@ ] } ], - "source": [ - "import json\n", - "import re\n", - "from datetime import datetime\n", - "from collections import defaultdict\n", - "\n", - "# Fetch all issues (both open and closed) to get complete picture\n", - "all_issues = fetch_issues(state=\"closed\", per_page=100, max_pages=10)\n", - "print(f\"Fetched {len(all_issues)} total issues (open + closed)\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Extract Labels" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], "source": [ "# Extract unique labels/badges from all issues\n", "all_labels = set()\n", @@ -514,6 +521,99 @@ "print(f\"{'='*60}\\n\")\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I'd rather dirrectly process this list into ai chat manually" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Non skill labels to skip" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "non_skill_badges = [\n", + " \"good first issue\",\n", + " \"hacktoberfest\",\n", + " \"in progress\",\n", + " \"bug\",\n", + " \"onlydust-wave\",\n", + " \"planning\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### git_label_to_badge" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "git_label_to_badge = {\n", + " \"rust\": \"Rust\",\n", + " \"solidity\": \"Solidity\",\n", + " \"typescript\": \"TypeScript\",\n", + " \"python\": \"Python\",\n", + " \"apis\": \"APIs\",\n", + "\n", + " \"back-end\": \"Back-end\",\n", + " \"backend\": \"Back-end\",\n", + "\n", + " \"blockchain\": \"Blockchain\",\n", + "\n", + " \"db\": \"DB\",\n", + " \"database\": \"DB\",\n", + " \"databases\": \"DB\",\n", + "\n", + " \"design\": \"Design\",\n", + "\n", + " \"discord-bot\": \"Discord-bot\",\n", + " \"discord bot\": \"Discord-bot\",\n", + "\n", + " \"documentation\": \"Documentation\",\n", + " \"docs\": \"Documentation\",\n", + "\n", + " \"foundry\": \"Foundry\",\n", + "\n", + " \"front end\": \"Front end\",\n", + " \"frontend\": \"Front end\",\n", + " \"front-end\": \"Front end\",\n", + "\n", + " \"jupyter-notebook\": \"Jupyter-notebook\",\n", + " \"jupyter notebook\": \"Jupyter-notebook\",\n", + "\n", + " \"nodejs\": \"Nodejs\",\n", + " \"node.js\": \"Nodejs\",\n", + " \"node\": \"Nodejs\",\n", + "\n", + " \"onlydust-wave\": \"Onlydust-wave\",\n", + "\n", + " \"planning\": \"Planning\",\n", + "\n", + " \"react\": \"React\",\n", + "\n", + " \"ux\": \"UX\",\n", + " \"user-experience\": \"UX\",\n", + "\n", + " \"wagmi\": \"Wagmi\"\n", + "}" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -523,7 +623,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -531,15 +631,12 @@ "output_type": "stream", "text": [ "https://theguild-backend-e2df290d177e.herokuapp.com/\n", - "Loaded 1 GitHub username -> address mappings from backend API\n", + "Loaded 4 GitHub username -> address mappings from backend API\n", "\n", - "Warning: 7 assignees without address mapping:\n", + "Warning: 4 assignees without address mapping:\n", " - oscarwroche\n", " - peteroche\n", - " - pheobeayo\n", " - rainwaters11\n", - " - teddy1792\n", - " - tusharshah21\n", " - yash-1104github\n", "These will be skipped in attestations.\n", "\n" @@ -590,61 +687,68 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Generated 27 attestations from 42 issues\n", - "Unique recipients: 1\n", - "Unique badges: 13\n" + "Generated 55 attestations from 42 issues\n", + "Unique recipients: 4\n", + "Unique badges: 15\n" ] } ], "source": [ "# Generate attestations from issues\n", "# For each issue with assignees and labels, create attestations\n", + "\n", "attestations = []\n", "\n", + "# Ensure non_skill_badges is a lowercased set for quick lookup\n", + "_non_skill_badges_set = set(b.lower() for b in non_skill_badges)\n", + "\n", "for issue in all_issues:\n", " assignees = issue.get(\"assignees\", [])\n", " labels = issue.get(\"labels\", [])\n", " issue_number = issue.get(\"number\")\n", " issue_title = issue.get(\"title\", \"\")\n", " issue_url = issue.get(\"html_url\", \"\")\n", - " \n", + "\n", " # Skip if no assignees or no labels\n", " if not assignees or not labels:\n", " continue\n", - " \n", - " # Process each assignee\n", + "\n", " for assignee in assignees:\n", " github_username = assignee.get(\"login\", \"\").lower()\n", " if not github_username:\n", " continue\n", - " \n", - " # Get Ethereum address for this GitHub user\n", + "\n", " recipient_address = github_to_address.get(github_username)\n", " if not recipient_address:\n", " continue # Skip if no address mapping\n", - " \n", - " # Create attestation for each label (badge) on this issue\n", + "\n", " for label in labels:\n", " label_name = label.get(\"name\", \"\")\n", " # Skip point labels\n", " if not label_name or re.match(r'^\\d+pts$', label_name):\n", " continue\n", - " \n", - " # Create justification based on issue\n", + " # Skip if label is in non_skill_badges (case-insensitive)\n", + " if label_name.lower() in _non_skill_badges_set:\n", + " continue\n", + " # Map label to badge, skip if mapping doesn't exist\n", + " badge_name = git_label_to_badge.get(label_name.lower())\n", + " if not badge_name:\n", + " continue\n", + "\n", " justification = f\"Awarded for contributions to issue #{issue_number}: {issue_title}\"\n", " if issue_url:\n", " justification += f\" ({issue_url})\"\n", - " \n", + "\n", " attestations.append({\n", " \"recipient\": recipient_address,\n", - " \"badgeName\": label_name,\n", + " \"badgeName\": badge_name,\n", " \"justification\": justification\n", " })\n", "\n", @@ -662,7 +766,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -670,13 +774,13 @@ "output_type": "stream", "text": [ "\n", - "✓ Saved 27 attestations to: ../the-guild-smart-contracts/attestations-2025-12-02.json\n", - " File size: 7751 bytes\n" + "✓ Saved 55 attestations to: ../the-guild-smart-contracts/attestations-2025-12-03.json\n", + " File size: 15808 bytes\n" ] } ], "source": [ - "# Save attestations to date-based file\n", + "# Save attestations to date-based file and also to attestations-latest.json\n", "from pathlib import Path\n", "\n", "output_dir = Path(\"../the-guild-smart-contracts\")\n", @@ -693,9 +797,26 @@ "with open(output_file, \"w\") as f:\n", " json.dump(output_data, f, indent=2)\n", "\n", + "# Save latest attestations\n", + "latest_file = output_dir / \"attestations-latest.json\"\n", + "with open(latest_file, \"w\") as f:\n", + " json.dump(output_data, f, indent=2)\n", + "\n", "print(f\"\\n✓ Saved {len(attestations)} attestations to: {output_file}\")\n", "print(f\" File size: {output_file.stat().st_size} bytes\")\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, run an ai prompt over the file to refactor duplicate attestations: \"Here we cannot have duplicate recipient/badgeName pairs. When it is the case, just condensate the multiple justifications into one justification for one badge.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] } ], "metadata": { diff --git a/the-guild-smart-contracts/README.md b/the-guild-smart-contracts/README.md index a238d9f..7071a21 100644 --- a/the-guild-smart-contracts/README.md +++ b/the-guild-smart-contracts/README.md @@ -203,9 +203,87 @@ forge script script/TheGuildBadgeRanking.s.sol:TheGuildBadgeRankingScript \ --broadcast ``` +### Batch Badge Creation + +The `CreateBadgesFromJson.s.sol` script allows batch creation of badges from JSON data. + +#### JSON Format + +Prepare your badges data in JSON format: +```json +{ + "badges": [ + { + "name": "Rust", + "description": "Know how to code in Rust" + }, + { + "name": "Solidity", + "description": "Know how to code in Solidity" + } + ] +} +``` + +- `name`: Name of the badge (max 32 characters, will be converted to bytes32) +- `description`: Description of the badge (max 32 characters, will be converted to bytes32) + +#### Usage + +```shell +# Using the helper script (recommended) +export PRIVATE_KEY=your_private_key +export RPC_URL=https://polygon-amoy.drpc.org +export BADGE_REGISTRY_ADDRESS=0x8ac95734e778322684f1d318fb7633777baa8427 + +# Dry run first (uses badges-latest.json by default) +./run_batch_badges.sh true + +# Dry run with custom JSON file +./run_batch_badges.sh badges.json true + +# Production run (uses badges-latest.json by default) +./run_batch_badges.sh false + +# Production run with custom JSON file +./run_batch_badges.sh badges.json false + +# Manual approach +# Set environment variables +export PRIVATE_KEY=your_private_key +export JSON_PATH=./badges.json +export BADGE_REGISTRY_ADDRESS=0x8ac95734e778322684f1d318fb7633777baa8427 + +# Dry run (recommended first) +export DRY_RUN=true +forge script script/CreateBadgesFromJson.s.sol:CreateBadgesFromJson \ + --rpc-url + +# Production run +unset DRY_RUN +forge script script/CreateBadgesFromJson.s.sol:CreateBadgesFromJson \ + --rpc-url \ + --broadcast +``` + +#### Environment Variables + +- `PRIVATE_KEY`: Private key for transaction signing +- `JSON_PATH`: Path to the JSON file (default: `badges-latest.json`) +- `BADGE_REGISTRY_ADDRESS`: Address of the deployed TheGuildBadgeRegistry contract (required) +- `DRY_RUN`: Set to `true` for validation without broadcasting (default: `false`) +- `RPC_URL`: RPC endpoint URL (for helper script) + +The script will: +1. Read the JSON file directly using `vm.readFile()` +2. Parse and validate all badges +3. Check if each badge already exists (skips if it does) +4. Create badges that don't exist yet +5. Log creation summary with success and skip counts + ### Batch Attestations -The `EmitAttestationsCsv.s.sol` script allows batch creation of attestations from JSON data using EAS's `multiAttest()` function for gas efficiency. +The `EmitAttestationsJson.s.sol` script allows batch creation of attestations from JSON data using EAS's `multiAttest()` function for gas efficiency. #### JSON Format @@ -238,10 +316,16 @@ Prepare your attestations data in JSON format: export PRIVATE_KEY=your_private_key export RPC_URL=https://polygon-amoy.drpc.org -# Dry run first +# Dry run first (uses attestations-latest.json by default) +./run_batch_attestations.sh true + +# Dry run with custom JSON file ./run_batch_attestations.sh attestations.json true -# Production run +# Production run (uses attestations-latest.json by default) +./run_batch_attestations.sh false + +# Production run with custom JSON file ./run_batch_attestations.sh attestations.json false # Manual approach @@ -252,12 +336,12 @@ export SCHEMA_ID=0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a4 # Dry run (recommended first) export DRY_RUN=true -forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \ +forge script script/EmitAttestationsJson.s.sol:EmitAttestationsJson \ --rpc-url # Production run unset DRY_RUN -forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \ +forge script script/EmitAttestationsJson.s.sol:EmitAttestationsJson \ --rpc-url \ --broadcast ``` @@ -265,7 +349,7 @@ forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \ #### Environment Variables - `PRIVATE_KEY`: Private key for transaction signing -- `JSON_PATH`: Path to the JSON file (default: `./attestations.json`) +- `JSON_PATH`: Path to the JSON file (default: `attestations-latest.json`) - `SCHEMA_ID`: EAS schema ID to use (default: Amoy production schema) - `DRY_RUN`: Set to `true` for validation without broadcasting (default: `false`) - `EAS_ADDRESS`: EAS contract address (auto-detected for Amoy/Base Sepolia, required for other networks) diff --git a/the-guild-smart-contracts/attestations-2025-12-02.json b/the-guild-smart-contracts/attestations-2025-12-02.json deleted file mode 100644 index c38a03a..0000000 --- a/the-guild-smart-contracts/attestations-2025-12-02.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "attestations": [ - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "jupyter-notebook", - "justification": "Awarded for contributions to issue #126: Use jupyter notebook to analyze github issues (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/126)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "python", - "justification": "Awarded for contributions to issue #126: Use jupyter notebook to analyze github issues (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/126)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "bug", - "justification": "Awarded for contributions to issue #119: Discord Link (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/119)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "documentation", - "justification": "Awarded for contributions to issue #119: Discord Link (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/119)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "documentation", - "justification": "Awarded for contributions to issue #91: Improve instructions to run things locally (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/91)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "documentation", - "justification": "Awarded for contributions to issue #72: Contribution token feature plan (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/72)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "planning", - "justification": "Awarded for contributions to issue #72: Contribution token feature plan (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/72)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "solidity", - "justification": "Awarded for contributions to issue #55: Could be a good idea to merge ActivityToken and BadgeRegistry contracts into one (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/55)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "foundry", - "justification": "Awarded for contributions to issue #55: Could be a good idea to merge ActivityToken and BadgeRegistry contracts into one (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/55)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "good first issue", - "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "front end", - "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "react", - "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "typescript", - "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "wagmi", - "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "documentation", - "justification": "Awarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "front end", - "justification": "Awarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "in progress", - "justification": "Awarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "solidity", - "justification": "Awarded for contributions to issue #48: Prevent duplicate attestations (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/48)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "foundry", - "justification": "Awarded for contributions to issue #48: Prevent duplicate attestations (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/48)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "bug", - "justification": "Awarded for contributions to issue #47: Submitting too many attestations in a row emits error (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/47)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "front end", - "justification": "Awarded for contributions to issue #47: Submitting too many attestations in a row emits error (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/47)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "documentation", - "justification": "Awarded for contributions to issue #45: Update contribution guide (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/45)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "front end", - "justification": "Awarded for contributions to issue #34: Api doesn't seem to work on mobile (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/34)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "good first issue", - "justification": "Awarded for contributions to issue #32: Issuer should link to profile (and use name) (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/32)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "front end", - "justification": "Awarded for contributions to issue #32: Issuer should link to profile (and use name) (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/32)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "solidity", - "justification": "Awarded for contributions to issue #29: Full contract test coverage (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/29)" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "badgeName": "front end", - "justification": "Awarded for contributions to issue #22: Add basic project description in the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/22)" - } - ] -} \ No newline at end of file diff --git a/the-guild-smart-contracts/attestations-latest.json b/the-guild-smart-contracts/attestations-latest.json new file mode 100644 index 0000000..454bbe1 --- /dev/null +++ b/the-guild-smart-contracts/attestations-latest.json @@ -0,0 +1,139 @@ +{ + "attestations": [ + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "Solidity", + "justification": "Awarded for contributions to issue #130: Add foundry batch attestation distribution script (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/130)\nAwarded for contributions to issue #122: Badge TCR/Vote ranking: Smart Contract (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/122)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "Foundry", + "justification": "Awarded for contributions to issue #130: Add foundry batch attestation distribution script (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/130)\nAwarded for contributions to issue #122: Badge TCR/Vote ranking: Smart Contract (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/122)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "Jupyter-notebook", + "justification": "Awarded for contributions to issue #126: Use jupyter notebook to analyze github issues (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/126)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "Python", + "justification": "Awarded for contributions to issue #126: Use jupyter notebook to analyze github issues (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/126)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "Blockchain", + "justification": "Awarded for contributions to issue #122: Badge TCR/Vote ranking: Smart Contract (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/122)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "Documentation", + "justification": "Awarded for contributions to issue #119: Discord Link (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/119)\nAwarded for contributions to issue #91: Improve instructions to run things locally (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/91)\nAwarded for contributions to issue #72: Contribution token feature plan (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/72)\nAwarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)\nAwarded for contributions to issue #45: Update contribution guide (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/45)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "Rust", + "justification": "Awarded for contributions to issue #114: Github handle errors (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/114)\nAwarded for contributions to issue #42: Improve SIWE logic to use dynamic, user-specific nonce - backend (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/42)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "Back-end", + "justification": "Awarded for contributions to issue #114: Github handle errors (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/114)\nAwarded for contributions to issue #42: Improve SIWE logic to use dynamic, user-specific nonce - backend (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/42)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "Front end", + "justification": "Awarded for contributions to issue #107: Implement SIWE for the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/107)\nAwarded for contributions to issue #50: Add copy to clipboard icons next to all ethereum addresses (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/50)\nAwarded for contributions to issue #49: Add description on the profile page (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/49)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "React", + "justification": "Awarded for contributions to issue #107: Implement SIWE for the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/107)\nAwarded for contributions to issue #50: Add copy to clipboard icons next to all ethereum addresses (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/50)\nAwarded for contributions to issue #49: Add description on the profile page (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/49)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "TypeScript", + "justification": "Awarded for contributions to issue #107: Implement SIWE for the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/107)\nAwarded for contributions to issue #49: Add description on the profile page (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/49)" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "badgeName": "Front end", + "justification": "Awarded for contributions to issue #98: Feature: add a github handle to profile in Front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/98)\nAwarded for contributions to issue #58: On the profile, there should be a section with the badges issued by that profile (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/58)" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "badgeName": "React", + "justification": "Awarded for contributions to issue #98: Feature: add a github handle to profile in Front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/98)\nAwarded for contributions to issue #58: On the profile, there should be a section with the badges issued by that profile (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/58)" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "badgeName": "TypeScript", + "justification": "Awarded for contributions to issue #98: Feature: add a github handle to profile in Front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/98)\nAwarded for contributions to issue #58: On the profile, there should be a section with the badges issued by that profile (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/58)" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "badgeName": "Documentation", + "justification": "Awarded for contributions to issue #83: Add doc for indexer (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/83)" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "badgeName": "Rust", + "justification": "Awarded for contributions to issue #83: Add doc for indexer (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/83)" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "badgeName": "Back-end", + "justification": "Awarded for contributions to issue #83: Add doc for indexer (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/83)" + }, + { + "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", + "badgeName": "Front end", + "justification": "Awarded for contributions to issue #64: Implement 3D Card Component for Badge Page (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/64)\nAwarded for contributions to issue #63: Improve landing page design with animated background (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/63)" + }, + { + "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", + "badgeName": "React", + "justification": "Awarded for contributions to issue #64: Implement 3D Card Component for Badge Page (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/64)\nAwarded for contributions to issue #63: Improve landing page design with animated background (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/63)" + }, + { + "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", + "badgeName": "UX", + "justification": "Awarded for contributions to issue #64: Implement 3D Card Component for Badge Page (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/64)\nAwarded for contributions to issue #63: Improve landing page design with animated background (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/63)" + }, + { + "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", + "badgeName": "Design", + "justification": "Awarded for contributions to issue #64: Implement 3D Card Component for Badge Page (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/64)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "Foundry", + "justification": "Awarded for contributions to issue #55: Could be a good idea to merge ActivityToken and BadgeRegistry contracts into one (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/55)\nAwarded for contributions to issue #48: Prevent duplicate attestations (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/48)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "Front end", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)\nAwarded for contributions to issue #51: Add a Getting Started page on the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/51)\nAwarded for contributions to issue #47: Submitting too many attestations in a row emits error (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/47)\nAwarded for contributions to issue #34: Api doesn't seem to work on mobile (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/34)\nAwarded for contributions to issue #32: Issuer should link to profile (and use name) (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/32)\nAwarded for contributions to issue #22: Add basic project description in the front end (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/22)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "React", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "TypeScript", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "badgeName": "Wagmi", + "justification": "Awarded for contributions to issue #54: Display user activity token balance in the top bar (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/54)" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "badgeName": "DB", + "justification": "Awarded for contributions to issue #42: Improve SIWE logic to use dynamic, user-specific nonce - backend (https://github.com/TheSoftwareDevGuild/TheGuildGenesis/issues/42)" + } + ] +} diff --git a/the-guild-smart-contracts/badges-latest.json b/the-guild-smart-contracts/badges-latest.json new file mode 100644 index 0000000..04cb2bb --- /dev/null +++ b/the-guild-smart-contracts/badges-latest.json @@ -0,0 +1,60 @@ +{ + "badges": [ + { + "name": "Back-end", + "description": "Back-end services & APIs." + }, + { + "name": "Blockchain", + "description": "Blockchain & dapp knowledge." + }, + { + "name": "DB", + "description": "Design & query databases." + }, + { + "name": "Design", + "description": "Visual design for products." + }, + { + "name": "Discord-bot", + "description": "Discord bots & integrations." + }, + { + "name": "Documentation", + "description": "Writes helpful documentation." + }, + { + "name": "Foundry", + "description": "Foundry smart contract dev." + }, + { + "name": "Front end", + "description": "Builds UI and client logic." + }, + { + "name": "Jupyter-notebook", + "description": "Jupyter Notebooks for data." + }, + { + "name": "Nodejs", + "description": "Node.js back-end services." + }, + { + "name": "Planning", + "description": "Plans work and milestones." + }, + { + "name": "React", + "description": "React components & apps." + }, + { + "name": "UX", + "description": "User experience focus." + }, + { + "name": "Wagmi", + "description": "Wagmi Web3 front-end use." + } + ] +} diff --git a/the-guild-smart-contracts/badges.json b/the-guild-smart-contracts/badges.json deleted file mode 100644 index ca7d4fd..0000000 --- a/the-guild-smart-contracts/badges.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "badges": [ - { - "name": "Python", - "description": "Know how to code in Python" - } - ] -} diff --git a/the-guild-smart-contracts/run_batch_attestations.sh b/the-guild-smart-contracts/run_batch_attestations.sh index 18615f8..16d931a 100755 --- a/the-guild-smart-contracts/run_batch_attestations.sh +++ b/the-guild-smart-contracts/run_batch_attestations.sh @@ -1,7 +1,9 @@ #!/bin/bash # Helper script for running TheGuild batch attestation script -# Usage: ./run_batch_attestations.sh [dry_run] +# Usage: ./run_batch_attestations.sh [json_file] [dry_run] +# json_file: Path to JSON file with attestations (default: attestations-latest.json) +# dry_run: Set to 'true' for dry run (default: false) set -e @@ -10,16 +12,28 @@ if [ -f .env ]; then source .env fi -if [ $# -lt 1 ]; then - echo "Usage: $0 [dry_run]" - echo " json_file: Path to JSON file with attestations" - echo " dry_run: Set to 'true' for dry run (default: false)" - exit 1 +# Parse arguments - JSON file is optional, defaults to attestations-latest.json +if [ $# -eq 0 ]; then + # No arguments: use default JSON file + JSON_FILE="attestations-latest.json" + DRY_RUN="false" +elif [ $# -eq 1 ]; then + # One argument: could be JSON file or dry_run flag + if [ "$1" = "true" ] || [ "$1" = "false" ]; then + # It's a dry_run flag + JSON_FILE="attestations-latest.json" + DRY_RUN="$1" + else + # It's a JSON file path + JSON_FILE="$1" + DRY_RUN="false" + fi +else + # Two arguments: JSON file and dry_run flag + JSON_FILE="$1" + DRY_RUN="$2" fi -JSON_FILE="$1" -DRY_RUN="${2:-false}" - if [ ! -f "$JSON_FILE" ]; then echo "Error: JSON file '$JSON_FILE' not found" exit 1 @@ -50,10 +64,10 @@ fi # Run the script if [ "$DRY_RUN" = "true" ]; then - forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \ + forge script script/EmitAttestationsJson.s.sol:EmitAttestationsJson \ --rpc-url "$RPC_URL" else - forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \ + forge script script/EmitAttestationsJson.s.sol:EmitAttestationsJson \ --rpc-url "$RPC_URL" \ --broadcast fi \ No newline at end of file diff --git a/the-guild-smart-contracts/run_batch_badges.sh b/the-guild-smart-contracts/run_batch_badges.sh new file mode 100755 index 0000000..9edb7e5 --- /dev/null +++ b/the-guild-smart-contracts/run_batch_badges.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Helper script for running TheGuild batch badge creation script +# Usage: ./run_batch_badges.sh [json_file] [dry_run] +# json_file: Path to JSON file with badges (default: badges-latest.json) +# dry_run: Set to 'true' for dry run (default: false) + +set -e + +# Source .env file if it exists +if [ -f .env ]; then + source .env +fi + +# Parse arguments - JSON file is optional, defaults to badges-latest.json +if [ $# -eq 0 ]; then + # No arguments: use default JSON file + JSON_FILE="badges-latest.json" + DRY_RUN="false" +elif [ $# -eq 1 ]; then + # One argument: could be JSON file or dry_run flag + if [ "$1" = "true" ] || [ "$1" = "false" ]; then + # It's a dry_run flag + JSON_FILE="badges-latest.json" + DRY_RUN="$1" + else + # It's a JSON file path + JSON_FILE="$1" + DRY_RUN="false" + fi +else + # Two arguments: JSON file and dry_run flag + JSON_FILE="$1" + DRY_RUN="$2" +fi + +if [ ! -f "$JSON_FILE" ]; then + echo "Error: JSON file '$JSON_FILE' not found" + exit 1 +fi + +# Set JSON file path +export JSON_PATH="$JSON_FILE" + +# Set dry run mode +if [ "$DRY_RUN" = "true" ]; then + export DRY_RUN=true + echo "Running in DRY RUN mode..." +else + unset DRY_RUN + echo "Running in PRODUCTION mode..." +fi + +# Check for required environment variables +if [ -z "$PRIVATE_KEY" ]; then + echo "Error: PRIVATE_KEY environment variable not set" + exit 1 +fi + +if [ -z "$RPC_URL" ]; then + echo "Error: RPC_URL environment variable not set" + exit 1 +fi + +if [ -z "$BADGE_REGISTRY_ADDRESS" ]; then + echo "Error: BADGE_REGISTRY_ADDRESS environment variable not set" + exit 1 +fi + +# Run the script +if [ "$DRY_RUN" = "true" ]; then + forge script script/CreateBadgesFromJson.s.sol:CreateBadgesFromJson \ + --rpc-url "$RPC_URL" +else + forge script script/CreateBadgesFromJson.s.sol:CreateBadgesFromJson \ + --rpc-url "$RPC_URL" \ + --broadcast +fi + diff --git a/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol b/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol index 43471b8..757d8c1 100644 --- a/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol +++ b/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol @@ -40,7 +40,7 @@ contract CreateBadgesFromJson is Script { // Read and validate JSON file string memory jsonPath = vm.envOr( "JSON_PATH", - string("badges.json") + string("badges-latest.json") ); console.log("Reading JSON from:", jsonPath); @@ -141,7 +141,10 @@ contract CreateBadgesFromJson is Script { description := mload(add(descriptionBytes, 32)) } - tempBadges[count] = BadgeData({name: name, description: description}); + tempBadges[count] = BadgeData({ + name: name, + description: description + }); count++; } @@ -280,4 +283,3 @@ contract CreateBadgesFromJson is Script { console.log("Execution completed successfully!"); } } - diff --git a/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol b/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol index ab312d4..563887d 100644 --- a/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol +++ b/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol @@ -7,7 +7,7 @@ import {AttestationRequestData, MultiAttestationRequest} from "eas-contracts/IEA import {EASUtils} from "./utils/EASUtils.s.sol"; import {console} from "forge-std/console.sol"; -contract EmitAttestationsCsv is Script { +contract EmitAttestationsJson is Script { using stdJson for string; // Configuration constants @@ -31,25 +31,62 @@ contract EmitAttestationsCsv is Script { console.log("EAS Address:", address(eas)); // Read and validate JSON file - string memory jsonPath = vm.envOr("JSON_PATH", string("attestations.json")); + string memory jsonPath = vm.envOr( + "JSON_PATH", + string("attestations-latest.json") + ); console.log("Reading JSON from:", jsonPath); string memory jsonData = vm.readFile(jsonPath); AttestationData[] memory attestations = parseAndValidateJson(jsonData); - console.log(string(abi.encodePacked("Parsed ", vm.toString(attestations.length), " attestations from JSON"))); + console.log( + string( + abi.encodePacked( + "Parsed ", + vm.toString(attestations.length), + " attestations from JSON" + ) + ) + ); // Get schema ID - bytes32 schemaId = vm.envOr("SCHEMA_ID", bytes32(0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a47de)); + bytes32 schemaId = vm.envOr( + "SCHEMA_ID", + bytes32( + 0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a47de + ) + ); console.log("Using Schema ID:", vm.toString(schemaId)); // Create batch requests - MultiAttestationRequest[] memory requests = createBatchRequests(attestations, schemaId); + MultiAttestationRequest[] memory requests = createBatchRequests( + attestations, + schemaId + ); if (isDryRun) { - console.log(string(abi.encodePacked("DRY RUN: Would process ", vm.toString(requests.length), " batches"))); + console.log( + string( + abi.encodePacked( + "DRY RUN: Would process ", + vm.toString(requests.length), + " batches" + ) + ) + ); for (uint256 i = 0; i < requests.length; i++) { - console.log(string(abi.encodePacked("Batch ", vm.toString(i), ": ", vm.toString(requests[i].data.length), " attestations"))); + console.log( + string( + abi.encodePacked( + "Batch ", + vm.toString(i), + ": ", + vm.toString(requests[i].data.length), + " attestations" + ) + ) + ); } console.log("Dry run completed successfully!"); return; @@ -59,26 +96,38 @@ contract EmitAttestationsCsv is Script { executeAttestations(eas, requests); } - function parseAndValidateJson(string memory jsonData) internal view returns (AttestationData[] memory) { + function parseAndValidateJson( + string memory jsonData + ) internal view returns (AttestationData[] memory) { // JSON parseJson returns data with types inferred from JSON - addresses as address, strings as string console.log("Parsing JSON attestations array..."); - + // Count attestations - parse the full array to get length // We'll use a temporary storage to collect attestations - AttestationData[] memory tempAttestations = new AttestationData[](MAX_ATTESTATIONS); + AttestationData[] memory tempAttestations = new AttestationData[]( + MAX_ATTESTATIONS + ); uint256 count = 0; - + // Parse attestations until we run out for (uint256 i = 0; i < MAX_ATTESTATIONS; i++) { - string memory basePath = string(abi.encodePacked(".attestations[", vm.toString(i), "]")); - + string memory basePath = string( + abi.encodePacked(".attestations[", vm.toString(i), "]") + ); + // Try to parse this attestation - if it doesn't exist, parseJson will revert - bytes memory testPath = vm.parseJson(jsonData, string(abi.encodePacked(basePath, ".recipient"))); - if (testPath.length == 0) break; // No more attestations - + bytes memory testPath = vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".recipient")) + ); + if (testPath.length == 0) break; // No more attestations + // Parse badgeName with proper bytes32 conversion string memory badgeNameStr = abi.decode( - vm.parseJson(jsonData, string(abi.encodePacked(basePath, ".badgeName"))), + vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".badgeName")) + ), (string) ); bytes32 badgeName; @@ -87,38 +136,63 @@ contract EmitAttestationsCsv is Script { assembly { badgeName := mload(add(badgeNameBytes, 32)) } - + bytes memory justification = abi.decode( - vm.parseJson(jsonData, string(abi.encodePacked(basePath, ".justification"))), + vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".justification")) + ), (bytes) ); - + tempAttestations[count] = AttestationData({ recipient: abi.decode(testPath, (address)), badgeName: badgeName, justification: justification }); - + count++; } - + // Copy to properly sized array AttestationData[] memory attestations = new AttestationData[](count); for (uint256 i = 0; i < count; i++) { attestations[i] = tempAttestations[i]; } - + console.log("Found", attestations.length, "attestations in JSON"); - require(attestations.length > 0, "JSON must contain at least 1 attestation"); - require(attestations.length <= MAX_ATTESTATIONS, "Too many attestations, max 1000 allowed"); + require( + attestations.length > 0, + "JSON must contain at least 1 attestation" + ); + require( + attestations.length <= MAX_ATTESTATIONS, + "Too many attestations, max 1000 allowed" + ); // Validate each attestation for (uint256 i = 0; i < attestations.length; i++) { - require(attestations[i].recipient != address(0), - string(abi.encodePacked("Attestation ", vm.toString(i), ": invalid recipient address"))); - require(attestations[i].badgeName != bytes32(0), - string(abi.encodePacked("Attestation ", vm.toString(i), ": badgeName cannot be empty"))); + require( + attestations[i].recipient != address(0), + string( + abi.encodePacked( + "Attestation ", + vm.toString(i), + ": invalid recipient address" + ) + ) + ); + require( + attestations[i].badgeName != bytes32(0), + string( + abi.encodePacked( + "Attestation ", + vm.toString(i), + ": badgeName cannot be empty" + ) + ) + ); } return attestations; @@ -128,8 +202,10 @@ contract EmitAttestationsCsv is Script { AttestationData[] memory attestations, bytes32 schemaId ) internal pure returns (MultiAttestationRequest[] memory) { - uint256 totalBatches = (attestations.length + MAX_BATCH_SIZE - 1) / MAX_BATCH_SIZE; - MultiAttestationRequest[] memory requests = new MultiAttestationRequest[](totalBatches); + uint256 totalBatches = (attestations.length + MAX_BATCH_SIZE - 1) / + MAX_BATCH_SIZE; + MultiAttestationRequest[] + memory requests = new MultiAttestationRequest[](totalBatches); for (uint256 batchIndex = 0; batchIndex < totalBatches; batchIndex++) { uint256 startIdx = batchIndex * MAX_BATCH_SIZE; @@ -156,37 +232,118 @@ contract EmitAttestationsCsv is Script { return requests; } - function executeAttestations(EAS eas, MultiAttestationRequest[] memory requests) internal { + function executeAttestations( + EAS eas, + MultiAttestationRequest[] memory requests + ) internal { uint256 pk = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(pk); console.log("Starting batch attestation execution..."); uint256 totalAttestations = 0; - for (uint256 batchIndex = 0; batchIndex < requests.length; batchIndex++) { - console.log(string(abi.encodePacked("Processing batch ", vm.toString(batchIndex + 1), "/", vm.toString(requests.length), " - ", vm.toString(requests[batchIndex].data.length), " attestations"))); + for ( + uint256 batchIndex = 0; + batchIndex < requests.length; + batchIndex++ + ) { + console.log( + string( + abi.encodePacked( + "Processing batch ", + vm.toString(batchIndex + 1), + "/", + vm.toString(requests.length), + " - ", + vm.toString(requests[batchIndex].data.length), + " attestations" + ) + ) + ); // Create single-element array for this batch - MultiAttestationRequest[] memory singleBatch = new MultiAttestationRequest[](1); + MultiAttestationRequest[] + memory singleBatch = new MultiAttestationRequest[](1); singleBatch[0] = requests[batchIndex]; try eas.multiAttest(singleBatch) returns (bytes32[] memory uids) { - console.log(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " completed with ", vm.toString(uids.length), " attestations"))); + console.log( + string( + abi.encodePacked( + "Batch ", + vm.toString(batchIndex + 1), + " completed with ", + vm.toString(uids.length), + " attestations" + ) + ) + ); totalAttestations += uids.length; // Log first few UIDs for (uint256 i = 0; i < uids.length && i < 3; i++) { - console.log(string(abi.encodePacked(" UID ", vm.toString(i), ": ", vm.toString(uids[i])))); + console.log( + string( + abi.encodePacked( + " UID ", + vm.toString(i), + ": ", + vm.toString(uids[i]) + ) + ) + ); } if (uids.length > 3) { - console.log(string(abi.encodePacked(" ... and ", vm.toString(uids.length - 3), " more"))); + console.log( + string( + abi.encodePacked( + " ... and ", + vm.toString(uids.length - 3), + " more" + ) + ) + ); } } catch Error(string memory reason) { - console.log(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed: ", reason))); - revert(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed: ", reason))); + console.log( + string( + abi.encodePacked( + "Batch ", + vm.toString(batchIndex + 1), + " failed: ", + reason + ) + ) + ); + revert( + string( + abi.encodePacked( + "Batch ", + vm.toString(batchIndex + 1), + " failed: ", + reason + ) + ) + ); } catch { - console.log(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed with unknown error"))); - revert(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed with unknown error"))); + console.log( + string( + abi.encodePacked( + "Batch ", + vm.toString(batchIndex + 1), + " failed with unknown error" + ) + ) + ); + revert( + string( + abi.encodePacked( + "Batch ", + vm.toString(batchIndex + 1), + " failed with unknown error" + ) + ) + ); } } @@ -197,6 +354,4 @@ contract EmitAttestationsCsv is Script { console.log("Total attestations created:", totalAttestations); console.log("Execution completed successfully!"); } - - -} \ No newline at end of file +} diff --git a/the-guild-smart-contracts/test/EmitAttestationsCsv.t.sol b/the-guild-smart-contracts/test/EmitAttestationsCsv.t.sol index 979e70d..7efc011 100644 --- a/the-guild-smart-contracts/test/EmitAttestationsCsv.t.sol +++ b/the-guild-smart-contracts/test/EmitAttestationsCsv.t.sol @@ -2,39 +2,49 @@ pragma solidity ^0.8.28; import {Test} from "forge-std/Test.sol"; -import {EmitAttestationsCsv} from "../script/EmitAttestationsCsv.s.sol"; +import {EmitAttestationsJson} from "../script/EmitAttestationsJson.s.sol"; -contract EmitAttestationsCsvTest is Test { - EmitAttestationsCsv private script; +contract EmitAttestationsJsonTest is Test { + EmitAttestationsJson private script; function setUp() public { - script = new EmitAttestationsCsv(); + script = new EmitAttestationsJson(); } function test_AttestationDataStructure() public pure { // Test that the AttestationData struct is properly defined - EmitAttestationsCsv.AttestationData memory testData = EmitAttestationsCsv.AttestationData({ - recipient: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e, - badgeName: bytes32(abi.encodePacked("Rust")), - justification: bytes("Awarded for outstanding Rust contributions") - }); - - assertEq(testData.recipient, 0x742d35Cc6634C0532925a3b844Bc454e4438f44e); + EmitAttestationsJson.AttestationData + memory testData = EmitAttestationsJson.AttestationData({ + recipient: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e, + badgeName: bytes32(abi.encodePacked("Rust")), + justification: bytes( + "Awarded for outstanding Rust contributions" + ) + }); + + assertEq( + testData.recipient, + 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + ); assertEq(testData.badgeName, bytes32(abi.encodePacked("Rust"))); - assertEq(testData.justification, bytes("Awarded for outstanding Rust contributions")); + assertEq( + testData.justification, + bytes("Awarded for outstanding Rust contributions") + ); } function test_AttestationDataArray() public pure { // Test that we can create arrays of AttestationData - EmitAttestationsCsv.AttestationData[] memory attestations = new EmitAttestationsCsv.AttestationData[](2); + EmitAttestationsJson.AttestationData[] + memory attestations = new EmitAttestationsJson.AttestationData[](2); - attestations[0] = EmitAttestationsCsv.AttestationData({ + attestations[0] = EmitAttestationsJson.AttestationData({ recipient: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e, badgeName: bytes32(abi.encodePacked("Rust")), justification: bytes("Rust expert") }); - attestations[1] = EmitAttestationsCsv.AttestationData({ + attestations[1] = EmitAttestationsJson.AttestationData({ recipient: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e, badgeName: bytes32(abi.encodePacked("Solidity")), justification: bytes("Solidity expert") @@ -42,6 +52,9 @@ contract EmitAttestationsCsvTest is Test { assertEq(attestations.length, 2); assertEq(attestations[0].badgeName, bytes32(abi.encodePacked("Rust"))); - assertEq(attestations[1].badgeName, bytes32(abi.encodePacked("Solidity"))); + assertEq( + attestations[1].badgeName, + bytes32(abi.encodePacked("Solidity")) + ); } -} \ No newline at end of file +}