From 3da49703f7b4c06725a9981655f1f904e93f67be Mon Sep 17 00:00:00 2001 From: PHADAR Date: Sun, 25 Oct 2026 16:30:58 +0100 Subject: [PATCH] feat: Add a GET /tokens endpoint listing tokens seen across stream (#107) --- src/routes/streams.ts | 35 +++++++++++++++++++++++++++++++++++ src/routes/tokenTotals.ts | 17 +++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/routes/streams.ts b/src/routes/streams.ts index b3fc138..cb16577 100644 --- a/src/routes/streams.ts +++ b/src/routes/streams.ts @@ -153,6 +153,41 @@ export async function streamRoutes(app: FastifyInstance): Promise { }, ); + /** + * Lists distinct token addresses seen across streams, with a stream count for each. + * @route GET /v1/tokens + * @auth Public + * @returns 200 `{ tokens: { tokenAddress: string, streamCount: number }[] }`, sorted by stream count descending then address ascending. + */ + app.get( + '/tokens', + { + schema: { + tags: ['Streams'], + summary: 'List tokens used by streams', + description: + 'Returns distinct token addresses across all streams with the number of streams per token.', + response: { + 200: { type: 'object', additionalProperties: true }, + }, + }, + }, + async (request, reply) => { + const grouped = await prisma.stream.groupBy({ + by: ['tokenAddress'], + _count: { _all: true }, + orderBy: [{ _count: { tokenAddress: 'desc' } }, { tokenAddress: 'asc' }], + }); + + return sendPublicCacheable(request, reply, { + tokens: grouped.map((row) => ({ + tokenAddress: row.tokenAddress, + streamCount: row._count._all, + })), + }); + }, + ); + /** * Returns a stream by its UUID. * @route GET /v1/streams/:id diff --git a/src/routes/tokenTotals.ts b/src/routes/tokenTotals.ts index e4c776e..1565e58 100644 --- a/src/routes/tokenTotals.ts +++ b/src/routes/tokenTotals.ts @@ -10,6 +10,11 @@ export interface TokenTotal { amount: string; } +export interface TokenSummary { + tokenAddress: string; + streamCount: number; +} + export function committedTotalsByToken(streams: StreamForTokenTotals[]): TokenTotal[] { const totals = new Map(); @@ -25,3 +30,15 @@ export function committedTotalsByToken(streams: StreamForTokenTotals[]): TokenTo .sort(([left], [right]) => left.localeCompare(right)) .map(([tokenAddress, amount]) => ({ tokenAddress, amount: amount.toString() })); } + +export function distinctTokensWithStreamCount(streams: StreamForTokenTotals[]): TokenSummary[] { + const counts = new Map(); + + for (const stream of streams) { + counts.set(stream.tokenAddress, (counts.get(stream.tokenAddress) ?? 0) + 1); + } + + return [...counts] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([tokenAddress, streamCount]) => ({ tokenAddress, streamCount })); +}