diff --git a/src/modules/keys/key-milestones.service.ts b/src/modules/keys/key-milestones.service.ts index 2a2d5d5a..12ba6dab 100644 --- a/src/modules/keys/key-milestones.service.ts +++ b/src/modules/keys/key-milestones.service.ts @@ -1,4 +1,8 @@ // src/modules/keys/key-milestones.service.ts +import { prisma } from '../../utils/prisma.utils'; +import { getBuyUnitPrice } from '../../utils/pricing.utils'; +import { KeyNotFoundError } from './key-supply.service'; + export interface Milestone { tier: number; @@ -125,3 +129,59 @@ export function getTierForSupply( } return tier; } + +export interface CurveMilestonesResponse { + currentSupply: number; + currentPrice: string; + isGraduated: boolean; + nextMilestone: { + tier: number; + threshold: number; + price: string; + } | null; + milestones: Array<{ + tier: number; + threshold: number; + price: string; + }>; +} + +export async function getKeyCurveMilestones(keyId: string): Promise { + // Resolve by id OR handle to match the pattern used elsewhere in keys routes. + const creator = await prisma.creatorProfile.findFirst({ + where: { OR: [{ id: keyId }, { handle: keyId }] }, + select: { circulatingSupply: true } + }); + + if (!creator) { + throw new KeyNotFoundError(keyId); + } + + const currentSupply = Number(creator.circulatingSupply); + const currentPrice = getBuyUnitPrice(currentSupply, 0).toString(); + + const milestonesWithPrice = MILESTONES.map(m => ({ + tier: m.tier, + threshold: m.threshold, + price: getBuyUnitPrice(m.threshold, 0).toString() + })); + + let nextMilestone = null; + for (const m of milestonesWithPrice) { + if (currentSupply < m.threshold) { + nextMilestone = m; + break; + } + } + + const finalMilestone = milestonesWithPrice.length > 0 ? milestonesWithPrice[milestonesWithPrice.length - 1] : null; + const isGraduated = finalMilestone ? currentSupply >= finalMilestone.threshold : false; + + return { + currentSupply, + currentPrice, + isGraduated, + nextMilestone, + milestones: milestonesWithPrice + }; +} diff --git a/src/modules/keys/keys.routes.ts b/src/modules/keys/keys.routes.ts index 69b51ae7..14d2cc49 100644 --- a/src/modules/keys/keys.routes.ts +++ b/src/modules/keys/keys.routes.ts @@ -98,6 +98,7 @@ import { matchTierForLockPeriod, calculateEffectiveWeight, } from '../staking/staking.service'; +import { getKeyCurveMilestones } from './key-milestones.service'; const priceHistoryQuerySchema = z.object({ from: z.string().datetime(), @@ -542,6 +543,35 @@ router.get('/:keyId/supply', async (req, res, next) => { } }); +/** + * GET /api/v1/keys/:keyId/curve/milestones + * Return all milestones, current progress, and graduation status. + */ +router.get( + '/:keyId/curve/milestones', + cacheControl({ maxAge: 30, type: 'public', mustRevalidate: true }), + async (req, res, next) => { + const keyId = String(req.params.keyId); + const cacheKey = `curve-milestones:${keyId}`; + try { + const cached = await cacheGetJson(cacheKey); + if (cached !== null) { + return sendSuccess(res, cached); + } + + const result = await getKeyCurveMilestones(keyId); + await cacheSetJson(cacheKey, result, 30); + sendSuccess(res, result); + } catch (error) { + if (error instanceof Error && error.name === 'KeyNotFoundError') { + sendNotFound(res, 'Key'); + return; + } + next(error); + } + } +); + /** * GET /api/v1/keys/:keyId/price-impact?quantity=&direction=buy|sell * Calculate price impact of a given trade quantity and direction.