From 20d9c5bbb93d7eb0181c7ef375e47d3bd9920f7c Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:23:05 -0700 Subject: [PATCH 01/52] Add automerge workflow for pull requests * Introduced a new GitHub Actions workflow to enable automatic merging of pull requests labeled with 'automerge'. * Configured the workflow to trigger on labeled, reopened, and synchronized pull requests. * Utilized the peter-evans/enable-pull-request-automerge action with squash merge method. --- .github/workflows/automerge.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/automerge.yml diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..f8ba46e --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,20 @@ +name: "Automerge Pull Requests" +on: + pull_request_target: + types: [labeled, reopened, synchronize] + +permissions: + pull-requests: write + contents: write + +jobs: + automerge: + if: contains(github.event.pull_request.labels.*.name, 'automerge') + runs-on: ubuntu-latest + steps: + - name: Enable Auto-Merge + uses: peter-evans/enable-pull-request-automerge@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + pull-request-number: ${{ github.event.pull_request.number }} + merge-method: squash \ No newline at end of file From 02c60375222aa19482efd91bd04515b4a69e2c22 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:35:35 -0700 Subject: [PATCH 02/52] Add automerge label to PR workflow --- .github/workflows/PR-build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/PR-build.yml b/.github/workflows/PR-build.yml index 0b35c0e..04b3e9f 100644 --- a/.github/workflows/PR-build.yml +++ b/.github/workflows/PR-build.yml @@ -57,3 +57,8 @@ jobs: else echo "No QIP files changed. Skipping validation." fi + - name: Add automerge label + if: success() + uses: actions-ecosystem/action-add-labels@v1 + with: + labels: automerge From 46362b2abe215f73719d37263740324e97b5a3b8 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:38:37 -0700 Subject: [PATCH 03/52] fix autolabel permissions --- .github/workflows/PR-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/PR-build.yml b/.github/workflows/PR-build.yml index 04b3e9f..64bc08f 100644 --- a/.github/workflows/PR-build.yml +++ b/.github/workflows/PR-build.yml @@ -5,6 +5,10 @@ on: branches: - main +permissions: + pull-requests: write + contents: read + jobs: build: name: Validate new/changed QIPs From 67696cee1bab81e6e68cf62acefab7ec136c6ae1 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:41:48 -0700 Subject: [PATCH 04/52] Add new workflow to label validated pull requests --- .github/workflows/PR-build.yml | 9 ------- .github/workflows/label-validated-prs.yml | 29 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/label-validated-prs.yml diff --git a/.github/workflows/PR-build.yml b/.github/workflows/PR-build.yml index 64bc08f..0b35c0e 100644 --- a/.github/workflows/PR-build.yml +++ b/.github/workflows/PR-build.yml @@ -5,10 +5,6 @@ on: branches: - main -permissions: - pull-requests: write - contents: read - jobs: build: name: Validate new/changed QIPs @@ -61,8 +57,3 @@ jobs: else echo "No QIP files changed. Skipping validation." fi - - name: Add automerge label - if: success() - uses: actions-ecosystem/action-add-labels@v1 - with: - labels: automerge diff --git a/.github/workflows/label-validated-prs.yml b/.github/workflows/label-validated-prs.yml new file mode 100644 index 0000000..a9990cf --- /dev/null +++ b/.github/workflows/label-validated-prs.yml @@ -0,0 +1,29 @@ +name: "Label Validated PRs" +on: + workflow_run: + workflows: ["QIP File Validation"] + types: + - completed + +permissions: + pull-requests: write + +jobs: + label: + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Get PR number + id: pr + run: | + echo "Fetching PR number from workflow run" + PR_NUMBER=$(echo '${{ github.event.workflow_run.pull_requests[0].number }}') + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + + - name: Add automerge label + if: steps.pr.outputs.pr_number + uses: actions-ecosystem/action-add-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + number: ${{ steps.pr.outputs.pr_number }} + labels: automerge \ No newline at end of file From 1611e0a58be0b36ebf6cdcaa2ce5320162dfdb4d Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:29:59 -0700 Subject: [PATCH 05/52] Improve PR number fetching in label validation workflow * Enhanced the logic to retrieve the pull request number from the workflow run. * Added checks for null or empty pull request data, ensuring robust handling of cases where no pull requests are found. * Improved output messaging for better clarity during workflow execution. --- .github/workflows/label-validated-prs.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/label-validated-prs.yml b/.github/workflows/label-validated-prs.yml index a9990cf..fc6e125 100644 --- a/.github/workflows/label-validated-prs.yml +++ b/.github/workflows/label-validated-prs.yml @@ -17,8 +17,19 @@ jobs: id: pr run: | echo "Fetching PR number from workflow run" - PR_NUMBER=$(echo '${{ github.event.workflow_run.pull_requests[0].number }}') - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + PR_DATA='${{ toJSON(github.event.workflow_run.pull_requests) }}' + if [ "$PR_DATA" != "null" ] && [ "$PR_DATA" != "[]" ]; then + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') + if [ -n "$PR_NUMBER" ]; then + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + else + echo "No PR number found" + echo "pr_number=" >> $GITHUB_OUTPUT + fi + else + echo "No pull requests found" + echo "pr_number=" >> $GITHUB_OUTPUT + fi - name: Add automerge label if: steps.pr.outputs.pr_number From 985c6cfe856e0d518df59a913d8e18b47a644a2f Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:45:59 -0700 Subject: [PATCH 06/52] Refactor automerge workflow to trigger on successful completion of label validation * Changed the trigger from pull_request_target to workflow_run for better integration with the label validation workflow. * Enhanced the logic to fetch the pull request number and check for the 'automerge' label. * Improved output handling for cases where no pull requests are found. --- .github/workflows/automerge.yml | 41 +++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index f8ba46e..bdd2643 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -1,7 +1,9 @@ name: "Automerge Pull Requests" on: - pull_request_target: - types: [labeled, reopened, synchronize] + workflow_run: + workflows: ["Label Validated PRs"] + types: + - completed permissions: pull-requests: write @@ -9,12 +11,43 @@ permissions: jobs: automerge: - if: contains(github.event.pull_request.labels.*.name, 'automerge') + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} runs-on: ubuntu-latest steps: + - name: Get PR number and check for automerge label + id: pr + run: | + echo "Fetching PR number from workflow run" + PR_DATA='${{ toJSON(github.event.workflow_run.pull_requests) }}' + if [ "$PR_DATA" != "null" ] && [ "$PR_DATA" != "[]" ]; then + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') + if [ -n "$PR_NUMBER" ]; then + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + + # Check if PR has automerge label + LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name') + if echo "$LABELS" | grep -q "automerge"; then + echo "has_automerge_label=true" >> $GITHUB_OUTPUT + else + echo "has_automerge_label=false" >> $GITHUB_OUTPUT + fi + else + echo "No PR number found" + echo "pr_number=" >> $GITHUB_OUTPUT + echo "has_automerge_label=false" >> $GITHUB_OUTPUT + fi + else + echo "No pull requests found" + echo "pr_number=" >> $GITHUB_OUTPUT + echo "has_automerge_label=false" >> $GITHUB_OUTPUT + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Enable Auto-Merge + if: steps.pr.outputs.pr_number && steps.pr.outputs.has_automerge_label == 'true' uses: peter-evans/enable-pull-request-automerge@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ github.event.pull_request.number }} + pull-request-number: ${{ steps.pr.outputs.pr_number }} merge-method: squash \ No newline at end of file From 245c92ed113e4d1de32cfc72f088cefbcd1466de Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:05:40 -0700 Subject: [PATCH 07/52] Update automerge workflow condition and enhance output messaging * Modified the condition for triggering the automerge job to only require a successful workflow run. * Improved output messages to indicate whether the pull request has the 'automerge' label, enhancing clarity during execution. --- .github/workflows/automerge.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index bdd2643..ba391c4 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -11,7 +11,7 @@ permissions: jobs: automerge: - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Get PR number and check for automerge label @@ -28,8 +28,10 @@ jobs: LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name') if echo "$LABELS" | grep -q "automerge"; then echo "has_automerge_label=true" >> $GITHUB_OUTPUT + echo "✅ PR #$PR_NUMBER has automerge label" else echo "has_automerge_label=false" >> $GITHUB_OUTPUT + echo "❌ PR #$PR_NUMBER does not have automerge label" fi else echo "No PR number found" From 43ec0dc3176111cb4cee3ba85eed42c32b3552c2 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:48:58 -0700 Subject: [PATCH 08/52] Swap automerge workflow to fetch PR number by commit SHA --- .github/workflows/automerge.yml | 37 +++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index ba391c4..d09298f 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -18,28 +18,29 @@ jobs: id: pr run: | echo "Fetching PR number from workflow run" - PR_DATA='${{ toJSON(github.event.workflow_run.pull_requests) }}' - if [ "$PR_DATA" != "null" ] && [ "$PR_DATA" != "[]" ]; then - PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') - if [ -n "$PR_NUMBER" ]; then - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - - # Check if PR has automerge label - LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name') - if echo "$LABELS" | grep -q "automerge"; then - echo "has_automerge_label=true" >> $GITHUB_OUTPUT - echo "✅ PR #$PR_NUMBER has automerge label" - else - echo "has_automerge_label=false" >> $GITHUB_OUTPUT - echo "❌ PR #$PR_NUMBER does not have automerge label" - fi + + # Get the head SHA from the workflow run + HEAD_SHA="${{ github.event.workflow_run.head_sha }}" + echo "Head SHA: $HEAD_SHA" + + # Find PR associated with this commit + PR_NUMBER=$(gh pr list --state open --json number,headRefOid --jq ".[] | select(.headRefOid == \"$HEAD_SHA\") | .number") + + if [ -n "$PR_NUMBER" ]; then + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "✅ Found PR #$PR_NUMBER for commit $HEAD_SHA" + + # Check if PR has automerge label + LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name') + if echo "$LABELS" | grep -q "automerge"; then + echo "has_automerge_label=true" >> $GITHUB_OUTPUT + echo "✅ PR #$PR_NUMBER has automerge label" else - echo "No PR number found" - echo "pr_number=" >> $GITHUB_OUTPUT echo "has_automerge_label=false" >> $GITHUB_OUTPUT + echo "❌ PR #$PR_NUMBER does not have automerge label" fi else - echo "No pull requests found" + echo "❌ No open PR found for commit $HEAD_SHA" echo "pr_number=" >> $GITHUB_OUTPUT echo "has_automerge_label=false" >> $GITHUB_OUTPUT fi From 5bb53055e203099cc26c4845a0909791e14bb455 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:52:02 -0700 Subject: [PATCH 09/52] Refactor automerge workflow to trigger on labeled pull requests --- .github/workflows/automerge.yml | 44 +++-------------------- .github/workflows/label-validated-prs.yml | 11 +++++- 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index d09298f..ff0e516 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -1,9 +1,7 @@ name: "Automerge Pull Requests" on: - workflow_run: - workflows: ["Label Validated PRs"] - types: - - completed + pull_request_target: + types: [labeled] permissions: pull-requests: write @@ -11,46 +9,12 @@ permissions: jobs: automerge: - if: ${{ github.event.workflow_run.conclusion == 'success' }} + if: contains(github.event.pull_request.labels.*.name, 'automerge') runs-on: ubuntu-latest steps: - - name: Get PR number and check for automerge label - id: pr - run: | - echo "Fetching PR number from workflow run" - - # Get the head SHA from the workflow run - HEAD_SHA="${{ github.event.workflow_run.head_sha }}" - echo "Head SHA: $HEAD_SHA" - - # Find PR associated with this commit - PR_NUMBER=$(gh pr list --state open --json number,headRefOid --jq ".[] | select(.headRefOid == \"$HEAD_SHA\") | .number") - - if [ -n "$PR_NUMBER" ]; then - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - echo "✅ Found PR #$PR_NUMBER for commit $HEAD_SHA" - - # Check if PR has automerge label - LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name') - if echo "$LABELS" | grep -q "automerge"; then - echo "has_automerge_label=true" >> $GITHUB_OUTPUT - echo "✅ PR #$PR_NUMBER has automerge label" - else - echo "has_automerge_label=false" >> $GITHUB_OUTPUT - echo "❌ PR #$PR_NUMBER does not have automerge label" - fi - else - echo "❌ No open PR found for commit $HEAD_SHA" - echo "pr_number=" >> $GITHUB_OUTPUT - echo "has_automerge_label=false" >> $GITHUB_OUTPUT - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Enable Auto-Merge - if: steps.pr.outputs.pr_number && steps.pr.outputs.has_automerge_label == 'true' uses: peter-evans/enable-pull-request-automerge@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ steps.pr.outputs.pr_number }} + pull-request-number: ${{ github.event.pull_request.number }} merge-method: squash \ No newline at end of file diff --git a/.github/workflows/label-validated-prs.yml b/.github/workflows/label-validated-prs.yml index fc6e125..48c9e7d 100644 --- a/.github/workflows/label-validated-prs.yml +++ b/.github/workflows/label-validated-prs.yml @@ -7,6 +7,7 @@ on: permissions: pull-requests: write + contents: write jobs: label: @@ -37,4 +38,12 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} number: ${{ steps.pr.outputs.pr_number }} - labels: automerge \ No newline at end of file + labels: automerge + + - name: Enable Auto-Merge + if: steps.pr.outputs.pr_number + uses: peter-evans/enable-pull-request-automerge@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + pull-request-number: ${{ steps.pr.outputs.pr_number }} + merge-method: squash \ No newline at end of file From 5ff0c521c8674d81a88cb9aeed77a80ea65a85bf Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:13:45 -0700 Subject: [PATCH 10/52] Switch up automerge workflow to support check suite events and improve PR detail retrieval --- .github/workflows/automerge.yml | 86 ++++++++++++++++++++++- .github/workflows/label-validated-prs.yml | 11 +-- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index ff0e516..fbcb0b4 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -1,20 +1,100 @@ name: "Automerge Pull Requests" on: pull_request_target: - types: [labeled] + types: [labeled, synchronize] + check_suite: + types: [completed] permissions: pull-requests: write contents: write + checks: read jobs: automerge: - if: contains(github.event.pull_request.labels.*.name, 'automerge') runs-on: ubuntu-latest steps: + - name: Get PR details + id: pr + run: | + # Get PR number based on event type + if [ "${{ github.event_name }}" = "pull_request_target" ]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + echo "Event: pull_request_target" + elif [ "${{ github.event_name }}" = "check_suite" ]; then + # For check_suite events, find the PR from the head SHA + HEAD_SHA="${{ github.event.check_suite.head_sha }}" + PR_DATA=$(gh api --paginate "repos/${{ github.repository }}/pulls" \ + --jq ".[] | select(.head.sha == \"$HEAD_SHA\") | .number" | head -n1) + PR_NUMBER="$PR_DATA" + echo "Event: check_suite for SHA $HEAD_SHA" + fi + + if [ -z "$PR_NUMBER" ]; then + echo "No PR number found" + echo "should_continue=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "PR Number: $PR_NUMBER" + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + + # Get PR details + PR_INFO=$(gh pr view $PR_NUMBER --json labels,mergeable,mergeStateStatus,statusCheckRollup) + + # Check if PR has automerge label + HAS_LABEL=$(echo "$PR_INFO" | jq -r '.labels[] | select(.name == "automerge") | .name') + if [ "$HAS_LABEL" = "automerge" ]; then + echo "✅ PR has automerge label" + echo "has_automerge_label=true" >> $GITHUB_OUTPUT + else + echo "❌ PR does not have automerge label" + echo "has_automerge_label=false" >> $GITHUB_OUTPUT + echo "should_continue=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check merge state status + MERGE_STATE=$(echo "$PR_INFO" | jq -r '.mergeStateStatus') + echo "Merge state: $MERGE_STATE" + + # Check if all status checks have passed + STATUS_ROLLUP=$(echo "$PR_INFO" | jq -r '.statusCheckRollup') + if [ "$STATUS_ROLLUP" = "null" ] || [ -z "$STATUS_ROLLUP" ]; then + echo "No status checks configured" + echo "all_checks_passed=true" >> $GITHUB_OUTPUT + else + # Check the conclusion of the status check rollup + PENDING=$(echo "$PR_INFO" | jq -r '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS")] | length') + FAILED=$(echo "$PR_INFO" | jq -r '[.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .status == "ERROR")] | length') + + if [ "$PENDING" -gt 0 ]; then + echo "⏳ $PENDING checks still pending" + echo "all_checks_passed=false" >> $GITHUB_OUTPUT + echo "should_continue=false" >> $GITHUB_OUTPUT + elif [ "$FAILED" -gt 0 ]; then + echo "❌ $FAILED checks failed" + echo "all_checks_passed=false" >> $GITHUB_OUTPUT + echo "should_continue=false" >> $GITHUB_OUTPUT + else + echo "✅ All checks passed" + echo "all_checks_passed=true" >> $GITHUB_OUTPUT + fi + fi + + # Determine if we should continue + if [ "$MERGE_STATE" = "CLEAN" ] || [ "$MERGE_STATE" = "UNSTABLE" ]; then + echo "should_continue=true" >> $GITHUB_OUTPUT + else + echo "should_continue=false" >> $GITHUB_OUTPUT + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Enable Auto-Merge + if: steps.pr.outputs.should_continue == 'true' && steps.pr.outputs.has_automerge_label == 'true' && steps.pr.outputs.all_checks_passed == 'true' uses: peter-evans/enable-pull-request-automerge@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ github.event.pull_request.number }} + pull-request-number: ${{ steps.pr.outputs.pr_number }} merge-method: squash \ No newline at end of file diff --git a/.github/workflows/label-validated-prs.yml b/.github/workflows/label-validated-prs.yml index 48c9e7d..fc6e125 100644 --- a/.github/workflows/label-validated-prs.yml +++ b/.github/workflows/label-validated-prs.yml @@ -7,7 +7,6 @@ on: permissions: pull-requests: write - contents: write jobs: label: @@ -38,12 +37,4 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} number: ${{ steps.pr.outputs.pr_number }} - labels: automerge - - - name: Enable Auto-Merge - if: steps.pr.outputs.pr_number - uses: peter-evans/enable-pull-request-automerge@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ steps.pr.outputs.pr_number }} - merge-method: squash \ No newline at end of file + labels: automerge \ No newline at end of file From 74111a9a0ac03ff0ba23789f55969ed618faac5d Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:57:29 -0700 Subject: [PATCH 11/52] Enhance automerge workflow to remove 'automerge' label on new commits and notify users * Added a new job to remove the 'automerge' label when a new commit is detected on a pull request. * Implemented a comment feature to inform users that the label has been removed and will be re-added if validation passes. * Simplified the condition for enabling auto-merge by directly using the pull request number from the event context. --- .github/workflows/automerge.yml | 108 ++++++++------------------------ 1 file changed, 25 insertions(+), 83 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index fbcb0b4..6800f94 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -2,99 +2,41 @@ name: "Automerge Pull Requests" on: pull_request_target: types: [labeled, synchronize] - check_suite: - types: [completed] permissions: pull-requests: write contents: write - checks: read jobs: automerge: + if: github.event.action == 'labeled' && contains(github.event.pull_request.labels.*.name, 'automerge') runs-on: ubuntu-latest steps: - - name: Get PR details - id: pr - run: | - # Get PR number based on event type - if [ "${{ github.event_name }}" = "pull_request_target" ]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - echo "Event: pull_request_target" - elif [ "${{ github.event_name }}" = "check_suite" ]; then - # For check_suite events, find the PR from the head SHA - HEAD_SHA="${{ github.event.check_suite.head_sha }}" - PR_DATA=$(gh api --paginate "repos/${{ github.repository }}/pulls" \ - --jq ".[] | select(.head.sha == \"$HEAD_SHA\") | .number" | head -n1) - PR_NUMBER="$PR_DATA" - echo "Event: check_suite for SHA $HEAD_SHA" - fi - - if [ -z "$PR_NUMBER" ]; then - echo "No PR number found" - echo "should_continue=false" >> $GITHUB_OUTPUT - exit 0 - fi - - echo "PR Number: $PR_NUMBER" - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - - # Get PR details - PR_INFO=$(gh pr view $PR_NUMBER --json labels,mergeable,mergeStateStatus,statusCheckRollup) - - # Check if PR has automerge label - HAS_LABEL=$(echo "$PR_INFO" | jq -r '.labels[] | select(.name == "automerge") | .name') - if [ "$HAS_LABEL" = "automerge" ]; then - echo "✅ PR has automerge label" - echo "has_automerge_label=true" >> $GITHUB_OUTPUT - else - echo "❌ PR does not have automerge label" - echo "has_automerge_label=false" >> $GITHUB_OUTPUT - echo "should_continue=false" >> $GITHUB_OUTPUT - exit 0 - fi - - # Check merge state status - MERGE_STATE=$(echo "$PR_INFO" | jq -r '.mergeStateStatus') - echo "Merge state: $MERGE_STATE" - - # Check if all status checks have passed - STATUS_ROLLUP=$(echo "$PR_INFO" | jq -r '.statusCheckRollup') - if [ "$STATUS_ROLLUP" = "null" ] || [ -z "$STATUS_ROLLUP" ]; then - echo "No status checks configured" - echo "all_checks_passed=true" >> $GITHUB_OUTPUT - else - # Check the conclusion of the status check rollup - PENDING=$(echo "$PR_INFO" | jq -r '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS")] | length') - FAILED=$(echo "$PR_INFO" | jq -r '[.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .status == "ERROR")] | length') - - if [ "$PENDING" -gt 0 ]; then - echo "⏳ $PENDING checks still pending" - echo "all_checks_passed=false" >> $GITHUB_OUTPUT - echo "should_continue=false" >> $GITHUB_OUTPUT - elif [ "$FAILED" -gt 0 ]; then - echo "❌ $FAILED checks failed" - echo "all_checks_passed=false" >> $GITHUB_OUTPUT - echo "should_continue=false" >> $GITHUB_OUTPUT - else - echo "✅ All checks passed" - echo "all_checks_passed=true" >> $GITHUB_OUTPUT - fi - fi - - # Determine if we should continue - if [ "$MERGE_STATE" = "CLEAN" ] || [ "$MERGE_STATE" = "UNSTABLE" ]; then - echo "should_continue=true" >> $GITHUB_OUTPUT - else - echo "should_continue=false" >> $GITHUB_OUTPUT - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Enable Auto-Merge - if: steps.pr.outputs.should_continue == 'true' && steps.pr.outputs.has_automerge_label == 'true' && steps.pr.outputs.all_checks_passed == 'true' uses: peter-evans/enable-pull-request-automerge@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ steps.pr.outputs.pr_number }} - merge-method: squash \ No newline at end of file + pull-request-number: ${{ github.event.pull_request.number }} + merge-method: squash + + remove-label-on-new-commit: + if: github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'automerge') + runs-on: ubuntu-latest + steps: + - name: Remove automerge label + uses: actions-ecosystem/action-remove-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + number: ${{ github.event.pull_request.number }} + labels: automerge + + - name: Comment on PR + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '🔄 New commit detected. The automerge label has been removed. It will be re-added if validation passes.' + }) \ No newline at end of file From 4009beb26885e7984400b18133123ca7ad9b586f Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:05:28 -0700 Subject: [PATCH 12/52] update automerge workflow --- .github/workflows/automerge.yml | 4 ++++ .github/workflows/label-validated-prs.yml | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 6800f94..4c69db2 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -1,4 +1,8 @@ name: "Automerge Pull Requests" +# Note: This workflow only triggers when labels are added manually. +# When labels are added by GitHub Actions (using GITHUB_TOKEN), +# this workflow won't trigger due to GitHub's security features. +# Programmatic automerge is handled in label-validated-prs.yml on: pull_request_target: types: [labeled, synchronize] diff --git a/.github/workflows/label-validated-prs.yml b/.github/workflows/label-validated-prs.yml index fc6e125..bc66cea 100644 --- a/.github/workflows/label-validated-prs.yml +++ b/.github/workflows/label-validated-prs.yml @@ -7,6 +7,7 @@ on: permissions: pull-requests: write + contents: write jobs: label: @@ -37,4 +38,16 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} number: ${{ steps.pr.outputs.pr_number }} - labels: automerge \ No newline at end of file + labels: automerge + + - name: Wait for label to be registered + if: steps.pr.outputs.pr_number + run: sleep 5 + + - name: Enable Auto-Merge + if: steps.pr.outputs.pr_number + uses: peter-evans/enable-pull-request-automerge@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + pull-request-number: ${{ steps.pr.outputs.pr_number }} + merge-method: squash \ No newline at end of file From a37853c66ba57b07010f5d7b5e0ffccd20548cba Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:13:59 -0700 Subject: [PATCH 13/52] rework automerge to wait for checks --- .github/workflows/automerge-after-checks.yml | 69 ++++++++++++++++++++ .github/workflows/label-validated-prs.yml | 16 +---- 2 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/automerge-after-checks.yml diff --git a/.github/workflows/automerge-after-checks.yml b/.github/workflows/automerge-after-checks.yml new file mode 100644 index 0000000..aab33af --- /dev/null +++ b/.github/workflows/automerge-after-checks.yml @@ -0,0 +1,69 @@ +name: "Automerge After All Checks" +on: + check_suite: + types: [completed] + +permissions: + pull-requests: write + contents: write + checks: read + +jobs: + automerge: + if: github.event.check_suite.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - name: Get PR details + id: pr_details + run: | + # Get associated pull requests + if [ -z "${{ github.event.check_suite.pull_requests[0].number }}" ]; then + echo "No PR associated with this check suite" + echo "should_proceed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + PR_NUMBER="${{ github.event.check_suite.pull_requests[0].number }}" + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + + # Get PR details including labels and status + PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER) + + # Check if PR has automerge label + HAS_AUTOMERGE=$(echo "$PR_DATA" | jq -r '.labels[] | select(.name == "automerge") | .name') + + if [ "$HAS_AUTOMERGE" = "automerge" ]; then + echo "✅ PR #$PR_NUMBER has automerge label" + + # Check if ALL required checks have passed + CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.check_suite.head_sha }}/check-runs --jq '.check_runs') + + # Count total checks and successful checks + TOTAL_CHECKS=$(echo "$CHECK_RUNS" | jq 'length') + SUCCESSFUL_CHECKS=$(echo "$CHECK_RUNS" | jq '[.[] | select(.conclusion == "success")] | length') + PENDING_CHECKS=$(echo "$CHECK_RUNS" | jq '[.[] | select(.status != "completed")] | length') + + echo "Check status: $SUCCESSFUL_CHECKS/$TOTAL_CHECKS successful, $PENDING_CHECKS pending" + + if [ "$PENDING_CHECKS" -eq 0 ] && [ "$SUCCESSFUL_CHECKS" -eq "$TOTAL_CHECKS" ]; then + echo "✅ All checks have passed!" + echo "should_proceed=true" >> $GITHUB_OUTPUT + else + echo "⏳ Not all checks have passed yet" + echo "should_proceed=false" >> $GITHUB_OUTPUT + fi + else + echo "❌ PR #$PR_NUMBER does not have automerge label" + echo "should_proceed=false" >> $GITHUB_OUTPUT + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge PR + if: steps.pr_details.outputs.should_proceed == 'true' + run: | + PR_NUMBER="${{ steps.pr_details.outputs.pr_number }}" + echo "🔀 Merging PR #$PR_NUMBER" + gh pr merge $PR_NUMBER --squash --delete-branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/label-validated-prs.yml b/.github/workflows/label-validated-prs.yml index bc66cea..dfc1826 100644 --- a/.github/workflows/label-validated-prs.yml +++ b/.github/workflows/label-validated-prs.yml @@ -7,7 +7,6 @@ on: permissions: pull-requests: write - contents: write jobs: label: @@ -23,6 +22,7 @@ jobs: PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') if [ -n "$PR_NUMBER" ]; then echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "✅ Found PR #$PR_NUMBER" else echo "No PR number found" echo "pr_number=" >> $GITHUB_OUTPUT @@ -38,16 +38,4 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} number: ${{ steps.pr.outputs.pr_number }} - labels: automerge - - - name: Wait for label to be registered - if: steps.pr.outputs.pr_number - run: sleep 5 - - - name: Enable Auto-Merge - if: steps.pr.outputs.pr_number - uses: peter-evans/enable-pull-request-automerge@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ steps.pr.outputs.pr_number }} - merge-method: squash \ No newline at end of file + labels: automerge \ No newline at end of file From 7906b59e6e2731ce68d7f7a631558bc888a16373 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:22:10 -0700 Subject: [PATCH 14/52] Refactor automerge workflow to trigger on successful workflow runs and enhance check validation * Updated the workflow trigger from check_suite to workflow_run for better integration with validation workflows. * Improved logic to retrieve pull request details and check for the 'automerge' label. * Enhanced output messaging to provide clearer status updates on check runs and PR merging process. --- .github/workflows/automerge-after-checks.yml | 109 +++++++++++++------ 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/.github/workflows/automerge-after-checks.yml b/.github/workflows/automerge-after-checks.yml index aab33af..526b09a 100644 --- a/.github/workflows/automerge-after-checks.yml +++ b/.github/workflows/automerge-after-checks.yml @@ -1,7 +1,11 @@ name: "Automerge After All Checks" on: - check_suite: - types: [completed] + workflow_run: + workflows: + - "QIP File Validation" + - "QIP Author Validation" + types: + - completed permissions: pull-requests: write @@ -10,50 +14,87 @@ permissions: jobs: automerge: - if: github.event.check_suite.conclusion == 'success' + if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: - name: Get PR details id: pr_details run: | - # Get associated pull requests - if [ -z "${{ github.event.check_suite.pull_requests[0].number }}" ]; then - echo "No PR associated with this check suite" + # Get associated pull requests from workflow run + PR_DATA='${{ toJSON(github.event.workflow_run.pull_requests) }}' + if [ "$PR_DATA" = "null" ] || [ "$PR_DATA" = "[]" ]; then + echo "No PR associated with this workflow run" + echo "should_proceed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') + if [ -z "$PR_NUMBER" ]; then + echo "No PR number found" echo "should_proceed=false" >> $GITHUB_OUTPUT exit 0 fi - PR_NUMBER="${{ github.event.check_suite.pull_requests[0].number }}" echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "Found PR #$PR_NUMBER from workflow: ${{ github.event.workflow.name }}" - # Get PR details including labels and status - PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER) + # Get PR details including labels + PR_INFO=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER) # Check if PR has automerge label - HAS_AUTOMERGE=$(echo "$PR_DATA" | jq -r '.labels[] | select(.name == "automerge") | .name') - - if [ "$HAS_AUTOMERGE" = "automerge" ]; then - echo "✅ PR #$PR_NUMBER has automerge label" - - # Check if ALL required checks have passed - CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.check_suite.head_sha }}/check-runs --jq '.check_runs') - - # Count total checks and successful checks - TOTAL_CHECKS=$(echo "$CHECK_RUNS" | jq 'length') - SUCCESSFUL_CHECKS=$(echo "$CHECK_RUNS" | jq '[.[] | select(.conclusion == "success")] | length') - PENDING_CHECKS=$(echo "$CHECK_RUNS" | jq '[.[] | select(.status != "completed")] | length') - - echo "Check status: $SUCCESSFUL_CHECKS/$TOTAL_CHECKS successful, $PENDING_CHECKS pending" - - if [ "$PENDING_CHECKS" -eq 0 ] && [ "$SUCCESSFUL_CHECKS" -eq "$TOTAL_CHECKS" ]; then - echo "✅ All checks have passed!" - echo "should_proceed=true" >> $GITHUB_OUTPUT + HAS_AUTOMERGE=$(echo "$PR_INFO" | jq -r '.labels[] | select(.name == "automerge") | .name') + + if [ "$HAS_AUTOMERGE" != "automerge" ]; then + echo "❌ PR #$PR_NUMBER does not have automerge label" + echo "should_proceed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "✅ PR #$PR_NUMBER has automerge label" + + # Get the commit SHA for this PR + HEAD_SHA=$(echo "$PR_INFO" | jq -r '.head.sha') + + # Check ALL workflow runs for this commit to ensure they're all complete and successful + echo "Checking all workflow runs for commit $HEAD_SHA" + + # Get all check runs for this commit + CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/$HEAD_SHA/check-runs --paginate --jq '.check_runs[]') + + # Filter out this current workflow run to avoid circular dependency + RELEVANT_CHECKS=$(echo "$CHECK_RUNS" | jq -s '[.[] | select(.name != "automerge" and .app.slug == "github-actions")]') + + TOTAL_CHECKS=$(echo "$RELEVANT_CHECKS" | jq 'length') + SUCCESSFUL_CHECKS=$(echo "$RELEVANT_CHECKS" | jq '[.[] | select(.conclusion == "success")] | length') + PENDING_CHECKS=$(echo "$RELEVANT_CHECKS" | jq '[.[] | select(.status != "completed")] | length') + FAILED_CHECKS=$(echo "$RELEVANT_CHECKS" | jq '[.[] | select(.conclusion != "success" and .conclusion != null)] | length') + + echo "Check status: $SUCCESSFUL_CHECKS/$TOTAL_CHECKS successful, $PENDING_CHECKS pending, $FAILED_CHECKS failed" + + # List pending checks if any + if [ "$PENDING_CHECKS" -gt 0 ]; then + echo "Pending checks:" + echo "$RELEVANT_CHECKS" | jq -r '.[] | select(.status != "completed") | " - \(.name)"' + fi + + # We need ALL checks to be complete and successful + REQUIRED_WORKFLOWS=("Validate new/changed QIPs" "Validate QIP Author") + FOUND_WORKFLOWS=0 + + for workflow in "${REQUIRED_WORKFLOWS[@]}"; do + if echo "$RELEVANT_CHECKS" | jq -e --arg wf "$workflow" '.[] | select(.name == $wf and .conclusion == "success")' > /dev/null; then + echo "✅ Found successful run for: $workflow" + ((FOUND_WORKFLOWS++)) else - echo "⏳ Not all checks have passed yet" - echo "should_proceed=false" >> $GITHUB_OUTPUT + echo "❌ Missing or unsuccessful: $workflow" fi + done + + if [ "$FOUND_WORKFLOWS" -eq "${#REQUIRED_WORKFLOWS[@]}" ] && [ "$PENDING_CHECKS" -eq 0 ] && [ "$FAILED_CHECKS" -eq 0 ]; then + echo "✅ All required checks have passed!" + echo "should_proceed=true" >> $GITHUB_OUTPUT else - echo "❌ PR #$PR_NUMBER does not have automerge label" + echo "⏳ Not all required checks have passed yet" echo "should_proceed=false" >> $GITHUB_OUTPUT fi env: @@ -63,7 +104,11 @@ jobs: if: steps.pr_details.outputs.should_proceed == 'true' run: | PR_NUMBER="${{ steps.pr_details.outputs.pr_number }}" - echo "🔀 Merging PR #$PR_NUMBER" - gh pr merge $PR_NUMBER --squash --delete-branch + echo "🔀 Attempting to merge PR #$PR_NUMBER" + + # Use auto flag to respect branch protection rules + gh pr merge $PR_NUMBER --squash --delete-branch --auto + + echo "✅ Auto-merge enabled for PR #$PR_NUMBER" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 21d107c8d00c7276405877a4e94b8a782d9725d1 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:36:14 -0700 Subject: [PATCH 15/52] Refactor automerge and validation workflows for improved handling of manual label additions * Updated the automerge workflow to handle manual label additions and new commits more effectively. * Removed the outdated PR build workflow and integrated its validation logic into a new pull-request-complete workflow. * Enhanced the validation process for QIP files, ensuring only valid changes are allowed and providing clearer feedback on validation outcomes. --- .github/workflows/PR-build.yml | 59 ---------- .github/workflows/automerge.yml | 44 +++++--- .github/workflows/pull-request-complete.yml | 119 ++++++++++++++++++++ 3 files changed, 149 insertions(+), 73 deletions(-) delete mode 100644 .github/workflows/PR-build.yml create mode 100644 .github/workflows/pull-request-complete.yml diff --git a/.github/workflows/PR-build.yml b/.github/workflows/PR-build.yml deleted file mode 100644 index 0b35c0e..0000000 --- a/.github/workflows/PR-build.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: QIP File Validation - -on: - pull_request: - branches: - - main - -jobs: - build: - name: Validate new/changed QIPs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 4 - - uses: oven-sh/setup-bun@v2 - - name: Get changed QIP files - id: changed_qips - run: | - BASE_SHA=${{ github.event.pull_request.base.sha }} - HEAD_SHA=${{ github.sha }} - echo "Comparing $BASE_SHA → $HEAD_SHA for contents/QIP/" - # get the list - CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA -- contents/QIP/) - - # write it to disk (in case you still want to inspect it) - echo "$CHANGED" > changed_qips.txt - echo "Wrote $(wc -l < changed_qips.txt) entries to changed_qips.txt" - - # now export it as a step output for the `if:` condition - echo "files<> $GITHUB_OUTPUT - echo "$CHANGED" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - uses: oven-sh/setup-bun@v2 - - name: Check only QIP .md files changed - run: | - BASE_SHA=${{ github.event.pull_request.base.sha }} - HEAD_SHA=${{ github.sha }} - echo "Checking for non-QIP or non-.md file changes between $BASE_SHA and $HEAD_SHA" - CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA) - echo "Changed files:" >&2 - echo "$CHANGED_FILES" >&2 - # Filter out allowed .md files in contents/QIP, ignore empty lines - NON_QIP=$(echo "$CHANGED_FILES" | grep -vE '^contents/QIP/[^/]+\.md$' | grep -vE '^$' || true) - if [ -n "$NON_QIP" ]; then - echo "Error: The following files are not allowed in this PR:" >&2 - echo "$NON_QIP" >&2 - exit 1 - else - echo "All changed files are .md files in contents/QIP." - fi - - name: Validate new/changed QIPs - if: ${{ steps.changed_qips.outputs.files != '' }} - run: | - if [ -s changed_qips.txt ]; then - bun scripts/validate-qip.ts $(cat changed_qips.txt) - else - echo "No QIP files changed. Skipping validation." - fi diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 4c69db2..834fa2f 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -1,10 +1,8 @@ -name: "Automerge Pull Requests" -# Note: This workflow only triggers when labels are added manually. -# When labels are added by GitHub Actions (using GITHUB_TOKEN), -# this workflow won't trigger due to GitHub's security features. -# Programmatic automerge is handled in label-validated-prs.yml +name: "Handle Manual Automerge" +# This workflow handles manual automerge label additions and new commits. +# The automatic flow (validation -> label -> automerge) is handled by pull-request-complete.yml on: - pull_request_target: + pull_request: types: [labeled, synchronize] permissions: @@ -12,16 +10,34 @@ permissions: contents: write jobs: - automerge: - if: github.event.action == 'labeled' && contains(github.event.pull_request.labels.*.name, 'automerge') + handle-manual-automerge: + if: github.event.action == 'labeled' && contains(github.event.label.name, 'automerge') runs-on: ubuntu-latest steps: - - name: Enable Auto-Merge - uses: peter-evans/enable-pull-request-automerge@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ github.event.pull_request.number }} - merge-method: squash + - name: Check if this is manual addition + id: check_manual + run: | + # Check if the label was added by a human (not a bot/action) + ACTOR="${{ github.actor }}" + if [[ "$ACTOR" != *"[bot]"* ]] && [[ "$ACTOR" != "github-actions" ]]; then + echo "is_manual=true" >> $GITHUB_OUTPUT + echo "✅ Manual label addition by $ACTOR" + else + echo "is_manual=false" >> $GITHUB_OUTPUT + echo "❌ Automated label addition, skipping" + fi + + - name: Enable auto-merge for manual label + if: steps.check_manual.outputs.is_manual == 'true' + run: | + echo "🔄 Enabling auto-merge for manually labeled PR #${{ github.event.pull_request.number }}" + gh pr merge ${{ github.event.pull_request.number }} \ + --squash \ + --auto \ + --delete-branch \ + --repo ${{ github.repository }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} remove-label-on-new-commit: if: github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'automerge') diff --git a/.github/workflows/pull-request-complete.yml b/.github/workflows/pull-request-complete.yml new file mode 100644 index 0000000..aa9b637 --- /dev/null +++ b/.github/workflows/pull-request-complete.yml @@ -0,0 +1,119 @@ +name: "Pull Request Complete Flow" +on: + pull_request: + branches: + - main + +permissions: + pull-requests: write + contents: write + +jobs: + validate-qips: + name: Validate new/changed QIPs + runs-on: ubuntu-latest + outputs: + validation_passed: ${{ steps.validate.outcome == 'success' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 4 + - uses: oven-sh/setup-bun@v2 + - name: Get changed QIP files + id: changed_qips + run: | + BASE_SHA=${{ github.event.pull_request.base.sha }} + HEAD_SHA=${{ github.sha }} + echo "Comparing $BASE_SHA → $HEAD_SHA for contents/QIP/" + CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA -- contents/QIP/) + echo "$CHANGED" > changed_qips.txt + echo "Wrote $(wc -l < changed_qips.txt) entries to changed_qips.txt" + echo "files<> $GITHUB_OUTPUT + echo "$CHANGED" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Check only QIP .md files changed + run: | + BASE_SHA=${{ github.event.pull_request.base.sha }} + HEAD_SHA=${{ github.sha }} + echo "Checking for non-QIP or non-.md file changes between $BASE_SHA and $HEAD_SHA" + CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA) + echo "Changed files:" >&2 + echo "$CHANGED_FILES" >&2 + NON_QIP=$(echo "$CHANGED_FILES" | grep -vE '^contents/QIP/[^/]+\.md$' | grep -vE '^$' || true) + if [ -n "$NON_QIP" ]; then + echo "Error: The following files are not allowed in this PR:" >&2 + echo "$NON_QIP" >&2 + exit 1 + else + echo "All changed files are .md files in contents/QIP." + fi + + - name: Validate new/changed QIPs + id: validate + if: ${{ steps.changed_qips.outputs.files != '' }} + run: | + if [ -s changed_qips.txt ]; then + bun scripts/validate-qip.ts $(cat changed_qips.txt) + else + echo "No QIP files changed. Skipping validation." + fi + + validate-author: + name: Validate QIP Author + runs-on: ubuntu-latest + outputs: + validation_passed: ${{ steps.validate_author.outcome == 'success' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + - name: Validate author can modify QIP + id: validate_author + run: | + BASE_SHA=${{ github.event.pull_request.base.sha }} + HEAD_SHA=${{ github.sha }} + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + + echo "PR Author: $PR_AUTHOR" + echo "Checking modified QIP files..." + + MODIFIED_QIPS=$(git diff --name-only $BASE_SHA $HEAD_SHA -- contents/QIP/ | xargs -I {} sh -c 'git show $BASE_SHA:{} >/dev/null 2>&1 && echo {}' || true) + + if [ -z "$MODIFIED_QIPS" ]; then + echo "No existing QIP files were modified. Validation passed." + exit 0 + fi + + bun scripts/validate-author.ts "$PR_AUTHOR" $MODIFIED_QIPS + + add-automerge-label: + name: Add automerge label + needs: [validate-qips, validate-author] + if: needs.validate-qips.outputs.validation_passed == 'true' && needs.validate-author.outputs.validation_passed == 'true' + runs-on: ubuntu-latest + steps: + - name: Add automerge label + uses: actions-ecosystem/action-add-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + number: ${{ github.event.pull_request.number }} + labels: automerge + + enable-automerge: + name: Enable auto-merge + needs: [validate-qips, validate-author, add-automerge-label] + if: needs.validate-qips.outputs.validation_passed == 'true' && needs.validate-author.outputs.validation_passed == 'true' + runs-on: ubuntu-latest + steps: + - name: Enable auto-merge for PR + run: | + echo "✅ All validations passed, enabling auto-merge for PR #${{ github.event.pull_request.number }}" + gh pr merge ${{ github.event.pull_request.number }} \ + --squash \ + --auto \ + --delete-branch \ + --repo ${{ github.repository }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From a3ac79e15c1974192e6d63ae041edd04f424c84d Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:27:20 -0700 Subject: [PATCH 16/52] Fix Snapshot submission formatting --- src/components/SnapshotSubmitter.tsx | 24 +++++++++---------- .../QIP-{MarkdownRemark.frontmatter__qip}.tsx | 5 ++-- ...late-{MarkdownRemark.frontmatter__qip}.tsx | 5 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/SnapshotSubmitter.tsx b/src/components/SnapshotSubmitter.tsx index 89b3148..1173d57 100644 --- a/src/components/SnapshotSubmitter.tsx +++ b/src/components/SnapshotSubmitter.tsx @@ -8,24 +8,25 @@ import { useQuery } from "@tanstack/react-query"; interface SnapshotSubmitterProps { frontmatter: any; html: string; + rawMarkdown: string; } -const SnapshotSubmitter: React.FC = ({ frontmatter, html }) => { +const SnapshotSubmitter: React.FC = ({ frontmatter, html, rawMarkdown }) => { const signer = useEthersSigner(); const [loading, setLoading] = useState(false); const [status, setStatus] = useState(null); const [highestQip, setHighestQip] = useState(null); - const stripHtml = (html: string) => { - if (typeof window === "undefined") return ""; - const tmp = document.createElement("DIV"); - tmp.innerHTML = html; - return tmp.textContent || tmp.innerText || ""; + const SNAPSHOT_SPACE = "qidao.eth"; + + const cleanMarkdown = (rawMarkdown: string) => { + // Remove frontmatter from the beginning of the markdown + return rawMarkdown.replace(/^---[\s\S]*?---\n?/, "").trim(); }; const { data: proposals, isLoading: loadingProposals } = useQuery({ - queryKey: ["proposals", "qidao.eth"], - queryFn: () => getProposals("qidao.eth"), + queryKey: ["proposals", SNAPSHOT_SPACE], + queryFn: () => getProposals(SNAPSHOT_SPACE), }); useEffect(() => { @@ -60,8 +61,7 @@ const SnapshotSubmitter: React.FC = ({ frontmatter, html }); const isQipValid = highestQip !== null && frontmatter.qip === highestQip + 1; - - const space = "qidao.eth"; + const space = SNAPSHOT_SPACE; const handleSubmit = async () => { if (!signer) { @@ -84,7 +84,7 @@ const SnapshotSubmitter: React.FC = ({ frontmatter, html space, type: "basic", title: `QIP${frontmatter.qip}: ${frontmatter.title}`, - body: `QIP #${frontmatter.qip}: ${frontmatter.title}\n\n${stripHtml(html)}`, + body: cleanMarkdown(rawMarkdown), choices: ["For", "Against", "Abstain"], start: now + startOffset, end: now + endOffset, @@ -98,7 +98,7 @@ const SnapshotSubmitter: React.FC = ({ frontmatter, html const receipt = await createProposal(signer, "https://hub.snapshot.org", proposalOptions); if (receipt && (receipt as any).id) { const proposalId = (receipt as any).id; - const proposalUrl = `https://snapshot.org/#/qidao.eth/proposal/${proposalId}`; + const proposalUrl = `https://snapshot.org/#/${space}/proposal/${proposalId}`; setStatus( Proposal created successfully! diff --git a/src/pages/qips/QIP-{MarkdownRemark.frontmatter__qip}.tsx b/src/pages/qips/QIP-{MarkdownRemark.frontmatter__qip}.tsx index f073243..fe91e98 100644 --- a/src/pages/qips/QIP-{MarkdownRemark.frontmatter__qip}.tsx +++ b/src/pages/qips/QIP-{MarkdownRemark.frontmatter__qip}.tsx @@ -12,7 +12,7 @@ interface Props { const Template: React.FC = ({ data }) => { const { markdownRemark } = data; - const { frontmatter, html, fileAbsolutePath } = markdownRemark; + const { frontmatter, html, rawMarkdownBody, fileAbsolutePath } = markdownRemark; const githubLink = getGithubLink(fileAbsolutePath); const [isClient, setIsClient] = useState(false); @@ -42,7 +42,7 @@ const Template: React.FC = ({ data }) => { Submit to Snapshot {isClient ? ( - + ) : (
Loading interactive module...
)} @@ -75,6 +75,7 @@ export const pageQuery = graphql` status } html + rawMarkdownBody } } `; diff --git a/src/pages/templates/Template-{MarkdownRemark.frontmatter__qip}.tsx b/src/pages/templates/Template-{MarkdownRemark.frontmatter__qip}.tsx index 6236ca7..639dbaa 100644 --- a/src/pages/templates/Template-{MarkdownRemark.frontmatter__qip}.tsx +++ b/src/pages/templates/Template-{MarkdownRemark.frontmatter__qip}.tsx @@ -12,7 +12,7 @@ interface Props { const Template: React.FC = ({ data }) => { const { markdownRemark } = data; - const { frontmatter, html } = markdownRemark; + const { frontmatter, html, rawMarkdownBody } = markdownRemark; const [isClient, setIsClient] = useState(false); useEffect(() => { @@ -31,7 +31,7 @@ const Template: React.FC = ({ data }) => {
{isClient ? ( - + ) : (
Loading interactive module...
)} @@ -71,6 +71,7 @@ export const pageQuery = graphql` status } html + rawMarkdownBody } } `; From f02d04ddfd4952d83c930b41d6f0e67ede308318 Mon Sep 17 00:00:00 2001 From: Royalaid <2439803+royalaid@users.noreply.github.com> Date: Wed, 16 Jul 2025 06:51:35 -0700 Subject: [PATCH 17/52] wip Implementation for decentralized QIP workflow on Base --- .claude/settings.local.json | 31 + .env.example | 23 + .github/workflows/author-validation.yml | 36 -- .github/workflows/gatsby.yml | 13 +- .gitignore | 14 + .gitmodules | 3 + README.md | 101 ++- bun.lock | 5 +- contracts/QIPGovernance.sol | 267 ++++++++ contracts/QIPRegistry.sol | 401 ++++++++++++ dependencies/forge-std | 1 + docs/LOCAL_DEVELOPMENT.md | 112 ++++ docs/LOCAL_IPFS_SETUP.md | 175 +++++ docs/NEW_QIP_WORKFLOW.md | 121 ++++ foundry.toml | 18 + gatsby-browser.tsx | 97 ++- gatsby-config.ts | 12 + gatsby-node.ts | 184 ++++++ package.json | 9 +- script/Deploy.s.sol | 43 ++ script/DeployDeterministic.s.sol | 75 +++ script/DeployLocal.s.sol | 27 + script/DeployOnly.s.sol | 18 + script/DeploySimple.s.sol | 32 + script/DeployWithStandardCreate2.s.sol | 89 +++ script/LocalQIPTest.s.sol | 225 +++++++ scripts/check-ipfs.sh | 73 +++ scripts/migrate-qips.ts | 268 ++++++++ scripts/start-anvil-automining.sh | 24 + scripts/start-local-dev.sh | 261 ++++++++ scripts/startAnvil.sh | 158 +++++ soldeer.toml | 5 + src/components/LocalModeBanner.tsx | 19 + src/components/Navigation.tsx | 5 + src/components/ProposalEditor.tsx | 384 +++++++++++ src/components/ProposalListItem.tsx | 51 +- src/hooks/useQIPData.ts | 275 ++++++++ src/layout/index.tsx | 2 + src/localQIPTest.ts | 220 +++++++ src/pages/all-proposals.tsx | 378 ++++++++--- src/pages/create-proposal.tsx | 46 ++ src/pages/qips/QIP-blockchain.tsx | 177 +++++ .../QIP-{MarkdownRemark.frontmatter__qip}.tsx | 3 - src/pages/qips/[qipNumber].tsx | 235 +++++++ ...late-{MarkdownRemark.frontmatter__qip}.tsx | 3 - src/services/ipfsService.ts | 609 ++++++++++++++++++ src/services/qipClient.ts | 523 +++++++++++++++ src/testHelpers.ts | 319 +++++++++ src/utils/qip-data-source.ts | 173 +++++ test/QIPRegistry.t.sol | 237 +++++++ 50 files changed, 6408 insertions(+), 172 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .env.example delete mode 100644 .github/workflows/author-validation.yml create mode 100644 .gitmodules create mode 100644 contracts/QIPGovernance.sol create mode 100644 contracts/QIPRegistry.sol create mode 160000 dependencies/forge-std create mode 100644 docs/LOCAL_DEVELOPMENT.md create mode 100644 docs/LOCAL_IPFS_SETUP.md create mode 100644 docs/NEW_QIP_WORKFLOW.md create mode 100644 foundry.toml create mode 100644 gatsby-node.ts create mode 100644 script/Deploy.s.sol create mode 100644 script/DeployDeterministic.s.sol create mode 100644 script/DeployLocal.s.sol create mode 100644 script/DeployOnly.s.sol create mode 100644 script/DeploySimple.s.sol create mode 100644 script/DeployWithStandardCreate2.s.sol create mode 100644 script/LocalQIPTest.s.sol create mode 100755 scripts/check-ipfs.sh create mode 100644 scripts/migrate-qips.ts create mode 100755 scripts/start-anvil-automining.sh create mode 100755 scripts/start-local-dev.sh create mode 100755 scripts/startAnvil.sh create mode 100644 soldeer.toml create mode 100644 src/components/LocalModeBanner.tsx create mode 100644 src/components/ProposalEditor.tsx create mode 100644 src/hooks/useQIPData.ts create mode 100644 src/localQIPTest.ts create mode 100644 src/pages/create-proposal.tsx create mode 100644 src/pages/qips/QIP-blockchain.tsx create mode 100644 src/pages/qips/[qipNumber].tsx create mode 100644 src/services/ipfsService.ts create mode 100644 src/services/qipClient.ts create mode 100644 src/testHelpers.ts create mode 100644 src/utils/qip-data-source.ts create mode 100644 test/QIPRegistry.t.sol diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..cc787ce --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,31 @@ +{ + "permissions": { + "allow": [ + "Bash(forge install:*)", + "Bash(./scripts/startAnvil.sh:*)", + "Bash(cp:*)", + "Bash(source .env)", + "Bash(forge script:*)", + "Bash(cast call:*)", + "Bash(cast code:*)", + "Bash(pkill:*)", + "Bash(find:*)", + "Bash(bun run:*)", + "Bash(chmod:*)", + "Bash(cat:*)", + "Bash(grep:*)", + "Bash(bun add:*)", + "WebFetch(domain:docs.pinata.cloud)", + "Bash(ipfs config:*)", + "Bash(curl:*)", + "Bash(forge clean:*)", + "Bash(forge build:*)", + "Bash(cast sig:*)", + "Bash(./scripts/start-local-dev.sh:*)", + "WebFetch(domain:github.com)", + "WebFetch(domain:snapshot.mirror.xyz)", + "Bash(lsof:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2cbd521 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Blockchain Configuration +PRIVATE_KEY=0x...your_private_key_here... +BASE_RPC_URL=https://mainnet.base.org +USE_TESTNET=false + +# Contract Addresses (after deployment) +QIP_REGISTRY_ADDRESS=0x... +QIP_GOVERNANCE_ADDRESS=0x... + +# IPFS Provider Keys (get free key at https://nft.storage) +NFT_STORAGE_KEY=eyJ...your_nft_storage_key... + +# Gatsby Environment Variables +GATSBY_QIP_REGISTRY_ADDRESS=0x... +GATSBY_NFT_STORAGE_KEY=eyJ...your_nft_storage_key... +GATSBY_BASE_RPC_URL=https://mainnet.base.org +GATSBY_USE_TESTNET=false + +# Migration Mode (set to true during transition period) +GATSBY_USE_LOCAL_FILES=true + +# Basescan API Key (for contract verification) +BASESCAN_API_KEY=your_basescan_api_key \ No newline at end of file diff --git a/.github/workflows/author-validation.yml b/.github/workflows/author-validation.yml deleted file mode 100644 index c26b391..0000000 --- a/.github/workflows/author-validation.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: QIP Author Validation - -on: - pull_request: - branches: - - main - paths: - - 'contents/QIP/**' - -jobs: - validate-author: - name: Validate QIP Author - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: oven-sh/setup-bun@v2 - - name: Validate author can modify QIP - run: | - BASE_SHA=${{ github.event.pull_request.base.sha }} - HEAD_SHA=${{ github.sha }} - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - - echo "PR Author: $PR_AUTHOR" - echo "Checking modified QIP files..." - - # Get modified QIP files (not newly added ones) - MODIFIED_QIPS=$(git diff --name-only $BASE_SHA $HEAD_SHA -- contents/QIP/ | xargs -I {} sh -c 'git show $BASE_SHA:{} >/dev/null 2>&1 && echo {}' || true) - - if [ -z "$MODIFIED_QIPS" ]; then - echo "No existing QIP files were modified. Validation passed." - exit 0 - fi - - bun scripts/validate-author.ts "$PR_AUTHOR" $MODIFIED_QIPS \ No newline at end of file diff --git a/.github/workflows/gatsby.yml b/.github/workflows/gatsby.yml index d7dc7bd..efcd4dc 100644 --- a/.github/workflows/gatsby.yml +++ b/.github/workflows/gatsby.yml @@ -1,8 +1,6 @@ -# Sample workflow for building and deploying a Gatsby site to GitHub Pages -# -# To get started with Gatsby see: https://www.gatsbyjs.com/docs/quick-start/ -# -name: Deploy Gatsby site to Pages +# Workflow for building and deploying QIPs site to GitHub Pages +# This workflow fetches QIPs from blockchain/IPFS and builds the static site +name: Deploy QIPs site to Pages on: # Runs on pushes targeting the default branch @@ -60,6 +58,11 @@ jobs: - name: Build with Gatsby env: PREFIX_PATHS: 'true' + GATSBY_QIP_REGISTRY_ADDRESS: ${{ secrets.QIP_REGISTRY_ADDRESS }} + GATSBY_NFT_STORAGE_KEY: ${{ secrets.NFT_STORAGE_KEY }} + GATSBY_BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }} + GATSBY_USE_TESTNET: ${{ secrets.USE_TESTNET }} + GATSBY_USE_LOCAL_FILES: ${{ secrets.USE_LOCAL_FILES }} run: bun run build - name: Upload artifact uses: actions/upload-pages-artifact@v3 diff --git a/.gitignore b/.gitignore index 0118a98..c233de5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,17 @@ gatsby-types.d.ts .DS_Store .idea .env +.env.local +.env.development.local +.env.test.local +.env.production.local +.env.development +.env.test +.env.production +.env.local +.env.development.local +.env.test.local +.env.production.local +broadcast/ +cache/ +out/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bfa9c16 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dependencies/forge-std"] + path = dependencies/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index 2f5b317..dae953a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,16 @@ -# QIPs (QiDAO Improvement Proposals) Page ReadMe +# QIPs (QiDAO Improvement Proposals) -## Description: -The QIPs page is a dedicated platform for the QiDAO community to propose, discuss, and track the evolution of various protocol improvements. It aims to foster transparency and collaborative decision-making within the QiDAO ecosystem. +## Description +The QIPs platform is a decentralized system for the QiDAO community to propose, discuss, and track protocol improvements. Proposals are stored on IPFS with an on-chain registry on Base, ensuring transparency, permanence, and permissionless access. + +## 🆕 New Decentralized Workflow +QIPs now use a fully on-chain system instead of GitHub PRs: +- **Create proposals** directly through the web interface +- **Store content** permanently on IPFS +- **Track status** via on-chain registry +- **Submit to Snapshot** with one click + +See [NEW_QIP_WORKFLOW.md](docs/NEW_QIP_WORKFLOW.md) for detailed instructions. ## Stages: 1. **Draft:** Initial proposal submission for community review. @@ -27,5 +36,89 @@ The QIPs page is a dedicated platform for the QiDAO community to propose, discus 5. **FAQ/Help Page:** - Assistance for common queries and guidelines on community conduct. - + +## Local Testing with QIP Registry + +### Quick Start + +1. **Start local test environment:** +```bash +./scripts/startAnvil.sh +``` + +This will: +- Start Anvil (local Ethereum node) +- Deploy the QIPRegistry contract +- Create sample QIPs with different statuses +- Set up test accounts with roles (governance, editor, authors) + +2. **Run the TypeScript test client:** +```bash +bun run src/localQIPTest.ts +``` + +### Test Accounts + +| Role | Address | Private Key (first 16 chars) | +|------|---------|------------------------------| +| Governance | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 | 0xac0974bec39a17... | +| Editor | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 0x47e179ec197488... | +| Author1 | 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC | 0x59c6995e998f97... | +| Author2 | 0x90F79bf6EB2c4f870365E785982E1f101E93b906 | 0x5de4111afa1a4b... | +| Author3 | 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 | 0x7c852118294e51... | + +### Pre-deployed Test QIPs + +The setup script creates several QIPs to demonstrate different states: + +- **QIP-249**: Dynamic Interest Rates (Draft → Implemented) +- **QIP-250**: Multi-Collateral Support (Draft → ReviewPending) +- **QIP-251**: Staking Rewards (Draft → Withdrawn) +- **QIP-100**: Historical - Protocol Launch (Implemented) +- **QIP-150**: Historical - Rejected Proposal (Rejected) +- **QIP-200**: Historical - Superseded Update (Superseded) + +### Testing Scenarios + +1. **Create a new QIP:** +```typescript +import { generateQIPContent } from './src/testHelpers'; + +const qipData = generateQIPContent(252, 'technicalUpgrade'); +await registry.write.createQIP([ + qipData.title, + qipData.network, + qipData.contentHash, + qipData.ipfsUrl +]); +``` + +2. **Update QIP status (Editor only):** +```bash +cast send $REGISTRY_ADDRESS "updateStatus(uint256,uint8)" 249 3 \ + --private-key $EDITOR_KEY --rpc-url http://localhost:8545 +``` + +3. **Query QIPs:** +```bash +# Get QIPs by status (e.g., Draft = 0) +cast call $REGISTRY_ADDRESS "getQIPsByStatus(uint8)" 0 --rpc-url http://localhost:8545 + +# Get QIPs by author +cast call $REGISTRY_ADDRESS "getQIPsByAuthor(address)" $AUTHOR1 --rpc-url http://localhost:8545 +``` + +### Advanced Testing + +Use the test helpers for complex scenarios: + +```typescript +import { TestScenarios, createBatchQIPs } from './src/testHelpers'; + +// Create multiple QIPs at once +const results = await createBatchQIPs(registry, 260, 5); + +// Run predefined test scenarios +// See TestScenarios in testHelpers.ts for available scenarios +``` diff --git a/bun.lock b/bun.lock index fe66142..7f36b40 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,7 @@ "@types/node": "^20.3.3", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", + "dotenv": "^17.2.0", "typescript": "^5.1.6", }, }, @@ -1332,7 +1333,7 @@ "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], - "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], + "dotenv": ["dotenv@17.2.0", "", {}, "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ=="], "dotenv-expand": ["dotenv-expand@5.1.0", "", {}, "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="], @@ -3614,6 +3615,8 @@ "fork-ts-checker-webpack-plugin/tapable": ["tapable@1.1.3", "", {}, "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA=="], + "gatsby/dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], + "gatsby-plugin-postcss/postcss-loader": ["postcss-loader@7.3.4", "", { "dependencies": { "cosmiconfig": "^8.3.5", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A=="], "gatsby-transformer-remark/mdast-util-toc": ["mdast-util-toc@5.1.0", "", { "dependencies": { "@types/mdast": "^3.0.3", "@types/unist": "^2.0.3", "extend": "^3.0.2", "github-slugger": "^1.2.1", "mdast-util-to-string": "^2.0.0", "unist-util-is": "^4.0.0", "unist-util-visit": "^2.0.0" } }, "sha512-csimbRIVkiqc+PpFeKDGQ/Ck2N4f9FYH3zzBMMJzcxoKL8m+cM0n94xXm0I9eaxHnKdY9n145SGTdyJC7i273g=="], diff --git a/contracts/QIPGovernance.sol b/contracts/QIPGovernance.sol new file mode 100644 index 0000000..40436bc --- /dev/null +++ b/contracts/QIPGovernance.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "./QIPRegistry.sol"; + +/** + * @title QIPGovernance + * @notice Advanced governance features for QIP management + * @dev Extends QIPRegistry with role-based permissions and automated workflows + */ +contract QIPGovernance { + QIPRegistry public immutable registry; + + enum Role { + None, + Proposer, + Reviewer, + Editor, + Admin + } + + struct RoleAssignment { + Role role; + uint256 assignedAt; + uint256 expiresAt; + string reason; + } + + struct ReviewComment { + address reviewer; + uint256 timestamp; + string comment; + bool isApproval; + } + + mapping(address => RoleAssignment) public roles; + mapping(uint256 => ReviewComment[]) public qipReviews; + mapping(uint256 => mapping(address => bool)) public hasReviewed; + + uint256 public constant MIN_REVIEW_PERIOD = 3 days; + uint256 public constant MAX_DRAFT_PERIOD = 30 days; + uint256 public requiredReviews = 2; + + event RoleGranted(address indexed user, Role role, uint256 expiresAt, string reason); + event RoleRevoked(address indexed user, Role previousRole, string reason); + event ReviewSubmitted(uint256 indexed qipNumber, address indexed reviewer, bool isApproval); + event AutoStatusUpdate(uint256 indexed qipNumber, QIPRegistry.QIPStatus newStatus, string reason); + + modifier onlyRole(Role _minRole) { + require(roles[msg.sender].role >= _minRole, "Insufficient role"); + require(roles[msg.sender].expiresAt == 0 || roles[msg.sender].expiresAt > block.timestamp, "Role expired"); + _; + } + + modifier onlyAdmin() { + require(roles[msg.sender].role == Role.Admin, "Only admin"); + require(roles[msg.sender].expiresAt == 0 || roles[msg.sender].expiresAt > block.timestamp, "Role expired"); + _; + } + + constructor(address _registry) { + registry = QIPRegistry(_registry); + + // Grant admin role to deployer + roles[msg.sender] = RoleAssignment({ + role: Role.Admin, + assignedAt: block.timestamp, + expiresAt: 0, // No expiration + reason: "Contract deployer" + }); + } + + /** + * @dev Grant role to a user + */ + function grantRole( + address _user, + Role _role, + uint256 _duration, + string memory _reason + ) external onlyAdmin { + require(_user != address(0), "Invalid address"); + require(_role != Role.None, "Cannot grant None role"); + + uint256 expiresAt = _duration > 0 ? block.timestamp + _duration : 0; + + roles[_user] = RoleAssignment({ + role: _role, + assignedAt: block.timestamp, + expiresAt: expiresAt, + reason: _reason + }); + + // Sync editor status with registry + if (_role >= Role.Editor) { + registry.setEditor(_user, true); + } else { + registry.setEditor(_user, false); + } + + emit RoleGranted(_user, _role, expiresAt, _reason); + } + + /** + * @dev Revoke role from a user + */ + function revokeRole(address _user, string memory _reason) external onlyAdmin { + Role previousRole = roles[_user].role; + require(previousRole != Role.None, "User has no role"); + + delete roles[_user]; + registry.setEditor(_user, false); + + emit RoleRevoked(_user, previousRole, _reason); + } + + /** + * @dev Submit a review for a QIP + */ + function submitReview( + uint256 _qipNumber, + string memory _comment, + bool _isApproval + ) external onlyRole(Role.Reviewer) { + (QIPRegistry.QIP memory qip,) = registry.getQIPWithVersions(_qipNumber); + require(qip.qipNumber > 0, "QIP does not exist"); + require(qip.status == QIPRegistry.QIPStatus.ReviewPending, "Not in review"); + require(!hasReviewed[_qipNumber][msg.sender], "Already reviewed"); + + qipReviews[_qipNumber].push(ReviewComment({ + reviewer: msg.sender, + timestamp: block.timestamp, + comment: _comment, + isApproval: _isApproval + })); + + hasReviewed[_qipNumber][msg.sender] = true; + + emit ReviewSubmitted(_qipNumber, msg.sender, _isApproval); + + // Check if enough approvals to move to vote pending + _checkReviewThreshold(_qipNumber); + } + + /** + * @dev Check if QIP has enough reviews to proceed + */ + function _checkReviewThreshold(uint256 _qipNumber) internal { + uint256 approvals = 0; + uint256 rejections = 0; + + ReviewComment[] storage reviews = qipReviews[_qipNumber]; + for (uint256 i = 0; i < reviews.length; i++) { + if (reviews[i].isApproval) { + approvals++; + } else { + rejections++; + } + } + + if (approvals >= requiredReviews) { + registry.updateStatus(_qipNumber, QIPRegistry.QIPStatus.VotePending); + emit AutoStatusUpdate(_qipNumber, QIPRegistry.QIPStatus.VotePending, "Required reviews reached"); + } else if (rejections > requiredReviews) { + registry.updateStatus(_qipNumber, QIPRegistry.QIPStatus.Rejected); + emit AutoStatusUpdate(_qipNumber, QIPRegistry.QIPStatus.Rejected, "Too many rejections"); + } + } + + /** + * @dev Request review for a QIP + */ + function requestReview(uint256 _qipNumber) external { + (QIPRegistry.QIP memory qip,) = registry.getQIPWithVersions(_qipNumber); + require(qip.qipNumber > 0, "QIP does not exist"); + require(qip.author == msg.sender || roles[msg.sender].role >= Role.Editor, "Not authorized"); + require(qip.status == QIPRegistry.QIPStatus.Draft, "Not in draft"); + require(block.timestamp >= qip.createdAt + MIN_REVIEW_PERIOD, "Too soon for review"); + + registry.updateStatus(_qipNumber, QIPRegistry.QIPStatus.ReviewPending); + emit AutoStatusUpdate(_qipNumber, QIPRegistry.QIPStatus.ReviewPending, "Review requested"); + } + + /** + * @dev Check and update stale QIPs + */ + function checkStaleQIPs(uint256[] calldata _qipNumbers) external { + for (uint256 i = 0; i < _qipNumbers.length; i++) { + (QIPRegistry.QIP memory qip,) = registry.getQIPWithVersions(_qipNumbers[i]); + + if (qip.status == QIPRegistry.QIPStatus.Draft && + block.timestamp > qip.lastUpdated + MAX_DRAFT_PERIOD) { + registry.updateStatus(_qipNumbers[i], QIPRegistry.QIPStatus.Withdrawn); + emit AutoStatusUpdate(_qipNumbers[i], QIPRegistry.QIPStatus.Withdrawn, "Stale draft"); + } + } + } + + /** + * @dev Update required reviews threshold + */ + function setRequiredReviews(uint256 _required) external onlyAdmin { + require(_required > 0, "Must require at least 1 review"); + requiredReviews = _required; + } + + /** + * @dev Get all reviews for a QIP + */ + function getQIPReviews(uint256 _qipNumber) external view returns (ReviewComment[] memory) { + return qipReviews[_qipNumber]; + } + + /** + * @dev Check if address has valid role + */ + function hasValidRole(address _user, Role _minRole) external view returns (bool) { + RoleAssignment memory assignment = roles[_user]; + return assignment.role >= _minRole && + (assignment.expiresAt == 0 || assignment.expiresAt > block.timestamp); + } + + /** + * @dev Batch grant roles + */ + function batchGrantRoles( + address[] calldata _users, + Role[] calldata _roles, + uint256[] calldata _durations, + string memory _reason + ) external onlyAdmin { + require(_users.length == _roles.length && _roles.length == _durations.length, "Array length mismatch"); + + for (uint256 i = 0; i < _users.length; i++) { + require(_users[i] != address(0), "Invalid address"); + require(_roles[i] != Role.None, "Cannot grant None role"); + + uint256 expiresAt = _durations[i] > 0 ? block.timestamp + _durations[i] : 0; + + roles[_users[i]] = RoleAssignment({ + role: _roles[i], + assignedAt: block.timestamp, + expiresAt: expiresAt, + reason: _reason + }); + + if (_roles[i] >= Role.Editor) { + registry.setEditor(_users[i], true); + } else { + registry.setEditor(_users[i], false); + } + + emit RoleGranted(_users[i], _roles[i], expiresAt, _reason); + } + } + + /** + * @dev Emergency pause for specific QIP + */ + function emergencyWithdrawQIP(uint256 _qipNumber, string memory _reason) + external + onlyAdmin + { + registry.updateStatus(_qipNumber, QIPRegistry.QIPStatus.Withdrawn); + emit AutoStatusUpdate(_qipNumber, QIPRegistry.QIPStatus.Withdrawn, _reason); + } +} \ No newline at end of file diff --git a/contracts/QIPRegistry.sol b/contracts/QIPRegistry.sol new file mode 100644 index 0000000..9941e3c --- /dev/null +++ b/contracts/QIPRegistry.sol @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/** + * @title QIPRegistry + * @notice On-chain registry for QiDAO Improvement Proposals stored on IPFS + * @dev Manages QIP metadata, versioning, and status transitions + */ +contract QIPRegistry { + struct QIP { + uint256 qipNumber; + address author; + string title; + string network; + bytes32 contentHash; // keccak256 of the full proposal content + string ipfsUrl; // IPFS CID in format: ipfs://Qm... + uint256 createdAt; + uint256 lastUpdated; + QIPStatus status; + string implementor; + uint256 implementationDate; + string snapshotProposalId; + uint256 version; // Version number for tracking edits + } + + struct QIPVersion { + bytes32 contentHash; + string ipfsUrl; + uint256 timestamp; + string changeNote; + } + + enum QIPStatus { + Draft, + ReviewPending, + VotePending, + Approved, + Rejected, + Implemented, + Superseded, + Withdrawn + } + + mapping(uint256 => QIP) public qips; + mapping(uint256 => mapping(uint256 => QIPVersion)) public qipVersions; + mapping(uint256 => uint256) public qipVersionCount; + mapping(bytes32 => uint256) public contentHashToQIP; + mapping(address => uint256[]) private authorQIPs; + mapping(address => bool) public editors; + + uint256 public nextQIPNumber = 249; // Starting after existing QIP-248 + address public governance; + bool public migrationMode = true; + + event QIPCreated( + uint256 indexed qipNumber, + address indexed author, + string title, + string network, + bytes32 contentHash, + string ipfsUrl + ); + + event QIPUpdated( + uint256 indexed qipNumber, + uint256 version, + bytes32 newContentHash, + string newIpfsUrl, + string changeNote + ); + + event QIPStatusChanged( + uint256 indexed qipNumber, + QIPStatus oldStatus, + QIPStatus newStatus + ); + + event SnapshotProposalLinked( + uint256 indexed qipNumber, + string snapshotProposalId + ); + + modifier onlyGovernance() { + require(msg.sender == governance, "Only governance"); + _; + } + + modifier onlyEditor() { + require(editors[msg.sender] || msg.sender == governance, "Only editor"); + _; + } + + modifier onlyAuthorOrEditor(uint256 _qipNumber) { + require( + qips[_qipNumber].author == msg.sender || + editors[msg.sender] || + msg.sender == governance, + "Only author or editor" + ); + _; + } + + constructor() { + governance = msg.sender; + editors[msg.sender] = true; + } + + /** + * @dev Create a new QIP + * @param _title QIP title + * @param _network Target network (Polygon, Ethereum, Base, etc.) + * @param _contentHash keccak256 hash of the full proposal content + * @param _ipfsUrl IPFS URL where content is stored + */ + function createQIP( + string memory _title, + string memory _network, + bytes32 _contentHash, + string memory _ipfsUrl + ) external returns (uint256) { + require(bytes(_title).length > 0, "Title required"); + require(bytes(_network).length > 0, "Network required"); + require(_contentHash != bytes32(0), "Invalid content hash"); + require(bytes(_ipfsUrl).length > 0, "IPFS URL required"); + require(contentHashToQIP[_contentHash] == 0, "Content already exists"); + + uint256 qipNumber = nextQIPNumber++; + + qips[qipNumber] = QIP({ + qipNumber: qipNumber, + author: msg.sender, + title: _title, + network: _network, + contentHash: _contentHash, + ipfsUrl: _ipfsUrl, + createdAt: block.timestamp, + lastUpdated: block.timestamp, + status: QIPStatus.Draft, + implementor: "None", + implementationDate: 0, + snapshotProposalId: "", + version: 1 + }); + + // Store initial version + qipVersions[qipNumber][1] = QIPVersion({ + contentHash: _contentHash, + ipfsUrl: _ipfsUrl, + timestamp: block.timestamp, + changeNote: "Initial version" + }); + qipVersionCount[qipNumber] = 1; + + contentHashToQIP[_contentHash] = qipNumber; + authorQIPs[msg.sender].push(qipNumber); + + emit QIPCreated( + qipNumber, + msg.sender, + _title, + _network, + _contentHash, + _ipfsUrl + ); + + return qipNumber; + } + + /** + * @dev Update QIP content (only allowed before snapshot submission) + */ + function updateQIP( + uint256 _qipNumber, + string memory _title, + bytes32 _newContentHash, + string memory _newIpfsUrl, + string memory _changeNote + ) external onlyAuthorOrEditor(_qipNumber) { + QIP storage qip = qips[_qipNumber]; + require(qip.qipNumber > 0, "QIP does not exist"); + require( + qip.status == QIPStatus.Draft || qip.status == QIPStatus.ReviewPending, + "Cannot update after voting" + ); + require(bytes(qip.snapshotProposalId).length == 0, "Already submitted to Snapshot"); + require(_newContentHash != bytes32(0), "Invalid content hash"); + require(contentHashToQIP[_newContentHash] == 0, "Content already exists"); + + // Update content hash mapping + delete contentHashToQIP[qip.contentHash]; + contentHashToQIP[_newContentHash] = _qipNumber; + + // Update QIP + qip.title = _title; + qip.contentHash = _newContentHash; + qip.ipfsUrl = _newIpfsUrl; + qip.lastUpdated = block.timestamp; + qip.version++; + + // Store new version + qipVersions[_qipNumber][qip.version] = QIPVersion({ + contentHash: _newContentHash, + ipfsUrl: _newIpfsUrl, + timestamp: block.timestamp, + changeNote: _changeNote + }); + qipVersionCount[_qipNumber] = qip.version; + + emit QIPUpdated( + _qipNumber, + qip.version, + _newContentHash, + _newIpfsUrl, + _changeNote + ); + } + + /** + * @dev Update QIP status + */ + function updateStatus(uint256 _qipNumber, QIPStatus _newStatus) + external + onlyEditor + { + QIP storage qip = qips[_qipNumber]; + require(qip.qipNumber > 0, "QIP does not exist"); + + QIPStatus oldStatus = qip.status; + qip.status = _newStatus; + qip.lastUpdated = block.timestamp; + + emit QIPStatusChanged(_qipNumber, oldStatus, _newStatus); + } + + /** + * @dev Link Snapshot proposal ID + */ + function linkSnapshotProposal( + uint256 _qipNumber, + string memory _snapshotProposalId + ) external onlyAuthorOrEditor(_qipNumber) { + QIP storage qip = qips[_qipNumber]; + require(qip.qipNumber > 0, "QIP does not exist"); + require(bytes(qip.snapshotProposalId).length == 0, "Snapshot already linked"); + + qip.snapshotProposalId = _snapshotProposalId; + qip.status = QIPStatus.VotePending; + qip.lastUpdated = block.timestamp; + + emit SnapshotProposalLinked(_qipNumber, _snapshotProposalId); + emit QIPStatusChanged(_qipNumber, qip.status, QIPStatus.VotePending); + } + + /** + * @dev Set implementor and implementation date + */ + function setImplementation( + uint256 _qipNumber, + string memory _implementor, + uint256 _implementationDate + ) external onlyEditor { + QIP storage qip = qips[_qipNumber]; + require(qip.qipNumber > 0, "QIP does not exist"); + + qip.implementor = _implementor; + qip.implementationDate = _implementationDate; + qip.lastUpdated = block.timestamp; + } + + /** + * @dev Add or remove editor + */ + function setEditor(address _editor, bool _status) external onlyGovernance { + editors[_editor] = _status; + } + + /** + * @dev Transfer governance + */ + function transferGovernance(address _newGovernance) external onlyGovernance { + require(_newGovernance != address(0), "Invalid address"); + governance = _newGovernance; + } + + /** + * @dev Disable migration mode (after migrating existing QIPs) + */ + function disableMigrationMode() external onlyGovernance { + migrationMode = false; + } + + /** + * @dev Migrate existing QIP (only during migration mode) + */ + function migrateQIP( + uint256 _qipNumber, + address _author, + string memory _title, + string memory _network, + bytes32 _contentHash, + string memory _ipfsUrl, + uint256 _createdAt, + QIPStatus _status, + string memory _implementor, + uint256 _implementationDate, + string memory _snapshotProposalId + ) external onlyEditor { + require(migrationMode, "Migration mode disabled"); + require(qips[_qipNumber].qipNumber == 0, "QIP already exists"); + + qips[_qipNumber] = QIP({ + qipNumber: _qipNumber, + author: _author, + title: _title, + network: _network, + contentHash: _contentHash, + ipfsUrl: _ipfsUrl, + createdAt: _createdAt, + lastUpdated: _createdAt, + status: _status, + implementor: _implementor, + implementationDate: _implementationDate, + snapshotProposalId: _snapshotProposalId, + version: 1 + }); + + // Store initial version + qipVersions[_qipNumber][1] = QIPVersion({ + contentHash: _contentHash, + ipfsUrl: _ipfsUrl, + timestamp: _createdAt, + changeNote: "Migrated from GitHub" + }); + qipVersionCount[_qipNumber] = 1; + + contentHashToQIP[_contentHash] = _qipNumber; + authorQIPs[_author].push(_qipNumber); + } + + /** + * @dev Get QIPs by author + */ + function getQIPsByAuthor(address _author) external view returns (uint256[] memory) { + return authorQIPs[_author]; + } + + /** + * @dev Get QIP with all versions + */ + function getQIPWithVersions(uint256 _qipNumber) external view returns ( + QIP memory qip, + QIPVersion[] memory versions + ) { + require(qips[_qipNumber].qipNumber > 0, "QIP does not exist"); + + qip = qips[_qipNumber]; + uint256 versionCount = qipVersionCount[_qipNumber]; + versions = new QIPVersion[](versionCount); + + for (uint256 i = 1; i <= versionCount; i++) { + versions[i - 1] = qipVersions[_qipNumber][i]; + } + } + + /** + * @dev Verify content hash matches QIP + */ + function verifyContent( + uint256 _qipNumber, + string memory _content + ) external view returns (bool) { + require(qips[_qipNumber].qipNumber > 0, "QIP does not exist"); + return keccak256(bytes(_content)) == qips[_qipNumber].contentHash; + } + + /** + * @dev Get active QIPs by status + */ + function getQIPsByStatus(QIPStatus _status) external view returns (uint256[] memory) { + uint256 count = 0; + + // First count matching QIPs + for (uint256 i = 1; i < nextQIPNumber; i++) { + if (qips[i].qipNumber > 0 && qips[i].status == _status) { + count++; + } + } + + // Then populate array + uint256[] memory result = new uint256[](count); + uint256 index = 0; + + for (uint256 i = 1; i < nextQIPNumber; i++) { + if (qips[i].qipNumber > 0 && qips[i].status == _status) { + result[index++] = i; + } + } + + return result; + } +} \ No newline at end of file diff --git a/dependencies/forge-std b/dependencies/forge-std new file mode 160000 index 0000000..77041d2 --- /dev/null +++ b/dependencies/forge-std @@ -0,0 +1 @@ +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/docs/LOCAL_DEVELOPMENT.md b/docs/LOCAL_DEVELOPMENT.md new file mode 100644 index 0000000..9970801 --- /dev/null +++ b/docs/LOCAL_DEVELOPMENT.md @@ -0,0 +1,112 @@ +# Local Development Guide + +This guide explains how to run the QIPs platform locally with a blockchain environment for testing. + +## Quick Start + +```bash +# Start everything with one command +bun run dev:local +``` + +This will: +1. Start Anvil (local Ethereum node forked from Base) +2. Deploy the QIP Registry smart contract +3. Set up test data (QIP-249, QIP-250, QIP-251) +4. Start the Gatsby development server +5. Open http://localhost:8000 in your browser + +## Manual Setup + +If you prefer to run components separately: + +```bash +# Terminal 1: Start Anvil +bun run dev:anvil + +# Terminal 2: Deploy contracts (after Anvil is running) +bun run dev:deploy-only + +# Terminal 3: Start Gatsby +bun run develop + +# Optional: Run test client +bun run dev:test-client +``` + +## Test Accounts + +The following accounts are available with 10,000 ETH each: + +| Role | Address | Private Key | +|------|---------|-------------| +| Governance | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | +| Editor | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | +| Author1 | 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC | 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a | +| Author2 | 0x90F79bf6EB2c4f870365E785982E1f101E93b906 | 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 | + +## Connecting Your Wallet + +1. Open your wallet (MetaMask, Rainbow, etc.) +2. Add a custom network: + - Network Name: Local Base Fork + - RPC URL: http://localhost:8545 + - Chain ID: 8453 + - Currency Symbol: ETH +3. Import one of the test accounts using its private key + +## Testing Features + +### Create a New QIP +1. Connect your wallet with a test account +2. Go to http://localhost:8000/create-proposal +3. Fill in the proposal details +4. Submit (gas is free on local network) + +### View Existing QIPs +- QIP-249: Dynamic Interest Rates (Draft) +- QIP-250: Multi-Collateral Support (Draft) +- QIP-251: Staking Rewards (Draft) + +### Test Permissions +- Only Editor account can change QIP status +- Only Authors can update their own QIPs +- Anyone can create new QIPs + +## Environment Variables + +The local environment uses `.env.local` with: +- `GATSBY_LOCAL_MODE=true` - Shows local mode banner +- `GATSBY_USE_LOCAL_FILES=false` - Uses only blockchain data +- `GATSBY_BASE_RPC_URL=http://localhost:8545` - Local RPC endpoint + +## Troubleshooting + +### Port Already in Use +```bash +# Kill existing processes +pkill -f anvil +pkill -f gatsby +``` + +### Contract Not Deployed +```bash +# Manually deploy +forge script script/DeployOnly.s.sol:DeployOnly --rpc-url http://localhost:8545 --broadcast +``` + +### Gatsby Not Loading Data +1. Check `.env` has correct contract address +2. Clear Gatsby cache: `bun run clean` +3. Restart: `bun run dev:local` + +## Advanced Usage + +### Custom Test Data +Edit `script/LocalQIPTest.s.sol` to modify initial QIPs. + +### Different Fork +Edit `scripts/start-local-dev.sh` to fork a different network. + +### Reset Everything +Just restart `bun run dev:local` - all data is ephemeral. \ No newline at end of file diff --git a/docs/LOCAL_IPFS_SETUP.md b/docs/LOCAL_IPFS_SETUP.md new file mode 100644 index 0000000..9835b4d --- /dev/null +++ b/docs/LOCAL_IPFS_SETUP.md @@ -0,0 +1,175 @@ +# Local IPFS Setup Guide + +This guide explains how to set up and use a local IPFS node for QIPs development. + +## Overview + +The QIPs platform supports two IPFS storage modes: +- **Production**: Uses Pinata as a pinning service +- **Local Development**: Uses a local IPFS daemon with fallback to in-memory storage + +## Installation + +### macOS (Homebrew) +```bash +brew install ipfs +``` + +### Linux/macOS (Official Installer) +Visit [https://docs.ipfs.tech/install/](https://docs.ipfs.tech/install/) for platform-specific instructions. + +### Using Go +```bash +go install github.com/ipfs/kubo/cmd/ipfs@latest +``` + +## Initial Setup + +1. **Initialize IPFS** (first time only): + ```bash + ipfs init + ``` + +2. **Configure CORS** for browser access: + ```bash + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://localhost:8000", "http://localhost:8545", "*"]' + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]' + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]' + ipfs config --json API.HTTPHeaders.Access-Control-Expose-Headers '["Location"]' + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]' + ``` + +## Running IPFS + +### Automatic (Recommended) +The `start-local-dev.sh` script automatically starts IPFS when `GATSBY_USE_LOCAL_IPFS=true`: + +```bash +# IPFS will start automatically +bun run start:local +``` + +### Manual +Start the IPFS daemon manually: + +```bash +# Foreground +ipfs daemon + +# Background +ipfs daemon & +``` + +## Configuration + +### Environment Variables + +Set these in `.env` or `.env.local`: + +```bash +# Enable local IPFS mode +GATSBY_USE_LOCAL_IPFS=true + +# IPFS API endpoint (default: http://localhost:5001) +GATSBY_LOCAL_IPFS_API=http://localhost:5001 + +# IPFS Gateway endpoint (default: http://localhost:8080) +GATSBY_LOCAL_IPFS_GATEWAY=http://localhost:8080 +``` + +### Switching Between Modes + +**Local IPFS Mode**: +```bash +GATSBY_USE_LOCAL_IPFS=true +``` + +**Pinata Mode** (production): +```bash +GATSBY_USE_LOCAL_IPFS=false +GATSBY_PINATA_JWT=your_pinata_jwt_token +GATSBY_PINATA_GATEWAY=https://gateway.pinata.cloud +``` + +## How It Works + +1. **With IPFS Running**: + - Proposals are stored in your local IPFS node + - Content is accessible via the local gateway + - Data persists between sessions + +2. **Without IPFS (Fallback)**: + - Falls back to in-memory storage + - Returns mock CIDs for development + - Data is lost on page refresh + - Console warnings indicate fallback mode + +## Verifying Your Setup + +Run the check script: +```bash +./scripts/check-ipfs.sh +``` + +Expected output: +``` +✅ IPFS is installed +✅ IPFS is initialized +✅ IPFS daemon is running +✅ IPFS API is accessible at http://localhost:5001 +✅ IPFS Gateway is accessible at http://localhost:8080 +``` + +## Testing IPFS Storage + +1. Start the development environment: + ```bash + bun run start:local + ``` + +2. Navigate to http://localhost:8000/create-proposal + +3. Create a test proposal + +4. Check the console for storage confirmation: + - Success: "Uploaded to IPFS with CID: Qm..." + - Fallback: "IPFS daemon not running, using in-memory storage" + +5. Verify content retrieval: + ```bash + # Replace with your actual CID + curl http://localhost:8080/ipfs/QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + ``` + +## Troubleshooting + +### IPFS Daemon Won't Start +- Check if another process is using port 5001: `lsof -i :5001` +- Kill existing IPFS processes: `pkill -f ipfs` +- Reinitialize if corrupted: `rm -rf ~/.ipfs && ipfs init` + +### CORS Errors +- Ensure CORS is configured (see Initial Setup) +- Restart IPFS daemon after configuration changes + +### Connection Refused +- Verify IPFS is running: `ps aux | grep ipfs` +- Check API availability: `curl http://localhost:5001/api/v0/version` +- Ensure firewall isn't blocking ports 5001 and 8080 + +### Content Not Found +- In local mode, content is only available while your IPFS node is running +- For persistence across team members, use Pinata in production + +## Best Practices + +1. **Development**: Use local IPFS for faster iteration +2. **Testing**: Test with both local IPFS and Pinata modes +3. **Production**: Always use Pinata for reliable persistence +4. **CI/CD**: Configure GitHub Actions with Pinata credentials + +## Additional Resources + +- [IPFS Documentation](https://docs.ipfs.tech/) +- [IPFS HTTP API Reference](https://docs.ipfs.tech/reference/http/api/) +- [Pinata Documentation](https://docs.pinata.cloud/) \ No newline at end of file diff --git a/docs/NEW_QIP_WORKFLOW.md b/docs/NEW_QIP_WORKFLOW.md new file mode 100644 index 0000000..2f4feb1 --- /dev/null +++ b/docs/NEW_QIP_WORKFLOW.md @@ -0,0 +1,121 @@ +# New QIP Workflow: Decentralized Proposal System + +## Overview + +The QiDAO Improvement Proposal (QIP) system has transitioned from a GitHub PR-based workflow to a fully decentralized system using: +- **On-chain registry** on Base for proposal metadata and status tracking +- **IPFS storage** for full proposal content +- **Web-based interface** for creating and editing proposals + +## Key Benefits + +1. **No GitHub Account Required**: Anyone with a wallet can create proposals +2. **Decentralized Storage**: Proposals stored permanently on IPFS +3. **On-chain Verification**: All proposals have verifiable on-chain records +4. **Direct Snapshot Integration**: Submit to Snapshot directly from the web interface +5. **Version Control**: All edits tracked on-chain with IPFS history + +## How to Create a New QIP + +### 1. Connect Your Wallet +- Visit the QIPs website +- Connect your wallet (MetaMask, WalletConnect, etc.) +- Ensure you're on Base network + +### 2. Create Proposal +- Navigate to "Create Proposal" page +- Fill in the required fields: + - **Title**: Clear, descriptive title + - **Network**: Target network (Polygon, Ethereum, Base, etc.) + - **Content**: Full proposal in markdown format + - **Implementor**: Who will implement (or "None") + +### 3. Submit to Blockchain +- Click "Create QIP" +- Approve the transaction in your wallet +- Your proposal will be: + - Uploaded to IPFS + - Registered on-chain with a unique QIP number + - Set to "Draft" status + +### 4. Edit Your Proposal +- You can edit your proposal while it's in "Draft" or "Review" status +- Each edit creates a new version on IPFS +- Edit history is tracked on-chain + +### 5. Request Review +- When ready, request review from the governance team +- Reviewers will provide feedback +- Make any necessary edits + +### 6. Submit to Snapshot +- Once approved for voting, submit to Snapshot +- This links your QIP to a Snapshot proposal +- Status automatically updates to "Vote Pending" + +## Proposal Lifecycle + +``` +Draft → Review Pending → Vote Pending → Approved/Rejected → Implemented +``` + +- **Draft**: Initial creation, editable by author +- **Review Pending**: Under review by governance team +- **Vote Pending**: Submitted to Snapshot for voting +- **Approved**: Passed community vote +- **Rejected**: Failed community vote +- **Implemented**: Successfully implemented on-chain + +## Technical Details + +### Smart Contracts +- **QIPRegistry**: Main registry contract storing proposal metadata +- **QIPGovernance**: Role management and review workflow + +### IPFS Providers +The system supports multiple IPFS providers: +- **NFT.Storage** (recommended): Unlimited free storage +- **Web3.Storage**: 1TB free storage +- **Pinata**: 1GB free storage + +### Gas Costs +- Creating a proposal: ~50,000 gas +- Updating a proposal: ~40,000 gas +- Significantly cheaper than storing content on-chain + +## Role-Based Permissions + +- **Proposer**: Can create and edit own proposals +- **Reviewer**: Can review and approve proposals +- **Editor**: Can update any proposal status +- **Admin**: Full system control + +## Migration from GitHub + +Existing QIPs have been migrated to the new system: +- All historical QIPs preserved on IPFS +- Original metadata maintained +- GitHub links preserved for reference + +## FAQ + +**Q: Do I need coding knowledge to create a QIP?** +A: No, just basic markdown formatting knowledge. + +**Q: What happens to my proposal after creation?** +A: It's permanently stored on IPFS and registered on-chain. + +**Q: Can I delete a proposal?** +A: No, but you can withdraw it by updating the status. + +**Q: How much does it cost?** +A: Only gas fees for blockchain transactions (typically < $1 on Base). + +**Q: Can I edit after Snapshot submission?** +A: No, proposals are locked once submitted to Snapshot. + +## Support + +For technical issues or questions: +- Discord: #governance channel +- GitHub: Create an issue (for technical problems only) \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..ae57835 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,18 @@ +[profile.default] +src = "contracts" +out = "out" +libs = ["dependencies"] +solc = "0.8.13" +optimizer = true +optimizer_runs = 200 +via_ir = true + +[etherscan] +base = { key = "${BASESCAN_API_KEY}" } +base-sepolia = { key = "${BASESCAN_API_KEY}" } + +[rpc_endpoints] +base = "${BASE_RPC_URL}" +base-sepolia = "${BASE_SEPOLIA_RPC_URL}" + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options \ No newline at end of file diff --git a/gatsby-browser.tsx b/gatsby-browser.tsx index a20d762..fa3a488 100644 --- a/gatsby-browser.tsx +++ b/gatsby-browser.tsx @@ -4,27 +4,88 @@ import "./src/styles/global.css"; import React from "react"; import { WagmiProvider, createConfig } from "wagmi"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { mainnet } from "wagmi/chains"; +import { mainnet, base } from "wagmi/chains"; import { http } from "wagmi"; import { ConnectKitProvider, getDefaultConfig } from "connectkit"; -const config = createConfig( - getDefaultConfig({ - appName: "Qidao", - enableFamily: false, - walletConnectProjectId: process.env.GATSBY_WALLETCONNECT_PROJECT_ID!, - chains: [mainnet], - transports: { - [mainnet.id]: http(), +// Local Base fork configuration +const localBase = { + ...base, + id: 8453, + name: "Local Base Fork", + network: "local-base", + rpcUrls: { + default: { + http: ["http://localhost:8545"], }, - }) -); + public: { + http: ["http://localhost:8545"], + }, + }, +}; + +// Check if we're in development mode +const isDevelopment = process.env.NODE_ENV === "development"; +const chains = isDevelopment ? [localBase, mainnet, base] : [mainnet, base]; + +// Debug logging +console.log("🔧 Wagmi Configuration Debug:"); +console.log("- NODE_ENV:", process.env.NODE_ENV); +console.log("- isDevelopment:", isDevelopment); +console.log("- GATSBY_WALLETCONNECT_PROJECT_ID:", process.env.GATSBY_WALLETCONNECT_PROJECT_ID); +console.log("- chains:", chains.map(c => ({ id: c.id, name: c.name }))); + +// Check if WalletConnect project ID exists +if (!process.env.GATSBY_WALLETCONNECT_PROJECT_ID) { + console.error("❌ GATSBY_WALLETCONNECT_PROJECT_ID is not set!"); + console.error("This will cause wallet connection issues."); +} + +let config; +try { + config = createConfig( + getDefaultConfig({ + appName: "Qidao", + enableFamily: false, + walletConnectProjectId: process.env.GATSBY_WALLETCONNECT_PROJECT_ID || "dummy-project-id", + chains: chains, + transports: { + [mainnet.id]: http(), + [base.id]: http(), + [localBase.id]: http("http://localhost:8545"), + }, + }) + ); + console.log("✅ Wagmi config created successfully"); +} catch (error) { + console.error("❌ Failed to create Wagmi config:", error); +} + const queryClient = new QueryClient(); -export const wrapRootElement = ({ element }) => ( - - - {element} - - -); +// Debug wrapper component +const DebugWrapper = ({ children }) => { + React.useEffect(() => { + console.log("🔍 WagmiProvider mounted with config:", config); + console.log("🔍 Checking window.ethereum:", typeof window !== 'undefined' ? window.ethereum : 'Not available (SSR)'); + }, []); + + return <>{children}; +}; + +export const wrapRootElement = ({ element }) => { + console.log("🚀 wrapRootElement called"); + console.log("- config exists:", !!config); + + return ( + + + + + {element} + + + + + ); +}; diff --git a/gatsby-config.ts b/gatsby-config.ts index ea28e4f..ff568a5 100644 --- a/gatsby-config.ts +++ b/gatsby-config.ts @@ -1,4 +1,16 @@ import type { GatsbyConfig } from "gatsby"; +import dotenv from "dotenv"; + +// Load environment variables from .env file +dotenv.config({ + path: `.env`, +}); + +console.log("📋 Gatsby Config - Environment Variables Check:"); +console.log("- GATSBY_QIP_REGISTRY_ADDRESS:", process.env.GATSBY_QIP_REGISTRY_ADDRESS); +console.log("- GATSBY_USE_LOCAL_IPFS:", process.env.GATSBY_USE_LOCAL_IPFS); +console.log("- GATSBY_PINATA_JWT:", process.env.GATSBY_PINATA_JWT ? "✅ Set" : "❌ Not set"); +console.log("- GATSBY_BASE_RPC_URL:", process.env.GATSBY_BASE_RPC_URL); const config: GatsbyConfig = { siteMetadata: { diff --git a/gatsby-node.ts b/gatsby-node.ts new file mode 100644 index 0000000..4966bde --- /dev/null +++ b/gatsby-node.ts @@ -0,0 +1,184 @@ +import type { GatsbyNode } from 'gatsby'; +import { sourceQIPsFromChain } from './src/utils/qip-data-source'; +import * as path from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config({ + path: `.env`, +}); + +// Configuration +const QIP_REGISTRY_ADDRESS = process.env.GATSBY_QIP_REGISTRY_ADDRESS as `0x${string}`; +const PINATA_JWT = process.env.GATSBY_PINATA_JWT || ''; +const PINATA_GATEWAY = process.env.GATSBY_PINATA_GATEWAY || 'https://gateway.pinata.cloud'; +const BASE_RPC_URL = process.env.GATSBY_BASE_RPC_URL; +const USE_TESTNET = process.env.GATSBY_USE_TESTNET === 'true'; +const USE_LOCAL_IPFS = process.env.GATSBY_USE_LOCAL_IPFS === 'true'; +const LOCAL_IPFS_API = process.env.GATSBY_LOCAL_IPFS_API || 'http://localhost:5001'; +const LOCAL_IPFS_GATEWAY = process.env.GATSBY_LOCAL_IPFS_GATEWAY || 'http://localhost:8080'; + +// During development/migration, we can still use local files +const USE_LOCAL_FILES = process.env.GATSBY_USE_LOCAL_FILES === 'true'; + +export const sourceNodes: GatsbyNode['sourceNodes'] = async ({ + actions, + createNodeId, + createContentDigest, + reporter +}) => { + const { createNode } = actions; + + if (QIP_REGISTRY_ADDRESS && (PINATA_JWT || USE_LOCAL_IPFS)) { + // Fetch QIPs from blockchain/IPFS + reporter.info('Fetching QIPs from blockchain and IPFS...'); + + try { + const qips = await sourceQIPsFromChain( + QIP_REGISTRY_ADDRESS, + PINATA_JWT, + PINATA_GATEWAY, + BASE_RPC_URL, + USE_TESTNET, + USE_LOCAL_IPFS, + LOCAL_IPFS_API, + LOCAL_IPFS_GATEWAY + ); + + reporter.info(`Fetched ${qips.length} QIPs from blockchain`); + + // Create nodes for each QIP + for (const qip of qips) { + const nodeContent = JSON.stringify(qip); + const nodeMeta = { + id: createNodeId(`qip-${qip.qipNumber}`), + parent: null, + children: [], + internal: { + type: 'QIP', + content: nodeContent, + contentDigest: createContentDigest(qip) + } + }; + + const node = { ...qip, ...nodeMeta }; + createNode(node); + } + } catch (error) { + reporter.error('Failed to fetch QIPs from blockchain:', error as Error); + reporter.warn('Falling back to local files if available'); + } + } + + if (USE_LOCAL_FILES) { + reporter.info('Using local QIP files (migration mode enabled)'); + } +}; + +export const createSchemaCustomization: GatsbyNode['createSchemaCustomization'] = ({ actions }) => { + const { createTypes } = actions; + + const typeDefs = ` + type QIP implements Node { + qipNumber: Int! + title: String! + network: String! + status: String! + author: String! + implementor: String! + implementationDate: String! + proposal: String! + created: String! + content: String! + ipfsUrl: String! + contentHash: String! + version: Int! + } + `; + + createTypes(typeDefs); +}; + +export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions, reporter }) => { + const { createPage } = actions; + + // Query for QIPs from blockchain + const qipResult = await graphql<{ + allQip: { + nodes: Array<{ + qipNumber: number; + title: string; + }>; + }; + }>(` + query { + allQip { + nodes { + qipNumber + title + } + } + } + `); + + if (qipResult.errors) { + reporter.panicOnBuild('Error loading QIPs from blockchain', qipResult.errors); + return; + } + + // Create pages for blockchain QIPs + qipResult.data?.allQip.nodes.forEach((qip) => { + createPage({ + path: `/qips/QIP-${qip.qipNumber}`, + component: path.resolve('./src/pages/qips/QIP-blockchain.tsx'), + context: { + qipNumber: qip.qipNumber + } + }); + }); + + // Create a catch-all route for dynamic QIPs (handles QIPs created after build) + createPage({ + path: '/qips/*', + component: path.resolve('./src/pages/qips/[qipNumber].tsx'), + matchPath: '/qips/QIP-:qipNumber' + }); + + // Also handle markdown files during migration + if (USE_LOCAL_FILES) { + const markdownResult = await graphql<{ + allMarkdownRemark: { + nodes: Array<{ + frontmatter: { + qip: number; + }; + }>; + }; + }>(` + query { + allMarkdownRemark(filter: { frontmatter: { qip: { ne: null } } }) { + nodes { + frontmatter { + qip + } + } + } + } + `); + + if (markdownResult.errors) { + reporter.panicOnBuild('Error loading markdown QIPs', markdownResult.errors); + return; + } + + markdownResult.data?.allMarkdownRemark.nodes.forEach((node) => { + createPage({ + path: `/qips/QIP-${node.frontmatter.qip}`, + component: path.resolve('./src/pages/qips/QIP-{MarkdownRemark.frontmatter__qip}.tsx'), + context: { + frontmatter__qip: node.frontmatter.qip + } + }); + }); + } +}; \ No newline at end of file diff --git a/package.json b/package.json index c231c7b..06bd9f5 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,13 @@ "build": "gatsby build", "serve": "gatsby serve", "clean": "gatsby clean", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "deploy:contracts": "forge script script/Deploy.s.sol --rpc-url $BASE_RPC_URL --broadcast", + "migrate:qips": "bun scripts/migrate-qips.ts", + "dev:local": "./scripts/start-local-dev.sh", + "dev:anvil": "./scripts/startAnvil.sh", + "dev:test-client": "bun run src/localQIPTest.ts", + "dev:deploy-only": "forge script script/DeployOnly.s.sol:DeployOnly --rpc-url http://localhost:8545 --broadcast" }, "dependencies": { "@mdx-js/react": "^2.3.0", @@ -49,6 +55,7 @@ "@types/node": "^20.3.3", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", + "dotenv": "^17.2.0", "typescript": "^5.1.6" } } diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..730a8d0 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "../contracts/QIPRegistry.sol"; +import "../contracts/QIPGovernance.sol"; + +contract Deploy is Script { + function setUp() public {} + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + console.log("Deploying contracts with account:", deployer); + console.log("Account balance:", deployer.balance); + + vm.startBroadcast(deployerPrivateKey); + + // Deploy QIPRegistry + QIPRegistry registry = new QIPRegistry(); + console.log("QIPRegistry deployed to:", address(registry)); + + // Deploy QIPGovernance + QIPGovernance governance = new QIPGovernance(address(registry)); + console.log("QIPGovernance deployed to:", address(governance)); + + // Transfer registry governance to the governance contract + registry.transferGovernance(address(governance)); + console.log("Registry governance transferred to governance contract"); + + // Grant initial editor roles (you can add addresses here) + // governance.grantRole(0xYourAddress, QIPGovernance.Role.Editor, 0, "Initial editor"); + + vm.stopBroadcast(); + + // Log deployment info for frontend + console.log("\n=== Deployment Complete ==="); + console.log("QIP_REGISTRY_ADDRESS=", address(registry)); + console.log("QIP_GOVERNANCE_ADDRESS=", address(governance)); + console.log("\nAdd these addresses to your .env file"); + } +} \ No newline at end of file diff --git a/script/DeployDeterministic.s.sol b/script/DeployDeterministic.s.sol new file mode 100644 index 0000000..4b43da5 --- /dev/null +++ b/script/DeployDeterministic.s.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {QIPRegistry} from "../contracts/QIPRegistry.sol"; + +// Simple CREATE2 Factory for deterministic deployment +contract Create2Factory { + event Deployed(address addr, bytes32 salt); + + function deploy(bytes memory bytecode, bytes32 salt) external returns (address) { + address addr; + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + emit Deployed(addr, salt); + return addr; + } + + function computeAddress(bytes memory bytecode, bytes32 salt) external view returns (address) { + bytes32 hash = keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(bytecode) + ) + ); + return address(uint160(uint256(hash))); + } +} + +contract DeployDeterministic is Script { + // Fixed salt for deterministic deployment + bytes32 constant SALT = keccak256("QIPRegistry.v1"); + + // Expected deterministic addresses (based on Anvil's deployment order) + address constant EXPECTED_FACTORY = 0x8615aCD086FEE64F11C7F00efBaD832DE7C5F216; + address constant EXPECTED_REGISTRY = 0xA8Fe3FFF47F517a8e0d0f3a9093Bc4D73ee75CBF; + + function run() public { + uint256 deployerPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + vm.startBroadcast(deployerPrivateKey); + + // Check if factory exists, deploy if not + Create2Factory factory; + if (EXPECTED_FACTORY.code.length == 0) { + factory = new Create2Factory(); + console.log("Create2Factory deployed at:", address(factory)); + require(address(factory) == EXPECTED_FACTORY, "Factory address mismatch"); + } else { + factory = Create2Factory(EXPECTED_FACTORY); + console.log("Create2Factory already deployed at:", EXPECTED_FACTORY); + } + + // Check if registry exists, deploy if not + if (EXPECTED_REGISTRY.code.length == 0) { + bytes memory bytecode = type(QIPRegistry).creationCode; + address registry = factory.deploy(bytecode, SALT); + console.log("QIPRegistry deployed at:", registry); + + // Verify it matches expected address + address computed = factory.computeAddress(bytecode, SALT); + console.log("Computed address:", computed); + require(registry == EXPECTED_REGISTRY, "Registry address mismatch"); + } else { + console.log("QIPRegistry already deployed at:", EXPECTED_REGISTRY); + } + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/DeployLocal.s.sol b/script/DeployLocal.s.sol new file mode 100644 index 0000000..114cfe1 --- /dev/null +++ b/script/DeployLocal.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {QIPRegistry} from "../contracts/QIPRegistry.sol"; + +/** + * @title Deploy Local QIP Registry + * @notice Simple deployment for local development + */ +contract DeployLocal is Script { + function run() public returns (address) { + uint256 deployerPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + + vm.startBroadcast(deployerPrivateKey); + + // Deploy QIPRegistry (governance will be set to msg.sender) + QIPRegistry registry = new QIPRegistry(); + + console.log("QIPRegistry deployed at:", address(registry)); + console.log("Governance set to:", registry.governance()); + + vm.stopBroadcast(); + + return address(registry); + } +} \ No newline at end of file diff --git a/script/DeployOnly.s.sol b/script/DeployOnly.s.sol new file mode 100644 index 0000000..fe8c4d4 --- /dev/null +++ b/script/DeployOnly.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {QIPRegistry} from "../contracts/QIPRegistry.sol"; + +contract DeployOnly is Script { + function run() public { + uint256 deployerPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + vm.startBroadcast(deployerPrivateKey); + + // Deploy QIPRegistry + QIPRegistry registry = new QIPRegistry(); + console.log("QIPRegistry deployed at:", address(registry)); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/DeploySimple.s.sol b/script/DeploySimple.s.sol new file mode 100644 index 0000000..228bcb2 --- /dev/null +++ b/script/DeploySimple.s.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {QIPRegistry} from "../contracts/QIPRegistry.sol"; + +contract DeploySimple is Script { + // With a fresh Anvil instance and deploying as the first transaction, + // the registry will always be at 0x5FbDB2315678afecb367f032d93F642f64180aa3 + address constant EXPECTED_REGISTRY = 0x5FbDB2315678afecb367f032d93F642f64180aa3; + + function run() public { + uint256 deployerPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + vm.startBroadcast(deployerPrivateKey); + + // Check if already deployed + if (EXPECTED_REGISTRY.code.length > 0) { + console.log("QIPRegistry already deployed at:", EXPECTED_REGISTRY); + } else { + // Deploy QIPRegistry as the first contract + QIPRegistry registry = new QIPRegistry(); + console.log("QIPRegistry deployed at:", address(registry)); + + // Log the actual address + console.log("WARNING: Registry deployed at different address than expected"); + console.log("Expected:", EXPECTED_REGISTRY); + console.log("Actual:", address(registry)); + } + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/DeployWithStandardCreate2.s.sol b/script/DeployWithStandardCreate2.s.sol new file mode 100644 index 0000000..238dd84 --- /dev/null +++ b/script/DeployWithStandardCreate2.s.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {QIPRegistry} from "../contracts/QIPRegistry.sol"; + +/** + * @title Deploy with Standard CREATE2 Deployer + * @notice Uses the standard CREATE2 deployer at 0x4e59b44847b379578588920ca78fbf26c0b4956c + * @dev This deployer exists on most EVM chains including Base + */ +contract DeployWithStandardCreate2 is Script { + // Standard CREATE2 deployer address (exists on most chains) + address constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; + + // Salt for deterministic deployment + bytes32 constant SALT = keccak256("QIPRegistry.v1.base"); + + function run() public returns (address) { + uint256 deployerPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + + // Get the bytecode + bytes memory bytecode = type(QIPRegistry).creationCode; + + // Compute the expected address + address expectedAddress = computeCreate2Address(bytecode, SALT); + console.log("Expected QIPRegistry address:", expectedAddress); + + // Check if already deployed + if (expectedAddress.code.length > 0) { + console.log("QIPRegistry already deployed at:", expectedAddress); + return expectedAddress; + } + + // Deploy using CREATE2 + vm.startBroadcast(deployerPrivateKey); + + // Call the CREATE2 deployer + // The deployer expects: salt (32 bytes) + initcode + bytes memory payload = abi.encodePacked(SALT, bytecode); + + (bool success, bytes memory result) = CREATE2_DEPLOYER.call(payload); + require(success, "CREATE2 deployment failed"); + + // The deployer returns the deployed address + address deployedAddress = address(uint160(bytes20(result))); + + console.log("QIPRegistry deployed at:", deployedAddress); + require(deployedAddress == expectedAddress, "Deployed address mismatch"); + + // Transfer governance to the deployer account + QIPRegistry registry = QIPRegistry(deployedAddress); + address governanceAccount = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + + // The current governance is the CREATE2 deployer, but we need to transfer it + // Since CREATE2 deployer is a contract, we need to do this differently + // We'll deploy with the proper governance account as constructor parameter + + vm.stopBroadcast(); + + return deployedAddress; + } + + /** + * @notice Computes the CREATE2 address for the registry + * @param bytecode The contract bytecode + * @param salt The salt value + * @return The computed address + */ + function computeCreate2Address(bytes memory bytecode, bytes32 salt) public pure returns (address) { + bytes32 hash = keccak256( + abi.encodePacked( + bytes1(0xff), + CREATE2_DEPLOYER, + salt, + keccak256(bytecode) + ) + ); + return address(uint160(uint256(hash))); + } + + /** + * @notice Helper to get the expected registry address without deploying + */ + function getExpectedAddress() public pure returns (address) { + bytes memory bytecode = type(QIPRegistry).creationCode; + return computeCreate2Address(bytecode, SALT); + } +} \ No newline at end of file diff --git a/script/LocalQIPTest.s.sol b/script/LocalQIPTest.s.sol new file mode 100644 index 0000000..7379e69 --- /dev/null +++ b/script/LocalQIPTest.s.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {QIPRegistry} from "../contracts/QIPRegistry.sol"; + +contract LocalQIPTest is Script { + QIPRegistry public registry; + + // Test accounts from Anvil's default mnemonic + address constant GOVERNANCE = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address constant EDITOR = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address constant AUTHOR1 = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; + address constant AUTHOR2 = 0x90F79bf6EB2c4f870365E785982E1f101E93b906; + address constant AUTHOR3 = 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65; + + function run() public { + uint256 deployerPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + + // Get registry address from environment variable + address registryAddress = vm.envAddress("QIP_REGISTRY_ADDRESS"); + registry = QIPRegistry(registryAddress); + console.log("Using QIPRegistry at:", address(registry)); + + vm.startBroadcast(deployerPrivateKey); + + // Setup roles (governance account should already have permissions) + registry.setEditor(EDITOR, true); + console.log("Editor role granted to:", EDITOR); + + vm.stopBroadcast(); + + // Create test QIPs as different authors + _createTestQIPs(); + + // Migrate historical QIPs + _migrateHistoricalQIPs(); + + // Simulate QIP lifecycle + _simulateQIPLifecycle(); + + console.log("\n=== Local QIP Test Setup Complete ==="); + console.log("Registry Address:", address(registry)); + console.log("Governance:", GOVERNANCE); + console.log("Editor:", EDITOR); + console.log("Test Authors:", AUTHOR1, AUTHOR2, AUTHOR3); + console.log("\nNext QIP Number:", registry.nextQIPNumber()); + } + + function _createTestQIPs() internal { + // Check if we need to create test QIPs or if they already exist + uint256 currentNextQIP = registry.nextQIPNumber(); + + if (currentNextQIP > 249) { + console.log("Test QIPs already exist, skipping creation"); + console.log("Current nextQIPNumber:", currentNextQIP); + return; + } + + // Create QIP as AUTHOR1 + vm.startBroadcast(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d); + + uint256 qip1 = registry.createQIP( + "Implement Dynamic Interest Rates", + "Polygon", + keccak256("QIP-249: Dynamic Interest Rate Model Implementation"), + "ipfs://QmTest249DynamicRates" + ); + console.log("Created QIP-249 (Draft) by AUTHOR1"); + + vm.stopBroadcast(); + + // Create QIP as AUTHOR2 + vm.startBroadcast(0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a); + + uint256 qip2 = registry.createQIP( + "Add Support for New Collateral Types", + "Base", + keccak256("QIP-250: Multi-Collateral Support"), + "ipfs://QmTest250MultiCollateral" + ); + console.log("Created QIP-250 (Draft) by AUTHOR2"); + + vm.stopBroadcast(); + + // Create QIP as AUTHOR3 + vm.startBroadcast(0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6); + + uint256 qip3 = registry.createQIP( + "Governance Token Staking Rewards", + "Ethereum", + keccak256("QIP-251: Staking Rewards Program"), + "ipfs://QmTest251StakingRewards" + ); + console.log("Created QIP-251 (Draft) by AUTHOR3"); + + vm.stopBroadcast(); + } + + function _migrateHistoricalQIPs() internal { + // Use governance account (which has migration permissions) + vm.startBroadcast(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); + + // Migrate some historical QIPs with different statuses + registry.migrateQIP( + 100, + 0x1234567890123456789012345678901234567890, + "Historical: Protocol Launch", + "Polygon", + keccak256("QIP-100: Protocol Launch"), + "ipfs://QmHistorical100", + 1640995200, // Jan 1, 2022 + QIPRegistry.QIPStatus.Implemented, + "Core Team", + 1641600000, // Jan 8, 2022 + "snapshot.org/#/qidao.eth/proposal/0x100" + ); + console.log("Migrated QIP-100 (Implemented)"); + + registry.migrateQIP( + 150, + 0x2345678901234567890123456789012345678901, + "Historical: Rejected Proposal", + "Ethereum", + keccak256("QIP-150: Rejected Proposal"), + "ipfs://QmHistorical150", + 1651017600, // Apr 27, 2022 + QIPRegistry.QIPStatus.Rejected, + "None", + 0, + "snapshot.org/#/qidao.eth/proposal/0x150" + ); + console.log("Migrated QIP-150 (Rejected)"); + + registry.migrateQIP( + 200, + 0x3456789012345678901234567890123456789012, + "Historical: Superseded Protocol Update", + "Base", + keccak256("QIP-200: Old Protocol Update"), + "ipfs://QmHistorical200", + 1667260800, // Nov 1, 2022 + QIPRegistry.QIPStatus.Superseded, + "Core Team", + 1668470400, // Nov 15, 2022 + "snapshot.org/#/qidao.eth/proposal/0x200" + ); + console.log("Migrated QIP-200 (Superseded)"); + + vm.stopBroadcast(); + } + + function _simulateQIPLifecycle() internal { + // Check if test QIPs exist before trying to simulate lifecycle + uint256 currentNextQIP = registry.nextQIPNumber(); + if (currentNextQIP <= 249) { + console.log("No test QIPs to simulate lifecycle for"); + return; + } + + // Only simulate lifecycle if QIP-249 exists and is in Draft status + (, , , , , , , , QIPRegistry.QIPStatus status249, , , ,) = registry.qips(249); + if (status249 != QIPRegistry.QIPStatus.Draft) { + console.log("QIP-249 is not in Draft status, skipping lifecycle simulation"); + return; + } + + // Move QIP-249 through lifecycle (use governance account for editor operations) + vm.startBroadcast(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); + + // Update status to ReviewPending + registry.updateStatus(249, QIPRegistry.QIPStatus.ReviewPending); + console.log("\nQIP-249: Draft -> ReviewPending"); + + vm.stopBroadcast(); + + // Author updates the QIP content + vm.startBroadcast(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d); + + registry.updateQIP( + 249, + "Implement Dynamic Interest Rates (Revised)", + keccak256("QIP-249: Dynamic Interest Rate Model Implementation v2"), + "ipfs://QmTest249DynamicRatesV2", + "Added more detailed implementation specs" + ); + console.log("QIP-249: Updated to version 2"); + + // Link to Snapshot + registry.linkSnapshotProposal(249, "snapshot.org/#/qidao.eth/proposal/0x249test"); + console.log("QIP-249: Linked to Snapshot (auto-status: VotePending)"); + + vm.stopBroadcast(); + + // Editor approves after vote passes (use governance account for editor operations) + vm.startBroadcast(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); + + registry.updateStatus(249, QIPRegistry.QIPStatus.Approved); + console.log("QIP-249: VotePending -> Approved"); + + // Set implementation details + registry.setImplementation(249, "Core Team", block.timestamp + 7 days); + console.log("QIP-249: Implementation scheduled"); + + // Mark as implemented + registry.updateStatus(249, QIPRegistry.QIPStatus.Implemented); + console.log("QIP-249: Approved -> Implemented"); + + // Move QIP-250 to voting + registry.updateStatus(250, QIPRegistry.QIPStatus.ReviewPending); + console.log("\nQIP-250: Draft -> ReviewPending"); + + // Update status for QIP-251 to withdrawn + registry.updateStatus(251, QIPRegistry.QIPStatus.Withdrawn); + console.log("\nQIP-251: Draft -> Withdrawn"); + + vm.stopBroadcast(); + + // Disable migration mode + vm.startBroadcast(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); + registry.disableMigrationMode(); + console.log("\nMigration mode disabled"); + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/scripts/check-ipfs.sh b/scripts/check-ipfs.sh new file mode 100755 index 0000000..bd80c93 --- /dev/null +++ b/scripts/check-ipfs.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Checking IPFS installation...${NC}" + +# Check if IPFS is installed +if command -v ipfs &> /dev/null; then + echo -e "${GREEN}✅ IPFS is installed${NC}" + ipfs version +else + echo -e "${RED}❌ IPFS is not installed${NC}" + echo "" + echo "To install IPFS, you can:" + echo "" + echo "1. Using Homebrew (macOS):" + echo " brew install ipfs" + echo "" + echo "2. Using the official installer:" + echo " Visit https://docs.ipfs.tech/install/" + echo "" + echo "3. Using Go (if you have Go installed):" + echo " go install github.com/ipfs/kubo/cmd/ipfs@latest" + exit 1 +fi + +# Check if IPFS is initialized +if [ -d "$HOME/.ipfs" ]; then + echo -e "${GREEN}✅ IPFS is initialized${NC}" +else + echo -e "${YELLOW}⚠️ IPFS is not initialized${NC}" + echo "Run: ipfs init" +fi + +# Check if IPFS daemon is running +if pgrep -x "ipfs" > /dev/null; then + echo -e "${GREEN}✅ IPFS daemon is running${NC}" + + # Check if API is accessible + if curl -s -X POST http://localhost:5001/api/v0/version > /dev/null 2>&1; then + echo -e "${GREEN}✅ IPFS API is accessible at http://localhost:5001${NC}" + else + echo -e "${RED}❌ IPFS API is not accessible${NC}" + fi + + # Check if Gateway is accessible + if curl -s http://localhost:8080/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG > /dev/null 2>&1; then + echo -e "${GREEN}✅ IPFS Gateway is accessible at http://localhost:8080${NC}" + else + echo -e "${YELLOW}⚠️ IPFS Gateway is not accessible${NC}" + fi +else + echo -e "${YELLOW}⚠️ IPFS daemon is not running${NC}" + echo "To start IPFS daemon:" + echo " ipfs daemon" + echo "" + echo "Or to run in background:" + echo " ipfs daemon &" +fi + +echo "" +echo -e "${YELLOW}IPFS Configuration for CORS (required for browser access):${NC}" +echo "Run these commands to configure IPFS for local development:" +echo "" +echo "ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '[\"http://localhost:8000\", \"http://localhost:8545\", \"*\"]'" +echo "ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '[\"PUT\", \"POST\", \"GET\"]'" +echo "ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '[\"Authorization\"]'" +echo "ipfs config --json API.HTTPHeaders.Access-Control-Expose-Headers '[\"Location\"]'" +echo "ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '[\"true\"]'" \ No newline at end of file diff --git a/scripts/migrate-qips.ts b/scripts/migrate-qips.ts new file mode 100644 index 0000000..de0781e --- /dev/null +++ b/scripts/migrate-qips.ts @@ -0,0 +1,268 @@ +#!/usr/bin/env bun + +import { readdir, readFile } from 'fs/promises'; +import { join } from 'path'; +import { createWalletClient, createPublicClient, http, type Address } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { base, baseSepolia } from 'viem/chains'; +import { QIPClient, QIPStatus } from '../src/services/qipClient'; +import { IPFSService, PinataProvider } from '../src/services/ipfsService'; + +// Configuration +const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; +const QIP_REGISTRY_ADDRESS = process.env.QIP_REGISTRY_ADDRESS as Address; +const PINATA_JWT = process.env.PINATA_JWT || ''; +const PINATA_GATEWAY = process.env.PINATA_GATEWAY || 'https://gateway.pinata.cloud'; +const USE_TESTNET = process.env.USE_TESTNET === 'true'; +const RPC_URL = process.env.BASE_RPC_URL; + +// Paths +const QIP_DIR = join(process.cwd(), 'contents/QIP'); + +interface QIPFrontmatter { + qip: number; + title: string; + network: string; + status: string; + author: string; + implementor: string; + 'implementation-date': string; + proposal: string; + created: string; +} + +async function parseQIPFile(filePath: string): Promise<{ frontmatter: QIPFrontmatter; content: string }> { + const fileContent = await readFile(filePath, 'utf-8'); + const match = fileContent.match(/^---\n([\s\S]+?)\n---\n([\s\S]*)$/); + + if (!match) { + throw new Error(`Invalid QIP format in ${filePath}`); + } + + const yamlContent = match[1]; + const content = match[2].trim(); + + // Parse YAML frontmatter + const frontmatter: any = {}; + const lines = yamlContent.split('\n'); + + for (const line of lines) { + const colonIndex = line.indexOf(':'); + if (colonIndex > 0) { + const key = line.slice(0, colonIndex).trim(); + const value = line.slice(colonIndex + 1).trim(); + frontmatter[key] = value; + } + } + + // Convert qip to number + if (frontmatter.qip) { + frontmatter.qip = parseInt(frontmatter.qip); + } + + return { frontmatter, content }; +} + +function getStatusEnum(statusString: string): QIPStatus { + const statusMap: Record = { + 'Draft': QIPStatus.Draft, + 'Review': QIPStatus.ReviewPending, + 'Review Pending': QIPStatus.ReviewPending, + 'Vote': QIPStatus.VotePending, + 'Vote Pending': QIPStatus.VotePending, + 'Approved': QIPStatus.Approved, + 'Rejected': QIPStatus.Rejected, + 'Implemented': QIPStatus.Implemented, + 'Superseded': QIPStatus.Superseded, + 'Withdrawn': QIPStatus.Withdrawn + }; + + return statusMap[statusString] || QIPStatus.Draft; +} + +async function migrateQIPs() { + console.log('🚀 Starting QIP migration to blockchain...\n'); + + // Validate configuration + if (!PRIVATE_KEY || !QIP_REGISTRY_ADDRESS) { + console.error('❌ Missing required environment variables:'); + console.error(' PRIVATE_KEY, QIP_REGISTRY_ADDRESS'); + process.exit(1); + } + + if (!PINATA_JWT) { + console.error('❌ Missing IPFS provider credentials:'); + console.error(' Please set PINATA_JWT environment variable'); + process.exit(1); + } + + // Setup blockchain clients + const chain = USE_TESTNET ? baseSepolia : base; + const account = privateKeyToAccount(PRIVATE_KEY); + + const walletClient = createWalletClient({ + account, + chain, + transport: http(RPC_URL) + }); + + const publicClient = createPublicClient({ + chain, + transport: http(RPC_URL) + }); + + // Initialize services + const qipClient = new QIPClient(QIP_REGISTRY_ADDRESS, RPC_URL, USE_TESTNET); + qipClient.setWalletClient(walletClient); + + const ipfsProvider = new PinataProvider(PINATA_JWT, PINATA_GATEWAY); + const ipfsService = new IPFSService(ipfsProvider); + + console.log(`📋 Registry Address: ${QIP_REGISTRY_ADDRESS}`); + console.log(`🔗 Network: ${chain.name}`); + console.log(`👤 Migration Account: ${account.address}`); + console.log(`📁 IPFS Provider: Pinata\n`); + + // Get all QIP files + const files = await readdir(QIP_DIR); + const qipFiles = files.filter(f => f.startsWith('QIP-') && f.endsWith('.md')); + + console.log(`Found ${qipFiles.length} QIP files to migrate\n`); + + // Sort by QIP number + qipFiles.sort((a, b) => { + const numA = parseInt(a.match(/QIP-(\d+)/)?.[1] || '0'); + const numB = parseInt(b.match(/QIP-(\d+)/)?.[1] || '0'); + return numA - numB; + }); + + // Migration results + const results = { + success: [] as number[], + failed: [] as { qip: number; error: string }[], + skipped: [] as number[] + }; + + // Process each QIP + for (const file of qipFiles) { + const filePath = join(QIP_DIR, file); + + try { + const { frontmatter, content } = await parseQIPFile(filePath); + console.log(`\n📄 Processing QIP-${frontmatter.qip}: ${frontmatter.title}`); + + // Check if already migrated + try { + const existing = await qipClient.getQIP(BigInt(frontmatter.qip)); + if (existing.qipNumber > 0n) { + console.log(` ⏭️ Already migrated, skipping...`); + results.skipped.push(frontmatter.qip); + continue; + } + } catch { + // QIP doesn't exist on-chain, proceed with migration + } + + // Format full content + const fullContent = `--- +qip: ${frontmatter.qip} +title: ${frontmatter.title} +network: ${frontmatter.network} +status: ${frontmatter.status} +author: ${frontmatter.author} +implementor: ${frontmatter.implementor} +implementation-date: ${frontmatter['implementation-date']} +proposal: ${frontmatter.proposal} +created: ${frontmatter.created} +--- + +${content}`; + + // Upload to IPFS + console.log(' 📤 Uploading to IPFS...'); + const { cid, ipfsUrl, contentHash } = await ipfsService.uploadRawContent(fullContent); + console.log(` ✅ IPFS CID: ${cid}`); + + // Convert dates + const createdTimestamp = new Date(frontmatter.created).getTime() / 1000; + const implTimestamp = frontmatter['implementation-date'] !== 'None' + ? new Date(frontmatter['implementation-date']).getTime() / 1000 + : 0; + + // Prepare author address (use a placeholder if it's a username) + const authorAddress = frontmatter.author.startsWith('0x') + ? frontmatter.author as Address + : '0x0000000000000000000000000000000000000001' as Address; // Placeholder + + // Call migrateQIP function + console.log(' 🔗 Submitting to blockchain...'); + const tx = await walletClient.writeContract({ + address: QIP_REGISTRY_ADDRESS, + abi: [{ + inputs: [ + { name: "_qipNumber", type: "uint256" }, + { name: "_author", type: "address" }, + { name: "_title", type: "string" }, + { name: "_network", type: "string" }, + { name: "_contentHash", type: "bytes32" }, + { name: "_ipfsUrl", type: "string" }, + { name: "_createdAt", type: "uint256" }, + { name: "_status", type: "uint8" }, + { name: "_implementor", type: "string" }, + { name: "_implementationDate", type: "uint256" }, + { name: "_snapshotProposalId", type: "string" } + ], + name: "migrateQIP", + outputs: [], + type: "function" + }], + functionName: 'migrateQIP', + args: [ + BigInt(frontmatter.qip), + authorAddress, + frontmatter.title, + frontmatter.network, + contentHash, + ipfsUrl, + BigInt(Math.floor(createdTimestamp)), + getStatusEnum(frontmatter.status), + frontmatter.implementor, + BigInt(Math.floor(implTimestamp)), + frontmatter.proposal || '' + ] + }); + + console.log(` ⏳ Waiting for transaction: ${tx}`); + await publicClient.waitForTransactionReceipt({ hash: tx }); + console.log(` ✅ QIP-${frontmatter.qip} migrated successfully!`); + + results.success.push(frontmatter.qip); + + // Add delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 2000)); + + } catch (error: any) { + console.error(` ❌ Failed to migrate QIP from ${file}:`, error.message); + const qipNum = parseInt(file.match(/QIP-(\d+)/)?.[1] || '0'); + results.failed.push({ qip: qipNum, error: error.message }); + } + } + + // Summary + console.log('\n\n📊 Migration Summary:'); + console.log(`✅ Successfully migrated: ${results.success.length}`); + console.log(`⏭️ Skipped (already migrated): ${results.skipped.length}`); + console.log(`❌ Failed: ${results.failed.length}`); + + if (results.failed.length > 0) { + console.log('\n❌ Failed QIPs:'); + results.failed.forEach(f => { + console.log(` - QIP-${f.qip}: ${f.error}`); + }); + } + + console.log('\n🎉 Migration complete!'); +} + +// Run migration +migrateQIPs().catch(console.error); \ No newline at end of file diff --git a/scripts/start-anvil-automining.sh b/scripts/start-anvil-automining.sh new file mode 100755 index 0000000..281f472 --- /dev/null +++ b/scripts/start-anvil-automining.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Start Anvil with auto-mining enabled +echo "Starting Anvil with auto-mining enabled..." + +# Kill any existing Anvil process +pkill -f anvil || true + +# Start Anvil with: +# - Auto-mining enabled (default) +# - Block time of 2 seconds +# - Fork from Base mainnet +# - Deterministic addresses +anvil \ + --fork-url https://mainnet.base.org \ + --chain-id 8453 \ + --block-time 2 \ + --accounts 10 \ + --balance 10000 \ + --mnemonic "test test test test test test test test test test test junk" \ + --port 8545 \ + --host 0.0.0.0 + +echo "Anvil started with auto-mining enabled on port 8545" \ No newline at end of file diff --git a/scripts/start-local-dev.sh b/scripts/start-local-dev.sh new file mode 100755 index 0000000..ea86cb5 --- /dev/null +++ b/scripts/start-local-dev.sh @@ -0,0 +1,261 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🚀 Starting QIPs Local Development Environment${NC}" +echo "================================================" + +# Function to cleanup on exit +cleanup() { + echo -e "\n${YELLOW}Cleaning up...${NC}" + if [ ! -z "$ANVIL_PID" ]; then + kill $ANVIL_PID 2>/dev/null + echo "Stopped Anvil (PID: $ANVIL_PID)" + fi + if [ ! -z "$GATSBY_PID" ]; then + kill $GATSBY_PID 2>/dev/null + echo "Stopped Gatsby (PID: $GATSBY_PID)" + fi + if [ ! -z "$IPFS_PID" ]; then + kill $IPFS_PID 2>/dev/null + echo "Stopped IPFS (PID: $IPFS_PID)" + fi + exit +} + +# Set trap to cleanup on script exit +trap cleanup EXIT INT TERM + +# Check dependencies +if ! command -v anvil &> /dev/null; then + echo -e "${RED}❌ Anvil not found. Please install Foundry first.${NC}" + exit 1 +fi + +if ! command -v forge &> /dev/null; then + echo -e "${RED}❌ Forge not found. Please install Foundry first.${NC}" + exit 1 +fi + +if ! command -v bun &> /dev/null; then + echo -e "${RED}❌ Bun not found. Please install Bun first.${NC}" + exit 1 +fi + +# Kill any existing Anvil/Gatsby processes +echo -e "${YELLOW}Stopping any existing processes...${NC}" +pkill -f anvil 2>/dev/null +pkill -f gatsby 2>/dev/null +sleep 2 + +# Check if IPFS should be started +if [ "$USE_LOCAL_IPFS" = "true" ]; then + echo -e "${YELLOW}Checking IPFS...${NC}" + + # Check if IPFS is installed + if ! command -v ipfs &> /dev/null; then + echo -e "${RED}❌ IPFS not found. Please install IPFS first.${NC}" + echo "Installation instructions: https://docs.ipfs.io/install/" + exit 1 + fi + + # Initialize IPFS if not already initialized + if [ ! -d ~/.ipfs ]; then + echo -e "${YELLOW}Initializing IPFS...${NC}" + ipfs init + fi + + # Configure IPFS for local development + echo -e "${YELLOW}Configuring IPFS for local development...${NC}" + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://localhost:8000", "http://localhost:8080", "*"]' 2>/dev/null + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["GET", "POST", "PUT", "DELETE"]' 2>/dev/null + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Content-Type"]' 2>/dev/null + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]' 2>/dev/null + + # Kill any existing IPFS process + pkill -f "ipfs daemon" 2>/dev/null + sleep 2 + + # Start IPFS daemon if not running + if ! pgrep -f "ipfs daemon" > /dev/null; then + echo -e "${YELLOW}Starting IPFS daemon...${NC}" + ipfs daemon > /tmp/ipfs.log 2>&1 & + IPFS_PID=$! + echo "IPFS PID: $IPFS_PID" + + # Wait for IPFS to start + echo -e "${YELLOW}Waiting for IPFS to start...${NC}" + MAX_IPFS_RETRIES=30 + IPFS_RETRY_COUNT=0 + while [ $IPFS_RETRY_COUNT -lt $MAX_IPFS_RETRIES ]; do + if curl -s http://localhost:5001/api/v0/version > /dev/null 2>&1; then + break + fi + IPFS_RETRY_COUNT=$((IPFS_RETRY_COUNT + 1)) + sleep 1 + done + + if [ $IPFS_RETRY_COUNT -eq $MAX_IPFS_RETRIES ]; then + echo -e "${RED}❌ IPFS failed to start${NC}" + exit 1 + fi + + echo -e "${GREEN}✅ IPFS daemon is running on http://localhost:5001${NC}" + else + echo -e "${GREEN}✅ IPFS daemon already running${NC}" + fi +else + echo -e "${YELLOW}Local IPFS disabled, using mock storage${NC}" +fi + +# Use local env file +echo -e "${GREEN}Loading local environment...${NC}" +cp .env.local .env + +# Install forge-std if needed +if [ ! -d "dependencies/forge-std" ]; then + echo -e "${YELLOW}Installing forge-std...${NC}" + forge install foundry-rs/forge-std +fi + +# Start Anvil +echo -e "\n${GREEN}1. Starting Anvil (Base fork)...${NC}" +anvil --fork-url https://mainnet.base.org \ + --accounts 10 \ + --balance 10000 \ + --block-time 2 \ + --port 8545 > /tmp/anvil.log 2>&1 & + +ANVIL_PID=$! +echo "Anvil PID: $ANVIL_PID" + +# Wait for Anvil to start +echo -e "${YELLOW}Waiting for Anvil to start...${NC}" +MAX_RETRIES=30 +RETRY_COUNT=0 +while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + if curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + http://localhost:8545 > /dev/null 2>&1; then + break + fi + RETRY_COUNT=$((RETRY_COUNT + 1)) + sleep 1 +done + +if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + echo -e "${RED}❌ Anvil failed to start${NC}" + exit 1 +fi + +echo -e "${GREEN}✅ Anvil is running on http://localhost:8545${NC}" + +# Deploy contracts using deterministic CREATE2 deployment +echo -e "\n${GREEN}2. Deploying QIP Registry contract (deterministic)...${NC}" +DEPLOY_OUTPUT=$(forge script script/DeployWithStandardCreate2.s.sol:DeployWithStandardCreate2 --rpc-url http://localhost:8545 --broadcast -vvv 2>&1) + +# Extract the registry address from the deploy output +REGISTRY_ADDRESS=$(echo "$DEPLOY_OUTPUT" | grep "QIPRegistry deployed at:" | awk '{print $4}') + +# If not found in deploy output, check if already deployed +if [ -z "$REGISTRY_ADDRESS" ]; then + # Check for "already deployed" message + REGISTRY_ADDRESS=$(echo "$DEPLOY_OUTPUT" | grep "QIPRegistry already deployed at:" | awk '{print $5}') +fi + +# Use the deterministic address as fallback +if [ -z "$REGISTRY_ADDRESS" ]; then + REGISTRY_ADDRESS="0x0CD8BAB86b4db4Ca1363533abaBcdebf53B0F8D7" + echo -e "${YELLOW}Using deterministic registry address: $REGISTRY_ADDRESS${NC}" +fi + +echo -e "${GREEN}✅ QIP Registry at: $REGISTRY_ADDRESS${NC}" + +# Verify the address matches the expected deterministic address +EXPECTED_ADDRESS="0x0CD8BAB86b4db4Ca1363533abaBcdebf53B0F8D7" +if [ "$REGISTRY_ADDRESS" != "$EXPECTED_ADDRESS" ]; then + echo -e "${RED}❌ Registry address mismatch!${NC}" + echo "Expected: $EXPECTED_ADDRESS" + echo "Got: $REGISTRY_ADDRESS" + exit 1 +fi + +# Run initial data setup +echo -e "\n${GREEN}3. Setting up initial test data...${NC}" +QIP_REGISTRY_ADDRESS=$REGISTRY_ADDRESS forge script script/LocalQIPTest.s.sol:LocalQIPTest --rpc-url http://localhost:8545 --broadcast --slow > /tmp/setup.log 2>&1 + +if grep -q "Error" /tmp/setup.log; then + echo -e "${YELLOW}⚠️ Some test data setup had errors (this is normal)${NC}" +else + echo -e "${GREEN}✅ Test data setup complete${NC}" +fi + +# Clean Gatsby cache +echo -e "\n${GREEN}4. Cleaning Gatsby cache...${NC}" +bun run clean + +# Start Gatsby in development mode +echo -e "\n${GREEN}5. Starting Gatsby development server...${NC}" +GATSBY_ENV=development bun run develop & +GATSBY_PID=$! +echo "Gatsby PID: $GATSBY_PID" + +# Wait for Gatsby to start +echo -e "${YELLOW}Waiting for Gatsby to start (this may take a minute)...${NC}" +MAX_GATSBY_RETRIES=60 +GATSBY_RETRY_COUNT=0 +while [ $GATSBY_RETRY_COUNT -lt $MAX_GATSBY_RETRIES ]; do + if curl -s http://localhost:8000 > /dev/null 2>&1; then + break + fi + GATSBY_RETRY_COUNT=$((GATSBY_RETRY_COUNT + 1)) + sleep 2 +done + +if [ $GATSBY_RETRY_COUNT -eq $MAX_GATSBY_RETRIES ]; then + echo -e "${RED}❌ Gatsby failed to start${NC}" + exit 1 +fi + +# Display information +echo -e "\n${GREEN}✅ Local Development Environment Ready!${NC}" +echo "================================================" +echo -e "${BLUE}📋 Services Running:${NC}" +echo "- Anvil (Blockchain): http://localhost:8545" +echo "- Gatsby (Frontend): http://localhost:8000" +echo "- GraphQL Explorer: http://localhost:8000/___graphql" +if [ "$USE_LOCAL_IPFS" = "true" ]; then + echo "- IPFS API: http://localhost:5001" + echo "- IPFS Gateway: http://localhost:8080" +fi +echo "" +echo -e "${BLUE}📊 Contract Information:${NC}" +echo "- QIP Registry: $REGISTRY_ADDRESS" +echo "- Chain ID: 8453 (Base Fork)" +echo "" +echo -e "${BLUE}🔑 Test Accounts:${NC}" +echo "- Governance: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +echo "- Editor: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +echo "- Author1: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" +echo "- Author2: 0x90F79bf6EB2c4f870365E785982E1f101E93b906" +echo "" +echo -e "${BLUE}📝 Initial QIPs:${NC}" +echo "- QIP-249: Dynamic Interest Rates (by Author1)" +echo "- QIP-250: Multi-Collateral Support (by Author2)" +echo "- QIP-251: Staking Rewards (by Author3)" +echo "" +echo -e "${YELLOW}💡 Tips:${NC}" +echo "- Connect your wallet to http://localhost:8545" +echo "- Import test accounts using private keys from Anvil" +echo "- Visit http://localhost:8000/create-proposal to create new QIPs" +echo "- All changes are local and will be reset on restart" +echo "" +echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}" + +# Keep script running and show logs +tail -f /tmp/anvil.log \ No newline at end of file diff --git a/scripts/startAnvil.sh b/scripts/startAnvil.sh new file mode 100755 index 0000000..80c2f97 --- /dev/null +++ b/scripts/startAnvil.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🚀 Starting QIP Registry Local Testing Environment${NC}" +echo "================================================" + +# Check if anvil is installed +if ! command -v anvil &> /dev/null; then + echo -e "${YELLOW}⚠️ Anvil not found. Please install Foundry first:${NC}" + echo "curl -L https://foundry.paradigm.xyz | bash" + echo "foundryup" + exit 1 +fi + +# Check if forge is installed +if ! command -v forge &> /dev/null; then + echo -e "${YELLOW}⚠️ Forge not found. Please install Foundry first.${NC}" + exit 1 +fi + +# Check if .env file exists, if not create from example +if [ ! -f .env ]; then + if [ -f .env.example ]; then + echo -e "${YELLOW}Creating .env file from .env.example...${NC}" + cp .env.example .env + # Update .env with local values + sed -i.bak 's|PRIVATE_KEY=.*|PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80|' .env + sed -i.bak 's|BASE_RPC_URL=.*|BASE_RPC_URL=http://localhost:8545|' .env + sed -i.bak 's|GATSBY_BASE_RPC_URL=.*|GATSBY_BASE_RPC_URL=http://localhost:8545|' .env + sed -i.bak 's|BASESCAN_API_KEY=.*|BASESCAN_API_KEY=dummy_key_for_local_testing|' .env + rm .env.bak + else + echo -e "${YELLOW}⚠️ .env file not found. Creating minimal .env...${NC}" + cat > .env << EOF +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +BASE_RPC_URL=http://localhost:8545 +BASESCAN_API_KEY=dummy_key_for_local_testing +EOF + fi +fi + +# Source environment variables +source .env + +# Check if forge-std is installed +if [ ! -d "dependencies/forge-std" ]; then + echo -e "${YELLOW}Installing forge-std dependency...${NC}" + forge install foundry-rs/forge-std +fi + +# Start Anvil in the background +echo -e "\n${GREEN}1. Starting Anvil...${NC}" +anvil --fork-url https://mainnet.base.org \ + --accounts 10 \ + --balance 10000 \ + --block-time 2 \ + --port 8545 & + +ANVIL_PID=$! +echo "Anvil PID: $ANVIL_PID" + +# Wait for Anvil to start +echo -e "${YELLOW}Waiting for Anvil to start...${NC}" + +# Wait for Anvil to be ready by checking if it responds to RPC calls +MAX_RETRIES=30 +RETRY_COUNT=0 +while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + if curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + http://localhost:8545 > /dev/null 2>&1; then + break + fi + RETRY_COUNT=$((RETRY_COUNT + 1)) + sleep 1 +done + +if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + echo -e "${YELLOW}❌ Anvil failed to start within 30 seconds${NC}" + kill $ANVIL_PID 2>/dev/null + exit 1 +fi + +# Check if Anvil is running +if ! kill -0 $ANVIL_PID 2>/dev/null; then + echo -e "${YELLOW}❌ Anvil process died${NC}" + exit 1 +fi + +echo -e "${GREEN}✅ Anvil is running on http://localhost:8545${NC}" + +# Deploy contracts +echo -e "\n${GREEN}2. Deploying QIP Registry contract...${NC}" +DEPLOY_OUTPUT=$(forge script script/LocalQIPTest.s.sol:LocalQIPTest --rpc-url http://localhost:8545 --broadcast -vvv 2>&1) + +# Extract registry address from output +REGISTRY_ADDRESS=$(echo "$DEPLOY_OUTPUT" | grep -o "QIPRegistry deployed at: 0x[a-fA-F0-9]\{40\}" | grep -o "0x[a-fA-F0-9]\{40\}") + +if [ -z "$REGISTRY_ADDRESS" ]; then + echo -e "${YELLOW}❌ Failed to deploy contracts${NC}" + echo "Deploy output:" + echo "$DEPLOY_OUTPUT" + kill $ANVIL_PID + exit 1 +fi + +echo -e "${GREEN}✅ QIP Registry deployed at: $REGISTRY_ADDRESS${NC}" + +# Update the TypeScript file with the correct address +echo -e "\n${GREEN}3. Updating TypeScript client with contract address...${NC}" +sed -i.bak "s/const REGISTRY_ADDRESS = '0x[a-fA-F0-9]*'/const REGISTRY_ADDRESS = '$REGISTRY_ADDRESS'/" src/localQIPTest.ts +rm src/localQIPTest.ts.bak + +# Display test accounts +echo -e "\n${BLUE}📋 Test Accounts:${NC}" +echo "================================================" +echo "Governance: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)" +echo "Editor: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)" +echo "Author1: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)" +echo "Author2: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)" +echo "Author3: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)" + +echo -e "\n${BLUE}📊 Initial QIP State:${NC}" +echo "================================================" +echo "QIP-249: Dynamic Interest Rates (Draft → Implemented)" +echo "QIP-250: Multi-Collateral Support (Draft → ReviewPending)" +echo "QIP-251: Staking Rewards (Draft → Withdrawn)" +echo "QIP-100: Historical - Protocol Launch (Implemented)" +echo "QIP-150: Historical - Rejected Proposal (Rejected)" +echo "QIP-200: Historical - Superseded Update (Superseded)" + +echo -e "\n${GREEN}✅ Setup Complete!${NC}" +echo "================================================" +echo -e "${YELLOW}To run the TypeScript test client:${NC}" +echo " bun run src/localQIPTest.ts" +echo "" +echo -e "${YELLOW}To interact with the contract using cast:${NC}" +echo " # Get next QIP number" +echo " cast call $REGISTRY_ADDRESS \"nextQIPNumber()\" --rpc-url http://localhost:8545" +echo "" +echo " # Get QIP details (e.g., QIP-249)" +echo " cast call $REGISTRY_ADDRESS \"qips(uint256)\" 249 --rpc-url http://localhost:8545" +echo "" +echo -e "${YELLOW}To stop Anvil:${NC}" +echo " kill $ANVIL_PID" +echo "" +echo -e "${BLUE}Anvil is running in the background (PID: $ANVIL_PID)${NC}" +echo -e "${YELLOW}Press Ctrl+C to stop watching logs${NC}" +echo "" + +# Keep script running and show Anvil logs +trap "kill $ANVIL_PID; exit" INT +wait $ANVIL_PID \ No newline at end of file diff --git a/soldeer.toml b/soldeer.toml new file mode 100644 index 0000000..ac82e27 --- /dev/null +++ b/soldeer.toml @@ -0,0 +1,5 @@ +[remappings] +enabled = true + +[dependencies] +"@forge-std" = { version = "1.9.5" } \ No newline at end of file diff --git a/src/components/LocalModeBanner.tsx b/src/components/LocalModeBanner.tsx new file mode 100644 index 0000000..89079c7 --- /dev/null +++ b/src/components/LocalModeBanner.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const LocalModeBanner: React.FC = () => { + const isLocalMode = process.env.GATSBY_LOCAL_MODE === 'true'; + + if (!isLocalMode || typeof window === 'undefined') { + return null; + } + + return ( +
+ 🚧 + Local Development Mode - Connected to Anvil at localhost:8545 + 🚧 +
+ ); +}; + +export default LocalModeBanner; \ No newline at end of file diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 5e9bb27..fb5043c 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -6,8 +6,13 @@ const Navigation = () => { const [isClient, setIsClient] = useState(false); useEffect(() => { + console.log("🔍 Navigation Debug:"); + console.log("- Setting isClient to true"); setIsClient(true); }, []); + + console.log("- isClient:", isClient); + console.log("- ConnectKitButton will render:", isClient); return (