Skip to content
Merged
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
60 changes: 60 additions & 0 deletions src/modules/keys/key-milestones.service.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<CurveMilestonesResponse> {
// 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
};
}
30 changes: 30 additions & 0 deletions src/modules/keys/keys.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<any>(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.
Expand Down
Loading