Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 10 additions & 4 deletions apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
referrerLeaderboardMiddleware: vi.fn(),
}));

import {

Check failure on line 25 in apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests

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

Error: Cannot find package '@namehash/ens-referrals/v1' imported from '/home/runner/_work/ensnode/ensnode/apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts' ❯ src/handlers/ensanalytics-api-v1.test.ts:25:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }
deserializeReferrerDetailResponse,
deserializeReferrerLeaderboardPageResponse,
ReferrerDetailResponseCodes,
Expand All @@ -30,13 +30,13 @@
ReferrerDetailTypeIds,
ReferrerLeaderboardPageResponseCodes,
type ReferrerLeaderboardPageResponseOk,
} from "@namehash/ens-referrals";
} from "@namehash/ens-referrals/v1";

import {
emptyReferralLeaderboard,
populatedReferrerLeaderboard,
referrerLeaderboardPageResponseOk,
} from "@/lib/ensanalytics/referrer-leaderboard/mocks";
} from "@/lib/ensanalytics/referrer-leaderboard/mocks-v1";

import app from "./ensanalytics-api-v1";

Expand Down Expand Up @@ -243,7 +243,10 @@
expect(response.data.referrer.finalScoreBoost).toBe(0);
expect(response.data.referrer.finalScore).toBe(0);
expect(response.data.referrer.awardPoolShare).toBe(0);
expect(response.data.referrer.awardPoolApproxValue).toBe(0);
expect(response.data.referrer.awardPoolApproxValue).toStrictEqual({
currency: "USDC",
amount: 0n,
});
expect(response.data.accurateAsOf).toBe(expectedAccurateAsOf);
}
});
Expand Down Expand Up @@ -281,7 +284,10 @@
expect(response.data.referrer.finalScoreBoost).toBe(0);
expect(response.data.referrer.finalScore).toBe(0);
expect(response.data.referrer.awardPoolShare).toBe(0);
expect(response.data.referrer.awardPoolApproxValue).toBe(0);
expect(response.data.referrer.awardPoolApproxValue).toStrictEqual({
currency: "USDC",
amount: 0n,
});
expect(response.data.accurateAsOf).toBe(expectedAccurateAsOf);
}
});
Expand Down
2 changes: 1 addition & 1 deletion apps/ensapi/src/handlers/ensanalytics-api-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ReferrerLeaderboardPageResponseCodes,
serializeReferrerDetailResponse,
serializeReferrerLeaderboardPageResponse,
} from "@namehash/ens-referrals";
} from "@namehash/ens-referrals/v1";
import { describeRoute } from "hono-openapi";
import { z } from "zod/v4";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
buildReferrerMetrics,
type ReferralProgramRules,
type ReferrerMetrics,
} from "@namehash/ens-referrals/v1";
import { and, 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";
import { deserializeDuration, formatAccountId, priceEth } from "@ensnode/ensnode-sdk";

import { db } from "@/lib/db";
import logger from "@/lib/logger";

/**
* Get Referrer Metrics from the database (V1 API).
*
* @param rules - The referral program rules for filtering registrar actions
* @returns A promise that resolves to an array of {@link ReferrerMetrics} values.
* @throws Error if the database query fails.
*/
export const getReferrerMetrics = async (
rules: ReferralProgramRules,
): Promise<ReferrerMetrics[]> => {
/**
* Step 1: Filter for referrals matching the provided rules:
* - timestamp is between startDate and endDate (inclusive)
* - decodedReferrer is not null and not the zero address
* - subregistryId matches the provided subregistryId
*
* Step 2: Group by decodedReferrer and calculate:
* - Sum total incrementalDuration for each decodedReferrer
* - Count of qualified referrals for each decodedReferrer
* - Sum total cost (revenue contribution) for each decodedReferrer
*
* Step 3: Sort by sum total incrementalDuration from highest to lowest
*/

try {
const records = await db
.select({
referrer: schema.registrarActions.decodedReferrer,
totalReferrals: count().as("total_referrals"),
totalIncrementalDuration: sum(schema.registrarActions.incrementalDuration).as(
"total_incremental_duration",
),
// Note: Using raw SQL for COALESCE because Drizzle doesn't natively support it yet.
// See: https://github.com/drizzle-team/drizzle-orm/issues/3708
totalRevenueContribution:
sql<string>`COALESCE(SUM(${schema.registrarActions.total}), 0)`.as(
"total_revenue_contribution",
),
})
.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)),
),
)
.groupBy(schema.registrarActions.decodedReferrer)
.orderBy(desc(sql`total_incremental_duration`));

// Type assertion: The WHERE clause in the query above guarantees non-null values for:
// 1. `referrer` is guaranteed to be non-null due to isNotNull filter
// 2. `totalIncrementalDuration` is guaranteed to be non-null as it is the sum of non-null bigint values
// 3. `totalRevenueContribution` is guaranteed to be non-null due to COALESCE with 0
interface NonNullRecord {
referrer: Address;
totalReferrals: number;
totalIncrementalDuration: string;
totalRevenueContribution: string;
}

return (records as NonNullRecord[]).map((record) => {
return buildReferrerMetrics(
record.referrer,
record.totalReferrals,
deserializeDuration(record.totalIncrementalDuration),
priceEth(BigInt(record.totalRevenueContribution)),
);
Comment thread
Goader marked this conversation as resolved.
});
Comment thread
lightwalker-eth marked this conversation as resolved.
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
logger.error({ error }, "Failed to fetch referrer metrics from database");
throw new Error(`Failed to fetch referrer metrics from database: ${errorMessage}`);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
buildReferrerLeaderboard,
type ReferralProgramRules,
type ReferrerLeaderboard,
} from "@namehash/ens-referrals/v1";

import type { UnixTimestamp } from "@ensnode/ensnode-sdk";

import { getReferrerMetrics } from "./database-v1";

/**
* Builds a `ReferralLeaderboard` from the database using the provided referral program rules (V1 API).
*
* @param rules - The referral program rules for filtering registrar actions
* @param accurateAsOf - The {@link UnixTimestamp} of when the data used to build the {@link ReferrerLeaderboard} was accurate as of.
* @returns A promise that resolves to a {@link ReferrerLeaderboard}
* @throws Error if the database query fails
*/
export async function getReferrerLeaderboard(
rules: ReferralProgramRules,
accurateAsOf: UnixTimestamp,
): Promise<ReferrerLeaderboard> {
const allReferrers = await getReferrerMetrics(rules);
return buildReferrerLeaderboard(allReferrers, rules, accurateAsOf);
}
Loading
Loading