Conversation
🦋 Changeset detectedLatest commit: 06bfd8a The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 3 Skipped Deployments
|
📝 WalkthroughWalkthroughReplaces single holiday-awards config with a referralProgramCycleSet (default + optional custom JSON URL), adds cycle types and defaults, initializes per-cycle SWR leaderboard caches, refactors config/env/validation, and makes ENSAnalytics v1 handlers, middleware, client, serializers, and tests cycle-aware. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API as "ENSApi v1 handler"
participant Config as "Config Loader"
participant CycleCache as "Cycle Cache Map"
participant Indexer as "Indexer Status"
participant Leaderboard as "Leaderboard Builder"
Client->>API: GET /v1/ensanalytics/referral-leaderboard?cycle=cycle-1
API->>Config: validateCycleExists(cycle-1)
Config-->>API: cycle-1 found
API->>CycleCache: select cache for cycle-1
CycleCache->>Indexer: read indexing status for cycle-1
Indexer-->>CycleCache: status (Following/Completed)
CycleCache->>Leaderboard: getReferrerLeaderboard(rules, timestamp)
Leaderboard-->>CycleCache: leaderboard data
CycleCache-->>API: SWRCache read result
API-->>Client: 200 ReferrerLeaderboardPageResponse
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Important Action Needed: IP Allowlist UpdateIf your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:
Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This pull request refactors the ENS referral program system from supporting a single program period to supporting multiple concurrent and sequential "cycles". Each cycle represents a distinct referral program period with its own rules, leaderboard, and award distribution.
Changes:
- Introduces a cycle-based architecture with
ReferralProgramCycletype andReferralProgramCycleSetmap structure - Updates API endpoints to require cycle ID parameter:
/v1/ensanalytics/referral-leaderboard?cycle=<id>and/v1/ensanalytics/referral-leaderboard/<address>(now returns data for all cycles) - Replaces single cache with per-cycle independent caches to allow individual cycle failures without affecting others
- Removes hardcoded ENS Holiday Awards configuration from environment variables, replacing with optional
CUSTOM_REFERRAL_PROGRAM_CYCLESURL - Adds Cycle 2 (March 2026) as a second default cycle alongside Cycle 1 (ENS Holiday Awards December 2025)
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ensnode-sdk/src/shared/config/environments.ts | Replaces EnsHolidayAwardsEnvironment with ReferralProgramCyclesEnvironment |
| packages/ens-referrals/src/v1/rules.ts | Removes hardcoded ENS Holiday Awards constants |
| packages/ens-referrals/src/v1/cycle.ts | Adds new cycle type definitions and type guards |
| packages/ens-referrals/src/v1/cycle-defaults.ts | Defines default cycles (Cycle 1 and Cycle 2) with configuration |
| packages/ens-referrals/src/v1/index.ts | Exports new cycle types |
| packages/ens-referrals/src/v1/client.ts | Updates SDK client to require cycle parameter for leaderboard queries |
| packages/ens-referrals/src/v1/api/*.ts | Updates API types to support all-cycles responses and cycle-specific requests |
| apps/ensapi/src/handlers/ensanalytics-api-v1.ts | Updates API handlers to support cycle parameter and multi-cycle responses |
| apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts | Updates tests to work with new cycle-based cache structure |
| apps/ensapi/src/cache/referral-leaderboard-cycles.cache.ts | New cache implementation with per-cycle independent caches |
| apps/ensapi/src/cache/referrer-leaderboard.cache-v1.ts | Deleted - replaced by cycle-based cache |
| apps/ensapi/src/config/*.ts | Updates configuration loading to support custom cycle URLs |
| apps/ensapi/.env.local.example | Updates environment variable documentation |
| .changeset/clever-laws-count.md | Empty changeset file |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/ens-referrals/src/v1/api/types.ts (1)
74-124:⚠️ Potential issue | 🟠 MajorBreaking public type renames—add compatibility aliases or ensure a major version bump.
Downstream TypeScript consumers using the oldReferrerDetailResponse*exports will break. Consider re‑exporting deprecated aliases or explicitly bumping the package major version and documenting the migration.🧩 Optional compatibility aliases
+/** `@deprecated` Use ReferrerDetailAllCyclesResponseCodes */ +export const ReferrerDetailResponseCodes = ReferrerDetailAllCyclesResponseCodes; +/** `@deprecated` Use ReferrerDetailAllCyclesResponseCode */ +export type ReferrerDetailResponseCode = ReferrerDetailAllCyclesResponseCode; +/** `@deprecated` Use ReferrerDetailAllCyclesResponseOk */ +export type ReferrerDetailResponseOk = ReferrerDetailAllCyclesResponseOk; +/** `@deprecated` Use ReferrerDetailAllCyclesResponseError */ +export type ReferrerDetailResponseError = ReferrerDetailAllCyclesResponseError; +/** `@deprecated` Use ReferrerDetailAllCyclesResponse */ +export type ReferrerDetailResponse = ReferrerDetailAllCyclesResponse;
🤖 Fix all issues with AI agents
In `@apps/ensapi/.env.local.example`:
- Around line 115-138: Update the env example comment for
CUSTOM_REFERRAL_PROGRAM_CYCLES to state that supplying a URL will completely
replace the default referral cycle set (i.e., it overrides defaults), and
instruct operators to include every cycle they want active in their hosted JSON;
reference the env variable name CUSTOM_REFERRAL_PROGRAM_CYCLES and the example
JSON structure in the comment so readers know to add any default cycles they
still want when providing a custom file.
In `@apps/ensapi/src/handlers/ensanalytics-api-v1.ts`:
- Around line 185-198: The handler currently fail-fast when any cycle
cache.read() returns an Error; to support partial-success change the response to
allow per-cycle results and errors by updating the
ReferrerDetailAllCyclesResponse/ReferrerDetailAllCyclesData types to map cycleId
-> { data?: ReferrerDetail; error?: string }, then modify the loop over
referralLeaderboardCyclesCaches to collect each cycle's result into
allCyclesData[cycleId] using getReferrerDetail(referrer, leaderboard) on success
or an error string on failure (using cycleCache.read()), and finally return a
200 with the aggregated per-cycle data (and optionally an overall status field)
instead of returning 500 on the first failure so clients can consume available
cycles while seeing which cycles failed.
In `@packages/ens-referrals/src/v1/api/deserialize.ts`:
- Around line 235-244: The type error comes from building `data` via
`Object.entries(maybeResponse.data)` which TypeScript widens to `Record<string,
...>`; fix by ensuring the final object is explicitly typed as
`ReferrerDetailAllCyclesResponse` — replace the assignment to `deserialized`
with an explicit cast: set `deserialized = { responseCode: "ok", data } as
ReferrerDetailAllCyclesResponse`; alternatively, build `data` with a typed
reducer (e.g., reduce to `Record<ReferralProgramCycleId, ReferrerDetail>` or
`ReferrerDetailAllCyclesData`) so the resulting `data` has the correct key type
before assigning to `deserialized` (use `deserializeReferrerDetail` inside the
reducer).
In `@packages/ens-referrals/src/v1/api/serialize.ts`:
- Around line 154-160: The switch in serializeReferrerDetail over ReferrerDetail
isn't exhaustive and can return undefined if a new detail.type is added; update
serializeReferrerDetail to include a default branch that asserts exhaustiveness
(e.g., call an assertNever/assertUnreachable helper or assign detail to a
never-typed variable and throw) so the compiler errors on new union members and
at runtime you throw a clear error; keep existing branches that call
serializeReferrerDetailRanked and serializeReferrerDetailUnranked and reference
ReferrerDetail/SerializedReferrerDetail in the assertion.
- Around line 201-218: The switch over response.responseCode
(ReferrerDetailAllCyclesResponseCodes) is not exhaustive and the serializedData
is unsafely asserted with {} as SerializedReferrerDetailAllCyclesData; update
the case for Ok to build the record in a type-safe way (e.g., const
serializedData: Partial<SerializedReferrerDetailAllCyclesData> = {}; for (const
[cycleId, detail] of Object.entries(response.data)) { serializedData[cycleId as
ReferralProgramCycleId] = serializeReferrerDetail(detail); } and return data:
serializedData as SerializedReferrerDetailAllCyclesData), and add a default (or
never) branch after the known cases that throws an error like throw new
Error(`Unhandled ReferrerDetailAllCyclesResponseCodes:
${response.responseCode}`) to enforce exhaustiveness for
ReferrerDetailAllCyclesResponseCodes.
In `@packages/ens-referrals/src/v1/api/zod-schemas.ts`:
- Around line 281-306: In makeReferralProgramCycleSetSchema, the refine
currently only checks that each value parses as a ReferralProgramCycle but
doesn't ensure the Map key equals the cycle's id; update the validate loop in
makeReferralProgramCycleSetSchema to parse each value with
makeReferralProgramCycleSchema(`${valueLabel}[${key}]`), extract the parsed
cycle.id, and return false if typeof key !== "string" or parsedCycle.id !== key
(so keys must match the cycle.id). Also update the refine error message to
indicate keys must equal the nested cycle.id for integrity.
In `@packages/ens-referrals/src/v1/client.ts`:
- Around line 220-229: The example accesses response.data["cycle-1"] without
guarding for undefined which can cause unsafe access; update the example around
the getReferrerDetail response handling (check response.responseCode ===
ReferrerDetailAllCyclesResponseCodes.Ok) to first confirm the key exists (e.g.,
verify response.data["cycle-1"] is truthy) before reading its .type and
.referrer, and adjust the branching that inspects ReferrerDetailTypeIds.Ranked
to handle the case where cycle1Detail is undefined so callers see the safer
pattern.
- Around line 99-104: The JSDoc example in client.ts references a non-existent
rules.cycleId; update the example to use an existing serialized field from
serializeReferralProgramRules (e.g., rules.subregistryId or
rules.startTime/endTime/totalAwardPoolValue) or, if you intended to show the
requested cycle id, reference the request parameter (e.g., the cycleId variable
used to call the client) instead; modify the example console.log lines in the
example block near the response handling to use one of those valid identifiers
so the docs match serializeReferralProgramRules and the request parameters.
In `@packages/ens-referrals/src/v1/cycle-defaults.ts`:
- Around line 45-55: The START_DATE and END_DATE constants in cycle-defaults.ts
are using incorrect Unix timestamps for the documented UTC dates; update
START_DATE (currently 1772524800) to 1772323200 and END_DATE (currently
1775116799) to 1775001599 so the cycle properly covers 2026-03-01T00:00:00Z
through 2026-03-31T23:59:59Z; edit the START_DATE and END_DATE values (the
symbols START_DATE and END_DATE) to the corrected integers and keep the
UnixTimestamp type assertions.
In `@packages/ens-referrals/src/v1/cycle.ts`:
- Around line 42-43: The type guard isPredefinedCycleId currently narrows to
ReferralProgramCycleId which is effectively any string; change its return type
to the concrete union of predefined IDs (e.g. value is typeof
ALL_REFERRAL_PROGRAM_CYCLE_IDS[number] or a named
PredefinedReferralProgramCycleId union) and keep the runtime check using
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as any) so callers get stronger
compile-time narrowing while preserving the existing runtime behavior; update
the function signature only (isPredefinedCycleId) to reference the more precise
union type (or create a named union) and leave ALL_REFERRAL_PROGRAM_CYCLE_IDS
usage unchanged.
| export const isPredefinedCycleId = (value: string): value is ReferralProgramCycleId => | ||
| ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider a more precise return type for the type guard.
The type guard narrows to ReferralProgramCycleId, but since that type includes string & {}, any string technically satisfies it. For better type safety, consider narrowing to just the predefined union:
♻️ Suggested refinement
-export const isPredefinedCycleId = (value: string): value is ReferralProgramCycleId =>
+export const isPredefinedCycleId = (value: string): value is (typeof ReferralProgramCycleIds)[keyof typeof ReferralProgramCycleIds] =>
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId);The current implementation works correctly at runtime, and the JSDoc clearly documents the behavior, so this is optional.
📝 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.
| export const isPredefinedCycleId = (value: string): value is ReferralProgramCycleId => | |
| ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId); | |
| export const isPredefinedCycleId = (value: string): value is (typeof ReferralProgramCycleIds)[keyof typeof ReferralProgramCycleIds] => | |
| ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId); |
🤖 Prompt for AI Agents
In `@packages/ens-referrals/src/v1/cycle.ts` around lines 42 - 43, The type guard
isPredefinedCycleId currently narrows to ReferralProgramCycleId which is
effectively any string; change its return type to the concrete union of
predefined IDs (e.g. value is typeof ALL_REFERRAL_PROGRAM_CYCLE_IDS[number] or a
named PredefinedReferralProgramCycleId union) and keep the runtime check using
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as any) so callers get stronger
compile-time narrowing while preserving the existing runtime behavior; update
the function signature only (isPredefinedCycleId) to reference the more precise
union type (or create a named union) and leave ALL_REFERRAL_PROGRAM_CYCLE_IDS
usage unchanged.
|
Deployment failed with the following error: |
|
Deployment failed with the following error: |
|
Deployment failed with the following error: |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async function loadReferralProgramCycleSet( | ||
| customCyclesUrl: string | undefined, | ||
| namespace: ENSNamespaceId, | ||
| ): Promise<ReferralProgramCycleSet> { | ||
| const subregistryId = getEthnamesSubregistryId(namespace); | ||
|
|
||
| if (!customCyclesUrl) { | ||
| logger.info("Using default referral program cycle set"); | ||
| return getReferralProgramCycleSet(subregistryId.address); | ||
| } | ||
|
|
||
| // Validate URL format | ||
| try { | ||
| new URL(customCyclesUrl); | ||
| } catch { | ||
| throw new Error(`CUSTOM_REFERRAL_PROGRAM_CYCLES is not a valid URL: ${customCyclesUrl}`); | ||
| } | ||
|
|
||
| // Fetch and validate | ||
| logger.info(`Fetching custom referral program cycles from: ${customCyclesUrl}`); | ||
| const response = await fetch(customCyclesUrl); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Failed to fetch custom referral program cycles from ${customCyclesUrl}: ${response.status} ${response.statusText}`, | ||
| ); | ||
| } | ||
|
|
||
| const json = await response.json(); | ||
| const schema = makeCustomReferralProgramCyclesSchema("CUSTOM_REFERRAL_PROGRAM_CYCLES"); | ||
| const validated = schema.parse(json); | ||
|
|
||
| // Convert array to Map, check for duplicates | ||
| const cycleSet: ReferralProgramCycleSet = new Map(); | ||
| for (const cycleObj of validated) { | ||
| const cycle = cycleObj as ReferralProgramCycle; | ||
| const cycleId = cycle.id; | ||
| if (cycleSet.has(cycleId)) { | ||
| throw new Error(`Duplicate cycle ID in CUSTOM_REFERRAL_PROGRAM_CYCLES: ${cycle.id}`); | ||
| } | ||
| cycleSet.set(cycleId, cycle); | ||
| } | ||
|
|
||
| logger.info(`Loaded ${cycleSet.size} custom referral program cycles`); | ||
| return cycleSet; | ||
| } |
There was a problem hiding this comment.
The loadReferralProgramCycleSet function performs a network request during application startup (via buildConfigFromEnvironment). If the custom cycles URL is unreachable, slow to respond, or returns invalid data, this will block application startup.
Consider adding:
- A timeout for the fetch operation to prevent hanging
- Retry logic with exponential backoff for transient network failures
- Better error context in the exception messages to help with debugging
- Optional: A fallback mechanism to use default cycles if custom cycles fail to load (with appropriate logging)
This would improve resilience during deployment and prevent startup failures due to temporary network issues.
| // Check all caches and fail immediately if any cache failed | ||
| for (const [cycleId, cycleCache] of c.var.referralLeaderboardCyclesCaches) { | ||
| const leaderboard = await cycleCache.read(); | ||
| if (leaderboard instanceof Error) { | ||
| return c.json( | ||
| serializeReferrerDetailAllCyclesResponse({ | ||
| responseCode: ReferrerDetailAllCyclesResponseCodes.Error, | ||
| error: "Internal Server Error", | ||
| errorMessage: `Referrer leaderboard data for cycle ${cycleId} has not been successfully cached yet.`, | ||
| } satisfies ReferrerDetailAllCyclesResponse), | ||
| 500, | ||
| ); | ||
| } | ||
| allCyclesData[cycleId] = getReferrerDetail(referrer, leaderboard); | ||
| } |
There was a problem hiding this comment.
The fail-fast approach means that if any single cycle fails to load, the entire request returns an error. This could be problematic from a user experience perspective - if cycle-1 is working but cycle-2 fails, users cannot access cycle-1 data either.
Consider implementing a more resilient approach where:
- Successfully loaded cycles are returned in the response
- Failed cycles are indicated with an error status or omitted with a warning
- The response includes information about which cycles loaded successfully vs. which failed
This would provide better availability and user experience, especially during partial outages or when one cycle has data issues.
| for (const [cycleId, cycleCache] of c.var.referralLeaderboardCyclesCaches) { | ||
| const leaderboard = await cycleCache.read(); | ||
| if (leaderboard instanceof Error) { | ||
| return c.json( | ||
| serializeReferrerDetailAllCyclesResponse({ | ||
| responseCode: ReferrerDetailAllCyclesResponseCodes.Error, | ||
| error: "Internal Server Error", | ||
| errorMessage: `Referrer leaderboard data for cycle ${cycleId} has not been successfully cached yet.`, | ||
| } satisfies ReferrerDetailAllCyclesResponse), | ||
| 500, | ||
| ); | ||
| } | ||
| allCyclesData[cycleId] = getReferrerDetail(referrer, leaderboard); | ||
| } |
There was a problem hiding this comment.
The code iterates over c.var.referralLeaderboardCyclesCaches using a for-of loop with .entries(). JavaScript Maps preserve insertion order, but the iteration order matters here because the first cycle that fails will determine the error response.
If the goal is to fail fast on any cycle failure, consider:
- Documenting the specific iteration order behavior (e.g., "cycles are checked in the order they were configured")
- Or explicitly sorting the cycles to ensure a predictable error reporting order
- Or collecting all failures and reporting them together
The current implementation may result in non-deterministic error reporting if the Map insertion order isn't guaranteed by the configuration loading code.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/ensapi/src/config/config.schema.ts`:
- Around line 121-129: The CI formatting failed for the try/catch block that
parses response.json() into the variable json (using customCyclesUrl in the
error message); run the project's formatter (e.g., Prettier/tsfmt) over
apps/ensapi/src/config/config.schema.ts and re-commit the formatted changes so
the try { json = await response.json(); } catch (error) { ... } block conforms
to the code style rules.
- Around line 104-107: In loadReferralProgramCycleSet, add an AbortController
with a short timeout (e.g. configurable constant) and pass controller.signal
into the fetch(customCyclesUrl) call to prevent indefinite hangs; start a timer
that calls controller.abort() after the timeout, clear the timer after a
successful response, and handle the abort case (check for an AbortError or treat
as a fetch failure) so the initialization can proceed/fail fast instead of
blocking on the response variable. Ensure you reference customCyclesUrl,
response, and loadReferralProgramCycleSet when implementing the timeout and
error handling.
In `@packages/ens-referrals/src/v1/cycle-defaults.ts`:
- Around line 72-75: The function declaration getReferralProgramCycleSet has
inconsistent formatting causing CI formatter failures; run the repo formatter
(e.g., Prettier/tsfmt) and reformat the file so the function signature,
parameters, and opening/closing braces are properly aligned and spaced (ensure
the AccountId type and return ReferralProgramCycleSet remain on the same
formatted block), then re-commit the formatted changes.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/ensapi/src/config/redact.ts`:
- Around line 18-24: The map callback is redundantly asserting the value to
ReferralProgramCycle when iterating config.referralProgramCycleSet (type
ReferralProgramCycleSet = Map<ReferralProgramCycleId, ReferralProgramCycle>);
remove the unnecessary cast in the Array.from(...).map(([cycleId, cycle]) =>
[cycleId, serializeReferralProgramCycle(cycle)]) so
serializeReferralProgramCycle receives the already-typed cycle; keep the rest of
the referralProgramCycleSet conversion intact.
In `@packages/ens-referrals/src/v1/cycle-defaults.ts`:
- Line 84: The March 2026 entry's rulesUrl property is pointing to a dead link;
locate the rulesUrl key in the cycle-defaults configuration (property name
"rulesUrl" in packages/ens-referrals/src/v1/cycle-defaults.ts, specifically the
March 2026 cycle object) and either replace
"https://ensawards.org/march-2026-rules" with the correct working rules URL for
March 2026 or remove/update the property (or point it to the canonical rules
page such as "https://ensawards.org/ens-holiday-awards-rules") so the entry no
longer references a 404. Ensure the change only affects the March 2026 cycle
object and keep the property name "rulesUrl" unchanged.
| // Convert Map to object for proper logging (Maps serialize to {} in JSON) | ||
| referralProgramCycleSet: Object.fromEntries( | ||
| Array.from(config.referralProgramCycleSet.entries()).map(([cycleId, cycle]) => [ | ||
| cycleId, | ||
| serializeReferralProgramCycle(cycle as ReferralProgramCycle), | ||
| ]), | ||
| ), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Unnecessary type assertion.
The cycle variable is already typed as ReferralProgramCycle from the Map's value type (ReferralProgramCycleSet = Map<ReferralProgramCycleId, ReferralProgramCycle>). The cast on line 22 is redundant.
♻️ Suggested simplification
// Convert Map to object for proper logging (Maps serialize to {} in JSON)
referralProgramCycleSet: Object.fromEntries(
- Array.from(config.referralProgramCycleSet.entries()).map(([cycleId, cycle]) => [
+ Array.from(config.referralProgramCycleSet).map(([cycleId, cycle]) => [
cycleId,
- serializeReferralProgramCycle(cycle as ReferralProgramCycle),
+ serializeReferralProgramCycle(cycle),
]),
),📝 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.
| // Convert Map to object for proper logging (Maps serialize to {} in JSON) | |
| referralProgramCycleSet: Object.fromEntries( | |
| Array.from(config.referralProgramCycleSet.entries()).map(([cycleId, cycle]) => [ | |
| cycleId, | |
| serializeReferralProgramCycle(cycle as ReferralProgramCycle), | |
| ]), | |
| ), | |
| // Convert Map to object for proper logging (Maps serialize to {} in JSON) | |
| referralProgramCycleSet: Object.fromEntries( | |
| Array.from(config.referralProgramCycleSet).map(([cycleId, cycle]) => [ | |
| cycleId, | |
| serializeReferralProgramCycle(cycle), | |
| ]), | |
| ), |
🤖 Prompt for AI Agents
In `@apps/ensapi/src/config/redact.ts` around lines 18 - 24, The map callback is
redundantly asserting the value to ReferralProgramCycle when iterating
config.referralProgramCycleSet (type ReferralProgramCycleSet =
Map<ReferralProgramCycleId, ReferralProgramCycle>); remove the unnecessary cast
in the Array.from(...).map(([cycleId, cycle]) => [cycleId,
serializeReferralProgramCycle(cycle)]) so serializeReferralProgramCycle receives
the already-typed cycle; keep the rest of the referralProgramCycleSet conversion
intact.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * URL to the rules for this cycle. | ||
| */ | ||
| RULES_URL: "https://ensawards.org/ens-holiday-awards-rules", |
There was a problem hiding this comment.
The RULES_URL for Cycle 2 (March 2026) points to "https://ensawards.org/ens-holiday-awards-rules", which appears to be specific to Cycle 1 (ENS Holiday Awards). Consider whether Cycle 2 should have its own dedicated rules URL, such as "https://ensawards.org/march-2026-rules" or a more generic URL.
| RULES_URL: "https://ensawards.org/ens-holiday-awards-rules", | |
| RULES_URL: "https://ensawards.org/rules", |
| async function loadReferralProgramCycleSet( | ||
| customCyclesUrl: string | undefined, | ||
| namespace: ENSNamespaceId, | ||
| ): Promise<ReferralProgramCycleSet> { | ||
| const subregistryId = getEthnamesSubregistryId(namespace); | ||
|
|
||
| if (!customCyclesUrl) { | ||
| logger.info("Using default referral program cycle set"); | ||
| return getReferralProgramCycleSet(subregistryId); | ||
| } | ||
|
|
||
| // Validate URL format | ||
| try { | ||
| new URL(customCyclesUrl); | ||
| } catch { | ||
| throw new Error(`CUSTOM_REFERRAL_PROGRAM_CYCLES is not a valid URL: ${customCyclesUrl}`); | ||
| } | ||
|
|
||
| // Fetch and validate | ||
| logger.info(`Fetching custom referral program cycles from: ${customCyclesUrl}`); | ||
|
|
||
| let response: Response; | ||
| try { | ||
| response = await fetch(customCyclesUrl); | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| throw new Error( | ||
| `Failed to fetch custom referral program cycles from ${customCyclesUrl}: ${errorMessage}. ` + | ||
| `Please verify the URL is accessible and the server is running.`, | ||
| ); | ||
| } | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Failed to fetch custom referral program cycles from ${customCyclesUrl}: ${response.status} ${response.statusText}`, | ||
| ); | ||
| } | ||
|
|
||
| let json: unknown; | ||
| try { | ||
| json = await response.json(); | ||
| } catch (_error) { | ||
| throw new Error( | ||
| `Failed to parse JSON from ${customCyclesUrl}: The response is not valid JSON. ` + | ||
| `Please verify the file contains valid JSON.`, | ||
| ); | ||
| } | ||
|
|
||
| const schema = makeCustomReferralProgramCyclesSchema("CUSTOM_REFERRAL_PROGRAM_CYCLES"); | ||
| const result = schema.safeParse(json); | ||
|
|
||
| if (result.error) { | ||
| throw new Error( | ||
| `Failed to validate custom referral program cycles from ${customCyclesUrl}:\n${prettifyError(result.error)}\n` + | ||
| `Please verify the JSON structure matches the expected schema.`, | ||
| ); | ||
| } | ||
|
|
||
| const validated = result.data; | ||
|
|
||
| const cycleSet: ReferralProgramCycleSet = new Map(); | ||
| for (const cycleObj of validated) { | ||
| const cycle = cycleObj as ReferralProgramCycle; | ||
| const cycleId = cycle.id; | ||
| cycleSet.set(cycleId, cycle); | ||
| } | ||
|
|
||
| logger.info(`Loaded ${cycleSet.size} custom referral program cycles`); | ||
| return cycleSet; | ||
| } |
There was a problem hiding this comment.
The loadReferralProgramCycleSet function, which handles loading custom cycles from a URL, lacks test coverage. This is a complex async function with multiple error paths (invalid URL, fetch failure, JSON parse failure, validation failure). Consider adding unit tests to ensure all error cases are properly handled and provide helpful error messages.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/ensapi/src/handlers/ensanalytics-api-v1.ts (1)
1-222:⚠️ Potential issue | 🟡 MinorFormatter check failed for this file.
Please run the repo formatter and re-commit to satisfy the CI formatting step.
apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts (1)
1-536:⚠️ Potential issue | 🟡 MinorFormatter check failed for this file.
Please run the repo formatter and re-commit to satisfy the CI formatting step.
Greptile OverviewGreptile SummaryThis PR successfully implements multi-cycle support for the ENS Referrals API v1, allowing the system to manage multiple referral program periods with independent leaderboards, rules, and caching. Key Changes
Strengths
Observations
Confidence Score: 4.5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client as API Client
participant API as ENSApi v1
participant Config as Config Loader
participant Cache as Cycle Caches
participant DB as Database
Note over Config: Startup: Load Configuration
Config->>Config: loadReferralProgramCycleSet()
alt Custom cycles URL provided
Config->>Config: Fetch JSON from URL
Config->>Config: Validate with Zod schema
Config->>Config: Convert array to Map
else No custom URL
Config->>Config: Use default cycle set
end
Config->>Cache: Initialize SWRCache per cycle
loop For each cycle
Cache->>Cache: Create independent SWRCache
end
Note over Client,DB: Runtime: GET /referral-leaderboard?cycle=X
Client->>API: GET /referral-leaderboard?cycle=cycle-1
API->>API: Validate cycle parameter
API->>Cache: Get cache for cycle-1
alt Cycle not found
API-->>Client: 404 with valid cycle IDs
else Cycle cache exists
Cache->>DB: Query referral data (if stale)
DB-->>Cache: Return referral data
Cache->>Cache: Build leaderboard
Cache-->>API: Return leaderboard or Error
alt Cache failed
API-->>Client: 500 error
else Cache succeeded
API->>API: Generate page from leaderboard
API-->>Client: 200 with leaderboard page
end
end
Note over Client,DB: Runtime: GET /referral-leaderboard/:referrer
Client->>API: GET /referral-leaderboard/0x123...
API->>Cache: Iterate all cycle caches
loop For each configured cycle
Cache->>Cache: Read cycle cache
alt Any cache failed
API-->>Client: 500 error (fail fast)
else Cache succeeded
API->>API: Extract referrer detail
end
end
API->>API: Aggregate all cycle data
API-->>Client: 200 with all-cycles response
|
| @@ -218,11 +258,13 @@ export function deserializeReferrerDetailResponse( | |||
| } | |||
There was a problem hiding this comment.
missing default case for exhaustiveness checking - deserialized could be uninitialized
| switch (maybeResponse.responseCode) { | |
| case "ok": { | |
| const data: ReferrerDetailAllCyclesData = {} as ReferrerDetailAllCyclesData; | |
| for (const [cycleId, detail] of Object.entries(maybeResponse.data)) { | |
| // Object.entries only returns existing entries, so detail is never undefined at runtime | |
| data[cycleId as ReferralProgramCycleId] = deserializeReferrerDetail( | |
| detail as SerializedReferrerDetail, | |
| ); | |
| } | |
| deserialized = { | |
| responseCode: "ok", | |
| data, | |
| }; | |
| break; | |
| } | |
| case "error": | |
| deserialized = maybeResponse; | |
| break; | |
| default: { | |
| const _exhaustiveCheck: never = maybeResponse; | |
| throw new Error( | |
| `Unknown response code: ${(_exhaustiveCheck as SerializedReferrerDetailAllCyclesResponse).responseCode}`, | |
| ); | |
| } | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ens-referrals/src/v1/api/deserialize.ts
Line: 237:258
Comment:
missing default case for exhaustiveness checking - `deserialized` could be uninitialized
```suggestion
switch (maybeResponse.responseCode) {
case "ok": {
const data: ReferrerDetailAllCyclesData = {} as ReferrerDetailAllCyclesData;
for (const [cycleId, detail] of Object.entries(maybeResponse.data)) {
// Object.entries only returns existing entries, so detail is never undefined at runtime
data[cycleId as ReferralProgramCycleId] = deserializeReferrerDetail(
detail as SerializedReferrerDetail,
);
}
deserialized = {
responseCode: "ok",
data,
};
break;
}
case "error":
deserialized = maybeResponse;
break;
default: {
const _exhaustiveCheck: never = maybeResponse;
throw new Error(
`Unknown response code: ${(_exhaustiveCheck as SerializedReferrerDetailAllCyclesResponse).responseCode}`,
);
}
}
```
How can I resolve this? If you propose a fix, please make it concise.| /** | ||
| * URL to the rules for this cycle. | ||
| */ | ||
| RULES_URL: "https://ensawards.org/ens-holiday-awards-rules", |
There was a problem hiding this comment.
cycle-2 reuses cycle-1's rules URL - update to cycle-specific URL when available
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ens-referrals/src/v1/cycle-defaults.ts
Line: 83:83
Comment:
cycle-2 reuses cycle-1's rules URL - update to cycle-specific URL when available
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (1)
Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/ens-referrals/src/v1/api/deserialize.ts
Line: 197:209
Comment:
missing default case for exhaustiveness checking - `deserialized` could be uninitialized
```suggestion
switch (maybeResponse.responseCode) {
case "ok": {
deserialized = {
responseCode: maybeResponse.responseCode,
data: deserializeReferrerLeaderboardPage(maybeResponse.data),
} as ReferrerLeaderboardPageResponse;
break;
}
case "error":
deserialized = maybeResponse;
break;
default: {
const _exhaustiveCheck: never = maybeResponse;
throw new Error(
`Unknown response code: ${(_exhaustiveCheck as SerializedReferrerLeaderboardPageResponse).responseCode}`,
);
}
}
```
How can I resolve this? If you propose a fix, please make it concise. |
lightwalker-eth
left a comment
There was a problem hiding this comment.
@Goader Nice work. Reviewed and shared feedback 👍
| "@ensnode/ensnode-sdk": patch | ||
| --- | ||
|
|
||
| Introduces referral program cycles support with pre-configured cycle definitions (ENS Holiday Awards December 2025, March 2026 cycle). Updated ENSAnalytics API v1 to support cycle-based leaderboard queries and added cycle configuration to environment schema. |
There was a problem hiding this comment.
When we create changesets, we should always follow the best practices described here: https://ensnode.io/docs/contributing/prs/#mapping-logical-changes-to-changesets
In other words, the description added to a changeset must apply to each of the apps / packages attached to the changeset. A PR may need multiple changesets to meet this requirement.
| * If CUSTOM_REFERRAL_PROGRAM_CYCLES is set, it should be a URL pointing to | ||
| * a JSON file containing custom cycle definitions. |
There was a problem hiding this comment.
| * If CUSTOM_REFERRAL_PROGRAM_CYCLES is set, it should be a URL pointing to | |
| * a JSON file containing custom cycle definitions. | |
| * If CUSTOM_REFERRAL_PROGRAM_CYCLES is set, it should be a URL that returns | |
| * the JSON for a valid serialized custom referral program cycles definition. |
Goal: We shouldn't say it has to be a file. It just needs to give us JSON in the right shape.
TODO: Ideally we could reference the specific data type definition for "serialized custom cycle definition" here. I'm still working my way through the PR and haven't gotten to that definition yet.
| ENS_HOLIDAY_AWARDS_END?: string; | ||
| export interface ReferralProgramCyclesEnvironment { | ||
| /** | ||
| * Optional URL to a JSON file containing custom referral program cycle definitions. |
There was a problem hiding this comment.
| * Optional URL to a JSON file containing custom referral program cycle definitions. | |
| * Optional URL that returns the JSON for a valid serialized custom referral program cycles definition. |
| # Note: ENS_HOLIDAY_AWARDS_START date must be before or the same as ENS_HOLIDAY_AWARDS_END | ||
| # ENS_HOLIDAY_AWARDS_START="2025-12-01T00:00:00Z" | ||
| # ENS_HOLIDAY_AWARDS_END="2025-12-31T23:59:59Z" | ||
| # Custom Referral Program Cycles (optional) |
There was a problem hiding this comment.
| # Custom Referral Program Cycles (optional) | |
| # Custom Referral Program Cycle Set Definition (optional) |
| # ENS_HOLIDAY_AWARDS_START="2025-12-01T00:00:00Z" | ||
| # ENS_HOLIDAY_AWARDS_END="2025-12-31T23:59:59Z" | ||
| # Custom Referral Program Cycles (optional) | ||
| # URL to a JSON file containing custom referral program cycle definitions. |
There was a problem hiding this comment.
| # URL to a JSON file containing custom referral program cycle definitions. | |
| # URL that returns the JSON for a valid serialized custom referral program cycle set definition. |
| * if (response.responseCode === ReferrerDetailResponseCodes.Ok) { | ||
| * if (response.data.type === ReferrerDetailTypeIds.Ranked) { | ||
| * if (response.responseCode === ReferrerDetailAllCyclesResponseCodes.Ok) { | ||
| * // If "cycle-1" is configured, it will be in response.data |
There was a problem hiding this comment.
Please see related comments warning about a design for this API where the set of cycle ids is assumed to always include some predefined set. I believe we should actively work to break such an assumption from being made for change management purposes.
| * Individual cycle caches maintain their own stale-while-revalidate behavior, so a previously | ||
| * successfully fetched cycle continues serving its data even if a subsequent refresh fails. | ||
| */ | ||
| referralLeaderboardCyclesCaches: ReferralLeaderboardCyclesCacheMap; |
There was a problem hiding this comment.
| referralLeaderboardCyclesCaches: ReferralLeaderboardCyclesCacheMap; | |
| referralLeaderboardCyclesCaches: ReferralLeaderboardCyclesCacheMap | Error; |
I believe we need to do this? Ex: If we haven't been able to fetch a custom set of referral program configs yet?
| cycleId: ReferralProgramCycleId, | ||
| ): () => Promise<ReferrerLeaderboard> { | ||
| return async (): Promise<ReferrerLeaderboard> => { | ||
| const cycle = config.referralProgramCycleSet.get(cycleId) as ReferralProgramCycle | undefined; |
There was a problem hiding this comment.
I believe no need for the "as ..." here?
| * | ||
| * @returns A map from cycle ID to its dedicated SWRCache | ||
| */ | ||
| function initializeCyclesCaches(): ReferralLeaderboardCyclesCacheMap { |
There was a problem hiding this comment.
Please see my related comments for how I believe we need to support the possibility of building this lazily / asynchronously after app startup without blocking app startup.
| @@ -117,66 +155,70 @@ const referrerAddressSchema = z.object({ | |||
| referrer: makeLowercaseAddressSchema("Referrer address").describe("Referrer Ethereum address"), | |||
| }); | |||
|
|
|||
There was a problem hiding this comment.
In other comments I shared feedback on how the config should at most hold the URL for a custom referral program config set, and should not hold the referral program config set itself.
Ideally clients would have a method to fetch the referral program config set the server is using. Could you introduce a new API for this and also add a convenient helper method for it in the client?
ENS Referrals API v1: Multi-Cycle Support
closes: #1522
Reviewer Focus (Read This First)
What reviewers should focus on
ReferralProgramCycleIdtype allows both predefined IDs (cycle-1,cycle-2) with autocomplete AND custom cycle IDs loaded from JSON (usingstring & {}trick for extensibility)apps/ensapi/src/config/config.schema.ts-loadReferralProgramCycleSet()with URL fetching and validationProblem & Motivation
Why this exists
What Changed (Concrete)
What actually changed
1. New Data Models (
packages/ens-referrals/src/v1/)cycle.ts- Core type definitions:ReferralProgramCycleIdsobject withCycle1: "cycle-1"andCycle2: "cycle-2"ReferralProgramCycleIdtype (extensible to custom IDs)ReferralProgramCycleinterface withid,displayName,rules,rulesUrlReferralProgramCycleSettype asMap<ReferralProgramCycleId, ReferralProgramCycle>cycle-defaults.ts- Default cycle definitions:getReferralProgramCycleSet()function returning pre-built Map with cycle-1 and cycle-2api/zod-schemas.ts- Comprehensive validation:makeReferralProgramCycleIdSchema()- validates cycle IDsmakeReferralProgramCycleSchema()- validates complete cycle objectsmakeCustomReferralProgramCyclesSchema()- validates JSON array format with duplicate ID checksmakeReferralProgramCycleSetSchema()- validates Map structureapi/types.ts- New response types:ReferrerDetailAllCyclesData- Record of cycle ID to referrer detailsReferrerDetailAllCyclesResponse- Discriminated union for OK/Error2. Configuration System (
apps/ensapi/)Environment variable (
packages/ensnode-sdk/src/shared/config/environments.ts):CUSTOM_REFERRAL_PROGRAM_CYCLES?: stringtoReferralProgramCyclesEnvironmentConfig schema (
apps/ensapi/src/config/config.schema.ts):loadReferralProgramCycleSet()async function:EnsApiConfigSchemato includereferralProgramCycleSetfieldbuildConfigFromEnvironment()to call loaderEnvironment documentation (
apps/ensapi/.env.local.example):CUSTOM_REFERRAL_PROGRAM_CYCLESENS_HOLIDAY_AWARDS_STARTandENS_HOLIDAY_AWARDS_END(no longer needed)3. Multi-Cycle Cache Architecture (
apps/ensapi/src/cache/)referral-leaderboard-cycles.cache.ts- New cache implementation:ReferralLeaderboardCyclesCacheMaptype:Map<ReferralProgramCycleId, SWRCache<ReferrerLeaderboard>>initializeCyclesCaches()creates independent SWRCache for each configured cycleV1 Middleware (
apps/ensapi/src/middleware/referrer-leaderboard.middleware-v1.ts):referralLeaderboardCyclesCachesto downstream handlers4. V1 API Routes (
apps/ensapi/src/handlers/ensanalytics-api-v1.ts)GET /v1/ensanalytics/referral-leaderboard(renamed from/referrers):cyclequery parameterGET /v1/ensanalytics/referral-leaderboard/:referrer(renamed from/referrers/:referrer):ReferrerDetailAllCyclesDatainstead of singleReferrerDetailDesign & Planning
How this approach was chosen
Self-Review
What you caught yourself
ENS_HOLIDAY_AWARDS_START/ENDenv vars from ensapi config, now depends on the referral program cycle custom jsonCross-Codebase Alignment
Related code you checked
ReferralProgram,ENS_HOLIDAY_AWARDS,referrerLeaderboard,SWRCache,cycle,v1,v0Downstream & Consumer Impact
Who this affects and how
v0 unaffected, v1 is allowed to work with multiple cycles
.env.local.exampledocumentation, same with the clientTesting Evidence
How this was validated
Scope Reductions
What you intentionally didn't do
Did not do partial success response for referrer details (all cycles) - if one cycle fails, return 500.
Risk Analysis
How this could go wrong
Low risk - v0 affected very little (only env vars, which were not used in production either way and defaulted to constants, which are now the only source of truth).
V1 is not used by any downstream consumer yet.
Pre-Review Checklist (Blocking)