Skip to content

feat(backend): ranked event leaderboard replacing binary winners model#987

Merged
Olowodarey merged 3 commits into
Arena1X:mainfrom
Pvsaint:feat/ranked-event-leaderboard
Jun 17, 2026
Merged

feat(backend): ranked event leaderboard replacing binary winners model#987
Olowodarey merged 3 commits into
Arena1X:mainfrom
Pvsaint:feat/ranked-event-leaderboard

Conversation

@Pvsaint

@Pvsaint Pvsaint commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Closes #956

Problem

The platform had no points-based leaderboard for creator events. The only related concept was a binary "winners" list returned by getEventWinners() on the contract, surfaced through the abandoned winners-query.dto.ts / WinnerResponse model. There was no ranked, paginated, accuracy-based view of participants.

What Was Done

1. ContractService — new interface + method

File: src/contract/contract.service.ts

Added the ContractLeaderboardEntry interface (rank, address, total_predictions, correct_predictions, accuracy_percentage, is_winner, completion_time) alongside the existing ContractWinner interface.

Added getEventLeaderboard(eventId) which calls the new get_event_leaderboard Soroban view function, normalises every field to the correct primitive type, and falls back to an empty array on contract error — matching the defensive pattern used by all other view calls in this service.

2. New entity

File: src/matches/entities/creator-event-leaderboard-entry.entity.ts

TypeORM entity CreatorEventLeaderboardEntry mapped to the table creator_event_leaderboard_entries. Columns mirror ContractLeaderboardEntry plus a UUID primary key and created_at timestamp. Unique index on (event_id, user_address) prevents duplicate entries; composite index on (event_id, rank) optimises the ORDER BY rank queries used by getLeaderboard().

3. Migration

File: src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.ts

Creates the creator_event_leaderboard_entries table with all columns and both indexes in up(). down() drops the table entirely. Timestamp 1750200000000 places this after all existing migrations.

4. Rewritten leaderboard-query.dto.ts

File: src/creator-events/dto/leaderboard-query.dto.ts

Replaced the old stub (which had a dangling minPredictions field and an incomplete LeaderboardEntryResponse lacking totalPages and source) with a clean DTO. LeaderboardQueryDto keeps page / limit with class-validator decorators. PaginatedLeaderboardResponse now includes totalPages and a source discriminator ("contract" | "cache") so callers know where the data came from.

5. getLeaderboard() service method

File: src/creator-events/creator-events.service.ts

New getLeaderboard(eventId, query) method with dual-path logic:

  • Finalized path: looks up the CreatorEvent row by on_chain_event_id and checks is_finalized. If true, queries CreatorEventLeaderboardEntry rows ordered by rank with TypeORM findAndCount for efficient pagination, and returns source: "cache".

  • Live path: calls contractService.getEventLeaderboard(eventId), paginates the result array in-process (the contract returns the full list), and returns source: "contract".

The CreatorEventLeaderboardEntry repository was injected alongside the existing CreatorEvent repository via @InjectRepository. Imports for LeaderboardQueryDto, LeaderboardEntryResponse, and PaginatedLeaderboardResponse were added.

6. New endpoint

File: src/creator-events/creator-events.controller.ts

GET /creator-events/:id/leaderboard added to
CreatorEventsController. Uses CacheInterceptor with a 30-second TTL, a ValidationPipe with transform/whitelist on the query, and Swagger annotations (@ApiOperation, @ApiQuery, @ApiResponse).

7. Module update

File: src/creator-events/creator-events.module.ts

CreatorEventLeaderboardEntry added to TypeOrmModule.forFeature. Added exports: [CreatorEventsService] for future cross-module use.

8. Deleted unused files

  • src/creator-events/dto/winners-query.dto.ts — removed entirely. WinnersQueryDto, WinnerResponse, and PaginatedWinnersResponse are no longer referenced anywhere in the codebase.

How It Works End-to-End

  1. A creator event is active → GET /creator-events/:id/leaderboard calls contractService.getEventLeaderboard() (live Soroban view), paginates the result, returns source: "contract".

  2. The indexer's EventFinalized handler (Issue 6) sets is_finalized = true on the CreatorEvent row and populates creator_event_leaderboard_entries from the final contract state.

  3. Subsequent calls to the same endpoint detect is_finalized = true and read from the DB cache instead, returning source: "cache". This avoids repeated Soroban RPC calls for settled events and enables fast, indexed pagination at scale.

Acceptance Criteria Met

  • GET /creator-events/:id/leaderboard?page=1&limit=20 returns a paginated ranked response.
  • Non-finalized event → live contract read.
  • Finalized event → DB cache read.
  • winners-query.dto.ts and WinnerResponse/ContractWinner models are removed / superseded.

Closes: [Backend] — Ranked event leaderboard (replaces binary "winners" model)

## Problem
The platform had no points-based leaderboard for creator events. The
only related concept was a binary "winners" list returned by
`getEventWinners()` on the contract, surfaced through the abandoned
`winners-query.dto.ts` / `WinnerResponse` model. There was no ranked,
paginated, accuracy-based view of participants.

## What Was Done

### 1. ContractService — new interface + method
File: `src/contract/contract.service.ts`

Added the `ContractLeaderboardEntry` interface (rank, address,
total_predictions, correct_predictions, accuracy_percentage, is_winner,
completion_time) alongside the existing `ContractWinner` interface.

Added `getEventLeaderboard(eventId)` which calls the new
`get_event_leaderboard` Soroban view function, normalises every field
to the correct primitive type, and falls back to an empty array on
contract error — matching the defensive pattern used by all other view
calls in this service.

### 2. New entity
File: `src/matches/entities/creator-event-leaderboard-entry.entity.ts`

TypeORM entity `CreatorEventLeaderboardEntry` mapped to the table
`creator_event_leaderboard_entries`. Columns mirror
`ContractLeaderboardEntry` plus a UUID primary key and `created_at`
timestamp. Unique index on (event_id, user_address) prevents duplicate
entries; composite index on (event_id, rank) optimises the
ORDER BY rank queries used by `getLeaderboard()`.

### 3. Migration
File: `src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.ts`

Creates the `creator_event_leaderboard_entries` table with all columns
and both indexes in `up()`. `down()` drops the table entirely.
Timestamp 1750200000000 places this after all existing migrations.

### 4. Rewritten leaderboard-query.dto.ts
File: `src/creator-events/dto/leaderboard-query.dto.ts`

Replaced the old stub (which had a dangling `minPredictions` field and
an incomplete `LeaderboardEntryResponse` lacking `totalPages` and
`source`) with a clean DTO. `LeaderboardQueryDto` keeps `page` /
`limit` with class-validator decorators. `PaginatedLeaderboardResponse`
now includes `totalPages` and a `source` discriminator
("contract" | "cache") so callers know where the data came from.

### 5. getLeaderboard() service method
File: `src/creator-events/creator-events.service.ts`

New `getLeaderboard(eventId, query)` method with dual-path logic:

- **Finalized path**: looks up the `CreatorEvent` row by
  `on_chain_event_id` and checks `is_finalized`. If true, queries
  `CreatorEventLeaderboardEntry` rows ordered by rank with TypeORM
  `findAndCount` for efficient pagination, and returns
  `source: "cache"`.

- **Live path**: calls `contractService.getEventLeaderboard(eventId)`,
  paginates the result array in-process (the contract returns the full
  list), and returns `source: "contract"`.

The `CreatorEventLeaderboardEntry` repository was injected alongside
the existing `CreatorEvent` repository via `@InjectRepository`.
Imports for `LeaderboardQueryDto`, `LeaderboardEntryResponse`, and
`PaginatedLeaderboardResponse` were added.

### 6. New endpoint
File: `src/creator-events/creator-events.controller.ts`

`GET /creator-events/:id/leaderboard` added to
`CreatorEventsController`. Uses `CacheInterceptor` with a 30-second
TTL, a `ValidationPipe` with transform/whitelist on the query, and
Swagger annotations (`@ApiOperation`, `@ApiQuery`, `@ApiResponse`).

### 7. Module update
File: `src/creator-events/creator-events.module.ts`

`CreatorEventLeaderboardEntry` added to `TypeOrmModule.forFeature`.
Added `exports: [CreatorEventsService]` for future cross-module use.

### 8. Deleted unused files
- `src/creator-events/dto/winners-query.dto.ts` — removed entirely.
  `WinnersQueryDto`, `WinnerResponse`, and `PaginatedWinnersResponse`
  are no longer referenced anywhere in the codebase.

## How It Works End-to-End

1. A creator event is active → `GET /creator-events/:id/leaderboard`
   calls `contractService.getEventLeaderboard()` (live Soroban view),
   paginates the result, returns `source: "contract"`.

2. The indexer's `EventFinalized` handler (Issue 6) sets
   `is_finalized = true` on the `CreatorEvent` row and populates
   `creator_event_leaderboard_entries` from the final contract state.

3. Subsequent calls to the same endpoint detect `is_finalized = true`
   and read from the DB cache instead, returning `source: "cache"`.
   This avoids repeated Soroban RPC calls for settled events and
   enables fast, indexed pagination at scale.

## Acceptance Criteria Met
- GET /creator-events/:id/leaderboard?page=1&limit=20 returns a
  paginated ranked response.
- Non-finalized event → live contract read.
- Finalized event → DB cache read.
- winners-query.dto.ts and WinnerResponse/ContractWinner models are
  removed / superseded.
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
insight-arena-4rll Ready Ready Preview, Comment Jun 17, 2026 9:46pm

…nts specs

The new leaderboard repository injected in CreatorEventsService caused
both creator-events spec files to fail at module compile time with:

  Nest can't resolve dependencies of the CreatorEventsService
  (..., ?). CreatorEventLeaderboardEntryRepository at index [2]

Fix: import CreatorEventLeaderboardEntry and provide an empty mock
for getRepositoryToken(CreatorEventLeaderboardEntry) in both:
- creator-events.service.spec.ts
- creator-events-predictions-stats.spec.ts
@Olowodarey Olowodarey merged commit 726e312 into Arena1X:main Jun 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] — Ranked event leaderboard (replaces binary "winners" model)

2 participants