Skip to content

Commit 75c8b01

Browse files
authored
Referral Program Statuses (#1621)
1 parent 4f6a289 commit 75c8b01

17 files changed

Lines changed: 309 additions & 12 deletions

.changeset/brave-waves-flow.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@namehash/ens-referrals": minor
3+
"ensapi": minor
4+
---
5+
6+
Added `status` field to referral program API responses (`ReferrerLeaderboardPage`, `ReferrerEditionMetricsRanked`, `ReferrerEditionMetricsUnranked`) indicating whether a program is "Scheduled", "Active", or "Closed" based on the program's timing relative to `accurateAsOf`.

.changeset/clever-frogs-detect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ensnode/ensnode-sdk": minor
3+
---
4+
5+
SWRCache `fn` now optionally receives the currently cached result as a parameter, allowing implementations to inspect cached data before deciding whether to return it or fetch fresh data. Fully backward compatible.

.changeset/lemon-moose-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ensapi": minor
3+
---
4+
5+
Referral program edition leaderboard caches now check for immutability within the cache builder function. Closed editions past the safety window return cached data without re-fetching.

apps/ensapi/src/cache/indexing-status.cache.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import config from "@/config";
22

3-
import { ENSNodeClient, IndexingStatusResponseCodes, SWRCache } from "@ensnode/ensnode-sdk";
3+
import {
4+
type CrossChainIndexingStatusSnapshot,
5+
ENSNodeClient,
6+
IndexingStatusResponseCodes,
7+
SWRCache,
8+
} from "@ensnode/ensnode-sdk";
49

510
import { makeLogger } from "@/lib/logger";
611

712
const logger = makeLogger("indexing-status.cache");
813
const client = new ENSNodeClient({ url: config.ensIndexerUrl });
914

10-
export const indexingStatusCache = new SWRCache({
11-
fn: async () =>
15+
export const indexingStatusCache = new SWRCache<CrossChainIndexingStatusSnapshot>({
16+
fn: async (_cachedResult) =>
1217
client
1318
.indexingStatus() // fetch a new indexing status snapshot
1419
.then((response) => {

apps/ensapi/src/cache/referral-leaderboard-editions.cache.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import {
88
import { minutesToSeconds } from "date-fns";
99

1010
import {
11+
type CachedResult,
1112
getLatestIndexedBlockRef,
1213
type OmnichainIndexingStatusId,
1314
OmnichainIndexingStatusIds,
1415
SWRCache,
1516
} from "@ensnode/ensnode-sdk";
1617

18+
import { assumeReferralProgramEditionImmutablyClosed } from "@/lib/ensanalytics/referrer-leaderboard/closeout";
1719
import { getReferrerLeaderboard } from "@/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1";
1820
import { makeLogger } from "@/lib/logger";
1921

@@ -48,15 +50,34 @@ const supportedOmnichainIndexingStatuses: OmnichainIndexingStatusId[] = [
4850
/**
4951
* Creates a cache builder function for a specific edition.
5052
*
53+
* The builder function checks if cached data exists and represents an immutably closed edition.
54+
* If so, it returns the cached data without re-fetching. Otherwise, it fetches fresh data.
55+
*
5156
* @param editionConfig - The edition configuration
5257
* @returns A function that builds the leaderboard for the given edition
5358
*/
5459
function createEditionLeaderboardBuilder(
5560
editionConfig: ReferralProgramEditionConfig,
56-
): () => Promise<ReferrerLeaderboard> {
57-
return async (): Promise<ReferrerLeaderboard> => {
61+
): (cachedResult?: CachedResult<ReferrerLeaderboard>) => Promise<ReferrerLeaderboard> {
62+
return async (cachedResult?: CachedResult<ReferrerLeaderboard>): Promise<ReferrerLeaderboard> => {
5863
const editionSlug = editionConfig.slug;
5964

65+
// Check if cached data is immutable and can be returned as-is
66+
if (cachedResult && !(cachedResult.result instanceof Error)) {
67+
const isImmutable = assumeReferralProgramEditionImmutablyClosed(
68+
cachedResult.result.rules,
69+
cachedResult.result.accurateAsOf,
70+
);
71+
72+
if (isImmutable) {
73+
logger.debug(
74+
{ editionSlug },
75+
`Edition is immutably closed, returning cached data without re-fetching`,
76+
);
77+
return cachedResult.result;
78+
}
79+
}
80+
6081
const indexingStatus = await indexingStatusCache.read();
6182
if (indexingStatus instanceof Error) {
6283
logger.error(

apps/ensapi/src/cache/referral-program-edition-set.cache.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from "@namehash/ens-referrals/v1";
88
import { minutesToSeconds } from "date-fns";
99

10-
import { SWRCache } from "@ensnode/ensnode-sdk";
10+
import { type CachedResult, SWRCache } from "@ensnode/ensnode-sdk";
1111

1212
import { makeLogger } from "@/lib/logger";
1313

@@ -16,7 +16,9 @@ const logger = makeLogger("referral-program-edition-set-cache");
1616
/**
1717
* Loads the referral program edition config set from custom URL or defaults.
1818
*/
19-
async function loadReferralProgramEditionConfigSet(): Promise<ReferralProgramEditionConfigSet> {
19+
async function loadReferralProgramEditionConfigSet(
20+
_cachedResult?: CachedResult<ReferralProgramEditionConfigSet>,
21+
): Promise<ReferralProgramEditionConfigSet> {
2022
// Check if custom URL is configured
2123
if (config.customReferralProgramEditionConfigSetUrl) {
2224
logger.info(

apps/ensapi/src/cache/referrer-leaderboard.cache.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ENS_HOLIDAY_AWARDS_MAX_QUALIFIED_REFERRERS,
77
ENS_HOLIDAY_AWARDS_START_DATE,
88
ENS_HOLIDAY_AWARDS_TOTAL_AWARD_POOL_VALUE,
9+
type ReferrerLeaderboard,
910
} from "@namehash/ens-referrals";
1011
import { minutesToSeconds } from "date-fns";
1112

@@ -44,8 +45,8 @@ const supportedOmnichainIndexingStatuses: OmnichainIndexingStatusId[] = [
4445
OmnichainIndexingStatusIds.Completed,
4546
];
4647

47-
export const referrerLeaderboardCache = new SWRCache({
48-
fn: async () => {
48+
export const referrerLeaderboardCache = new SWRCache<ReferrerLeaderboard>({
49+
fn: async (_cachedResult) => {
4950
const indexingStatus = await indexingStatusCache.read();
5051
if (indexingStatus instanceof Error) {
5152
throw new Error(

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
deserializeReferrerMetricsEditionsResponse,
3535
ReferralProgramEditionConfigSetResponseCodes,
3636
type ReferralProgramEditionSlug,
37+
ReferralProgramStatuses,
3738
ReferrerEditionMetricsTypeIds,
3839
type ReferrerLeaderboard,
3940
ReferrerLeaderboardPageResponseCodes,
@@ -118,6 +119,7 @@ describe("/v1/ensanalytics", () => {
118119
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
119120
data: {
120121
...populatedReferrerLeaderboard,
122+
status: ReferralProgramStatuses.Active,
121123
pageContext: {
122124
endIndex: 9,
123125
hasNext: true,
@@ -139,6 +141,7 @@ describe("/v1/ensanalytics", () => {
139141
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
140142
data: {
141143
...populatedReferrerLeaderboard,
144+
status: ReferralProgramStatuses.Active,
142145
pageContext: {
143146
endIndex: 19,
144147
hasNext: true,
@@ -159,6 +162,7 @@ describe("/v1/ensanalytics", () => {
159162
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
160163
data: {
161164
...populatedReferrerLeaderboard,
165+
status: ReferralProgramStatuses.Active,
162166
pageContext: {
163167
endIndex: 28,
164168
hasNext: false,
@@ -223,6 +227,7 @@ describe("/v1/ensanalytics", () => {
223227
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
224228
data: {
225229
...emptyReferralLeaderboard,
230+
status: ReferralProgramStatuses.Active,
226231
pageContext: {
227232
hasNext: false,
228233
hasPrev: false,
@@ -364,13 +369,15 @@ describe("/v1/ensanalytics", () => {
364369
referrer: expectedMetrics,
365370
aggregatedMetrics: populatedReferrerLeaderboard.aggregatedMetrics,
366371
accurateAsOf: expectedAccurateAsOf,
372+
status: ReferralProgramStatuses.Active,
367373
},
368374
"2026-03": {
369375
type: ReferrerEditionMetricsTypeIds.Ranked,
370376
rules: populatedReferrerLeaderboard.rules,
371377
referrer: expectedMetrics,
372378
aggregatedMetrics: populatedReferrerLeaderboard.aggregatedMetrics,
373379
accurateAsOf: expectedAccurateAsOf,
380+
status: ReferralProgramStatuses.Active,
374381
},
375382
},
376383
} satisfies ReferrerMetricsEditionsResponseOk;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { ReferralProgramRules } from "@namehash/ens-referrals/v1";
2+
import { minutesToSeconds } from "date-fns";
3+
4+
import { addDuration, type Duration, type UnixTimestamp } from "@ensnode/ensnode-sdk";
5+
6+
/**
7+
* Duration after which we assume a closed edition is safe from chain reorganizations.
8+
*
9+
* This is a heuristic value (10 minutes) chosen to provide a reasonable safety margin
10+
* beyond typical chain finality assumptions on supported networks. It is not a guarantee
11+
* of immutability.
12+
*/
13+
export const ASSUMED_CHAIN_REORG_SAFE_DURATION: Duration = minutesToSeconds(10);
14+
15+
/**
16+
* Assumes a referral program edition is immutably closed if it ended more than
17+
* ASSUMED_CHAIN_REORG_SAFE_DURATION ago.
18+
*
19+
* This is a practical heuristic for determining when edition data can be cached
20+
* indefinitely, based on the assumption that chain reorgs become extremely unlikely
21+
* after the safety window has passed.
22+
*
23+
* @param rules - The referral program rules containing endTime
24+
* @param referenceTime - The timestamp to check against (typically accurateAsOf from cached leaderboard)
25+
* @returns true if we assume the edition is immutably closed
26+
*/
27+
export function assumeReferralProgramEditionImmutablyClosed(
28+
rules: ReferralProgramRules,
29+
referenceTime: UnixTimestamp,
30+
): boolean {
31+
const immutabilityThreshold = addDuration(rules.endTime, ASSUMED_CHAIN_REORG_SAFE_DURATION);
32+
return referenceTime > immutabilityThreshold;
33+
}

apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ReferralProgramStatuses,
23
type ReferrerLeaderboard,
34
ReferrerLeaderboardPageResponseCodes,
45
type ReferrerLeaderboardPageResponseOk,
@@ -1093,6 +1094,7 @@ export const referrerLeaderboardPageResponseOk: ReferrerLeaderboardPageResponseO
10931094
startIndex: 0,
10941095
endIndex: 28,
10951096
},
1097+
status: ReferralProgramStatuses.Active,
10961098
accurateAsOf: 1735689600,
10971099
},
10981100
};

0 commit comments

Comments
 (0)