Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/brave-eagles-award.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@namehash/ens-referrals": minor
"ensapi": minor
---

Introduces a pluggable award model architecture for referral program editions. The original Holiday Awards logic is now encapsulated as the `pie-split` model. A new `rev-share-limit` model is added to support the upcoming referral program edition. `ReferralProgramRules` is now a discriminated union over `awardModel`, with an `Unrecognized` variant for forward compatibility — older clients safely skip editions with unknown models rather than crashing.
15 changes: 15 additions & 0 deletions apps/ensapi/src/cache/referral-program-edition-set.cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import config from "@/config";
import {
ENSReferralsClient,
getDefaultReferralProgramEditionConfigSet,
ReferralProgramAwardModels,
type ReferralProgramEditionConfigSet,
} from "@namehash/ens-referrals/v1";
import { minutesToSeconds } from "date-fns";
Expand All @@ -28,6 +29,20 @@ async function loadReferralProgramEditionConfigSet(
const editionConfigSet = await ENSReferralsClient.getReferralProgramEditionConfigSet(
config.customReferralProgramEditionConfigSetUrl,
);

// Strip any unrecognized editions immediately — they are client-side forward-compatibility
// placeholders that must never enter the server's operational config set (they can't be
// serialized and would cause API handlers to crash).
for (const [slug, editionConfig] of editionConfigSet) {
if (editionConfig.rules.awardModel === ReferralProgramAwardModels.Unrecognized) {
logger.warn(
{ editionSlug: slug, originalAwardModel: editionConfig.rules.originalAwardModel },
`Skipping custom edition with unrecognized award model`,
);
editionConfigSet.delete(slug);
}
}

logger.info(`Successfully loaded ${editionConfigSet.size} custom referral program editions`);
return editionConfigSet;
} catch (error) {
Expand Down
89 changes: 53 additions & 36 deletions apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ vi.mock("../middleware/referral-leaderboard-editions-caches.middleware", () => (
}));

import {
buildReferralProgramRules,
buildReferralProgramRulesPieSplit,
deserializeReferralProgramEditionConfigSetResponse,
deserializeReferrerLeaderboardPageResponse,
deserializeReferrerMetricsEditionsResponse,
ReferralProgramAwardModels,
ReferralProgramEditionConfigSetResponseCodes,
type ReferralProgramEditionSlug,
ReferralProgramStatuses,
Expand Down Expand Up @@ -362,6 +363,7 @@ describe("/v1/ensanalytics", () => {
responseCode: ReferrerMetricsEditionsResponseCodes.Ok,
data: {
"2025-12": {
awardModel: populatedReferrerLeaderboard.awardModel,
type: ReferrerEditionMetricsTypeIds.Ranked,
rules: populatedReferrerLeaderboard.rules,
referrer: expectedMetrics,
Expand All @@ -370,6 +372,7 @@ describe("/v1/ensanalytics", () => {
status: ReferralProgramStatuses.Active,
},
"2026-03": {
awardModel: populatedReferrerLeaderboard.awardModel,
type: ReferrerEditionMetricsTypeIds.Ranked,
rules: populatedReferrerLeaderboard.rules,
referrer: expectedMetrics,
Expand Down Expand Up @@ -441,23 +444,31 @@ describe("/v1/ensanalytics", () => {
const edition2 = response.data["2026-03"]!;

// Check 2025-12
expect(edition1.awardModel).toBe(ReferralProgramAwardModels.PieSplit);
expect(edition1.type).toBe(ReferrerEditionMetricsTypeIds.Unranked);
expect(edition1.rules).toEqual(populatedReferrerLeaderboard.rules);
expect(edition1.aggregatedMetrics).toEqual(populatedReferrerLeaderboard.aggregatedMetrics);
expect(edition1.referrer.referrer).toBe(nonExistingReferrer);
expect(edition1.referrer.rank).toBe(null);
expect(edition1.referrer.totalReferrals).toBe(0);
expect(edition1.referrer.totalIncrementalDuration).toBe(0);
expect(edition1.referrer.score).toBe(0);
expect(edition1.referrer.isQualified).toBe(false);
expect(edition1.referrer.finalScoreBoost).toBe(0);
expect(edition1.referrer.finalScore).toBe(0);
expect(edition1.referrer.awardPoolShare).toBe(0);
expect(edition1.referrer.awardPoolApproxValue).toStrictEqual({
currency: "USDC",
amount: 0n,
});
expect(edition1.accurateAsOf).toBe(expectedAccurateAsOf);
if (
edition1.awardModel === ReferralProgramAwardModels.PieSplit &&
edition1.type === ReferrerEditionMetricsTypeIds.Unranked
) {
expect(edition1.rules).toEqual(populatedReferrerLeaderboard.rules);
expect(edition1.aggregatedMetrics).toEqual(
populatedReferrerLeaderboard.aggregatedMetrics,
);
expect(edition1.referrer.referrer).toBe(nonExistingReferrer);
expect(edition1.referrer.rank).toBe(null);
expect(edition1.referrer.totalReferrals).toBe(0);
expect(edition1.referrer.totalIncrementalDuration).toBe(0);
expect(edition1.referrer.score).toBe(0);
expect(edition1.referrer.isQualified).toBe(false);
expect(edition1.referrer.finalScoreBoost).toBe(0);
expect(edition1.referrer.finalScore).toBe(0);
expect(edition1.referrer.awardPoolShare).toBe(0);
expect(edition1.referrer.awardPoolApproxValue).toStrictEqual({
currency: "USDC",
amount: 0n,
});
expect(edition1.accurateAsOf).toBe(expectedAccurateAsOf);
}

// Check 2026-03
expect(edition2.type).toBe(ReferrerEditionMetricsTypeIds.Unranked);
Expand Down Expand Up @@ -522,23 +533,29 @@ describe("/v1/ensanalytics", () => {
const edition2 = response.data["2026-03"]!;

// Check 2025-12
expect(edition1.awardModel).toBe(ReferralProgramAwardModels.PieSplit);
expect(edition1.type).toBe(ReferrerEditionMetricsTypeIds.Unranked);
expect(edition1.rules).toEqual(emptyReferralLeaderboard.rules);
expect(edition1.aggregatedMetrics).toEqual(emptyReferralLeaderboard.aggregatedMetrics);
expect(edition1.referrer.referrer).toBe(referrer);
expect(edition1.referrer.rank).toBe(null);
expect(edition1.referrer.totalReferrals).toBe(0);
expect(edition1.referrer.totalIncrementalDuration).toBe(0);
expect(edition1.referrer.score).toBe(0);
expect(edition1.referrer.isQualified).toBe(false);
expect(edition1.referrer.finalScoreBoost).toBe(0);
expect(edition1.referrer.finalScore).toBe(0);
expect(edition1.referrer.awardPoolShare).toBe(0);
expect(edition1.referrer.awardPoolApproxValue).toStrictEqual({
currency: "USDC",
amount: 0n,
});
expect(edition1.accurateAsOf).toBe(expectedAccurateAsOf);
if (
edition1.awardModel === ReferralProgramAwardModels.PieSplit &&
edition1.type === ReferrerEditionMetricsTypeIds.Unranked
) {
expect(edition1.rules).toEqual(emptyReferralLeaderboard.rules);
expect(edition1.aggregatedMetrics).toEqual(emptyReferralLeaderboard.aggregatedMetrics);
expect(edition1.referrer.referrer).toBe(referrer);
expect(edition1.referrer.rank).toBe(null);
expect(edition1.referrer.totalReferrals).toBe(0);
expect(edition1.referrer.totalIncrementalDuration).toBe(0);
expect(edition1.referrer.score).toBe(0);
expect(edition1.referrer.isQualified).toBe(false);
expect(edition1.referrer.finalScoreBoost).toBe(0);
expect(edition1.referrer.finalScore).toBe(0);
expect(edition1.referrer.awardPoolShare).toBe(0);
expect(edition1.referrer.awardPoolApproxValue).toStrictEqual({
currency: "USDC",
amount: 0n,
});
expect(edition1.accurateAsOf).toBe(expectedAccurateAsOf);
}

// Check 2026-03
expect(edition2.type).toBe(ReferrerEditionMetricsTypeIds.Unranked);
Expand Down Expand Up @@ -802,7 +819,7 @@ describe("/v1/ensanalytics", () => {
{
slug: "2025-12",
displayName: "December 2025",
rules: buildReferralProgramRules(
rules: buildReferralProgramRulesPieSplit(
parseUsdc("10000"),
100,
parseTimestamp("2025-12-01T00:00:00Z"),
Expand All @@ -817,7 +834,7 @@ describe("/v1/ensanalytics", () => {
{
slug: "2026-03",
displayName: "March 2026",
rules: buildReferralProgramRules(
rules: buildReferralProgramRulesPieSplit(
parseUsdc("10000"),
100,
parseTimestamp("2026-03-01T00:00:00Z"),
Expand All @@ -832,7 +849,7 @@ describe("/v1/ensanalytics", () => {
{
slug: "2026-06",
displayName: "June 2026",
rules: buildReferralProgramRules(
rules: buildReferralProgramRulesPieSplit(
parseUsdc("10000"),
100,
parseTimestamp("2026-06-01T00:00:00Z"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
buildReferrerMetrics,
type ReferralEvent,
type ReferralProgramRules,
type ReferrerMetrics,
} from "@namehash/ens-referrals/v1";
import { and, count, desc, eq, gte, isNotNull, lte, ne, sql, sum } from "drizzle-orm";
import { and, asc, count, desc, eq, gte, isNotNull, lte, ne, sql, sum } from "drizzle-orm";
import { type Address, zeroAddress } from "viem";

import * as schema from "@ensnode/ensnode-schema";
Expand Down Expand Up @@ -93,3 +94,77 @@ export const getReferrerMetrics = async (
throw new Error(`Failed to fetch referrer metrics from database: ${errorMessage}`);
}
};

/**
* Get raw referral events from the database for the sequential race algorithm (V1 API).
*
* Returns individual rows (no GROUP BY) ordered chronologically for deterministic race processing.
*
* @param rules - The referral program rules for filtering registrar actions
* @returns A promise that resolves to an array of {@link ReferralEvent} values.
* @throws Error if the database query fails.
*/
export const getReferralEvents = async (rules: ReferralProgramRules): Promise<ReferralEvent[]> => {
try {
const records = await db
.select({
id: schema.registrarActions.id,
referrer: schema.registrarActions.decodedReferrer,
timestamp: schema.registrarActions.timestamp,
blockNumber: schema.registrarActions.blockNumber,
transactionHash: schema.registrarActions.transactionHash,
incrementalDuration: schema.registrarActions.incrementalDuration,
// Note: Using raw SQL for COALESCE because Drizzle doesn't natively support it yet.
// See: https://github.com/drizzle-team/drizzle-orm/issues/3708
total: sql<string>`COALESCE(${schema.registrarActions.total}, 0)`.as("total"),
})
.from(schema.registrarActions)
.where(
and(
// Filter by timestamp range
gte(schema.registrarActions.timestamp, BigInt(rules.startTime)),
lte(schema.registrarActions.timestamp, BigInt(rules.endTime)),
// Filter by decodedReferrer not null
isNotNull(schema.registrarActions.decodedReferrer),
// Filter by decodedReferrer not zero address
ne(schema.registrarActions.decodedReferrer, zeroAddress),
// Filter by subregistryId matching the provided subregistryId
eq(schema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)),
),
)
.orderBy(
asc(schema.registrarActions.timestamp),
asc(schema.registrarActions.blockNumber),
asc(schema.registrarActions.transactionHash),
asc(schema.registrarActions.id),
);

// Type assertion: All fields in NonNullRecord are guaranteed non-null:
// 1. `referrer` is guaranteed non-null by isNotNull WHERE filter
// 2. `timestamp`, `blockNumber`, `transactionHash`, `incrementalDuration` are guaranteed non-null by database schema constraints (NOT NULL columns)
// 3. `total` is guaranteed non-null by COALESCE with 0
interface NonNullRecord {
id: string;
referrer: Address;
timestamp: bigint;
blockNumber: bigint;
transactionHash: `0x${string}`;
incrementalDuration: bigint;
total: string;
}

return (records as NonNullRecord[]).map((record) => ({
id: record.id,
referrer: record.referrer,
timestamp: Number(record.timestamp),
blockNumber: record.blockNumber,
transactionHash: record.transactionHash,
incrementalDuration: Number(record.incrementalDuration),
incrementalRevenueContribution: priceEth(BigInt(record.total)),
}));
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
logger.error({ error }, "Failed to fetch referral events from database");
throw new Error(`Failed to fetch referral events from database: ${errorMessage}`);
}
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { buildReferralProgramRules, type ReferrerLeaderboard } from "@namehash/ens-referrals/v1";
import {
buildReferralProgramRulesPieSplit,
ReferralProgramAwardModels,
type ReferrerLeaderboard,
} from "@namehash/ens-referrals/v1";
import { describe, expect, it, vi } from "vitest";

import { parseTimestamp, parseUsdc } from "@ensnode/ensnode-sdk";
Expand All @@ -12,7 +16,7 @@ vi.mock("./database-v1", () => ({
getReferrerMetrics: vi.fn(),
}));

const rules = buildReferralProgramRules(
const rules = buildReferralProgramRulesPieSplit(
parseUsdc("10000"),
10, // maxQualifiedReferrers
parseTimestamp("2025-01-01T00:00:00Z"),
Expand All @@ -33,13 +37,19 @@ describe("ENSAnalytics Referrer Leaderboard", () => {

const result = await getReferrerLeaderboard(rules, accurateAsOf);

expect(result.awardModel).toBe(ReferralProgramAwardModels.PieSplit);
if (result.awardModel !== ReferralProgramAwardModels.PieSplit) {
throw new Error("Expected PieSplit leaderboard");
}

expect(result).toMatchObject({
rules,
});

const referrers = result.referrers.entries();
const qualifiedReferrers = referrers.take(rules.maxQualifiedReferrers);
const unqualifiedReferrers = referrers.drop(rules.maxQualifiedReferrers);
// result.referrers is expected to be in rank order (rank 1 first), matching Map insertion order
const referrerEntries = Array.from(result.referrers.entries());
const qualifiedReferrers = referrerEntries.slice(0, rules.maxQualifiedReferrers);
const unqualifiedReferrers = referrerEntries.slice(rules.maxQualifiedReferrers);

/**
* Assert {@link RankedReferrerMetrics}.
Expand All @@ -57,31 +67,32 @@ describe("ENSAnalytics Referrer Leaderboard", () => {
expect(qualifiedReferrers.every(([_, referrer]) => referrer.isQualified)).toBe(true);
expect(unqualifiedReferrers.every(([_, referrer]) => !referrer.isQualified)).toBe(true);

// Assert `finalScoreBoost`
expect(qualifiedReferrers.every(([_, referrer]) => referrer.finalScoreBoost > 0)).toBe(true);
expect(unqualifiedReferrers.every(([_, referrer]) => referrer.finalScoreBoost === 0)).toBe(
true,
);

// Assert `finalScore`
expect(
qualifiedReferrers.every(
([_, referrer]) => referrer.finalScore === referrer.score * referrer.finalScoreBoost,
),
).toBe(true);
// Assert `finalScoreBoost` (pie-split specific)
// All qualified referrers except the last have boost > 0; the last qualified referrer
// receives boost === 0 by design (formula: 1 - (rank-1)/(maxQualifiedReferrers-1)),
// but only when the qualified slots are fully filled (length === maxQualifiedReferrers).
// With fewer referrers, the last qualified referrer is below the cutoff rank and has boost > 0.
const topQualifiedReferrers = qualifiedReferrers.slice(0, -1);
const lastQualifiedReferrer = qualifiedReferrers.at(-1);
expect(topQualifiedReferrers.every(([_, r]) => r.finalScoreBoost > 0)).toBe(true);
if (qualifiedReferrers.length === rules.maxQualifiedReferrers) {
expect(lastQualifiedReferrer![1].finalScoreBoost).toBe(0);
}
expect(unqualifiedReferrers.every(([_, r]) => r.finalScoreBoost === 0)).toBe(true);

// Assert `finalScore` (pie-split specific)
expect(
unqualifiedReferrers.every(([_, referrer]) => referrer.finalScore === referrer.score),
qualifiedReferrers.every(([_, r]) => r.finalScore === r.score * (1 + r.finalScoreBoost)),
).toBe(true);
expect(unqualifiedReferrers.every(([_, r]) => r.finalScore === r.score)).toBe(true);

/**
* Assert {@link AwardedReferrerMetrics}.
*/

// Assert `awardPoolShare`
expect(qualifiedReferrers.every(([_, referrer]) => referrer.awardPoolShare > 0)).toBe(true);
expect(unqualifiedReferrers.every(([_, referrer]) => referrer.awardPoolShare === 0)).toBe(
true,
);
// Assert `awardPoolShare` (pie-split specific)
expect(qualifiedReferrers.every(([_, r]) => r.awardPoolShare > 0)).toBe(true);
expect(unqualifiedReferrers.every(([_, r]) => r.awardPoolShare === 0)).toBe(true);

// Assert `awardPoolApproxValue`
expect(
Expand All @@ -100,6 +111,7 @@ describe("ENSAnalytics Referrer Leaderboard", () => {
const result = await getReferrerLeaderboard(rules, accurateAsOf);

expect(result).toMatchObject({
awardModel: rules.awardModel,
aggregatedMetrics: {
grandTotalIncrementalDuration: 0,
grandTotalRevenueContribution: {
Expand Down
Loading