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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 304 additions & 81 deletions notebooks/01_github_issues.ipynb

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions scripts/test_projects.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
set -euo pipefail

# Test project API endpoints:
# - List projects (public)
# - Create a project (protected) using signature headers (no JWT)
# Requirements: curl, node, npm. Installs ethers locally into /tmp by default.
#
# Inputs (env):
# PUBLIC_ADDRESS (required) - wallet address
# PRIVATE_KEY (required) - wallet private key (0x-prefixed)
# API_URL (optional) - defaults to http://localhost:3001
# PROJECT_NAME (optional) - defaults to "Shell Project"
# PROJECT_DESC (optional) - defaults to "Created via script"
# PROJECT_STATUS (optional) - defaults to "proposal" (proposal|ongoing|rejected)

API_URL="${API_URL:-http://localhost:3001}"
ADDRESS="${PUBLIC_ADDRESS:-}"
PRIVATE_KEY="${PRIVATE_KEY:-}"

# If not provided via env, prompt interactively (input hidden for private key).
if [[ -z "${ADDRESS}" ]]; then
read -r -p "Enter PUBLIC_ADDRESS (0x...): " ADDRESS
fi
if [[ -z "${PRIVATE_KEY}" ]]; then
read -r -s -p "Enter PRIVATE_KEY (0x..., hidden): " PRIVATE_KEY
echo
fi
if [[ -z "${ADDRESS}" || -z "${PRIVATE_KEY}" ]]; then
echo "PUBLIC_ADDRESS and PRIVATE_KEY are required. Aborting."
exit 1
fi

PROJECT_NAME="${PROJECT_NAME:-Shell Project}"
PROJECT_DESC="${PROJECT_DESC:-Created via script}"
PROJECT_STATUS="${PROJECT_STATUS:-proposal}"

# Ensure we have ethers available without polluting the repo.
TOOLS_DIR="${TOOLS_DIR:-/tmp/theguildgenesis-login}"
export NODE_PATH="${TOOLS_DIR}/node_modules${NODE_PATH:+:${NODE_PATH}}"
export PATH="${TOOLS_DIR}/node_modules/.bin:${PATH}"
if ! node -e "require('ethers')" >/dev/null 2>&1; then
echo "Installing ethers@6 to ${TOOLS_DIR}..."
mkdir -p "${TOOLS_DIR}"
npm install --prefix "${TOOLS_DIR}" ethers@6 >/dev/null
fi

echo "Fetching nonce for ${ADDRESS}..."
nonce_resp="$(curl -sS "${API_URL}/auth/nonce/${ADDRESS}")"
echo "Nonce response: ${nonce_resp}"
# Parse nonce safely
nonce="$(RESP="${nonce_resp}" python3 - <<'PY'
import json, os
data = json.loads(os.environ["RESP"])
print(data["nonce"])
PY
)"
if [[ -z "${nonce}" ]]; then
echo "Failed to parse nonce from response"
exit 1
fi

message=$'Sign this message to authenticate with The Guild.\n\nNonce: '"${nonce}"

echo "Signing nonce..."
signature="$(
ADDRESS="${ADDRESS}" PRIVATE_KEY="${PRIVATE_KEY}" MESSAGE="${message}" \
node - <<'NODE'
const { Wallet } = require('ethers');

const address = process.env.ADDRESS;
const pk = process.env.PRIVATE_KEY;
const message = process.env.MESSAGE;

if (!address || !pk || !message) {
console.error("Missing ADDRESS, PRIVATE_KEY or MESSAGE");
process.exit(1);
}

const wallet = new Wallet(pk);
if (wallet.address.toLowerCase() !== address.toLowerCase()) {
console.error(`Private key does not match address. Wallet: ${wallet.address}, Provided: ${address}`);
process.exit(1);
}

(async () => {
const sig = await wallet.signMessage(message);
console.log(sig);
})();
NODE
)"

echo "Signature: ${signature}"

echo "Listing projects (public)..."
list_tmp="$(mktemp)"
list_status="$(curl -sS -o "${list_tmp}" -w "%{http_code}" \
"${API_URL}/projects")"
list_resp="$(cat "${list_tmp}")"
rm -f "${list_tmp}"
echo "List HTTP ${list_status}: ${list_resp}"
if [[ "${list_status}" != "200" ]]; then
echo "List projects failed with status ${list_status}"
exit 1
fi

create_payload=$(cat <<EOF
{
"name": "${PROJECT_NAME}",
"description": "${PROJECT_DESC}",
"status": "${PROJECT_STATUS}"
}
EOF
)

echo "Creating project..."
create_tmp="$(mktemp)"
create_status="$(curl -sS -o "${create_tmp}" -w "%{http_code}" -X POST \
-H "x-eth-address: ${ADDRESS}" \
-H "x-eth-signature: ${signature}" \
-H "Content-Type: application/json" \
-d "${create_payload}" \
"${API_URL}/projects")"
create_resp="$(cat "${create_tmp}")"
rm -f "${create_tmp}"
echo "Create project HTTP ${create_status}: ${create_resp}"
if [[ "${create_status}" != "201" && "${create_status}" != "200" ]]; then
echo "Project creation failed with status ${create_status}"
exit 1
fi

echo "Done."
5 changes: 4 additions & 1 deletion the-guild-smart-contracts/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ PRIVATE_KEY=
JSON_PATH=./attestations.json
SCHEMA_ID=0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a47de
DRY_RUN=false
RPC_URL=
RPC_URL=

# contribution token mint
TGC_PROXY_ADDRESS=
98 changes: 98 additions & 0 deletions the-guild-smart-contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Salt: "theguild_v_0.1.2"
TheGuildActivityToken
https://amoy.polygonscan.com/address/0x4649490B118389d0Be8F48b8953eFb235d8CB545

TheGuildContributionToken (proxy)
https://amoy.polygonscan.com/address/0x14d403EaE3E0b2E2dc6379C9729Df6906fF38bE7

TheGuildBadgeRegistry
https://amoy.polygonscan.com/address/0x94f5F12BE60a338D263882a1A49E81ca8A0c30F4

Expand Down Expand Up @@ -169,6 +172,101 @@ forge script script/TheGuildAttestationResolver.s.sol:TheGuildActivityTokenScrip
--broadcast
```

### Contribution Token (TGC)

`TheGuildContributionToken` (symbol `TGC`) is an **upgradeable ERC20** used to reward contributions.

- **Contract**: `src/TheGuildContributionToken.sol`
- Upgradable via UUPS (`UUPSUpgradeable`)
- Standard 18 decimals
- `mint(address to, uint256 amount)` – owner-only mint
- `mintWithReason(address to, uint256 amount, bytes reason)` – owner-only mint that emits `MintedWithReason`
- `batchMint(address[] recipients, uint256[] amounts)` – owner-only batch mint
- `batchMintWithReason(address[] recipients, uint256[] amounts, bytes[] reasons)` – owner-only batch mint with reasons
- **Tests**: `test/TheGuildContributionToken.t.sol`
- Covers metadata, ownership, minting, `MintedWithReason` event, and batch mint helpers.

#### Deploying the upgradable TGC proxy

Use `script/DeployTGC.s.sol` to deploy the implementation + ERC1967 proxy and call `initialize()` on the proxy:

```shell
export PRIVATE_KEY=your_private_key
forge script script/DeployTGC.s.sol:DeployTGC \
--rpc-url <your_rpc_url> \
--broadcast
```

The script logs both the proxy (TGC) address and the implementation address.

#### Batch minting TGC from JSON

Use `script/MintTGCFromJson.s.sol` to batch-mint TGC using `mintWithReason` from a JSON file.

JSON format:

```json
{
"mints": [
{
"recipient": "0x...",
"amount": "1000000000000000000",
"reason": "0x..."
}
]
}
```

- `recipient`: recipient address
- `amount`: amount as a uint256 (string-encoded in JSON)
- `reason`: ABI-encoded bytes explaining the reason (e.g. `abi.encodePacked("issue-123")`)

Usage:

```shell
export PRIVATE_KEY=your_private_key
export TGC_PROXY_ADDRESS=0xYourTGCProxy

# Optional: override JSON path (default: tgc-mints.json)
export JSON_PATH=contribution-tokens-latest.json

# Dry run
export DRY_RUN=true
forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \
--rpc-url <your_rpc_url>

# Production run
unset DRY_RUN
forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \
--rpc-url <your_rpc_url> \
--broadcast
```

Environment variables:

- `PRIVATE_KEY`: signer that owns the TGC proxy
- `TGC_PROXY_ADDRESS`: address of the deployed TGC proxy
- `JSON_PATH`: path to the JSON file (default: `contribution-tokens-latest.json`)
- `DRY_RUN`: set to `true` to simulate without broadcasting (default: `false`)

#### Upgrading the TGC implementation

Use `script/UpgradeTGCImplementation.s.sol` to deploy a new implementation and upgrade the existing proxy.

```shell
export PRIVATE_KEY=your_private_key
export TGC_PROXY_ADDRESS=0xYourTGCProxy

forge script script/UpgradeTGCImplementation.s.sol:UpgradeTGCImplementation \
--rpc-url <your_rpc_url> \
--broadcast
```

The script:
- Deploys a new `TheGuildContributionToken` implementation
- Calls `upgradeToAndCall` on the proxy (with empty data)
- Logs the proxy and new implementation addresses

### Badge Ranking

`TheGuildBadgeRanking` enables voting/ranking of badges for relevancy. Features:
Expand Down
54 changes: 54 additions & 0 deletions the-guild-smart-contracts/contribution-tokens-latest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"mints": [
{
"recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8",
"amount": 40,
"reason": "Github handle errors"
},
{
"recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8",
"amount": 40,
"reason": "Implement SIWE for the front end"
},
{
"recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172",
"amount": 80,
"reason": "Feature: add a github handle to profile in Front end"
},
{
"recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172",
"amount": 40,
"reason": "Add doc for indexer"
},
{
"recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2",
"amount": 160,
"reason": "Add a backend for the contribution points (api, db)"
},
{
"recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE",
"amount": 20,
"reason": "Improve landing page design with animated background"
},
{
"recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172",
"amount": 40,
"reason": "On the profile, there should be a section with the badges issued by that profile"
},
{
"recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8",
"amount": 20,
"reason": "Add copy to clipboard icons next to all ethereum addresses"
},
{
"recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8",
"amount": 40,
"reason": "Add description on the profile page"
},
{
"recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8",
"amount": 320,
"reason": "Improve SIWE logic to use dynamic, user-specific nonce - backend"
}
]
}
80 changes: 80 additions & 0 deletions the-guild-smart-contracts/run_batch_contribution_tokens.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash

# Helper script for running TheGuild batch contribution token mint script
# Usage: ./run_batch_contribution_tokens.sh [json_file] [dry_run]
# json_file: Path to JSON file with contribution token mints (default: contribution-tokens-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 contribution-tokens-latest.json
if [ $# -eq 0 ]; then
# No arguments: use default JSON file
JSON_FILE="contribution-tokens-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="contribution-tokens-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 "$TGC_PROXY_ADDRESS" ]; then
echo "Error: TGC_PROXY_ADDRESS environment variable not set"
exit 1
fi

# Run the script
if [ "$DRY_RUN" = "true" ]; then
forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \
--rpc-url "$RPC_URL"
else
forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \
--rpc-url "$RPC_URL" \
--broadcast
fi


Loading
Loading