diff --git a/notebooks/01_github_issues.ipynb b/notebooks/01_github_issues.ipynb index 9b77e78..e6db440 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": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Fetched 42 issues from TheSoftwareDevGuild/TheGuildGenesis\n" + "Fetched 47 issues from TheSoftwareDevGuild/TheGuildGenesis\n" ] }, { @@ -69,122 +69,122 @@ " \n", " \n", " 0\n", - " 126\n", - " Use jupyter notebook to analyze github issues\n", + " 146\n", + " Figure out how to admin delete profile\n", " open\n", - " 2025-10-30T13:17:36Z\n", - " 2025-10-30T13:17:36Z\n", + " 2025-12-03T17:26:37Z\n", + " 2025-12-03T17:26:37Z\n", " joelamouche\n", - " joelamouche\n", - " jupyter-notebook,python\n", + " \n", + " good first issue,back-end,planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 1\n", - " 125\n", - " Improve UX and design of theguild.dev\n", + " 145\n", + " I think we should allow duplicate attestations\n", " open\n", - " 2025-10-28T11:23:19Z\n", - " 2025-10-28T11:23:19Z\n", + " 2025-12-03T15:18:42Z\n", + " 2025-12-03T15:18:42Z\n", " joelamouche\n", " \n", - " good first issue,ux,design,hacktoberfest\n", + " solidity,planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 2\n", - " 118\n", - " Badge enhancements\n", + " 144\n", + " Implement Upgradable pattern for TheGuildAttes...\n", " open\n", - " 2025-10-17T08:35:11Z\n", - " 2025-10-30T13:44:59Z\n", + " 2025-12-03T10:28:34Z\n", + " 2025-12-03T10:28:55Z\n", " joelamouche\n", - " joelamouche\n", - " enhancement,planning,hacktoberfest\n", + " \n", + " good first issue,solidity,foundry\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 3\n", - " 110\n", - " Discord bot: relay git activity on a discord c...\n", + " 143\n", + " TheGuildAttestationResolver should have admin ...\n", " open\n", - " 2025-10-10T09:57:28Z\n", - " 2025-10-30T09:00:26Z\n", + " 2025-12-03T10:27:07Z\n", + " 2025-12-03T10:27:51Z\n", " joelamouche\n", " \n", - " good first issue,nodejs,typescript,discord-bot...\n", + " good first issue,solidity,foundry\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 4\n", - " 106\n", - " Implement JWT for our profile api\n", + " 142\n", + " Add contributor leaderboard\n", " open\n", - " 2025-10-09T13:18:18Z\n", - " 2025-10-27T08:48:29Z\n", + " 2025-11-24T15:04:07Z\n", + " 2025-11-24T15:04:42Z\n", " joelamouche\n", - " tusharshah21\n", - " front end,rust,back-end,react,typescript,hackt...\n", + " \n", + " good first issue,front end,react,typescript,80pts\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 5\n", - " 105\n", - " Add backend endpoints to fetch attestations\n", + " 141\n", + " Add a new leaderboard page, with link in the s...\n", " open\n", - " 2025-10-09T12:31:06Z\n", - " 2025-10-09T12:31:06Z\n", + " 2025-11-24T15:01:00Z\n", + " 2025-11-24T15:01:00Z\n", " joelamouche\n", " \n", - " rust,back-end,db\n", + " good first issue,front end,react,typescript,20...\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 6\n", - " 104\n", - " Blockchain Indexer - Get blockchain data from ...\n", + " 140\n", + " Create top badge owner leaderboard\n", " open\n", - " 2025-10-09T12:20:02Z\n", - " 2025-10-09T12:33:22Z\n", + " 2025-11-24T14:59:35Z\n", + " 2025-11-24T15:02:16Z\n", " joelamouche\n", - " oscarwroche\n", - " enhancement,planning\n", + " \n", + " good first issue,front end,react,typescript,40pts\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 7\n", - " 103\n", - " Improve auth logic for API\n", + " 138\n", + " Public contributor leaderboard\n", " open\n", - " 2025-10-09T12:14:41Z\n", - " 2025-10-14T08:05:29Z\n", + " 2025-11-17T13:58:58Z\n", + " 2025-11-24T15:04:19Z\n", + " joelamouche\n", " joelamouche\n", - " oscarwroche\n", - " enhancement,planning\n", + " enhancement,front end,planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 8\n", - " 102\n", - " Add twitter handle to profiles in the backend\n", + " 137\n", + " Figure out sharing new badges on X\n", " open\n", - " 2025-10-08T13:18:45Z\n", - " 2025-10-27T08:43:18Z\n", + " 2025-11-17T13:58:23Z\n", + " 2025-11-17T13:58:28Z\n", " joelamouche\n", - " ayushhh101\n", - " good first issue,rust,back-end,db,hacktoberfest\n", + " joelamouche\n", + " planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", " 9\n", - " 101\n", - " Add twitter account to profiles\n", + " 135\n", + " Add projects to the Backend\n", " open\n", - " 2025-10-08T13:16:50Z\n", - " 2025-10-16T13:09:30Z\n", + " 2025-11-17T12:38:30Z\n", + " 2025-11-27T12:15:32Z\n", " joelamouche\n", - " \n", - " enhancement,planning\n", + " pheobeayo\n", + " good first issue,rust,back-end,160pts,db,hackt...\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 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-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-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 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", - "6 enhancement,planning \n", - "7 enhancement,planning \n", - "8 good first issue,rust,back-end,db,hacktoberfest \n", - "9 enhancement,planning \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": 3, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -330,7 +330,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -347,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "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,408 @@ "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": 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": [ + "\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": [ + "# 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": [ + "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": {}, + "source": [ + "### Get github handles from API" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://theguild-backend-e2df290d177e.herokuapp.com/\n", + "Loaded 4 GitHub username -> address mappings from backend API\n", + "\n", + "Warning: 4 assignees without address mapping:\n", + " - oscarwroche\n", + " - peteroche\n", + " - rainwaters11\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": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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", + " # Skip if no assignees or no labels\n", + " if not assignees or not labels:\n", + " continue\n", + "\n", + " for assignee in assignees:\n", + " github_username = assignee.get(\"login\", \"\").lower()\n", + " if not github_username:\n", + " continue\n", + "\n", + " recipient_address = github_to_address.get(github_username)\n", + " if not recipient_address:\n", + " continue # Skip if no address mapping\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", + " # 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", + " attestations.append({\n", + " \"recipient\": recipient_address,\n", + " \"badgeName\": badge_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": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\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 and also to attestations-latest.json\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", + "# 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/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/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-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.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/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 new file mode 100644 index 0000000..757d8c1 --- /dev/null +++ b/the-guild-smart-contracts/script/CreateBadgesFromJson.s.sol @@ -0,0 +1,285 @@ +// 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-latest.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!"); + } +} diff --git a/the-guild-smart-contracts/script/EmitAttestationsCsv.s.sol b/the-guild-smart-contracts/script/EmitAttestationsCsv.s.sol deleted file mode 100644 index ab312d4..0000000 --- a/the-guild-smart-contracts/script/EmitAttestationsCsv.s.sol +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {Script, stdJson} from "forge-std/Script.sol"; -import {EAS} from "eas-contracts/EAS.sol"; -import {AttestationRequestData, MultiAttestationRequest} from "eas-contracts/IEAS.sol"; -import {EASUtils} from "./utils/EASUtils.s.sol"; -import {console} from "forge-std/console.sol"; - -contract EmitAttestationsCsv is Script { - using stdJson for string; - - // Configuration constants - uint256 constant MAX_BATCH_SIZE = 50; // Limit batch size to prevent gas issues - uint256 constant MAX_ATTESTATIONS = 1000; // Reasonable limit for processing - - struct AttestationData { - address recipient; - bytes32 badgeName; - bytes justification; - } - - function run() public { - bool isDryRun = vm.envOr("DRY_RUN", false); - - console.log("=== TheGuild Batch Attestation Script ==="); - console.log("Dry run mode:", isDryRun ? "ENABLED" : "DISABLED"); - - // Get EAS address for current network - EAS eas = EAS(EASUtils.getEASAddress(vm)); - console.log("EAS Address:", address(eas)); - - // Read and validate JSON file - string memory jsonPath = vm.envOr("JSON_PATH", string("attestations.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"))); - - // Get schema ID - 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); - - if (isDryRun) { - 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("Dry run completed successfully!"); - return; - } - - // Execute attestations - executeAttestations(eas, requests); - } - - 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); - 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), "]")); - - // 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 - - // Parse badgeName with proper bytes32 conversion - string memory badgeNameStr = abi.decode( - vm.parseJson(jsonData, string(abi.encodePacked(basePath, ".badgeName"))), - (string) - ); - bytes32 badgeName; - bytes memory badgeNameBytes = bytes(badgeNameStr); - require(badgeNameBytes.length <= 32, "badgeName too long"); - assembly { - badgeName := mload(add(badgeNameBytes, 32)) - } - - bytes memory justification = abi.decode( - 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"); - - // 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"))); - } - - return attestations; - } - - function createBatchRequests( - 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); - - for (uint256 batchIndex = 0; batchIndex < totalBatches; batchIndex++) { - uint256 startIdx = batchIndex * MAX_BATCH_SIZE; - uint256 endIdx = startIdx + MAX_BATCH_SIZE; - if (endIdx > attestations.length) endIdx = attestations.length; - - uint256 batchSize = endIdx - startIdx; - requests[batchIndex].schema = schemaId; - requests[batchIndex].data = new AttestationRequestData[](batchSize); - - for (uint256 i = 0; i < batchSize; i++) { - AttestationData memory att = attestations[startIdx + i]; - requests[batchIndex].data[i] = AttestationRequestData({ - recipient: att.recipient, - expirationTime: 0, // No expiration - revocable: true, - refUID: bytes32(0), // No reference - data: abi.encode(att.badgeName, att.justification), - value: 0 // No ETH value - }); - } - } - - return requests; - } - - 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"))); - - // Create single-element array for this batch - 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"))); - 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])))); - } - if (uids.length > 3) { - 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))); - } 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"))); - } - } - - vm.stopBroadcast(); - - console.log("=== Attestation Summary ==="); - console.log("Total batches processed:", requests.length); - console.log("Total attestations created:", totalAttestations); - console.log("Execution completed successfully!"); - } - - -} \ No newline at end of file diff --git a/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol b/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol new file mode 100644 index 0000000..563887d --- /dev/null +++ b/the-guild-smart-contracts/script/EmitAttestationsJson.s.sol @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, stdJson} from "forge-std/Script.sol"; +import {EAS} from "eas-contracts/EAS.sol"; +import {AttestationRequestData, MultiAttestationRequest} from "eas-contracts/IEAS.sol"; +import {EASUtils} from "./utils/EASUtils.s.sol"; +import {console} from "forge-std/console.sol"; + +contract EmitAttestationsJson is Script { + using stdJson for string; + + // Configuration constants + uint256 constant MAX_BATCH_SIZE = 50; // Limit batch size to prevent gas issues + uint256 constant MAX_ATTESTATIONS = 1000; // Reasonable limit for processing + + struct AttestationData { + address recipient; + bytes32 badgeName; + bytes justification; + } + + function run() public { + bool isDryRun = vm.envOr("DRY_RUN", false); + + console.log("=== TheGuild Batch Attestation Script ==="); + console.log("Dry run mode:", isDryRun ? "ENABLED" : "DISABLED"); + + // Get EAS address for current network + EAS eas = EAS(EASUtils.getEASAddress(vm)); + console.log("EAS Address:", address(eas)); + + // Read and validate JSON file + 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" + ) + ) + ); + + // Get schema ID + 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 + ); + + if (isDryRun) { + 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("Dry run completed successfully!"); + return; + } + + // Execute attestations + executeAttestations(eas, requests); + } + + 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 + ); + 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), "]") + ); + + // 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 + + // Parse badgeName with proper bytes32 conversion + string memory badgeNameStr = abi.decode( + vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".badgeName")) + ), + (string) + ); + bytes32 badgeName; + bytes memory badgeNameBytes = bytes(badgeNameStr); + require(badgeNameBytes.length <= 32, "badgeName too long"); + assembly { + badgeName := mload(add(badgeNameBytes, 32)) + } + + bytes memory justification = abi.decode( + 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" + ); + + // 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" + ) + ) + ); + } + + return attestations; + } + + function createBatchRequests( + 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); + + for (uint256 batchIndex = 0; batchIndex < totalBatches; batchIndex++) { + uint256 startIdx = batchIndex * MAX_BATCH_SIZE; + uint256 endIdx = startIdx + MAX_BATCH_SIZE; + if (endIdx > attestations.length) endIdx = attestations.length; + + uint256 batchSize = endIdx - startIdx; + requests[batchIndex].schema = schemaId; + requests[batchIndex].data = new AttestationRequestData[](batchSize); + + for (uint256 i = 0; i < batchSize; i++) { + AttestationData memory att = attestations[startIdx + i]; + requests[batchIndex].data[i] = AttestationRequestData({ + recipient: att.recipient, + expirationTime: 0, // No expiration + revocable: true, + refUID: bytes32(0), // No reference + data: abi.encode(att.badgeName, att.justification), + value: 0 // No ETH value + }); + } + } + + return requests; + } + + 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" + ) + ) + ); + + // Create single-element array for this batch + 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" + ) + ) + ); + 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]) + ) + ) + ); + } + if (uids.length > 3) { + 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 + ) + ) + ); + } 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" + ) + ) + ); + } + } + + vm.stopBroadcast(); + + console.log("=== Attestation Summary ==="); + console.log("Total batches processed:", requests.length); + console.log("Total attestations created:", totalAttestations); + console.log("Execution completed successfully!"); + } +} 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 +}