Skip to content

Conversation

@Goader
Copy link
Contributor

@Goader Goader commented Jan 26, 2026

ENS Referrals API v1: Price Types Migration

closes: #1521


Reviewer Focus (Read This First)

What reviewers should focus on
  • New v1 API implementation in packages/ens-referrals/src/v1/*
  • /v1 subpath export
  • Package configuration
  • Calculation of awardPoolApproxAmount

Error I got during testing:

Location: apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts

Error: Tests fail with:

{
  "responseCode": "error",
  "error": "Internal server error",
  "errorMessage": "(0 , __vite_ssr_import_0__.serializePriceUsdc) is not a function"
}

In my various experiments, I found it to be also failing with serializePriceEth, which was implemented before this PR. Tried untangling imports in ensnode-sdk/ens and ensnode-sdk/shared to make this dependence a single-direction one, but the problem persisted. Importing this directly from ENSApi seemed to work, and importing it from the dist version worked as well. The current workaround is to export it from internal and use it for importing in ens-referrals, which is not what I intended, but from the fixes I found seemed to be the most benign.

Fixed through: #1572


Problem & Motivation

Why this exists
  • When the ens-referrals package was first implemented, we took a few temporary shortcuts to ensure ENS Holiday Awards could be shipped before Dec 1. These temporary shortcuts included quick fixes for the USDQuantity and RevenueContribution price data models currently defined in ens-referrals.
  • A more mature price data model for PriceUsdc and PriceEth are defined in ensnode-sdk, however we were previously blocked on using these more mature data models because of issue 1519 and our need to ensure ENS Holiday Awards was shipped on schedule.
  • See issue: Use more mature Price data models in "v1" referral-related APIs #1521

What Changed (Concrete)

What actually changed
  1. Copy-pasted the whole ens-referrals to v1 directory
  2. Added appropriate entry point for v1 and v1/internal
  3. Updated USDQuantity and RevenueContribution types to use mature price types from ensnode-sdk
  4. Added V1 middleware to ENSApi to properly handle new types that appear in Referrer Leaderboard
  5. Changed imports from ensnode-sdk/shared to be explicit to avoid Vitest issues
  6. Changed the endpoint path from /ensanalytics/v1 to /v1/ensanalytics

Design & Planning

How this approach was chosen

Self-Review

What you caught yourself
  • Using old middleware, which produced an old version of referrer leaderboard
  • Lack of proper award pool shared calculation
  • Old logging of leaderboard building was trying to build JSON out of rules, that now need explicit serialization, because of bigint
  • Referrer Detail function in client was using incorrect endpoint path starting with api. It was a bug introduced previously in ensnode-sdk and migrated to ens-referrals. Fixed as of now

Cross-Codebase Alignment

Related code you checked
  • Verified v0 API remains completely unchanged
  • Checked PriceEth and its serialization implementation for consistency
  • Search terms used: PriceUsdc, PriceEth, awardPoolApproxValue, totalAwardPoolValue, RevenueContribution, USDQuantity
  • Reviewed but unchanged: v0 API implementation (still uses USDQuantity and RevenueContribution)
  • Deferred alignment: Some of the issues outlined by CodeRabbit in comments (zero division if maxQualifiedReferrers is 1, using set for uniqueReferrers)

Downstream & Consumer Impact

Who this affects and how
  • Shouldn't impact anything apart from v1 endpoints
  • Public APIs affected: v1 API only
  • Docs updated: Copied from v0
  • Naming decisions: used v1 path for imports

Testing Evidence

How this was validated
  • Standard CI testing
  • Manual validation
  • Testing performed: Automated testing + manual validation
  • Known gaps: None
  • What reviewers have to reason about manually: v1 for importing, Vite issues, proper currency calculations

Scope Reductions

What you intentionally didn't do

No changes to v0 implementation.

  • Follow-ups: None
  • Why they were deferred: N/A

Risk Analysis

How this could go wrong

Low risk - v0 API completely unchanged, v1 is additive only.

  • Risk areas: None (v0 unchanged, v1 additive)
  • Mitigations or rollback options: Trivial to revert, nothing depends on v1 yet
  • Named owner if this causes problems: @Goader

Pre-Review Checklist (Blocking)

  • I reviewed every line of this diff and understand it end-to-end
  • I'm prepared to defend this PR line-by-line in review
  • I'm comfortable being the on-call owner for this change
  • Relevant changesets are included (or explicitly not required)

@Goader Goader self-assigned this Jan 26, 2026
Copilot AI review requested due to automatic review settings January 26, 2026 13:43
@changeset-bot
Copy link

changeset-bot bot commented Jan 26, 2026

🦋 Changeset detected

Latest commit: 6b507fd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@namehash/ens-referrals Major
@ensnode/ensnode-sdk Major
ensapi Major
ensadmin Major
ensindexer Major
ensrainbow Major
fallback-ensapi Major
@ensnode/ensnode-react Major
@ensnode/ensrainbow-sdk Major
@namehash/namehash-ui Major
@ensnode/datasources Major
@ensnode/ponder-metadata Major
@ensnode/ensnode-schema Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Jan 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Jan 31, 2026 6:45am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
ensnode.io Skipped Skipped Jan 31, 2026 6:45am
ensrainbow.io Skipped Skipped Jan 31, 2026 6:45am

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

Adds a v1 ENS referrals API: new v1 types, serialization/deserialization, client, leaderboard construction/aggregation/pagination, DB metric query, SWR cache + middleware, route wiring, many v1 test fixtures, and switches v1 price types to PriceEth/PriceUsdc and related shared-import refactors.

Changes

Cohort / File(s) Summary
V1 Referral API Core
packages/ens-referrals/src/v1/*, packages/ens-referrals/src/v1/index.ts
Adds complete v1 API surface: address utils, referrer-metrics, scoring/ranking, aggregations, leaderboard, leaderboard-page pagination, referrer-detail, rules, status, time, client, and v1 barrel export.
V1 API Types & Validation
packages/ens-referrals/src/v1/api/{types,zod-schemas,serialized-types,serialize,deserialize,index}.ts
Adds serialized types, Zod schemas, (de)serializers and discriminated request/response shapes for leaderboard pages and referrer details.
Leaderboard DB & Service (v1)
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/{database-v1,get-referrer-leaderboard-v1,mocks-v1}.ts
New DB query getReferrerMetrics, service getReferrerLeaderboard, and comprehensive v1 mocks/fixtures (empty & populated).
Cache & Middleware (v1)
apps/ensapi/src/cache/referrer-leaderboard.cache-v1.ts, apps/ensapi/src/middleware/referrer-leaderboard.middleware-v1.ts
Adds SWR cache for v1 leaderboard with init logic and middleware exposing referrerLeaderboardV1 in handler context.
Handler wiring & tests
apps/ensapi/src/handlers/ensanalytics-api-v1.ts, apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts, apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts
Switches handlers/tests to v1 imports, renames context keys to referrerLeaderboardV1, updates mocks to mocks-v1, and adjusts expectations for PriceUsdc/PriceEth shapes.
Package exports & build
packages/ens-referrals/package.json, packages/ens-referrals/tsup.config.ts
Adds ./v1 and ./v1/internal exports and includes src/v1/index.ts in tsup build entries.
Serialization / Currencies / Numbers
packages/ensnode-sdk/src/shared/{currencies,numbers,zod-schemas,deserialize,serialize}.ts, packages/ensnode-sdk/src/shared/{currencies,numbers}.test.ts
Adds price parsers/serializers (parseEth/Usdc/Dai, serializePriceUsdc/Dai), deserializers, scaleBigintByNumber, scalePrice, and tests for currency/number utilities.
Shared import refactor
packages/ensnode-sdk/src/**
Replaces aggregated shared re-exports with granular shared/* paths across many modules (type import relocations and re-export reorganization).
App wiring & infra
apps/ensapi/src/index.ts, apps/ensapi/src/lib/hono-factory.ts, .changeset/*
Mounts /v1/ensanalytics route, wires v1 cache into shutdown, expands MiddlewareVariables to include v1 middleware variables, and updates changelog.
Tests & misc
packages/ens-referrals/src/v1/*.test.ts, packages/ensnode-sdk/src/.../*.test.ts
Adds/updates multiple v1-focused unit tests (leaderboard-page, link, get-referrer-leaderboard-v1) and expands currency/number test coverage.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as "V1 Handler"
    participant Middleware as "referrerLeaderboardMiddlewareV1"
    participant Cache as "referrerLeaderboardCacheV1"
    participant Indexer as "Indexing Status"
    participant DB as "Database"
    participant Builder as "Leaderboard Builder"

    Client->>Handler: GET /v1/ensanalytics/referrers?page=1
    Handler->>Middleware: apply
    Middleware->>Cache: read()
    alt cache miss / revalidate
        Cache->>Indexer: getIndexingStatus()
        Indexer-->>Cache: status (Following|Completed)
        Cache->>Indexer: getLatestIndexedBlockRef()
        Indexer-->>Cache: blockRef (timestamp)
        Cache->>DB: getReferrerMetrics(rules)
        DB-->>Cache: ReferrerMetrics[]
        Cache->>Builder: buildReferrerLeaderboard(metrics, rules, timestamp)
        Builder-->>Cache: ReferrerLeaderboard
    else cache hit
        Cache-->>Middleware: ReferrerLeaderboard (cached)
    end
    Middleware->>Handler: set c.var.referrerLeaderboardV1
    Handler->>Handler: build page / detail
    Handler-->>Client: Serialized ReferrerLeaderboardPageResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ensanalytics, ensnode-sdk

Poem

🐰 I hopped through code at break of day,
Built leaderboards that show the way,
Prices tidy, pages neat,
Cached and ranked for folks to greet—
A rabbit's cheer for v1's play.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes some out-of-scope changes: extensive import path reorganization in ensnode-sdk (changing imports from '../shared' to '../shared/types', '../shared/serialize', etc.) and removal of re-exports from shared/index.ts. These changes go beyond the stated objective of migrating price models to v1. Clarify whether the import path reorganization in ensnode-sdk is necessary for the v1 price model migration. Consider separating these changes into a distinct PR if they address a different concern than #1521.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Mature Price Models for "v1" ENSAnalytics' clearly and concisely describes the main change: transitioning the v1 API to use mature price models from ensnode-sdk.
Description check ✅ Passed The PR description extensively covers the problem, motivation, concrete changes, design decisions, self-review, testing, and risk analysis. It aligns with the template structure and provides thorough context.
Linked Issues check ✅ Passed The PR successfully fulfills #1521 requirements: replaces USDQuantity with PriceUsdc and RevenueContribution with PriceEth in v1 API only, uses mature price models from ensnode-sdk, maintains v0 unchanged, and updates related serialization/deserialization logic.
Docstring Coverage ✅ Passed Docstring coverage is 88.06% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mature-price-models

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a mature v1 API for ENS referral analytics with comprehensive price model support, adding USDC and DAI currencies alongside ETH. The changes establish a parallel v1 API structure that can coexist with the existing v0 API while providing enhanced functionality for the ENS Holiday Awards referral program.

Changes:

  • Added USDC and DAI price type support to @ensnode/ensnode-sdk with corresponding schemas, serialization, and deserialization functions
  • Created comprehensive v1 API modules for @namehash/ens-referrals including leaderboard, metrics, rankings, and award calculations
  • Implemented v1-specific database queries, handlers, and tests in ensapi to support the new API structure

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/ensnode-sdk/src/shared/zod-schemas.ts Added Zod schemas for PriceUsdc and PriceDai types
packages/ensnode-sdk/src/shared/serialize.ts Added serialization functions for USDC and DAI prices; included inline copy of uint256ToHex32 to avoid module resolution issues
packages/ensnode-sdk/src/shared/deserialize.ts Added deserialization functions for ETH, USDC, and DAI prices
packages/ens-referrals/tsup.config.ts Added v1.ts as a build entry point to support parallel v1 exports
packages/ens-referrals/src/v1.ts New entry point that exports all v1-specific modules
packages/ens-referrals/src/rules-v1.ts Defines referral program rules including award pool value, qualification thresholds, and time boundaries
packages/ens-referrals/src/referrer-metrics-v1.ts Core metrics types and calculations for referrers including scoring, ranking, and award distribution
packages/ens-referrals/src/referrer-detail-v1.ts Implements referrer detail lookup with support for both ranked and unranked referrers
packages/ens-referrals/src/rank-v1.ts Ranking logic including score boost calculations and comparisons
packages/ens-referrals/src/leaderboard-v1.ts Leaderboard construction from raw metrics with full calculations
packages/ens-referrals/src/leaderboard-page-v1.ts Pagination logic for leaderboard queries
packages/ens-referrals/src/api/zod-schemas-v1.ts Zod schemas for v1 API types including validation rules
packages/ens-referrals/src/api/types-v1.ts TypeScript types for v1 API request/response structures
packages/ens-referrals/src/api/serialized-types-v1.ts Serialized type definitions for v1 API data transfer
packages/ens-referrals/src/api/serialize-v1.ts Serialization functions for converting runtime types to wire format
packages/ens-referrals/src/api/deserialize-v1.ts Deserialization functions with Zod validation for v1 API responses
packages/ens-referrals/src/aggregations-v1.ts Aggregation calculations for leaderboard-wide metrics
packages/ens-referrals/package.json Updated exports to include ./v1 entry point for consumers
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts Mock data for v1 API testing
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts Database-backed leaderboard construction for v1 API
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts Drizzle ORM queries for fetching referrer metrics from the database
apps/ensapi/src/handlers/ensanalytics-api-v1.ts Updated import to use v1 entry point
apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts Updated tests to use v1 imports and properly test USDC price objects

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

rules: ReferralProgramRules,
): number {
if (!isReferrerQualified(rank, rules)) return 0;

Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential division by zero issue when maxQualifiedReferrers is 1. In line 46, the expression (rules.maxQualifiedReferrers - 1) becomes 0 when maxQualifiedReferrers is 1, resulting in division by zero. This would cause calcReferrerFinalScoreBoost to return Infinity or NaN.

While it may be unlikely that maxQualifiedReferrers would be set to 1 in practice, this should be handled explicitly to avoid runtime errors. Consider adding a check: if maxQualifiedReferrers === 1, return 1 (since rank 1 is the only qualified referrer and should get maximum boost).

Suggested change
// Avoid division by zero when only a single referrer is qualified.
// In this case, that single referrer (rank 1) should receive the maximum boost.
if (rules.maxQualifiedReferrers === 1) return 1;

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Goader This looks like a good suggestion.

@@ -13,7 +13,14 @@ import { z } from "zod/v4";

import { ENSNamespaceIds, type InterpretedName, Node } from "../ens";
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Node' is defined but never used.

Suggested change
import { ENSNamespaceIds, type InterpretedName, Node } from "../ens";
import { ENSNamespaceIds, type InterpretedName } from "../ens";

Copilot uses AI. Check for mistakes.
import {
type CurrencyId,
CurrencyIds,
Price,
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Price' is defined but never used.

Suggested change
Price,

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ensnode-sdk/src/shared/serialize.ts (1)

2-36: Consider centralizing uint256ToHex32 to avoid drift.

This is now a local copy of packages/ensnode-sdk/src/ens/subname-helpers.ts:uint256ToHex32. If that implementation changes, these can diverge. A small shared util imported by both modules would keep behavior in sync while still avoiding the Vite SSR import issue noted in the comment.

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts`:
- Around line 71-89: Add lightweight runtime validation before casting records
to NonNullRecord to avoid runtime crashes if DB shape changes: in the mapping
code around records → NonNullRecord[] (used with buildReferrerMetrics,
deserializeDuration, and priceEth) check each record has a non-null/defined
referrer, a numeric totalReferrals, and stringifiable
totalIncrementalDuration/totalRevenueContribution; if any check fails either
skip the record with a warning log or throw a descriptive error. Alternatively,
replace the manual checks with a small Zod schema that validates/refines {
referrer: string, totalReferrals: number, totalIncrementalDuration: string,
totalRevenueContribution: string } and parse records through it before calling
buildReferrerMetrics.

In `@packages/ens-referrals/src/api/deserialize-v1.ts`:
- Around line 151-181: The switch in deserializeReferrerLeaderboardPageResponse
can leave deserialized undefined if maybeResponse.responseCode is unexpected;
add a default branch (or an exhaustive check) after the existing cases that
throws a clear error including the unexpected maybeResponse.responseCode (and
optionally the serialized payload) so callers fail-fast before calling
makeReferrerLeaderboardPageResponseSchema/safeParse; ensure the thrown message
uses prettifyError or similar context so debugging the malformed response is
easy.
- Around line 190-229: deserializeReferrerDetailResponse lacks default branches
in the outer switch on maybeResponse.responseCode and the inner switch on
maybeResponse.data.type, so add defensive default cases that throw clear Errors
(including the unexpected value) to make the function exhaustive and ensure
deserialized is always assigned; update the outer switch to handle unknown
responseCode by throwing an Error indicating the unexpected responseCode and
include a default in the inner switch to throw an Error indicating the
unexpected data.type (or use a never-exhaustiveness helper if preferred) before
the final zod validation in deserializeReferrerDetailResponse.

In `@packages/ens-referrals/src/api/zod-schemas-v1.ts`:
- Around line 67-86: Update makeUnrankedReferrerMetricsSchema so the eight
fields that must be zero are validated at parse time: totalReferrals,
totalIncrementalDuration, totalRevenueContribution, score, finalScoreBoost,
finalScore, awardPoolShare, and awardPoolApproxValue. Replace their current
permissive schemas (e.g., makeNonNegativeIntegerSchema, makeDurationSchema,
makePriceEthSchema, makeFiniteNonNegativeNumberSchema, makePriceUsdcSchema) with
explicit zero constraints—either z.literal(0) for exact-zero or apply
.min(0).max(0, "<field> must be 0") to the existing schema factories—to ensure
these fields can only be 0 and include a clear error message referencing the
field via the valueLabel.

In `@packages/ens-referrals/src/leaderboard-page-v1.ts`:
- Around line 208-214: Doc invariant for ReferrerLeaderboardPage.referrers
conflicts with ReferrerLeaderboard/getReferrerLeaderboardPage which preserve Map
order (ascending by rank). Fix by aligning docs: update the invariant on
ReferrerLeaderboardPage.referrers to state entries are ordered by `rank`
(ascending) instead of descending (or alternatively, if you prefer descending,
reverse the array returned by `getReferrerLeaderboardPage`/the Map iteration);
reference symbols: ReferrerLeaderboard, getReferrerLeaderboardPage,
ReferrerLeaderboardPage.referrers, AwardedReferrerMetrics.
- Around line 118-148: The validator must also check totalPages and that the
provided startIndex/endIndex match derived values: inside
validateReferrerLeaderboardPageContext compute expectedTotalPages =
Math.ceil(context.totalRecords / context.recordsPerPage) (handle zero records),
compute expectedStartIndex = (context.page - 1) * context.recordsPerPage and
expectedEndIndex = Math.min(expectedStartIndex + context.recordsPerPage,
context.totalRecords), then validate that context.totalPages is a non-negative
integer and equals expectedTotalPages, that context.page <= context.totalPages
(and valid for zero/empty cases), and that context.startIndex ===
expectedStartIndex and context.endIndex === expectedEndIndex (also ensure
endIndex >= startIndex); throw the same style of Error with clear messages when
any of these invariants fail.

In `@packages/ens-referrals/src/leaderboard-v1.ts`:
- Around line 56-61: The duplicate-referrer check is wrong because
uniqueReferrers is just a mapping copy; replace it by creating a Set of referrer
strings from allReferrers (e.g., new Set(allReferrers.map(r => r.referrer))) and
compare set.size to allReferrers.length inside buildReferrerLeaderboard (or
wherever uniqueReferrers is defined); if sizes differ, throw the existing Error
so duplicates are detected before constructing the Map and silently overwriting
entries.

In `@packages/ens-referrals/src/rank-v1.ts`:
- Around line 40-47: calcReferrerFinalScoreBoost currently divides by
(rules.maxQualifiedReferrers - 1) causing divide-by-zero when
maxQualifiedReferrers === 1; update calcReferrerFinalScoreBoost to guard that
case by returning the correct boost (return 1 for the sole qualified referrer)
when rules.maxQualifiedReferrers <= 1 (and keep the existing isReferrerQualified
check), otherwise compute 1 - (rank - 1) / (rules.maxQualifiedReferrers - 1) as
before.

In `@packages/ens-referrals/src/referrer-metrics-v1.ts`:
- Around line 280-303: In buildAwardedReferrerMetrics the code converts
rules.totalAwardPoolValue.amount to Number which can overflow; keep all math in
bigint by using fixed‑point scaling or an integer fraction instead of Number().
Change calcReferrerAwardPoolShare usage so you either obtain a rational
numerator/denominator (or use a fixed SCALE as a BigInt) and compute
awardPoolApproxAmount with bigint arithmetic: (BigInt(numerator) *
rules.totalAwardPoolValue.amount) / BigInt(denominator) (or
(BigInt(Math.round(awardPoolShare*SCALE)) * totalAmount) / SCALE as BigInt),
then set awardPoolApproxValue.amount to that bigint and run
validateAwardedReferrerMetrics as before.

In `@packages/ens-referrals/src/rules-v1.ts`:
- Line 7: validateReferralProgramRules currently omits validation for the
subregistryId field; update the function (and the same checks around lines
67-85) to validate subregistryId alongside price/timestamp/count invariants:
locate the validateReferralProgramRules function and add a check that
subregistryId is present and matches your expected account-id format (e.g.,
non-empty string and the same pattern used elsewhere for ENS account IDs or a
shared validator helper if one exists), and throw or return an error when it
fails so invalid account IDs cannot pass through.

In `@packages/ens-referrals/src/v1.ts`:
- Around line 14-15: Remove the public re-export of the internal Zod schemas by
deleting the export of "./api/zod-schemas-v1" from the v1 public entry (leave
"export * from \"./api/types-v1\""), so the internal module "zod-schemas-v1" is
no longer exposed; ensure any consumers that relied on that export are updated
to import from the internal re-export location (internal.ts) instead and run the
build/tests to confirm no public API leak remains.

Comment on lines 14 to 15
export * from "./api/types-v1";
export * from "./api/zod-schemas-v1";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check the current state of v1.ts
cat -n packages/ens-referrals/src/v1.ts

Repository: namehash/ensnode

Length of output: 1170


🏁 Script executed:

# Check the header and content of zod-schemas-v1.ts
head -40 packages/ens-referrals/src/api/zod-schemas-v1.ts

Repository: namehash/ensnode

Length of output: 1481


🏁 Script executed:

# Check if internal.ts exists and its content
cat -n packages/ens-referrals/src/internal.ts 2>/dev/null || echo "internal.ts not found"

Repository: namehash/ensnode

Length of output: 723


🏁 Script executed:

# List all files in the src directory to understand the structure
find packages/ens-referrals/src -type f -name "*.ts" | head -20

Repository: namehash/ensnode

Length of output: 941


Remove Zod schemas from public v1 entry point.

The header in packages/ens-referrals/src/api/zod-schemas-v1.ts explicitly states that schemas must remain internal and only be re-exported from internal.ts. Re-exporting from the public v1 entry point violates this policy and locks an internal implementation detail into the public API surface. Remove this export from line 15.

🔧 Suggested change
 export * from "./api/types-v1";
-export * from "./api/zod-schemas-v1";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export * from "./api/types-v1";
export * from "./api/zod-schemas-v1";
export * from "./api/types-v1";
🤖 Prompt for AI Agents
In `@packages/ens-referrals/src/v1.ts` around lines 14 - 15, Remove the public
re-export of the internal Zod schemas by deleting the export of
"./api/zod-schemas-v1" from the v1 public entry (leave "export * from
\"./api/types-v1\""), so the internal module "zod-schemas-v1" is no longer
exposed; ensure any consumers that relied on that export are updated to import
from the internal re-export location (internal.ts) instead and run the
build/tests to confirm no public API leak remains.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts`:
- Around line 44-88: The SUM(incrementalDuration) can be NULL when all rows are
NULL, so change the select for totalIncrementalDuration to mirror revenue and
wrap the sum in COALESCE to default to 0; specifically replace
sum(schema.registrarActions.incrementalDuration).as("total_incremental_duration")
with sql`COALESCE(SUM(${schema.registrarActions.incrementalDuration}),
0)`.as("total_incremental_duration") so
deserializeDuration(record.totalIncrementalDuration) and the ordering by
total_incremental_duration are safe and non-null.

In `@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts`:
- Around line 949-1540: The referrerLeaderboardPageResponseOk fixture duplicates
the populatedReferrerLeaderboard data; replace the repeated literal by
constructing referrerLeaderboardPageResponseOk from the existing
populatedReferrerLeaderboard fixture (e.g., import/populate
populatedReferrerLeaderboard and then build referrerLeaderboardPageResponseOk by
referencing its properties for data.rules, data.referrers,
data.aggregatedMetrics, data.pageContext and data.accurateAsOf) so changes to
populatedReferrerLeaderboard automatically flow to
referrerLeaderboardPageResponseOk and avoid drift; ensure the top-level
responseCode remains ReferrerLeaderboardPageResponseCodes.Ok and any
BigInt/const types are preserved when mapping.
- Around line 282-947: The populatedReferrerLeaderboard fixture has several
referrer entries (inside populatedReferrerLeaderboard.referrers Map) whose
totalRevenueContribution.amount are incorrectly set to 6_000_000_000_000_000n
but should match the distinct values in dbResultsReferrerLeaderboard (e.g.,
0.007, 0.0065, 0.0075, 0.008, 0.0085 ETH → 7_000_000_000_000_000n,
6_500_000_000_000_000n, 7_500_000_000_000_000n, 8_000_000_000_000_000n,
8_500_000_000_000_000n); locate the affected referrer keys in the Map (those
currently using 6_000_000_000_000_000n) and replace each amount to the exact
big-int values from dbResultsReferrerLeaderboard, then update any dependent
fields in populatedReferrerLeaderboard (scores, awardPoolShare,
awardPoolApproxValue, and aggregatedMetrics like grandTotalRevenueContribution)
so the fixture stays consistent with the source.

In `@packages/ens-referrals/src/aggregations-v1.ts`:
- Around line 69-72: The function buildAggregatedReferrerMetrics assumes
referrers is a complete, globally ranked list starting at rank 1 (the invariant
check expects ranks to be absolute), so add a clear JSDoc on the
buildAggregatedReferrerMetrics export stating that referrers must contain a full
ranked list with ranks starting at 1 (not a paginated/partial slice), and
document the expected shape and consequences (that maxQualifiedReferrers > 0
with no qualified referrers will throw) so callers know to pass the full ranking
or to pre-aggregate before calling.

In `@packages/ens-referrals/src/api/types-v1.ts`:
- Around line 46-50: The ReferrerLeaderboardPageResponseError type (and
ReferrerDetailResponseError) currently carries both error and errorMessage;
decide and implement a single consistent pattern: either consolidate into one
field (e.g., errorMessage: string) by removing the redundant property and
updating all usages of ReferrerLeaderboardPageResponseError and
ReferrerDetailResponseError, or explicitly document and rename to make the
distinction clear (e.g., errorCode: string for machine-readable values and
message: string for human-readable text) and update all references and
serializers/deserializers accordingly; update the type definitions
(ReferrerLeaderboardPageResponseError, ReferrerDetailResponseError) and any code
that constructs or reads these objects to match the chosen shape and ensure
tests/API clients reflect the change.

In `@packages/ens-referrals/src/api/zod-schemas-v1.ts`:
- Around line 110-125: The schema makeReferrerLeaderboardPageContextSchema
currently allows optional startIndex and endIndex but doesn't enforce endIndex
>= startIndex; update the returned z.object to add a refinement (using .refine
or .superRefine on the object) that, when both startIndex and endIndex are
defined, asserts endIndex >= startIndex and returns a clear error path (e.g.,
`${valueLabel}.endIndex` or `${valueLabel}.startIndex`) on failure; reference
the object produced by makeReferrerLeaderboardPageContextSchema and the
startIndex/endIndex fields when implementing the check so the validator triggers
only when both values are present.

In `@packages/ens-referrals/src/leaderboard-v1.ts`:
- Around line 56-61: The duplicate detection is wrong because uniqueReferrers is
created with map which preserves length; replace it with a Set-based check:
compute const uniqueReferrerSet = new Set(allReferrers.map(r => r.referrer)) and
then compare uniqueReferrerSet.size to allReferrers.length (or recreate
uniqueReferrers via Array.from(uniqueReferrerSet)) in the
buildReferrerLeaderboard logic to properly detect duplicates and throw the same
Error when sizes differ; update references to uniqueReferrers accordingly.

In `@packages/ens-referrals/src/referrer-metrics-v1.ts`:
- Around line 287-300: The current calculation for awardPoolApproxAmount loses
precision by converting rules.totalAwardPoolValue.amount (a bigint) to Number;
instead compute the amount using precise integer or big-decimal arithmetic:
either represent awardPoolShare as an integer fraction (numerator/denominator)
and compute awardPoolApproxAmount = (BigInt(rules.totalAwardPoolValue.amount) *
numerator) / denominator, or use a BigNumber/Decimal library to multiply a
Decimal(awardPoolShare) by the bigint total and round/floor to a bigint; update
the code around awardPoolApproxAmount, awardPoolShare and
result.awardPoolApproxValue to use the chosen precise method.

In `@packages/ens-referrals/src/rules-v1.ts`:
- Around line 67-86: The function validateReferralProgramRules currently omits
validation for the subregistryId field; add a call to the existing AccountId
validator (e.g., validateAccountId) to check rules.subregistryId inside
validateReferralProgramRules, placed alongside the other field checks (after
totalAwardPoolValue parsing and before/after maxQualifiedReferrers validation),
and throw a clear Error if validation fails so the code validates that
subregistryId is a valid AccountId.

In `@packages/ensnode-sdk/src/shared/serialize.ts`:
- Around line 23-36: Add a short maintenance note next to the duplicated
uint256ToHex32 definition explaining that this is an intentional inline copy of
the implementation in ../ens/subname-helpers.ts due to Vite SSR resolution, and
include a TODO/link or issue number to track changes so both implementations
stay synchronized; reference the function name uint256ToHex32 and the original
file subname-helpers.ts in the comment to make future updates clear.
♻️ Duplicate comments (9)
packages/ensnode-sdk/src/shared/zod-schemas.ts (1)

16-23: Unused import Price should be removed.

The Price import on line 19 is not used anywhere in this file. This was previously flagged and remains unresolved.

Suggested fix
 import {
   type CurrencyId,
   CurrencyIds,
-  Price,
   type PriceDai,
   type PriceEth,
   type PriceUsdc,
 } from "./currencies";
packages/ens-referrals/src/rank-v1.ts (1)

40-47: Division by zero when maxQualifiedReferrers is 1.

When rules.maxQualifiedReferrers === 1 and rank === 1, line 46 computes (rank - 1) / (rules.maxQualifiedReferrers - 1) which evaluates to 0 / 0 = NaN, causing the function to return NaN.

Proposed fix
 export function calcReferrerFinalScoreBoost(
   rank: ReferrerRank,
   rules: ReferralProgramRules,
 ): number {
   if (!isReferrerQualified(rank, rules)) return 0;
 
+  // When only one referrer can qualify, that referrer gets the maximum boost
+  if (rules.maxQualifiedReferrers === 1) return 1;
+
   return 1 - (rank - 1) / (rules.maxQualifiedReferrers - 1);
 }
packages/ens-referrals/src/api/types-v1.ts (1)

12-12: Consider using a type alias instead of an empty interface.

An interface that declares no additional members is equivalent to its supertype. Using a type alias is more explicit.

packages/ens-referrals/src/api/serialized-types-v1.ts (1)

8-15: Remove unused imports.

ReferrerDetailResponse (line 9) and ReferrerLeaderboardPageResponse (line 12) are imported but never used in this file. Only their constituent types (*Ok and *Error variants) are used.

packages/ens-referrals/src/api/zod-schemas-v1.ts (3)

25-25: Remove unused import ReferrerDetailRanked.

The type ReferrerDetailRanked is imported but only used in JSDoc comments, not in code. The ReferrerDetailTypeIds import is sufficient.


153-160: Remove unused _valueLabel parameter or use it.

The _valueLabel parameter is declared but never used in makeReferrerLeaderboardPageResponseErrorSchema. Either remove it or use it for field descriptions like the other schema factories.


213-220: Remove unused _valueLabel parameter or use it.

Same issue as above - _valueLabel is declared but unused in makeReferrerDetailResponseErrorSchema.

packages/ens-referrals/src/api/serialize-v1.ts (1)

164-187: Unreachable break statement after nested switch.

The break on line 182 follows a nested switch where both branches ("ranked" and "unranked") return early. If TypeScript's type narrowing ensures exhaustiveness, this break is dead code. If not, the function could implicitly return undefined.

Consider removing the break since each nested case already returns, or add an exhaustive check after the nested switch for defensive coding:

Suggested fix
         case "unranked":
           return {
             responseCode: response.responseCode,
             data: serializeReferrerDetailUnranked(response.data),
           };
       }
-      break;

     case ReferrerDetailResponseCodes.Error:
       return response;
   }
 }
packages/ens-referrals/src/api/deserialize-v1.ts (1)

190-229: Potential uninitialized variable if nested switch is non-exhaustive.

The let deserialized variable (line 194) may remain unassigned if maybeResponse.data.type doesn't match "ranked" or "unranked". The break on line 212 would exit the outer switch, leaving deserialized undefined when accessed on line 222.

If TypeScript's type system guarantees exhaustiveness of the nested switch, the break is dead code. Otherwise, this could cause a runtime error. Consider restructuring to eliminate the intermediate variable or adding an exhaustive guard.

Suggested fix
 export function deserializeReferrerDetailResponse(
   maybeResponse: SerializedReferrerDetailResponse,
   valueLabel?: string,
 ): ReferrerDetailResponse {
-  let deserialized: ReferrerDetailResponse;
   switch (maybeResponse.responseCode) {
     case "ok": {
+      let data: ReferrerDetailRanked | ReferrerDetailUnranked;
       switch (maybeResponse.data.type) {
         case "ranked":
-          deserialized = {
-            responseCode: maybeResponse.responseCode,
-            data: deserializeReferrerDetailRanked(maybeResponse.data),
-          } as ReferrerDetailResponse;
+          data = deserializeReferrerDetailRanked(maybeResponse.data);
           break;
-
         case "unranked":
-          deserialized = {
-            responseCode: maybeResponse.responseCode,
-            data: deserializeReferrerDetailUnranked(maybeResponse.data),
-          } as ReferrerDetailResponse;
+          data = deserializeReferrerDetailUnranked(maybeResponse.data);
           break;
       }
-      break;
+      const deserialized = {
+        responseCode: maybeResponse.responseCode,
+        data,
+      } as ReferrerDetailResponse;
+      const schema = makeReferrerDetailResponseSchema(valueLabel);
+      const parsed = schema.safeParse(deserialized);
+      if (parsed.error) {
+        throw new Error(`Cannot deserialize ReferrerDetailResponse:\n${prettifyError(parsed.error)}\n`);
+      }
+      return parsed.data;
     }
-
-    case "error":
-      deserialized = maybeResponse;
-      break;
+    case "error": {
+      const schema = makeReferrerDetailResponseSchema(valueLabel);
+      const parsed = schema.safeParse(maybeResponse);
+      if (parsed.error) {
+        throw new Error(`Cannot deserialize ReferrerDetailResponse:\n${prettifyError(parsed.error)}\n`);
+      }
+      return parsed.data;
+    }
   }
-
-  // Then validate the deserialized structure using zod schemas
-  const schema = makeReferrerDetailResponseSchema(valueLabel);
-  const parsed = schema.safeParse(deserialized);
-
-  if (parsed.error) {
-    throw new Error(`Cannot deserialize ReferrerDetailResponse:\n${prettifyError(parsed.error)}\n`);
-  }
-
-  return parsed.data;
 }

Comment on lines 23 to 36
/**
* Encodes a uint256 bigint as hex string sized to 32 bytes.
* Uses include, in the context of ENS, decoding the uint256-encoded tokenId of NFT-issuing contracts
* into Node or LabelHash, which is a common behavior in the ENS ecosystem.
* (see NameWrapper, ETHRegistrarController)
*
* @remarks
* This is an inline copy of the function from `../ens/subname-helpers.ts` to avoid
* cross-module imports that cause Vite SSR module resolution issues. When Vitest loads
* TypeScript sources in a pnpm workspace, importing from ../ens creates a complex module
* graph that Vite SSR cannot properly resolve, resulting in imported functions appearing
* as undefined at runtime.
*/
const uint256ToHex32 = (num: bigint): Hex => toHex(num, { size: 32 });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that both uint256ToHex32 implementations are consistent

echo "=== Searching for uint256ToHex32 in subname-helpers.ts ==="
rg -n 'uint256ToHex32' --glob '**/subname-helpers.ts' -A 2

echo ""
echo "=== Searching for uint256ToHex32 in serialize.ts ==="
rg -n 'uint256ToHex32' --glob '**/serialize.ts' -A 2

echo ""
echo "=== Checking if subname-helpers.ts exists and checking its exports ==="
find . -name 'subname-helpers.ts' -type f 2>/dev/null

echo ""
echo "=== Checking relative path from serialize.ts ==="
find . -path '*/shared/serialize.ts' -type f 2>/dev/null

Repository: namehash/ensnode

Length of output: 950


Documentation is thorough; code duplication requires maintenance tracking.

The inline copy of uint256ToHex32 from ../ens/subname-helpers.ts is intentional and well-documented due to Vite SSR module resolution constraints. Both implementations are currently identical. However, this duplication creates a maintenance burden: if the original function in subname-helpers.ts is modified, this private copy must be updated in parallel to maintain consistency.

Consider adding a comment or tracking mechanism (e.g., a maintenance note or linked issue) to ensure the implementations remain synchronized if changes occur to either version.

🤖 Prompt for AI Agents
In `@packages/ensnode-sdk/src/shared/serialize.ts` around lines 23 - 36, Add a
short maintenance note next to the duplicated uint256ToHex32 definition
explaining that this is an intentional inline copy of the implementation in
../ens/subname-helpers.ts due to Vite SSR resolution, and include a TODO/link or
issue number to track changes so both implementations stay synchronized;
reference the function name uint256ToHex32 and the original file
subname-helpers.ts in the comment to make future updates clear.

Copilot AI review requested due to automatic review settings January 26, 2026 14:27
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 26, 2026 14:27 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 26, 2026 14:27 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 26, 2026 14:27 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 288 to 291
// We need to convert the share (a number between 0 and 1) to a bigint amount
const awardPoolApproxAmount = BigInt(
Math.floor(awardPoolShare * Number(rules.totalAwardPoolValue.amount)),
);
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calculation on lines 289-290 converts the totalAwardPoolValue.amount (a bigint) to a Number before multiplying by awardPoolShare. This conversion could cause precision loss if totalAwardPoolValue.amount exceeds Number.MAX_SAFE_INTEGER (2^53 - 1, approximately 9 quadrillion). Given that USDC has 6 decimals, this would happen with amounts above 9 million USDC, which is possible for large award pools. Consider using bigint arithmetic throughout: multiply the bigint by a scaled integer representation of the share, then divide by the scaling factor.

Suggested change
// We need to convert the share (a number between 0 and 1) to a bigint amount
const awardPoolApproxAmount = BigInt(
Math.floor(awardPoolShare * Number(rules.totalAwardPoolValue.amount)),
);
// Use bigint arithmetic for the large amount to avoid precision loss. We represent the
// share as a scaled integer, multiply in bigint space, then divide by the scale.
const SHARE_SCALE = 1_000_000_000n;
const scaledShare = BigInt(Math.floor(awardPoolShare * Number(SHARE_SCALE)));
const awardPoolApproxAmount =
(rules.totalAwardPoolValue.amount * scaledShare) / SHARE_SCALE;

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 28, 2026 12:06
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 28, 2026 12:06 Inactive
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Goader Hey thanks for this! Reviewed and shared some suggestions. Please take the lead to merge when ready 👍

} from "@ensnode/ensnode-sdk/internal";

import { REFERRERS_PER_LEADERBOARD_PAGE_MAX } from "../leaderboard-page";
import { type ReferrerDetailRanked, ReferrerDetailTypeIds } from "../referrer-detail";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Goader Could you check this?

export const makeReferrerLeaderboardPageResponseSchema = (
valueLabel: string = "ReferrerLeaderboardPageResponse",
) =>
z.union([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe here we could make use of Zod's support for discriminated unions.

discriminatedUnion instead of union.

Related docs: https://zod.dev/api?id=discriminated-unions

const result = {
...referrer,
awardPoolShare,
awardPoolApproxValue: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see related comments above in this file. awardPoolApproxAmount will already be a PriceUsdc if we follow those suggestions and won't need to be manually converted back into a PriceUsdc here.

* @returns An {@link UnrankedReferrerMetrics} with zero values for all metrics and null rank
*/
export const buildUnrankedReferrerMetrics = (referrer: Address): UnrankedReferrerMetrics => {
const baseMetrics = buildReferrerMetrics(referrer, 0, 0, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not create Price objects inline like this. Suggest instead to define utility functions such as buildPriceEth that take in an amount (in wei) and then return back a PriceEth object.

finalScoreBoost: 0,
finalScore: 0,
awardPoolShare: 0,
awardPoolApproxValue: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see related comment above. This could be a buildPriceUsdc utility function.

finalScoreBoost: 1,
finalScore: 6.001492159318809,
awardPoolShare: 0.36258040801323277,
awardPoolApproxValue: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each of our supported currency types, suggest to define a utility function such as:

parseUsdc which should take as input a string such as "123.45678" (representing $123.45678 in USDC) and then return back a PriceUsdc where the amount is converted into the appropriate representation. This will need to use getCurrencyInfo and CurrencyInfo.decimals.

Each of these helper functions should have good unit tests defined.

Here's some related utility functions that operate on a slightly different data model but are fundamentally solving the same problem:

  1. parseUnits in viem: https://github.com/wevm/viem/blob/f11456b6907e024eb0b34e333a2d39c28a6d417e/src/utils/unit/parseUnits.ts
  2. ... related unit tests: https://github.com/wevm/viem/blob/f11456b6907e024eb0b34e333a2d39c28a6d417e/src/utils/unit/parseUnits.test.ts
  3. parseEther in viem, which would be something like a parseEth in our data model and terminology: https://github.com/wevm/viem/blob/f11456b6907e024eb0b34e333a2d39c28a6d417e/src/utils/unit/parseEther.ts
  4. ... related unit tests: https://github.com/wevm/viem/blob/f11456b6907e024eb0b34e333a2d39c28a6d417e/src/utils/unit/parseEther.test.ts

referrer: "0x538e35b2888ed5bc58cf2825d76cf6265aa4e31e",
totalReferrals: 3,
totalIncrementalDuration: 94694400,
totalRevenueContribution: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be implemented as parseEth. Please see my related comment on parseUsdc.

Copilot AI review requested due to automatic review settings January 31, 2026 05:47
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 31, 2026 05:47 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 31, 2026 05:47 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 85 out of 85 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts`:
- Around line 197-214: The populatedReferrerLeaderboard fixture contains two
referrers (0x54e7c79aceb6b736da4c29da088aae30991635bb and
0xe3cc38fb4da8a96a6ab245022e6778a1ed32619c) that are missing from
dbResultsReferrerLeaderboard, causing
aggregatedMetrics.grandTotalRevenueContribution to be incorrect; fix by either
(A) adding those two referrers with correct metric fields to
dbResultsReferrerLeaderboard so the populatedReferrerLeaderboard aggregation
matches the DB source, or (B) removing those two entries from
populatedReferrerLeaderboard and updating aggregatedMetrics
(grandTotalRevenueContribution, grandTotalReferrals,
grandTotalIncrementalDuration, grandTotalQualifiedReferrersFinalScore, etc.) to
reflect the true sums from dbResultsReferrerLeaderboard; update whichever
function/fixture references populatedReferrerLeaderboard and
dbResultsReferrerLeaderboard to keep them consistent.

In `@packages/ens-referrals/src/v1/api/zod-schemas.ts`:
- Around line 31-38: Add an object-level validation to
makeReferralProgramRulesSchema to enforce the documented invariant that endTime
must be >= startTime: after building the z.object with totalAwardPoolValue,
maxQualifiedReferrers, startTime, endTime, subregistryId, call .refine(...) on
the object returned by makeReferralProgramRulesSchema to compare the parsed
startTime and endTime values (they are Unix timestamps), returning a clear error
message and setting the error path to the endTime field if endTime < startTime
so Zod rejects invalid API responses at schema-parse time.

In `@packages/ens-referrals/src/v1/referrer-metrics.ts`:
- Around line 297-304: Replace the type assertion on the constructed object with
a compile-time check using TypeScript's `satisfies` operator: change the
`result` declaration that currently uses `as AwardedReferrerMetrics` to use
`satisfies AwardedReferrerMetrics` (e.g., the `result` const created before
calling `validateAwardedReferrerMetrics`). Keep the same object shape (spreading
`referrer` and adding `awardPoolShare` and `awardPoolApproxValue`) and retain
the `validateAwardedReferrerMetrics(result, rules)` call; this enforces
structural type correctness at compile time rather than bypassing checks with
`as`.

In `@packages/ensnode-sdk/src/shared/currencies.ts`:
- Around line 217-305: The parseEth, parseUsdc, and parseDai functions currently
allow empty and negative inputs because they rely on viem.parseUnits; add input
validation in each function (parseEth, parseUsdc, parseDai) to trim the incoming
string, throw an Error if the trimmed value is empty, and throw an Error if the
trimmed value represents a negative number (e.g., startsWith('-') or parses
negative), before calling getCurrencyInfo/parseUnits; ensure the thrown error
message is clear (e.g., "amount must be a non-negative decimal string") and keep
using priceEth/priceUsdc/priceDai to return the typed result, and update the
JSDoc examples to remove or change the cases showing "" and "-1" as valid
results.

In `@packages/ensnode-sdk/src/shared/numbers.ts`:
- Around line 54-85: The function scaleBigintByNumber can call
scaleFactor.toFixed(20) which will produce exponential notation for magnitudes
>= 1e21 and later cause BigInt parsing to throw; add an explicit guard before
calling toFixed inside scaleBigintByNumber that rejects scaleFactor values >=
1e21 (and likewise any scaleFactor >= 1e21 or NaN already guarded) by throwing a
clear Error (e.g., "scaleBigintByNumber: scaleFactor must be less than 1e21,
got: <value>") so the function fails fast and avoids producing exponential
strings for fixedStr.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/ens-referrals/src/v1/api/zod-schemas.ts`:
- Line 25: Remove the unused type import ReferrerDetailRanked from the import
statement in zod-schemas (leave ReferrerDetailTypeIds intact); since
ReferrerDetailRanked is only referenced in a JSDoc `@link` comment it does not
need to be imported, so update the import that currently mentions
ReferrerDetailRanked to only import ReferrerDetailTypeIds.

In `@packages/ensnode-sdk/src/shared/currencies.ts`:
- Around line 217-312: validateAmountToParse currently trims for validation but
discards the trimmed value, causing parseUnits (used in parseEth, parseUsdc,
parseDai) to receive untrimmed input; change validateAmountToParse to return the
trimmed string (or alternatively have parseEth/parseUsdc/parseDai call .trim()
themselves) and then pass that trimmed value into parseUnits so parsing and
validation are consistent (update signatures/usages of validateAmountToParse and
ensure parseEth, parseUsdc, parseDai use the returned trimmed value when calling
parseUnits).

Copilot AI review requested due to automatic review settings January 31, 2026 06:44
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 31, 2026 06:44 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 31, 2026 06:44 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 85 out of 85 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Goader Goader merged commit 84a4c5e into main Jan 31, 2026
20 of 22 checks passed
@Goader Goader deleted the feat/mature-price-models branch January 31, 2026 06:51
@github-actions github-actions bot mentioned this pull request Jan 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use more mature Price data models in "v1" referral-related APIs

4 participants