diff --git a/.changeset/fancy-pets-bet.md b/.changeset/fancy-pets-bet.md new file mode 100644 index 000000000..0223be8bf --- /dev/null +++ b/.changeset/fancy-pets-bet.md @@ -0,0 +1,5 @@ +--- +"ensadmin": patch +--- + +Fixes an issue where `?name=` param stays on page regardless of route. diff --git a/.changeset/sixty-mugs-notice.md b/.changeset/sixty-mugs-notice.md new file mode 100644 index 000000000..cfbff739a --- /dev/null +++ b/.changeset/sixty-mugs-notice.md @@ -0,0 +1,5 @@ +--- +"@namehash/ens-referrals": minor +--- + +Moves referral program status business logic to ens-referrals. diff --git a/.github/actions/setup_node_environment/action.yml b/.github/actions/setup_node_environment/action.yml new file mode 100644 index 000000000..f59392408 --- /dev/null +++ b/.github/actions/setup_node_environment/action.yml @@ -0,0 +1,19 @@ +name: 'Setup Node Environment' +description: 'Sets up pnpm, Node.js, and installs dependencies' +runs: + using: 'composite' + steps: + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: useblacksmith/setup-node@v5 + with: + node-version-file: .nvmrc + # NOTE: This registry-url is required so NODE_AUTH_TOKEN / NPM_TOKEN work for npm auth, + # especially in workflows that publish packages. Do not remove unless auth is no longer needed. + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile diff --git a/.github/workflows/build_ensnode.yml b/.github/workflows/build_ensnode.yml deleted file mode 100644 index 586ec35a2..000000000 --- a/.github/workflows/build_ensnode.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: "Build: ENSNode" - -on: - workflow_dispatch: - inputs: - version: - description: "ENSNode Version (Docker Tag)" - type: string - required: true - -jobs: - build-and-push-ensnode: - name: ${{ matrix.app }} - runs-on: blacksmith-4vcpu-ubuntu-2204 - strategy: - fail-fast: false - matrix: - app: [ensindexer, ensadmin, ensrainbow, ensapi] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Build & Push - uses: ./.github/actions/build_docker_image - with: - image: ghcr.io/${{ github.repository }}/${{ matrix.app }} - dockerfile: apps/${{ matrix.app }}/Dockerfile - registry_user: ${{ github.actor }} - registry_token: ${{ secrets.GITHUB_TOKEN }} - tags: | - type=semver,pattern={{version}},value=${{ inputs.version }} - type=ref,event=branch - type=sha - - publish-lambdas: - name: ${{ matrix.lambda }} - runs-on: blacksmith-4vcpu-ubuntu-2204 - strategy: - fail-fast: false - matrix: - lambda: [fallback-ensapi] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Build and Publish Lambda - uses: ./.github/actions/build_and_publish_lambda - with: - name: ${{ matrix.lambda }} - version: ${{ inputs.version }} diff --git a/.github/workflows/release-npm-rc.yml b/.github/workflows/release-npm-rc.yml deleted file mode 100644 index 841035ca0..000000000 --- a/.github/workflows/release-npm-rc.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Release NPM RC - -on: - workflow_dispatch: - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - publish-rc: - name: Publish Release Candidate Packages to NPM - if: github.repository == 'namehash/ensnode' - runs-on: blacksmith-4vcpu-ubuntu-2204 - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: useblacksmith/setup-node@v5 - with: - node-version-file: .nvmrc - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Enter pre-release mode - run: pnpm changeset pre enter rc - - - name: Version packages - run: pnpm changeset version - - - name: Build packages - run: pnpm packages:prepublish - - - name: Publish to NPM with rc tag - run: pnpm changeset publish --tag rc - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Summary - run: | - echo "✅ Release candidate packages published to NPM with 'rc' tag" - echo "" - echo "Install with: pnpm install @ensnode/package-name@rc" - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc7e89872..e2dbb4e29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,37 @@ +# Full Release Workflow (@latest) +# +# This workflow creates stable releases of ENSNode packages and Docker images. +# It is triggered automatically when the Changesets Release PR is merged to main. +# +# How it works: +# 1. When PRs with changesets are merged to main, the Changesets bot creates/updates a Release PR +# 2. When the Release PR is merged, this workflow: +# - Publishes NPM packages with @latest tag +# - Builds and publishes Docker images to GitHub Container Registry with @latest tag +# - Creates a GitHub release with version tags and release notes +# - Sends Slack notification +# +# Published artifacts: +# - NPM packages: @ensnode/* packages published to npm registry with @latest tag +# - Docker images: ensindexer, ensadmin, ensapi, ensrainbow published to ghcr.io with @latest tag +# - GitHub Release: Created with version tag (e.g., v1.2.3) and autogenerated release notes +# +# Version management: +# - All ENSNode packages use "fixed" versioning - they all advance to the same version +# - The version is determined by the changesets included in the Release PR +# - Version bump type (major/minor/patch) is based on the changeset severity levels +# +# Important notes: +# - Only Full Releases are considered stable for production use +# - Only NameHash Labs ensnode team members can merge the Release PR +# - This workflow does NOT run on regular commits to main (see release_snapshot.yml for that) +# - GitHub releases and tags are only created for Full Releases +# +# Documentation: https://ensnode.io/docs/contributing/releases#full-release + name: Release on: - workflow_dispatch: push: branches: - main @@ -27,16 +57,7 @@ jobs: with: fetch-depth: 1 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: useblacksmith/setup-node@v5 - with: - node-version-file: .nvmrc - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup_node_environment - name: Create Release Pull Request or Publish to npm uses: changesets/action@v1.4.10 diff --git a/.github/workflows/release_preview.yml b/.github/workflows/release_preview.yml new file mode 100644 index 000000000..390c6acc5 --- /dev/null +++ b/.github/workflows/release_preview.yml @@ -0,0 +1,431 @@ +# Release Preview Workflow (@preview-*) +# +# This workflow creates preview releases for testing features before they are merged to main. +# It must be triggered manually via the GitHub Actions UI. +# +# Requirements: +# - The branch must have an open PR targeting main +# - Cannot be run on the main branch +# +# How to trigger: +# 1. Navigate to Actions > Release Preview in GitHub +# 2. Click "Run workflow" and select: +# - Branch: The branch you want to create a preview release from (must have an open PR) +# - Publish target: +# - npm-only: NPM packages only +# - npm-and-lambdas: NPM packages + Lambda functions +# - npm-and-ghcr: NPM packages + Docker images +# - all: NPM packages + Lambda functions + Docker images +# - Custom tag suffix (optional): Custom tag suffix instead of branch name +# +# What it does: +# 1. Validates the branch (cannot run on main) and verifies an open PR exists +# 2. Generates a sanitized tag from the branch name or custom tag suffix (e.g., @preview-feat-add-api-route) +# 3. Versions packages using changesets in snapshot mode +# 4. Publishes NPM packages with @preview-* dist-tag +# 5. Optionally builds and publishes Lambda functions as GitHub Actions artifacts +# 6. Optionally builds and publishes Docker images with preview-* tag +# 7. Posts/updates a comment on the PR with installation instructions +# +# Published artifacts: +# - NPM packages: Published with @preview-{branch-name} or @preview-{custom-suffix} tag +# - Lambda functions (optional): Published as GitHub Actions artifacts +# - Docker images (optional): Published with preview-{branch-name} or preview-{custom-suffix} tag +# - No GitHub releases or tags are created +# +# Use cases: +# - Testing PR features before merging to main +# - Collaborating on development work +# - Validating fixes for specific issues +# - QA testing of work-in-progress features +# +# Important notes: +# - Preview releases require an open PR targeting main +# - Preview releases are NOT stable and should not be used in production +# - They are ephemeral and don't modify the repository (no commits/tags in git) +# - Only authorized NameHash team members can trigger preview releases +# - Each preview release will update the PR comment with installation instructions +# - Installation: npm install @ensnode/package-name@preview-branch-name +# - Cleanup: Use npm dist-tag rm to manually remove preview tags when no longer needed +# +# Documentation: https://ensnode.io/docs/contributing/releases#preview-release + +name: Release Preview (@preview) + +on: + workflow_dispatch: + inputs: + custom_suffix: + description: 'Optional custom suffix to append to the tag assigned to all assets published in the release preview. If not defined, defaults to the name of the branch associated with the release preview.' + type: string + required: false + publish_target: + description: 'Where should the release preview be published?' + type: choice + required: true + default: 'all' + options: + - npm-only + - npm-and-lambdas + - npm-and-ghcr + - all + +concurrency: + group: prerelease-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + validate-and-prepare: + name: Validate Branch & Prepare + runs-on: blacksmith-4vcpu-ubuntu-2204 + if: github.repository == 'namehash/ensnode' + outputs: + branch-suffix: ${{ steps.prepare.outputs.branch-suffix }} + dist-tag: ${{ steps.prepare.outputs.dist-tag }} + snapshot-tag: ${{ steps.prepare.outputs.snapshot-tag }} + docker-tag-base: ${{ steps.prepare.outputs.docker-tag-base }} + commit-sha: ${{ steps.prepare.outputs.commit-sha }} + pr-number: ${{ steps.validate.outputs.pr-number }} + pr-title: ${{ steps.validate.outputs.pr-title }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate branch and PR + id: validate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [[ "${{ github.ref_name }}" == "main" ]]; then + echo "❌ Cannot run prerelease workflow on main branch" + echo "Use the snapshot workflow instead for main branch previews" + exit 1 + fi + echo "✅ Branch validation passed: ${{ github.ref_name }}" + + # Require an open PR for this branch + PR_DATA=$(gh pr list --state=open --head="${{ github.ref_name }}" --base=main --json number,title --limit 1) + + if [[ "$PR_DATA" == "[]" || -z "$PR_DATA" ]]; then + echo "❌ No open PR found for branch '${{ github.ref_name }}'" + echo "Preview releases require an open PR targeting main." + echo "Please create a PR for this branch before running a preview release." + exit 1 + fi + + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number') + PR_TITLE=$(echo "$PR_DATA" | jq -r '.[0].title') + echo "pr-number=${PR_NUMBER}" >> $GITHUB_OUTPUT + echo "pr-title=${PR_TITLE}" >> $GITHUB_OUTPUT + echo "✅ Found PR #${PR_NUMBER}: ${PR_TITLE}" + + - name: Prepare tags and identifiers + id: prepare + run: | + # Get short commit SHA + COMMIT_SHA=$(git rev-parse --short HEAD) + + # Sanitize branch name for use in versions and tags + if [[ -n "${{ inputs.custom_suffix }}" ]]; then + BRANCH_SUFFIX="${{ inputs.custom_suffix }}" + else + BRANCH_SUFFIX="${{ github.ref_name }}" + fi + # Replace / with - + BRANCH_SUFFIX="${BRANCH_SUFFIX//\//-}" + # Replace special characters with - + BRANCH_SUFFIX="${BRANCH_SUFFIX//[^a-zA-Z0-9-]/-}" + # Convert to lowercase + BRANCH_SUFFIX=$(echo "$BRANCH_SUFFIX" | tr '[:upper:]' '[:lower:]') + # Remove consecutive dashes (repeat until no more changes) + while [[ "$BRANCH_SUFFIX" =~ --+ ]]; do + BRANCH_SUFFIX="${BRANCH_SUFFIX//--/-}" + done + # Truncate to 30 characters + BRANCH_SUFFIX="${BRANCH_SUFFIX:0:30}" + # Remove trailing dashes + BRANCH_SUFFIX="${BRANCH_SUFFIX%-}" + # Remove leading dashes + BRANCH_SUFFIX="${BRANCH_SUFFIX#-}" + + # Create dist-tag (NPM doesn't allow dots in tags) + DIST_TAG="preview-${BRANCH_SUFFIX}" + + # Snapshot tag for changesets (used in version string) + SNAPSHOT_TAG="preview-${BRANCH_SUFFIX}" + + # Docker tag base (no version, just branch) + DOCKER_TAG_BASE="preview-${BRANCH_SUFFIX}" + + echo "branch-suffix=${BRANCH_SUFFIX}" >> $GITHUB_OUTPUT + echo "dist-tag=${DIST_TAG}" >> $GITHUB_OUTPUT + echo "snapshot-tag=${SNAPSHOT_TAG}" >> $GITHUB_OUTPUT + echo "docker-tag-base=${DOCKER_TAG_BASE}" >> $GITHUB_OUTPUT + echo "commit-sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT + + echo "🏷️ NPM dist-tag: ${DIST_TAG}" + echo "📸 Snapshot tag: ${SNAPSHOT_TAG}" + echo "🐳 Docker tag base: ${DOCKER_TAG_BASE}" + echo "📝 Commit SHA: ${COMMIT_SHA}" + + build-and-publish-npm: + name: Build & Publish Release Preview Packages to NPM + runs-on: blacksmith-4vcpu-ubuntu-2204 + needs: validate-and-prepare + if: inputs.publish_target == 'npm-only' || inputs.publish_target == 'npm-and-lambdas' || inputs.publish_target == 'npm-and-ghcr' || inputs.publish_target == 'all' + permissions: + contents: read + id-token: write + outputs: + published-version: ${{ steps.publish.outputs.published-version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: ./.github/actions/setup_node_environment + + - name: Build packages + run: pnpm packages:prepublish + + - name: Version packages with changesets (snapshot) + run: | + # Use changesets to create snapshot versions + # The snapshot tag will be used as part of the version string + pnpm changeset version --snapshot ${{ needs.validate-and-prepare.outputs.snapshot-tag }} + + echo "📦 Packages versioned with snapshot tag: ${{ needs.validate-and-prepare.outputs.snapshot-tag }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish release preview packages to NPM with changesets + id: publish + run: | + # Publish with changesets using snapshot mode + # --no-git-tag: Don't create git tags for preview releases + # --snapshot: Publish as snapshot/preview release + # --tag: Use our custom dist-tag + pnpm changeset publish --no-git-tag --snapshot --tag ${{ needs.validate-and-prepare.outputs.dist-tag }} + + # Extract published version from package.json + PUBLISHED_VERSION=$(node -p "require('./packages/ensnode-sdk/package.json').version") + echo "published-version=${PUBLISHED_VERSION}" >> $GITHUB_OUTPUT + + echo "✅ Published release preview packages to NPM with version: ${PUBLISHED_VERSION}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + build-and-publish-ghcr: + name: Build & Publish Docker Images to GHCR + runs-on: blacksmith-4vcpu-ubuntu-2204 + needs: [validate-and-prepare, build-and-publish-npm] + if: inputs.publish_target == 'npm-and-ghcr' || inputs.publish_target == 'all' + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + app: [ensindexer, ensadmin, ensapi, ensrainbow] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build & Publish Docker Image + uses: ./.github/actions/build_docker_image + with: + image: ghcr.io/${{ github.repository }}/${{ matrix.app }} + dockerfile: apps/${{ matrix.app }}/Dockerfile + registry_user: ${{ github.actor }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + tags: | + type=raw,value=${{ needs.validate-and-prepare.outputs.docker-tag-base }} + type=raw,value=${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} + type=sha + + build-and-publish-lambdas: + name: Build & Publish Preview Lambdas + runs-on: blacksmith-4vcpu-ubuntu-2204 + needs: [validate-and-prepare, build-and-publish-npm] + if: inputs.publish_target == 'npm-and-lambdas' || inputs.publish_target == 'all' + strategy: + fail-fast: false + matrix: + lambda: [fallback-ensapi] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build and Publish Lambda + uses: ./.github/actions/build_and_publish_lambda + with: + name: ${{ matrix.lambda }} + version: ${{ needs.validate-and-prepare.outputs.dist-tag }} + + update-pr-comment: + name: Update PR Comment + runs-on: blacksmith-4vcpu-ubuntu-2204 + needs: [validate-and-prepare, build-and-publish-npm, build-and-publish-ghcr, build-and-publish-lambdas] + if: always() && needs.build-and-publish-npm.result == 'success' + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Generate PR comment body + id: comment + run: | + # Build timestamp for display + BUILD_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC") + + # Start building comment + COMMENT_BODY="## 🚀 Preview Packages - \`${{ github.ref_name }}\` + + " + + # NPM section - always present since both options include NPM + if [[ "${{ needs.build-and-publish-npm.result }}" == "success" ]]; then + COMMENT_BODY+="**NPM Packages:** + \`\`\`bash + # Install latest preview for this branch + pnpm add @ensnode/datasources@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ensnode-react@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ensrainbow-sdk@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ensnode-schema@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ensnode-sdk@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ponder-subgraph@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ponder-metadata@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ens-referrals@${{ needs.validate-and-prepare.outputs.dist-tag }} + + # Or install specific version + pnpm add @ensnode/ensnode-sdk@${{ needs.build-and-publish-npm.outputs.published-version }} + \`\`\` + + " + fi + + # Lambdas section - only if lambdas were requested + if [[ "${{ inputs.publish_target }}" == "npm-and-lambdas" || "${{ inputs.publish_target }}" == "all" ]]; then + if [[ "${{ needs.build-and-publish-lambdas.result }}" == "success" ]]; then + COMMENT_BODY+="**Lambda Functions:** + \`\`\` + Published as GitHub Actions artifacts: + - fallback-ensapi-${{ needs.validate-and-prepare.outputs.dist-tag }} + + Download from: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + \`\`\` + + " + else + COMMENT_BODY+="**Lambda Functions:** ❌ Build failed - see [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + " + fi + fi + + # GHCR section - only if ghcr was requested + if [[ "${{ inputs.publish_target }}" == "npm-and-ghcr" || "${{ inputs.publish_target }}" == "all" ]]; then + if [[ "${{ needs.build-and-publish-ghcr.result }}" == "success" ]]; then + COMMENT_BODY+="**Docker Images:** + \`\`\`bash + docker pull ghcr.io/namehash/ensnode/ensindexer:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} + docker pull ghcr.io/namehash/ensnode/ensadmin:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} + docker pull ghcr.io/namehash/ensnode/ensapi:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} + docker pull ghcr.io/namehash/ensnode/ensrainbow:${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }} + \`\`\` + + " + else + COMMENT_BODY+="**Docker Images:** ❌ Build failed - see [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + " + fi + fi + + # Publish target info + PUBLISH_TARGET_DISPLAY="" + if [[ "${{ inputs.publish_target }}" == "npm-only" ]]; then + PUBLISH_TARGET_DISPLAY="📦 NPM packages only" + elif [[ "${{ inputs.publish_target }}" == "npm-and-lambdas" ]]; then + PUBLISH_TARGET_DISPLAY="📦 NPM packages + ⚡ Lambda functions" + elif [[ "${{ inputs.publish_target }}" == "npm-and-ghcr" ]]; then + PUBLISH_TARGET_DISPLAY="📦 NPM packages + 🐳 Docker images" + elif [[ "${{ inputs.publish_target }}" == "all" ]]; then + PUBLISH_TARGET_DISPLAY="📦 NPM packages + ⚡ Lambda functions + 🐳 Docker images" + fi + + # Build info + COMMENT_BODY+="**Build Info:** + - 🎯 Target: \`${PUBLISH_TARGET_DISPLAY}\` + - 📦 Version: \`${{ needs.build-and-publish-npm.outputs.published-version }}\` + - 📝 Commit: \`${{ needs.validate-and-prepare.outputs.commit-sha }}\` + - 🌿 Branch: \`${{ github.ref_name }}\` + - ⏰ Built: \`${BUILD_TIME}\` + - 🔗 [Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + --- + *🤖 This comment will be updated on subsequent publishing of release previews from the branch associated with this PR* + + > **Note:** Preview packages are managed by changesets. NPM dist-tags can be cleaned up manually using \`npm dist-tag rm @ensnode/ensnode-sdk ${{ needs.validate-and-prepare.outputs.dist-tag }}\`" + + # Save to file for proper multiline handling + echo "$COMMENT_BODY" > comment_body.txt + echo "comment-file=comment_body.txt" >> $GITHUB_OUTPUT + + - name: Find existing comment + id: find-comment + run: | + # Look for existing comment from github-actions bot + COMMENT_ID=$(gh pr view ${{ needs.validate-and-prepare.outputs.pr-number }} --json comments --jq '.comments[] | select(.author.login == "github-actions" and (.body | contains("🚀 Preview Packages"))) | .id' | head -1) + + if [[ -n "$COMMENT_ID" ]]; then + echo "existing-comment-id=${COMMENT_ID}" >> $GITHUB_OUTPUT + echo "✅ Found existing comment ID: ${COMMENT_ID}" + else + echo "ℹ️ No existing comment found, will create new one" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update or create PR comment + run: | + if [[ -n "${{ steps.find-comment.outputs.existing-comment-id }}" ]]; then + # Update existing comment + gh pr comment ${{ needs.validate-and-prepare.outputs.pr-number }} --edit-last --body-file ${{ steps.comment.outputs.comment-file }} + echo "✅ Updated existing PR comment" + else + # Create new comment + gh pr comment ${{ needs.validate-and-prepare.outputs.pr-number }} --body-file ${{ steps.comment.outputs.comment-file }} + echo "✅ Created new PR comment" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Log summary + run: | + echo "## 📋 Preview Release Summary" + echo "- Branch: ${{ github.ref_name }}" + echo "- PR: #${{ needs.validate-and-prepare.outputs.pr-number }}" + echo "- Publish target: ${{ inputs.publish_target }}" + echo "- Published version: ${{ needs.build-and-publish-npm.outputs.published-version }}" + echo "- NPM dist-tag: ${{ needs.validate-and-prepare.outputs.dist-tag }}" + echo "- NPM publish: ${{ needs.build-and-publish-npm.result }}" + if [[ "${{ inputs.publish_target }}" == "npm-and-lambdas" || "${{ inputs.publish_target }}" == "all" ]]; then + echo "- Lambda publish: ${{ needs.build-and-publish-lambdas.result }}" + fi + if [[ "${{ inputs.publish_target }}" == "npm-and-ghcr" || "${{ inputs.publish_target }}" == "all" ]]; then + echo "- Docker tag: ${{ needs.validate-and-prepare.outputs.docker-tag-base }}-${{ needs.validate-and-prepare.outputs.commit-sha }}" + echo "- GHCR publish: ${{ needs.build-and-publish-ghcr.result }}" + fi diff --git a/.github/workflows/release_snapshot.yml b/.github/workflows/release_snapshot.yml new file mode 100644 index 000000000..f06a843ec --- /dev/null +++ b/.github/workflows/release_snapshot.yml @@ -0,0 +1,146 @@ +# Release Snapshot Workflow (@next) +# +# This workflow automatically creates snapshot releases for every commit to main. +# It allows users to install and test the latest features before they become full releases. +# +# How it works: +# 1. Triggered automatically on every push to main (except (full) Release PR merges) +# 2. Builds all packages +# 3. Versions packages using changesets based on the merged PR's changeset +# 4. Publishes NPM packages with @next tag +# 5. Builds and publishes Docker images with @next tag to GitHub Container Registry +# +# Published artifacts: +# - NPM packages: All @ensnode/* packages published with @next tag +# - Docker images: ensindexer, ensadmin, ensapi, ensrainbow published with @next tag +# - No GitHub releases or tags are created +# +# Version behavior: +# - Packages advance to the version indicated in the changeset of the merged PR +# - All packages use "fixed" versioning and advance together +# - Each snapshot represents the pre-stable state of main +# +# Use cases: +# - Development and testing environments +# - Testing upcoming features before stable release +# - Experimenting with new functionality +# - Integration testing with latest main branch +# +# Important notes: +# - Snapshot releases are NOT stable and should be used with caution +# - Only use @next for development/testing, not production +# - Automatically triggered - cannot be run manually +# - Only runs if there are changes to publish (hasChanges check) +# - Requires PR to be approved by ensnode team (main branch is protected) +# - Installation: npm install @ensnode/package-name@next +# +# Documentation: https://ensnode.io/docs/contributing/releases#snapshot-release + +name: Release Snapshot (@next) + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + build-and-publish-npm: + name: Build & Publish Snapshot Packages to NPM + if: github.repository == 'namehash/ensnode' && !contains(github.event.head_commit.message, 'changeset-release/main') + runs-on: blacksmith-4vcpu-ubuntu-2204 + permissions: + contents: read + id-token: write + outputs: + hasChanges: ${{ steps.snapshot.outputs.hasChanges }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: ./.github/actions/setup_node_environment + + - name: Build snapshot packages + run: pnpm packages:prepublish + + - name: Version snapshot packages + id: snapshot + run: | + pnpm changeset:next + # Check if there are any changes to publish + if [ -n "$(git status --porcelain)" ]; then + echo "hasChanges=true" >> $GITHUB_OUTPUT + else + echo "hasChanges=false" >> $GITHUB_OUTPUT + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish snapshot packages to NPM + if: steps.snapshot.outputs.hasChanges == 'true' + run: pnpm changeset-publish:next + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + build-and-publish-ghcr: + name: Build & Publish Snapshot Docker Images to GHCR + runs-on: blacksmith-4vcpu-ubuntu-2204 + if: github.repository == 'namehash/ensnode' && !contains(github.event.head_commit.message, 'changeset-release/main') && needs.snapshot.outputs.hasChanges == 'true' + needs: [build-and-publish-npm] + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + app: [ensindexer, ensadmin, ensrainbow, ensapi] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Generate docker image snapshot tag + id: snapshot-tag + run: | + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + SHORT_SHA=$(git rev-parse --short HEAD) + SNAPSHOT_TAG="next-${TIMESTAMP}-${SHORT_SHA}" + echo "tag=$SNAPSHOT_TAG" >> $GITHUB_OUTPUT + + - name: Build and publish snapshot docker images to GHCR + uses: ./.github/actions/build_docker_image + with: + image: ghcr.io/${{ github.repository }}/${{ matrix.app }} + dockerfile: apps/${{ matrix.app }}/Dockerfile + registry_user: ${{ github.actor }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + tags: | + type=raw,value=next + type=raw,value=${{ steps.snapshot-tag.outputs.tag }} + type=sha + + build-and-publish-lambdas: + name: Build & Publish Snapshot Lambdas + runs-on: blacksmith-4vcpu-ubuntu-2204 + if: github.repository == 'namehash/ensnode' && !contains(github.event.head_commit.message, 'changeset-release/main') && needs.snapshot.outputs.hasChanges == 'true' + needs: [build-and-publish-npm] + strategy: + fail-fast: false + matrix: + lambda: [fallback-ensapi] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build and Publish Lambda + uses: ./.github/actions/build_and_publish_lambda + with: + name: ${{ matrix.lambda }} + version: next diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index cb0b82812..1e4419d58 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -13,10 +13,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: useblacksmith/setup-node@v5 - with: - node-version-file: .nvmrc + - uses: ./.github/actions/setup_node_environment - run: pnpm audit --audit-level=moderate prepublish: @@ -24,11 +21,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: useblacksmith/setup-node@v5 - with: - node-version-file: .nvmrc - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup_node_environment - run: pnpm packages:prepublish static-analysis: @@ -36,11 +29,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: useblacksmith/setup-node@v5 - with: - node-version-file: .nvmrc - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup_node_environment - name: Run Biome CI run: pnpm lint:ci @@ -56,11 +45,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: useblacksmith/setup-node@v5 - with: - node-version-file: .nvmrc - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup_node_environment - run: pnpm test integrity-check: @@ -83,11 +68,7 @@ jobs: --health-retries 5 steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: useblacksmith/setup-node@v5 - with: - node-version-file: .nvmrc - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup_node_environment # This will run the dev command in background, and wait up to # HEALTH_CHECK_TIMEOUT seconds. It will monitor the log output to diff --git a/apps/ensadmin/src/app/layout.tsx b/apps/ensadmin/src/app/layout.tsx index 244b08d13..c6c0d7694 100644 --- a/apps/ensadmin/src/app/layout.tsx +++ b/apps/ensadmin/src/app/layout.tsx @@ -8,6 +8,7 @@ import { Suspense } from "react"; import { LayoutWrapper } from "@/components/layout-wrapper"; import { QueryClientProvider } from "@/components/query-client/components"; import { Toaster } from "@/components/ui/sonner"; +import { UseClearUrlParams } from "@/hooks/use-clear-url-params"; import { ConnectionsLibraryProvider } from "@/hooks/use-connections-library"; import { ensAdminPublicUrl } from "@/lib/env"; @@ -62,6 +63,7 @@ export default function Layout({ + {children} diff --git a/apps/ensadmin/src/components/connection/config-info/app-card.tsx b/apps/ensadmin/src/components/connection/config-info/app-card.tsx index 6d7ceb7ac..817fc1d73 100644 --- a/apps/ensadmin/src/components/connection/config-info/app-card.tsx +++ b/apps/ensadmin/src/components/connection/config-info/app-card.tsx @@ -15,7 +15,6 @@ export interface ConfigInfoItemProps { export interface ConfigInfoFeatureProps { label: string; description: ReactNode; - isActivated?: boolean; icon: ReactElement; } @@ -69,12 +68,7 @@ export function ConfigInfoItem({ label, value, additionalInfo }: ConfigInfoItemP /** * ConfigInfoFeature - Renders a single feature badge with tooltip */ -export function ConfigInfoFeature({ - label, - description, - isActivated = true, - icon, -}: ConfigInfoFeatureProps) { +export function ConfigInfoFeature({ label, description, icon }: ConfigInfoFeatureProps) { return (
diff --git a/apps/ensadmin/src/components/connection/config-info/config-info.tsx b/apps/ensadmin/src/components/connection/config-info/config-info.tsx index 2fe0b6973..c23e931c2 100644 --- a/apps/ensadmin/src/components/connection/config-info/config-info.tsx +++ b/apps/ensadmin/src/components/connection/config-info/config-info.tsx @@ -226,6 +226,61 @@ function ENSNodeConfigCardContent({

); + const healReverseAddressesFeature = ( + } + /> + ); + + const indexAdditionalRecordsFeature = ( + } + /> + ); + + const replaceUnnormalizedLabelsFeature = ( + } + /> + ); + + const subgraphCompatabilityFeature = ( + } + /> + ); + + const ensIndexerFeatures = [ + { + isActivated: healReverseAddressesActivated, + feature: healReverseAddressesFeature, + }, + { + isActivated: indexAdditionalRecordsActivated, + feature: indexAdditionalRecordsFeature, + }, + { + isActivated: replaceUnnormalizedLabelsActivated, + feature: replaceUnnormalizedLabelsFeature, + }, + { + isActivated: subgraphCompatibilityActivated, + feature: subgraphCompatabilityFeature, + }, + ]; + const ensRootChainId = getENSRootChainId(ensIndexerPublicConfig.namespace); return ( @@ -285,7 +340,7 @@ function ENSNodeConfigCardContent({ } /> - + ) } - isActivated={ensApiPublicConfig.theGraphFallback.canFallback} icon={} /> @@ -471,33 +525,15 @@ function ENSNodeConfigCardContent({ } /> - - } - /> - } - /> - } - /> - - } - /> + + {ensIndexerFeatures + .filter((feature) => feature.isActivated) + .map((feature) => feature.feature)} + + + {ensIndexerFeatures + .filter((feature) => !feature.isActivated) + .map((feature) => feature.feature)} ; +} + +/** + * Clear URL params + * + * Scans the current URL and clears all parameters that are present outside their allowed paths. + * + * @throws Error if the hook is set up to clear the `connection` URL param. + * + * @example + * allowedParams={name: ["/name"]} + * Initial URL: https://admin.ensnode.io/name?name=lightwalker.eth&connection=https%3A%2F%2Fapi.alpha.ensnode.io%2F + * URL after the hook: https://admin.ensnode.io/name?connection=https%3A%2F%2Fapi.alpha.ensnode.io%2F + * + * @example + * allowedParams={name: ["/name"]} + * Initial URL: https://admin.ensnode.io/name?example=lightwalker.eth&connection=https%3A%2F%2Fapi.alpha.ensnode.io%2F + * URL after the hook: https://admin.ensnode.io/name?example=lightwalker.eth&connection=https%3A%2F%2Fapi.alpha.ensnode.io%2F + * + */ +export function UseClearUrlParams({ allowedParams }: useClearUrlParamsProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + const path = usePathname(); + + if (Object.keys(allowedParams).includes("connection")) + throw new Error( + `Invariant(useClearUrlParams): 'connection' parameter should not be edited by this hook`, + ); + + useEffect(() => { + const currentParams = new URLSearchParams(searchParams.toString()); + const paramsToRemove = []; + + // for each provided parameter + for (const [param, allowedPaths] of Object.entries(allowedParams)) { + // if we're not on a path where a param is allowed, and it's still present, + // mark it as 'to be removed' + if (!allowedPaths.includes(path) && searchParams.has(param)) { + paramsToRemove.push(param); + } + } + + // Clear all parameters determined as 'to be removed' + if (paramsToRemove.length > 0) { + paramsToRemove.forEach((param) => currentParams.delete(param)); + + // update the URL without changing history + router.replace(`${path}?${currentParams}`); + } + }, [searchParams, path]); + + return null; +} diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts deleted file mode 100644 index 62e78a075..000000000 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ /dev/null @@ -1,130 +0,0 @@ -import config from "@/config"; - -import { DatasourceNames, type ENSNamespaceId } from "@ensnode/datasources"; -import { - type AccountId, - accountIdEqual, - getDatasourceContract, - type InterpretedName, - type LabelHash, - maybeGetDatasourceContract, - type Name, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -const ethnamesNameWrapper = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "NameWrapper", -); - -const lineanamesNameWrapper = maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "NameWrapper", -); - -/** - * Mapping of RegistrarManagedName to its related Registrar and Registrar-adjacent contracts. - */ -const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { - eth: [ - getDatasourceContract( - config.namespace, // - DatasourceNames.ENSRoot, - "BaseRegistrar", - ), - getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "LegacyEthRegistrarController", - ), - getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "WrappedEthRegistrarController", - ), - getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "UnwrappedEthRegistrarController", - ), - ethnamesNameWrapper, - ], - "base.eth": [ - maybeGetDatasourceContract( - config.namespace, // - DatasourceNames.Basenames, - "BaseRegistrar", - ), - maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "EARegistrarController", - ), - maybeGetDatasourceContract( - config.namespace, // - DatasourceNames.Basenames, - "RegistrarController", - ), - maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "UpgradeableRegistrarController", - ), - ].filter((c) => !!c), - "linea.eth": [ - maybeGetDatasourceContract(config.namespace, DatasourceNames.Lineanames, "BaseRegistrar"), - maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "EthRegistrarController", - ), - lineanamesNameWrapper, - ].filter((c) => !!c), -}; - -/** - * Certain RegistrarManagedNames are different depending on the ENSNamespace — this encodes that - * relationship. - */ -const RMN_NAMESPACE_OVERRIDE: Partial>> = { - sepolia: { - "base.eth": "basetest.eth", - "linea.eth": "linea-sepolia.eth", - }, -}; - -/** - * Given a `contract`, identify its RegistrarManagedName. - */ -export const getRegistrarManagedName = (contract: AccountId) => { - for (const [managedName, contracts] of Object.entries(REGISTRAR_CONTRACTS_BY_MANAGED_NAME)) { - const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); - if (isAnyOfTheContracts) { - const namespaceSpecificManagedName = - RMN_NAMESPACE_OVERRIDE[config.namespace]?.[managedName] ?? managedName; - // override the rmn with namespace-specific version if available - return namespaceSpecificManagedName as InterpretedName; - } - } - - throw new Error("never"); -}; - -/** - * Determines whether `contract` is the NameWrapper. - */ -export function isNameWrapper(contract: AccountId) { - if (accountIdEqual(ethnamesNameWrapper, contract)) return true; - if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; - return false; -} - -/** - * BaseRegistrar-derived Registrars register direct subnames of a RegistrarManagedName. As such, the - * tokens issued by them are keyed by the direct subname's label's labelHash. - * - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - */ -export const registrarTokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); diff --git a/apps/ensindexer/src/lib/managed-names.test.ts b/apps/ensindexer/src/lib/managed-names.test.ts new file mode 100644 index 000000000..2f1b54949 --- /dev/null +++ b/apps/ensindexer/src/lib/managed-names.test.ts @@ -0,0 +1,72 @@ +import { namehash, zeroAddress } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { type AccountId, ENSNamespaceIds, getDatasourceContract } from "@ensnode/ensnode-sdk"; + +import { getManagedName } from "./managed-names"; + +const { spy } = vi.hoisted(() => { + return { spy: vi.fn() }; +}); + +vi.mock("viem", async () => { + const actual = await vi.importActual("viem"); + return { + ...actual, + namehash: (name: string) => { + spy(name); + return actual.namehash(name); + }, + }; +}); + +// mock config.namespace as mainnet +vi.mock("@/config", () => ({ default: { namespace: ENSNamespaceIds.Mainnet } })); + +const registrar = getDatasourceContract( + ENSNamespaceIds.Mainnet, + DatasourceNames.ENSRoot, + "BaseRegistrar", +); + +const controller = getDatasourceContract( + ENSNamespaceIds.Mainnet, + DatasourceNames.ENSRoot, + "LegacyEthRegistrarController", +); + +const ETH_NODE = namehash("eth"); + +describe("managed-names", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + // NOTE: because the cache isn't resettable between test runs (exporting a reset method isn't worth), + // we simply enforce that the cache test case be run first via .sequential + describe.sequential("getManagedName", () => { + it("should cache the result of viem#namehash", () => { + expect(spy.mock.calls).toHaveLength(0); + + expect(getManagedName(registrar)).toStrictEqual({ name: "eth", node: ETH_NODE }); + + // first call should invoke namehash + expect(spy.mock.calls).toHaveLength(1); + + expect(getManagedName(controller)).toStrictEqual({ name: "eth", node: ETH_NODE }); + + // second call should not invoke namehash + expect(spy.mock.calls).toHaveLength(1); + }); + + it("should return the managed name and node for the BaseRegistrar contract", () => { + expect(getManagedName(registrar)).toStrictEqual({ name: "eth", node: ETH_NODE }); + }); + + it("should throw an error for a contract without a managed name", () => { + const unknownContract: AccountId = { chainId: 1, address: zeroAddress }; + expect(() => getManagedName(unknownContract)).toThrow(); + }); + }); +}); diff --git a/apps/ensindexer/src/lib/managed-names.ts b/apps/ensindexer/src/lib/managed-names.ts new file mode 100644 index 000000000..79bf5f7f4 --- /dev/null +++ b/apps/ensindexer/src/lib/managed-names.ts @@ -0,0 +1,165 @@ +import config from "@/config"; + +import { namehash } from "viem"; + +import { DatasourceNames, type ENSNamespaceId } from "@ensnode/datasources"; +import { + type AccountId, + accountIdEqual, + getDatasourceContract, + maybeGetDatasourceContract, + type Name, + type Node, +} from "@ensnode/ensnode-sdk"; + +import { toJson } from "@/lib/json-stringify-with-bigints"; + +/** + * Many contracts within the ENSv1 Ecosystem are relative to a parent Name. For example, + * the .eth BaseRegistrar (and RegistrarControllers) manage direct subnames of .eth. As such, they + * operate on relative Labels, not fully qualified Names. We must know the parent name whose subnames + * they manage in order to index them correctly. + * + * Because we use shared indexing logic for each instance of these contracts (BaseRegistrar, + * RegistrarControllers, NameWrapper), the concept of "which name is this contract operating in the + * context of" must be generalizable: this is the contract's 'Managed Name'. + * + * Concretely, a .eth RegistrarController will emit a _LabelHash_ indicating a new Registration, but + * correlating that LabelHash with the NameHash of the Name requires knowing the NameHash of the + * Registrar's Managed Name ('eth' in this case). + * + * The NameWrapper contracts are relevant here as well because they include specialized logic for + * wrapping direct subnames of specific Managed Names. + */ + +const ethnamesNameWrapper = getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "NameWrapper", +); + +const lineanamesNameWrapper = maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Lineanames, + "NameWrapper", +); + +/** + * Mapping of a Managed Name to contracts that operate in the context of a (sub)Registry associated + * with that Name. + */ +const CONTRACTS_BY_MANAGED_NAME: Record = { + eth: [ + getDatasourceContract( + config.namespace, // + DatasourceNames.ENSRoot, + "BaseRegistrar", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "LegacyEthRegistrarController", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "WrappedEthRegistrarController", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "UnwrappedEthRegistrarController", + ), + ethnamesNameWrapper, + ], + "base.eth": [ + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Basenames, + "BaseRegistrar", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Basenames, + "EARegistrarController", + ), + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Basenames, + "RegistrarController", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Basenames, + "UpgradeableRegistrarController", + ), + ].filter((c) => !!c), + "linea.eth": [ + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Lineanames, + "BaseRegistrar", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Lineanames, + "EthRegistrarController", + ), + lineanamesNameWrapper, + ].filter((c) => !!c), +}; + +/** + * Certain Managed Names are different depending on the ENSNamespace — this encodes that relationship. + */ +const MANAGED_NAME_BY_NAMESPACE: Partial>> = { + sepolia: { + "base.eth": "basetest.eth", + "linea.eth": "linea-sepolia.eth", + }, +}; + +// Because we access a contract's Managed Name (and Node) frequently in event handlers, it's likely +// that caching the namehash() fn for these few values is beneficial, so we do so here. +const namehashCache = new Map(); +const cachedNamehash = (name: Name): Node => { + const cached = namehashCache.get(name); + if (cached !== undefined) return cached; + + const node = namehash(name); + namehashCache.set(name, node); + return node; +}; + +/** + * Given a `contract`, identify its Managed Name and Node. + * + * @dev Caches the result of namehash(name). + */ +export const getManagedName = (contract: AccountId): { name: Name; node: Node } => { + for (const [managedName, contracts] of Object.entries(CONTRACTS_BY_MANAGED_NAME)) { + const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); + if (isAnyOfTheContracts) { + const namespaceSpecific = MANAGED_NAME_BY_NAMESPACE[config.namespace]?.[managedName]; + + // use the namespace-specific Managed Name if specified, otherwise use the default from CONTRACTS_BY_MANAGED_NAME + const name = namespaceSpecific ?? managedName; + const node = cachedNamehash(name); + + return { name, node }; + } + } + + throw new Error( + `The following contract ${toJson(contract)} does not have a configured Managed Name. See apps/ensindexer/src/lib/managed-names.ts.`, + ); +}; + +/** + * Determines whether `contract` is a NameWrapper. + */ +export function isNameWrapper(contract: AccountId) { + if (accountIdEqual(ethnamesNameWrapper, contract)) return true; + if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; + return false; +} diff --git a/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts b/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts index 0ac43078c..0be58ea9b 100644 --- a/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts +++ b/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts @@ -8,13 +8,13 @@ import { type DomainAssetId, ETH_NODE, getDatasourceContract, - type LabelHash, + interpretTokenIdAsLabelHash, + interpretTokenIdAsNode, LINEANAMES_NODE, makeSubdomainNode, maybeGetDatasourceContract, type Node, type TokenId, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; /** @@ -50,9 +50,7 @@ export interface SupportedNFTIssuer { * @param tokenId - The tokenId to convert * @returns The Node of the tokenId */ -const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { - return uint256ToHex32(tokenId); -}; +const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => interpretTokenIdAsNode(tokenId); /** * Converts the tokenId from an ENS name token-issuing contract to a Node @@ -64,7 +62,7 @@ const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { * @returns The Node of the tokenId issued under the parentNode */ const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): Node => { - const labelHash: LabelHash = uint256ToHex32(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); return makeSubdomainNode(labelHash, parentNode); }; @@ -112,9 +110,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC721, contract: ethBaseRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, ETH_NODE); - }, + getDomainId: (tokenId: TokenId): Node => labelHashGeneratedTokenIdToNode(tokenId, ETH_NODE), }); } @@ -122,9 +118,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC1155, contract: nameWrapper, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, + getDomainId: nameHashGeneratedTokenIdToNode, }); } @@ -132,9 +126,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC1155, contract: threeDnsBaseRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, + getDomainId: nameHashGeneratedTokenIdToNode, }); } @@ -142,9 +134,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC1155, contract: threeDnsOptimismRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, + getDomainId: nameHashGeneratedTokenIdToNode, }); } @@ -152,9 +142,8 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC721, contract: lineanamesRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, LINEANAMES_NODE); - }, + getDomainId: (tokenId: TokenId): Node => + labelHashGeneratedTokenIdToNode(tokenId, LINEANAMES_NODE), }); } @@ -162,9 +151,8 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC721, contract: basenamesRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, BASENAMES_NODE); - }, + getDomainId: (tokenId: TokenId): Node => + labelHashGeneratedTokenIdToNode(tokenId, BASENAMES_NODE), }); } diff --git a/apps/ensindexer/src/lib/types.ts b/apps/ensindexer/src/lib/types.ts deleted file mode 100644 index 8df8cb7ec..000000000 --- a/apps/ensindexer/src/lib/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Name } from "@ensnode/ensnode-sdk"; - -/** - * RegistrarManagedName is an explicit type representing this concept within the shared handlers: - * "the parent name a Registrar contract registers subnames of" - * - * ENSIndexer uses "shared handlers" for common indexing logic across plugins. While not suitable - * for all theoretical ENS datasources, they work for current ones, particularly those that re-use the - * original ENS contracts (i.e. Basenames, Lineanames). When indexing onchain events, these handlers - * sometimes need context about parent ENS names of indexed subnames, which is what RegistrarManagedName - * provides. - * - * ex: .eth for the ETH Registry - * ex: .base.eth for Basenames Registry - * ex: .linea.eth for the Lineanames Registry - * - * Currently, the relationship between a plugin and a RegistrarManagedName is simplified to be 1:1. - * In the future, we plan to enhance this data model to support indexing any number of Registrars - * in a single plugin, which will be important for supporting 3DNS and other data sources. - * - * Additionally, our current implementation assumes data sources will share common indexing logic - * (via our shared registrar indexing handlers). We will be working to support more expressive - * or custom cases in the future, which will be necessary for 3DNS and other specialized integrations. - */ -export type RegistrarManagedName = Name; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 6d0e4774f..11e436340 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -1,10 +1,11 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; import { GRACE_PERIOD_SECONDS } from "@ensdomains/ensjs/utils"; -import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; import { interpretAddress, + interpretTokenIdAsLabelHash, isRegistrationFullyExpired, makeENSv1DomainId, makeLatestRegistrationId, @@ -15,7 +16,6 @@ import { import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers"; -import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, getLatestRenewal, @@ -24,6 +24,7 @@ import { } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -70,9 +71,9 @@ export default function () { // // in all such cases, a Registration is expected and we can conditionally materialize Domain owner - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -101,9 +102,9 @@ export default function () { const { id: tokenId, owner, expires: expiry } = event.args; const registrant = owner; - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -159,9 +160,9 @@ export default function () { }) => { const { id: tokenId, expires: expiry } = event.args; - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 9e33eae2f..cfb9e187d 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -1,12 +1,13 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; import { type DNSEncodedLiteralName, type DNSEncodedName, decodeDNSEncodedLiteralName, interpretAddress, + interpretTokenIdAsNode, isPccFuseSet, isRegistrationExpired, isRegistrationFullyExpired, @@ -19,13 +20,11 @@ import { makeSubdomainNode, type Node, PluginName, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; -import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, getLatestRenewal, @@ -34,19 +33,12 @@ import { } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; const pluginName = PluginName.ENSv2; -/** - * When a name is wrapped in the NameWrapper contract, an ERC1155 token is minted that tokenizes - * ownership of the name. The minted token will be assigned a unique tokenId represented as - * uint256(namehash(name)) where name is the fqdn of the name being wrapped. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/wrapper/ERC1155Fuse.sol#L262 - */ -const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); - /** * NameWrapper emits expiry as 0 to mean 'doesn't expire', so we interpret as null. */ @@ -64,7 +56,7 @@ const interpretExpiry = (expiry: bigint): bigint | null => (expiry === 0n ? null // .eth 2LDs always have PARENT_CANNOT_CONTROL set ('burned'), they cannot be transferred during grace period -const isDirectSubnameOfRegistrarManagedName = ( +const isDirectSubnameOfManagedName = ( managedNode: Node, name: DNSEncodedLiteralName, node: Node, @@ -78,7 +70,7 @@ const isDirectSubnameOfRegistrarManagedName = ( } catch { // must be decodable throw new Error( - `Invariant(isSubnameOfRegistrarManagedName): NameWrapper emitted DNSEncodedNames for direct-subnames-of-registrar-managed-names MUST be decodable`, + `Invariant(isDirectSubnameOfManagedName): NameWrapper emitted DNSEncodedNames for direct-subnames-of-managed-names MUST be decodable`, ); } @@ -120,7 +112,8 @@ export default function () { // otherwise is transfer of existing registration - const domainId = makeENSv1DomainId(tokenIdToNode(tokenId)); + // the NameWrapper's ERC1155 TokenIds are the ENSv1Domain's Node so we `interpretTokenIdAsNode` + const domainId = makeENSv1DomainId(interpretTokenIdAsNode(tokenId)); const registration = await getLatestRegistration(context, domainId); const isExpired = registration && isRegistrationExpired(registration, event.block.timestamp); @@ -187,12 +180,12 @@ export default function () { // handle wraps of direct-subname-of-registrar-managed-names if (registration && !isFullyExpired && registration.type === "BaseRegistrar") { - const managedNode = namehash(getRegistrarManagedName(getThisAccountId(context, event))); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); - // Invariant: Emitted name is a direct subname of the RegistrarManagedName - if (!isDirectSubnameOfRegistrarManagedName(managedNode, name, node)) { + // Invariant: Emitted name is a direct subname of the Managed Name + if (!isDirectSubnameOfManagedName(managedNode, name, node)) { throw new Error( - `Invariant(NameWrapper:NameWrapped): An unexpired BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's RegistrarManagedName — wtf?`, + `Invariant(NameWrapper:NameWrapped): An unexpired BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's Managed Name`, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index 9c68c6b2e..300ab4204 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts @@ -1,7 +1,7 @@ /** biome-ignore-all lint/correctness/noUnusedVariables: ignore for now */ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { labelhash, namehash } from "viem"; +import { labelhash } from "viem"; import { type EncodedReferrer, @@ -15,10 +15,10 @@ import { } from "@ensnode/ensnode-sdk"; import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; -import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, getLatestRenewal } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -49,7 +49,7 @@ export default function () { } const controller = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(controller)); + const { node: managedNode } = getManagedName(controller); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -91,7 +91,7 @@ export default function () { const label = _label as LiteralLabel; const controller = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(controller)); + const { node: managedNode } = getManagedName(controller); const labelHash = labelhash(label); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts index deebd7c44..9009c646e 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -1,18 +1,16 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { type BlockRef, bigIntToNumber, - getDatasourceContract, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -20,32 +18,25 @@ import { handleRegistrarEventRenewal, } from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; -import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "BaseRegistrar", - ); - const subregistry = { - subregistryId, - node: parentNode, - } satisfies Subregistry; // support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers ponder.on( namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRegisteredWithRecord"), async ({ context, event }) => { const id = event.id; - const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -72,8 +63,13 @@ export default function () { namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRegistered"), async ({ context, event }) => { const id = event.id; - const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -100,8 +96,12 @@ export default function () { namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { const id = event.id; - const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts index 1d303d7aa..ab54593d7 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts @@ -1,34 +1,23 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { - getDatasourceContract, makeSubdomainNode, PluginName, type RegistrarActionPricingUnknown, type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "BaseRegistrar", - ); /** * No Registrar Controller for Basenames implements premiums or @@ -59,9 +48,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_EARegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -82,9 +79,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_RegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -101,9 +106,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_RegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -124,9 +137,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -143,9 +164,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { diff --git a/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts deleted file mode 100644 index e0a4d3b39..000000000 --- a/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; -import { - getBasenamesSubregistryManagedName, - type LabelHash, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * When direct subnames of Basenames are registered through - * the Basenames RegistrarController contract, - * an ERC721 NFT is minted that tokenizes ownership of the registration. - * The minted NFT will be assigned a unique tokenId represented as - * uint256(labelhash(label)) where label is the direct subname of - * the Basename that was registered. - * https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 - */ -export function tokenIdToLabelHash(tokenId: bigint): LabelHash { - return uint256ToHex32(tokenId); -} - -/** - * Get registrar managed name for `basenames` subregistry for selected ENS namespace. - * - * @param namespaceId - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - return getBasenamesSubregistryManagedName(namespaceId); -} diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts index c8524e12d..ee93838d6 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -1,18 +1,16 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { type BlockRef, bigIntToNumber, - getDatasourceContract, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -20,31 +18,24 @@ import { handleRegistrarEventRenewal, } from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; -import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BaseRegistrar", - ); - const subregistry = { - subregistryId, - node: parentNode, - } satisfies Subregistry; ponder.on( namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRegistered"), async ({ context, event }) => { const id = event.id; - const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -71,8 +62,12 @@ export default function () { namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { const id = event.id; - const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts index 4e6864f9c..0af1e320f 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts @@ -1,13 +1,8 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { namehash } from "viem"; -import { DatasourceNames } from "@ensnode/datasources"; import { addPrices, decodeEncodedReferrer, - getDatasourceContract, makeSubdomainNode, PluginName, priceEth, @@ -16,23 +11,17 @@ import { type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BaseRegistrar", - ); /** * Ethnames_LegacyEthRegistrarController Event Handlers @@ -41,9 +30,18 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); + const transactionHash = event.transaction.hash; /** * Ethnames_LegacyEthRegistrarController does not implement premiums, @@ -67,8 +65,6 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - const transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -83,9 +79,18 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); + const transactionHash = event.transaction.hash; /** * Ethnames_LegacyEthRegistrarController does not implement premiums, @@ -111,8 +116,6 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - const transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -131,9 +134,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -171,9 +182,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -217,9 +236,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.labelhash; - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // rename to labelHash + labelhash: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -260,9 +287,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.labelhash; - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // rename to labelHash + labelhash: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts index d266b3856..18c7345af 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts @@ -1,41 +1,34 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { namehash } from "viem"; -import { DatasourceNames } from "@ensnode/datasources"; import { decodeEncodedReferrer, - getDatasourceContract, makeSubdomainNode, PluginName, type RegistrarActionReferralAvailable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { handleUniversalRegistrarRenewalEvent } from "@/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BaseRegistrar", - ); ponder.on( namespaceContract(pluginName, "Ethnames_UniversalRegistrarRenewalWithReferrer:RenewalReferred"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.labelHash; - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { labelHash }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts deleted file mode 100644 index e91d6ca92..000000000 --- a/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; -import { - getEthnamesSubregistryManagedName, - type LabelHash, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * When direct subnames of Ethnames are registered through - * the Ethnames ETHRegistrarController contract, - * an ERC721 NFT is minted that tokenizes ownership of the registration. - * The minted NFT will be assigned a unique tokenId which is - * uint256(labelhash(label)) where label is the direct subname of - * the Ethname that was registered. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - */ -export function tokenIdToLabelHash(tokenId: bigint): LabelHash { - return uint256ToHex32(tokenId); -} - -/** - * Get the registrar managed name for the Ethnames subregistry for the selected ENS namespace. - * - * @param namespaceId - * @returns registrar managed name - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - return getEthnamesSubregistryManagedName(namespaceId); -} diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts index fc99512c7..723456344 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -1,18 +1,16 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { type BlockRef, bigIntToNumber, - getDatasourceContract, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -20,31 +18,24 @@ import { handleRegistrarEventRenewal, } from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; -import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "BaseRegistrar", - ); - const subregistry = { - subregistryId, - node: parentNode, - } satisfies Subregistry; ponder.on( namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRegistered"), async ({ context, event }) => { const id = event.id; - const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -71,8 +62,12 @@ export default function () { namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { const id = event.id; - const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts index 0175c7c92..19a181a1c 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts @@ -1,12 +1,7 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { addPrices, - getDatasourceContract, makeSubdomainNode, PluginName, priceEth, @@ -14,9 +9,10 @@ import { type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; -import { getRegistrarManagedName } from "../../lineanames/lib/registrar-helpers"; import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; /** @@ -24,13 +20,6 @@ import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-contr */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "BaseRegistrar", - ); /** * No Registrar Controller for Lineanames implements referrals or @@ -48,9 +37,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:OwnerNameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -78,9 +75,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:PohNameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -108,9 +113,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; const baseCost = priceEth(event.args.baseCost); @@ -136,9 +149,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; const baseCost = priceEth(event.args.cost); diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts deleted file mode 100644 index 29d73486a..000000000 --- a/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; -import { - getLineanamesSubregistryManagedName, - type LabelHash, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * When direct subnames of Lineanames are registered through - * the Lineanames ETHRegistrarController contract, - * an ERC721 NFT is minted that tokenizes ownership of the registration. - * The minted NFT will be assigned a unique tokenId represented as - * uint256(labelhash(label)) where label is the direct subname of - * Lineanames that was registered. - * https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 - */ -export function tokenIdToLabelHash(tokenId: bigint): LabelHash { - return uint256ToHex32(tokenId); -} - -/** - * Get registrar managed name for `lineanames` subregistry for selected ENS namespace. - * - * @param namespaceId - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - return getLineanamesSubregistryManagedName(namespaceId); -} diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts index 37f647197..c36b8958a 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts @@ -1,23 +1,10 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - -/** - * When direct subnames of base.eth are registered through the base.eth RegistrarController contract - * on Base, an ERC721 NFT is minted that tokenizes ownership of the registration. The minted NFT will be - * assigned a unique tokenId represented as uint256(labelhash(label)) where label is the direct - * subname of base.eth that was registered. - * https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 - */ -const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); - /** * Registers event handlers with Ponder. */ @@ -30,12 +17,7 @@ export default function () { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, - } = makeRegistrarHandlers({ - pluginName, - // the shared Registrar handlers in this plugin index direct subnames of - // the name returned from `getRegistrarManagedName` function call - registrarManagedName: getRegistrarManagedName(config.namespace), - }); + } = makeRegistrarHandlers({ pluginName }); // support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers ponder.on( @@ -43,7 +25,10 @@ export default function () { async ({ context, event }) => { await handleNameRegistered({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -53,7 +38,10 @@ export default function () { async ({ context, event }) => { await handleNameRegistered({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -63,7 +51,10 @@ export default function () { async ({ context, event }) => { await handleNameRenewed({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -73,7 +64,7 @@ export default function () { context, event: { ...event, - args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.tokenId) }, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.tokenId) }, }, }); }); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts deleted file mode 100644 index 74d74988c..000000000 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * Get registrar managed name for `basenames` plugin for selected ENS namespace. - * - * @param namespaceId - * @param pluginName - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - switch (namespaceId) { - case "mainnet": - return "base.eth"; - case "sepolia": - return "basetest.eth"; - case "ens-test-env": - throw new Error( - `No registrar managed name is known for the Basenames plugin within the "${namespaceId}" namespace.`, - ); - } -} diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts index 8ae71b514..ac1ec5f38 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { PluginName } from "@ensnode/ensnode-sdk"; @@ -7,8 +5,6 @@ import { PluginName } from "@ensnode/ensnode-sdk"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeNameWrapperHandlers } from "@/plugins/subgraph/shared-handlers/NameWrapper"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - /** * Registers event handlers with Ponder. */ @@ -22,11 +18,7 @@ export default function () { handleExpiryExtended, handleTransferSingle, handleTransferBatch, - } = makeNameWrapperHandlers({ - // the shared Registrar handlers in this plugin index direct subnames of - // the name returned from `getRegistrarManagedName` function call - registrarManagedName: getRegistrarManagedName(config.namespace), - }); + } = makeNameWrapperHandlers(); ponder.on(namespaceContract(pluginName, "NameWrapper:NameWrapped"), handleNameWrapped); ponder.on(namespaceContract(pluginName, "NameWrapper:NameUnwrapped"), handleNameUnwrapped); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts index a80682d22..d37c68b03 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts @@ -1,23 +1,10 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - -/** - * When direct subnames of linea.eth are registered through the linea.eth ETHRegistrarController - * contract on Linea, an ERC721 NFT is minted that tokenizes ownership of the registration. The minted NFT - * will be assigned a unique tokenId represented as uint256(labelhash(label)) where label is the - * direct subname of linea.eth that was registered. - * https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 - */ -const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); - /** * Registers event handlers with Ponder. */ @@ -30,19 +17,17 @@ export default function () { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, - } = makeRegistrarHandlers({ - pluginName, - // the shared Registrar handlers in this plugin index direct subnames of - // the name returned from `getRegistrarManagedName` function call - registrarManagedName: getRegistrarManagedName(config.namespace), - }); + } = makeRegistrarHandlers({ pluginName }); ponder.on( namespaceContract(pluginName, "BaseRegistrar:NameRegistered"), async ({ context, event }) => { await handleNameRegistered({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -52,7 +37,10 @@ export default function () { async ({ context, event }) => { await handleNameRenewed({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -62,7 +50,7 @@ export default function () { context, event: { ...event, - args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.tokenId) }, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.tokenId) }, }, }); }); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts deleted file mode 100644 index 6a99aed2f..000000000 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * Get registrar managed name for `lineanames` plugin for selected ENS namespace. - * - * @param namespaceId - * @param pluginName - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - switch (namespaceId) { - case "mainnet": - return "linea.eth"; - case "sepolia": - return "linea-sepolia.eth"; - case "ens-test-env": - throw new Error( - `No registrar managed name is known for the Linea Names plugin within the "${namespaceId}" namespace.`, - ); - } -} diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts index 3fb093715..8b667cf7e 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts @@ -18,10 +18,7 @@ export default function () { handleNameWrapped, handleTransferBatch, handleTransferSingle, - } = makeNameWrapperHandlers({ - // the shared Registrar handlers in this plugin index direct subnames of '.eth' - registrarManagedName: "eth", - }); + } = makeNameWrapperHandlers(); ponder.on(namespaceContract(pluginName, "NameWrapper:NameWrapped"), handleNameWrapped); ponder.on(namespaceContract(pluginName, "NameWrapper:NameUnwrapped"), handleNameUnwrapped); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts index 4681b314b..293b0ad6b 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts @@ -1,22 +1,10 @@ import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; -/** - * When direct subnames of .eth are registered through the ETHRegistrarController contract on - * Ethereum mainnet, an ERC721 NFT is minted that tokenizes ownership of the registration. The minted NFT - * will be assigned a unique tokenId which is uint256(labelhash(label)) where label is the - * direct subname of .eth that was registered. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - */ -const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); - -// the shared Registrar handlers in this plugin index direct subnames of '.eth' -const registrarManagedName = "eth" as const; - /** * Registers event handlers with Ponder. */ @@ -29,10 +17,7 @@ export default function () { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, - } = makeRegistrarHandlers({ - pluginName, - registrarManagedName, - }); + } = makeRegistrarHandlers({ pluginName }); /////////////////////////////// // BaseRegistrar @@ -48,7 +33,7 @@ export default function () { ...event, args: { ...event.args, - labelHash: tokenIdToLabelHash(event.args.id), + labelHash: interpretTokenIdAsLabelHash(event.args.id), }, }, }); @@ -64,7 +49,7 @@ export default function () { ...event, args: { ...event.args, - labelHash: tokenIdToLabelHash(event.args.id), + labelHash: interpretTokenIdAsLabelHash(event.args.id), }, }, }); @@ -80,7 +65,7 @@ export default function () { args: { from, to, - labelHash: tokenIdToLabelHash(tokenId), + labelHash: interpretTokenIdAsLabelHash(tokenId), }, }, }); diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts index 5d1e22caf..845995078 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts @@ -17,7 +17,7 @@ import schema from "ponder:schema"; * Related GitHub issue: https://github.com/ensdomains/ens-subgraph/issues/88 */ import { checkPccBurned as isPccFuseUnset } from "@ensdomains/ensjs/utils"; -import { type Address, namehash } from "viem"; +import type { Address } from "viem"; import { type DNSEncodedLiteralName, @@ -25,28 +25,21 @@ import { decodeDNSEncodedLiteralName, type InterpretedLabel, type InterpretedName, + interpretTokenIdAsNode, literalLabelsToInterpretedName, literalLabelToInterpretedLabel, type Node, type SubgraphInterpretedLabel, type SubgraphInterpretedName, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { subgraph_decodeDNSEncodedLiteralName } from "@/lib/dns-helpers"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { bigintMax } from "@/lib/lib-helpers"; +import { getManagedName } from "@/lib/managed-names"; import type { EventWithArgs } from "@/lib/ponder-helpers"; import { sharedEventValues, upsertAccount } from "@/lib/subgraph/db-helpers"; import { makeEventId } from "@/lib/subgraph/ids"; -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * When a name is wrapped in the NameWrapper contract, an ERC1155 token is minted that tokenizes - * ownership of the name. The minted token will be assigned a unique tokenId represented as - * uint256(namehash(name)) where name is the fully qualified ENS name being wrapped. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/wrapper/ERC1155Fuse.sol#L262 - */ -const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); /** * Determines whether the PCC fuse is SET in the provided `fuses`. @@ -121,16 +114,8 @@ async function materializeDomainExpiryDate(context: Context, node: Node) { /** * makes a set of shared handlers for the NameWrapper contract - * - * @param registrarManagedName the name of the Registrar that NameWrapper interacts with registers subnames of */ -export const makeNameWrapperHandlers = ({ - registrarManagedName, -}: { - registrarManagedName: RegistrarManagedName; -}) => { - const registrarManagedNode = namehash(registrarManagedName); - +export const makeNameWrapperHandlers = () => { async function handleTransfer( context: Context, event: EventWithArgs, @@ -139,7 +124,9 @@ export const makeNameWrapperHandlers = ({ to: Address, ) { await upsertAccount(context, to); - const node = tokenIdToNode(tokenId); + + // the NameWrapper's ERC1155 TokenIds are the ENSv1Domain's Node so we `interpretTokenIdAsNode` + const node = interpretTokenIdAsNode(tokenId); // NOTE: subgraph technically upserts domain with `createOrLoadDomain()` here, but domain // is guaranteed to exist. we encode this stricter logic here to illustrate that fact. @@ -250,6 +237,8 @@ export const makeNameWrapperHandlers = ({ }) { const { node, owner } = event.args; + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); + await upsertAccount(context, owner); await context.db.update(schema.subgraph_domain, { id: node }).set((domain) => ({ @@ -259,7 +248,7 @@ export const makeNameWrapperHandlers = ({ // expiry to null because it does not expire. // via https://github.com/ensdomains/ens-subgraph/blob/c844791/src/nameWrapper.ts#L123 // NOTE: undefined = no change, null = null - expiryDate: domain.parentId === registrarManagedNode ? undefined : null, + expiryDate: domain.parentId === managedNode ? undefined : null, wrappedOwnerId: null, })); diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts index be51fd305..fa33d74e8 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts @@ -2,7 +2,7 @@ import config from "@/config"; import type { Context } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, namehash } from "viem"; +import type { Address } from "viem"; import { encodeLabelHash, @@ -18,13 +18,14 @@ import { type SubgraphInterpretedName, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { labelByLabelHash } from "@/lib/graphnode-helpers"; +import { getManagedName } from "@/lib/managed-names"; import { pluginSupportsPremintedNames } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; import { sharedEventValues, upsertAccount, upsertRegistration } from "@/lib/subgraph/db-helpers"; import { makeRegistrationId } from "@/lib/subgraph/ids"; import { isLabelSubgraphIndexable } from "@/lib/subgraph/is-label-subgraph-indexable"; -import type { RegistrarManagedName } from "@/lib/types"; import { handleNewOwner } from "@/plugins/subgraph/shared-handlers/Registry"; const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds @@ -35,21 +36,17 @@ const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds * @param pluginName the name of the plugin using these shared handlers * @param registrarManagedName the name that the Registrar contract indexes subnames of */ -export const makeRegistrarHandlers = ({ - pluginName, - registrarManagedName, -}: { - pluginName: PluginName; - registrarManagedName: RegistrarManagedName; -}) => { - const registrarManagedNode = namehash(registrarManagedName); - +export const makeRegistrarHandlers = ({ pluginName }: { pluginName: PluginName }) => { async function setNamePreimage( context: Context, - label: LiteralLabel, - labelHash: LabelHash, - cost: bigint, + event: EventWithArgs<{ + label: LiteralLabel; + labelHash: LabelHash; + cost: bigint; + }>, ) { + const { label, labelHash, cost } = event.args; + // NOTE(subgraph-compat): if the label is not subgraph-indexable, ignore it entirely if (config.isSubgraphCompatible && !isLabelSubgraphIndexable(label)) return; @@ -61,7 +58,8 @@ export const makeRegistrarHandlers = ({ // see https://ensnode.io/docs/reference/terminology#interpreted-label literalLabelToInterpretedLabel(label); - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); + const node = makeSubdomainNode(labelHash, managedNode); const domain = await context.db.find(schema.subgraph_domain, { id: node }); // encode the runtime assertion here https://github.com/ensdomains/ens-subgraph/blob/c68a889/src/ethRegistrar.ts#L101 @@ -70,7 +68,7 @@ export const makeRegistrarHandlers = ({ // materialize the domain's name and labelName using the emitted values if (domain.labelName !== interpretedLabel) { // in either case a Name composed of (Subgraph) Interpreted Labels is (Subgraph) Interpreted - const interpretedName = `${interpretedLabel}.${registrarManagedName}` as + const interpretedName = `${interpretedLabel}.${managedNode}` as | InterpretedName | SubgraphInterpretedName; @@ -101,7 +99,10 @@ export const makeRegistrarHandlers = ({ await upsertAccount(context, owner); - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const { name: managedName, node: managedNode } = getManagedName( + getThisAccountId(context, event), + ); + const node = makeSubdomainNode(labelHash, managedNode); // NOTE(preminted-names): The mainnet ENS Registrar(s) _always_ register a node with the ENS // registry (emitting Registry#NewOwner) before emitting Registrar#NameRegistered. @@ -148,7 +149,7 @@ export const makeRegistrarHandlers = ({ ...event, args: { owner, - node: registrarManagedNode, + node: managedNode, label: labelHash, }, }, @@ -167,7 +168,7 @@ export const makeRegistrarHandlers = ({ // if subgraph-indexable, the label is Subgraph Interpreted label = healedLabel as Label as SubgraphInterpretedLabel; // a name constructed of Subgraph Interpreted Labels is Subgraph Interpreted - name = `${label}.${registrarManagedName}` as SubgraphInterpretedName; + name = `${label}.${managedName}` as SubgraphInterpretedName; } } else { // Interpret the `healedLabel` Literal Label into an Interpreted Label @@ -180,7 +181,7 @@ export const makeRegistrarHandlers = ({ ) as InterpretedLabel; // a name constructed of Interpreted Labels is Interpreted - name = `${label}.${registrarManagedName}` as InterpretedName; + name = `${label}.${managedName}` as InterpretedName; } // update Domain @@ -224,14 +225,10 @@ export const makeRegistrarHandlers = ({ cost: bigint; }>; }) { - const { label, labelHash, cost } = event.args; + const { label: _label, labelHash, cost } = event.args; + const label = _label as LiteralLabel; // NameRegistered emits Literal Labels - await setNamePreimage( - context, - label as LiteralLabel, // NameRegistered emits Literal Labels - labelHash, - cost, - ); + await setNamePreimage(context, { ...event, args: { label, labelHash, cost } }); }, async handleNameRenewedByController({ @@ -245,14 +242,10 @@ export const makeRegistrarHandlers = ({ cost: bigint; }>; }) { - const { label, labelHash, cost } = event.args; + const { label: _label, labelHash, cost } = event.args; + const label = _label as LiteralLabel; // NameRenewed emits Literal Labels - await setNamePreimage( - context, - label as LiteralLabel, // NameRenewed emits Literal Labels - labelHash, - cost, - ); + await setNamePreimage(context, { ...event, args: { label, labelHash, cost } }); }, async handleNameRenewed({ @@ -264,7 +257,8 @@ export const makeRegistrarHandlers = ({ }) { const { labelHash, expires } = event.args; - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); + const node = makeSubdomainNode(labelHash, managedNode); const id = makeRegistrationId(labelHash, node); // update Registration expiry @@ -295,7 +289,8 @@ export const makeRegistrarHandlers = ({ // NOTE(subgraph-compat): despite the short-circuits below, upsertAccount must always be run await upsertAccount(context, to); - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); + const node = makeSubdomainNode(labelHash, managedNode); const id = makeRegistrationId(labelHash, node); // if the Transfer event occurs before the Registration entity exists (i.e. the initial diff --git a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx index 1b7d3512e..7a2834df0 100644 --- a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx +++ b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx @@ -68,7 +68,7 @@ Each plugin requires two files: Because plugins indexing subregistries use the shared handlers and may clobber entities created by the `subgraph` plugin—which didn't expect multichain or multi-source entities—, id-generating code is abstracted to be plugin-specific. See the helpers in `apps/ensindexer/src/lib/ids.ts`. In these cases, for the `subgraph` plugin, the original behavior is left un-modified to facilitate 1:1 responses from the subgraph-compatible api. -This scoping also applies to the concept of a `RegistrarManagedName` (see `apps/ensindexer/src/lib/types.ts` and `makeRegistrarHandlers` in `apps/ensindexer/src/handlers/Registrar.ts`) — teh shared handlers derived from the subgraph which are used by some plugins expect the context of a name whos subnames they manage. In the original subgraph implementation, this was hardcoded as the `.eth` name, and operations under the `Registrar` are in the context of direct subnames of `.eth`. +This scoping also applies to the concept of a "Managed Name" (see `apps/ensindexer/src/lib/managed-names.ts`): some contracts (like Registrars and RegistrarControllers) operate in the context of a Managed Name (like 'eth', 'base.eth', or 'linea.eth'). In the original subgraph implementation, this was hardcoded for the .eth Registrar/RegistrarControllers as the `eth` name, but because ENSNode shares indexing logic between these similar contracts across `.eth` names, `.base.eth` names, etc, we introduce the concept of a contract's "Managed Name" within the context of which it operates. ### Ponder Plugin Integration diff --git a/docs/ensnode.io/src/content/docs/docs/contributing/releases.mdx b/docs/ensnode.io/src/content/docs/docs/contributing/releases.mdx index ce5a6e62e..def6d65e7 100644 --- a/docs/ensnode.io/src/content/docs/docs/contributing/releases.mdx +++ b/docs/ensnode.io/src/content/docs/docs/contributing/releases.mdx @@ -7,41 +7,161 @@ sidebar: import { LinkCard } from '@astrojs/starlight/components'; -We use [changesets](https://github.com/changesets/changesets) to manage version bumps and release notes for our NPM packages and Docker images (ENSIndexer, ENSAdmin, and ENSRainbow). +We use [changesets](https://github.com/changesets/changesets) to manage version bumps and release notes for our monorepo's artifacts (NPM packages, Docker images, and Lambda zips). ## Adding a Changeset to Your PR When you open a PR with feature or bug fix changes, you'll need to include a changeset file that documents your changes: -1. Run `changeset` or `pnpm changeset` in the repository root -2. Select the packages/apps your changes affect using the interactive prompt +1. Run `changeset` or `pnpm changeset` in the repository root. +2. Select the packages/apps your changes affect using the interactive prompt. 3. Choose whether your changes are "major" (breaking changes), "minor" (features), or "patch" (bug fixes). -4. Write a clear description of your changes - this will appear in the release notes +4. Write a clear description of your changes - this will appear in the release notes. The `changesets/bot` will automatically comment on your PR to either remind you to add a changeset or confirm the version changes that will happen when your PR is merged. **Important notes:** -- Changesets are only required for user-facing changes (features, bug fixes) -- You don't need a changeset for documentation changes or internal refactoring -- All our packages use "fixed" versioning - they all share the same version number regardless of which package triggered the version bump +- Changesets are only required for user-facing changes (features, bug fixes). +- You don't need a changeset for documentation changes or internal refactoring. +- All our apps and packages (with an exception as noted below) use "fixed" versioning - they all share the same version number regardless of which app or package triggered the version bump. +- An exception to the above "fixed" versioning is the "fallback-ensapi" app. This is a Lambda containing logic specific to NameHash deployments of ENSNode and is versioned independently. -## Upon a New Release +# Creating a Release -Upon the publishing of a new release, your change will be included in the produced packages/images and your contributions will be referenced in the GitHub Release notes. +Upon the publishing of a new release, your change will be included in the produced artifacts and your contributions will be referenced in the GitHub Release notes. There are three different types of releases that your changes can be included in for produced artifacts. -## Publishing Release Candidates (RC) of packages to NPM +## Full Release -Publishing release candidates (RCs) of packages to NPM supports apps outside of the ENSNode monorepo to make use of prerelease packages for testing new ENSNode versions before making a full ENSNode release. Package RCs are published to NPM with the `rc` dist-tag: +Workflow File: [release.yml](https://github.com/namehash/ensnode/blob/main/.github/workflows/release.yml) -1. Ensure your changesets are committed to `main` -2. Navigate to [Actions > Release NPM RC](https://github.com/namehash/ensnode/actions/workflows/npm-release-rc.yml) -3. Click "Run workflow" and select the `main` branch -4. RC packages are published to NPM with the `rc` tag (e.g., `1.0.0-rc.0`) -5. Install RCs with: `npm install @ensnode/package-name@rc` +If your PR includes a changeset and is merged to `main` then it will automatically be added to a new automated Release PR by the Changesets bot. As more changesets are added to `main` the Release PR will continue to update. Once a Release PR is merged into `main` it triggers a "full" release that will: + +- Build and publish all of the monorepo's artifacts (NPM packages, Docker images, and Lambda zips). +- Create a release on GitHub with autogenerated release notes from all the included changesets. + +**Important notes:** +- Among all release types, only Full Releases are considered stable. +- Full releases are triggered through merging the Release PR to `main`. +- All ENSNode packages use "fixed" versioning. Once a full release is published they will all advance to the version indicated in the Release PR based on the included changesets. +- Only members of the NameHash Labs `ensnode` team have the required permissions to merge the Release PR to `main`. +- Full releases will create GitHub tags and release notes. + +## Snapshot Release + +Workflow File: [release_snapshot.yml](https://github.com/namehash/ensnode/blob/main/.github/workflows/release_snapshot.yml) + +Each commit to `main` will automatically trigger the `release_snapshot.yml` workflow to build and publish all of the monorepo's artifacts. These public releases will be under the tag `@next`, allowing anyone to use the artifacts associated with each commit to main. To install snapshot releases run `pnpm install @ensnode/[package-name]@next` or `docker run ghcr.io/namehash/ensnode/[app-name]:next`. + +**Important notes:** +- Snapshot releases are part of the pre-stable state of the `main` branch and should be installed with caution until a [full release](#full-release) is published. +- Release snapshots are automatic and cannot be triggered manually. +- Snapshot releases will include the `@next` tag for published artifacts. +- Published artifacts will advance to the version that was included in the changeset of the PR merged to `main`. +- The `main` branch of `ensnode` is protected. Only PRs approved by the `ensnode` team can be merged to `main` and trigger a snapshot release. +- No GitHub releases or tags are created for snapshot releases. + +## Preview Release + +Workflow File: [release_preview.yml](https://github.com/namehash/ensnode/blob/main/.github/workflows/release_preview.yml) + +To test or install a package before merging it into `main`, a preview release can be used. Each preview release is associated with a PR, and the PR will receive a comment with installation instructions. To manually trigger a preview release, follow these steps: + +1. Navigate to [Actions > Release Preview](https://github.com/namehash/ensnode/actions/workflows/release_preview.yml) +2. Click "Run workflow" and select from the following options: + - The branch on which to run the preview release workflow. The branch must have an open PR. + - Choose which artifacts to build and publish: + - `npm-only`: NPM packages only + - `npm-and-lambdas`: NPM packages + Lambda functions + - `npm-and-ghcr`: NPM packages + Docker images + - `all`: NPM packages + Lambda functions + Docker images + - (Optional) Provide a custom suffix for the preview release tag. For example, if you had a branch called `feat/add-api-route` and left this custom suffix field blank, the preview release would be `@ensnode/[package-name]@preview-feat-add-api-route`. If you fill in the custom suffix field with `users-route` then the resulting tag name would be `@ensnode/[package-name]@preview-users-route`. +3. The workflow will post a comment on the PR with installation instructions. If multiple preview releases are triggered for the same PR, the comment will update with the latest release info. +4. Install preview packages with: `npm install @ensnode/[package-name]@preview-branch-name`. **Important notes:** -- NPM Package RC versions are ephemeral - they don't create commits or modify the repository -- Each workflow run increments the RC version (rc.0, rc.1, rc.2, etc.) -- Docker images are NOT built for RCs -- No GitHub releases or tags are created for RCs -- The `main` branch remains unchanged with regular versions +- Preview releases require an open PR. The workflow will abort if no PR exists for the branch. +- Preview releases are not guaranteed to be stable as they are still under active development. +- Preview releases can only be triggered manually by authorized NameHash team members. +- Preview releases will include the `@preview-*` tag for published artifacts, followed by either the name of the branch or the custom suffix chosen during the action trigger. +- Published artifacts will advance to the version that was included in the changeset of the selected branch. +- No GitHub releases or tags are created for preview releases. + +# Selecting a Release for Deployment or Installation + +:::caution +ENSNode is currently not using [Semantic Versioning](https://semver.org/). Patches and minor releases may include breaking changes. +::: + +When using ENSNode artifacts, you have several release types to choose from. + +## Where to Find Releases + +- **NPM Packages**: Available on the [npm registry](https://www.npmjs.com/search?q=%40ensnode) under the `@ensnode` organization. +- **Docker Images**: Available on [GitHub Container Registry](https://github.com/orgs/namehash/packages?repo_name=ensnode). +- **GitHub Releases**: Full releases are documented with release notes on the [ENSNode GitHub releases page](https://github.com/namehash/ensnode/releases). +- **Lambda Zip Artifacts**: Available in the [Artifact section](https://github.com/actions/upload-artifact?tab=readme-ov-file#where-does-the-upload-go) of a successful workflow run. These Action Artifacts are [retained for 90 days](https://github.com/actions/upload-artifact?tab=readme-ov-file#retention-period). + +## Choosing the Right Release Type + +### Pinned Full Release Versions + +When deploying ENSNode to production environments, it is advisable to use a Pinned Full Release. Pinned full releases are required for those who want to use any published ENSNode artifacts. By using a pinned version you can maintain full control over features or patches that might be included. Review the release notes on the [Releases Page](https://github.com/namehash/ensnode/releases) to help decide which version to install. + +```bash +npm install @ensnode/[package-name]@[version] +docker run ghcr.io/namehash/ensnode/[app-name]:[version] +``` + +:::caution +When installing NPM packages for use in production environments, it is also advisable to pin a specific Pinned Full Release version number. ENSNode patch version increments MAY include breaking changes, so the usage of exact version package specifiers is encouraged. + +For example: + +✅ `"@ensnode/[package-name]": "1.0.0"` +❌ `"@ensnode/[package-name]": "^1.0.0"` + +And when installing from the command line: + +✅ `pnpm install @ensnode/[package-name]@1.0.0` +❌ `pnpm install @ensnode/[package-name]` +::: + +:::caution +In particular, when deploying ENSNode to production environments, using the `latest` Docker tag without strict pull policies could result in a version mismatch between interdependent ENSNode apps (which will helpfully crash at startup). Due to this, we highly recommend using a specific Pinned Full Release version number tag like `1.0.0` instead of the tag `latest`, which could point to different versions of the ENSNode app depending on the platform's pull policy. +::: + +:::caution +Each ENSIndexer version update is likely to produce an updated [Ponder Build Id](https://ponder.sh/docs/api-reference/ponder/database). When updating your ENSIndexer version, you should expect to update the `DATABASE_SCHEMA` environment variable to point to a new Postgres Database Schema for a complete reindexing with the new Ponder Build Id. A complete reindexing may take over 24 hours depending on your configuration. ENSNode version updates require special coordination and should not be assumed to be a simple version bump. +::: + + +### Snapshot Releases +:::caution +The `next` tag is a floating pointer that always references the most recent snapshot release. When using Docker images with the `next` tag, you must run `docker pull` to update your local Docker cache if you want to get the actual latest image. Without pulling, you may continue using an older cached version. +::: + +```bash +npm install @ensnode/[package-name]@next +docker run ghcr.io/namehash/ensnode/[app-name]:next +``` + +Snapshot releases should be used by those who need to test features or patches before they are included in full releases. These releases follow the `main` branch and are not referenced in the GitHub Releases page. Instead they are installed by using the `next` tag for published artifacts. + +:::caution +Snapshot releases may contain unstable changes and should only be used in development environments. +::: + +### Preview Releases +:::note +The example below is a mock of what a preview release might look like. Read the [preview releases section](#preview-release) for more information. +::: +```bash +npm install @ensnode/[package-name]@[preview-branch-name] +docker run ghcr.io/namehash/ensnode/[app-name]:[preview-branch-name] +``` + +Preview releases are designed to test features or patches on a PR branch before it is merged to `main`. Each preview release is associated with a PR, and the PR will have a comment with installation instructions. Since preview releases can contain experimental and unstable changes, they should be avoided unless you are actively contributing through a PR and need to test work on a branch. + +:::caution +Avoid using preview releases unless you are actively contributing to ENSNode and need to test changes on an active PR. +::: diff --git a/package.json b/package.json index 3068c9af6..d15a829ea 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "test": "vitest --silent passed-only", "typecheck": "pnpm -r --parallel --aggregate-output typecheck", "changeset": "changeset", + "changeset:next": "changeset version --snapshot next", "changeset-publish": "changeset publish", + "changeset-publish:next": "changeset publish --no-git-tag --snapshot --tag next", "packages:prepublish": "pnpm -r prepublish", "docker:build:ensnode": "pnpm run -w --parallel \"/^docker:build:.*/\"", "docker:build:ensindexer": "docker build -f apps/ensindexer/Dockerfile -t ghcr.io/namehash/ensnode/ensindexer:latest .", diff --git a/packages/ens-referrals/src/index.ts b/packages/ens-referrals/src/index.ts index fde4e6b50..83ffb9bed 100644 --- a/packages/ens-referrals/src/index.ts +++ b/packages/ens-referrals/src/index.ts @@ -12,4 +12,5 @@ export * from "./referrer-metrics"; export * from "./revenue-contribution"; export * from "./rules"; export * from "./score"; +export * from "./status"; export * from "./time"; diff --git a/packages/ens-referrals/src/status.ts b/packages/ens-referrals/src/status.ts new file mode 100644 index 000000000..30a63fd50 --- /dev/null +++ b/packages/ens-referrals/src/status.ts @@ -0,0 +1,51 @@ +import type { ReferralProgramRules } from "./rules.ts"; +import type { UnixTimestamp } from "./time.ts"; + +/** + * The type of referral program's status. + */ +export const ReferralProgramStatuses = { + /** + * Represents a referral program that has been announced, but hasn't started yet. + */ + Scheduled: "Scheduled", + + /** + * Represents a currently ongoing referral program. + */ + Active: "Active", + + /** + * Represents a referral program that has already ended. + */ + Closed: "Closed", +} as const; + +/** + * The derived string union of possible {@link ReferralProgramStatuses}. + */ +export type ReferralProgramStatusId = + (typeof ReferralProgramStatuses)[keyof typeof ReferralProgramStatuses]; + +/** + * Calculate the status of the referral program based on the current date + * and program's timeframe available in its rules. + * + * @param referralProgramRules - Related referral program's rules containing + * program's start date and end date. + * + * @param now - Current date in {@link UnixTimestamp} format. + */ +export const calcReferralProgramStatus = ( + referralProgramRules: ReferralProgramRules, + now: UnixTimestamp, +): ReferralProgramStatusId => { + // if the program has not started return "Scheduled" + if (now < referralProgramRules.startTime) return ReferralProgramStatuses.Scheduled; + + // if the program has ended return "Closed" + if (now > referralProgramRules.endTime) return ReferralProgramStatuses.Closed; + + // otherwise, return "Active" + return ReferralProgramStatuses.Active; +}; diff --git a/packages/ensnode-sdk/src/shared/interpretation/index.ts b/packages/ensnode-sdk/src/shared/interpretation/index.ts index 1741768c1..1dbbd7d06 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/index.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/index.ts @@ -1,3 +1,4 @@ export * from "./interpret-address"; export * from "./interpret-record-values"; +export * from "./interpret-tokenid"; export * from "./interpreted-names-and-labels"; diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpret-tokenid.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-tokenid.ts new file mode 100644 index 000000000..9903609d9 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/interpretation/interpret-tokenid.ts @@ -0,0 +1,38 @@ +import { type LabelHash, type Node, uint256ToHex32 } from "../../ens"; + +/** + * Decodes a uint256-encoded-LabelHash (eg. from a tokenId) into a {@link LabelHash}. + * + * Remember that contracts that operate in the context of a Managed Name frequently store and operate + * over _LabelHashes_ that represent a direct subname of a Managed Name. These contracts also frequently + * implement ERC721 or ERC1155 to represent ownership of these Names. As such, to construct the + * ERC721/ERC1155 tokenId, they may encode the direct subnames's LabelHash as a uint256. + * + * This is true for the ENSv1 BaseRegistrar, RegistrarControllers, as well as any + * contracts forked from it (which includes Basenames' and Lineanames' implementations). + * + * So, in order to turn the tokenId into a LabelHash, we perform the opposite operation, decoding + * from a uint256 into a Hex (of size 32) and cast it as our semantic {@link LabelHash} type. + * + * @see https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 + * @see https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 + * @see https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 + */ +export const interpretTokenIdAsLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); + +/** + * Decodes a uint256-encoded-Node (eg. from a tokenId) into a {@link Node}. + * + * Contracts in the ENSv1 ecosystem frequently implement ERC721 or ERC1155 to represent + * ownership of a Domain. As such, to construct the ERC721/ERC1155 tokenId, they may encode the + * domain's {@link Node} as a uint256. + * + * This is true for the ENSv1 NameWrapper, as well as any contracts forked from it (which includes + * Lineanames' implementation). + * + * So, in order to turn the tokenId into a Node, we perform the opposite operation, decoding + * from a uint256 into a Hex (of size 32) and cast it as our semantic {@link Node} type. + * + * @see https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/wrapper/ERC1155Fuse.sol#L262 + */ +export const interpretTokenIdAsNode = (tokenId: bigint): Node => uint256ToHex32(tokenId);