feat(backend): ranked event leaderboard replacing binary winners model#987
Merged
Merged
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…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
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 abandonedwinners-query.dto.ts/WinnerResponsemodel. There was no ranked, paginated, accuracy-based view of participants.What Was Done
1. ContractService — new interface + method
File:
src/contract/contract.service.tsAdded the
ContractLeaderboardEntryinterface (rank, address, total_predictions, correct_predictions, accuracy_percentage, is_winner, completion_time) alongside the existingContractWinnerinterface.Added
getEventLeaderboard(eventId)which calls the newget_event_leaderboardSoroban 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.tsTypeORM entity
CreatorEventLeaderboardEntrymapped to the tablecreator_event_leaderboard_entries. Columns mirrorContractLeaderboardEntryplus a UUID primary key andcreated_attimestamp. Unique index on (event_id, user_address) prevents duplicate entries; composite index on (event_id, rank) optimises the ORDER BY rank queries used bygetLeaderboard().3. Migration
File:
src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.tsCreates the
creator_event_leaderboard_entriestable with all columns and both indexes inup().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.tsReplaced the old stub (which had a dangling
minPredictionsfield and an incompleteLeaderboardEntryResponselackingtotalPagesandsource) with a clean DTO.LeaderboardQueryDtokeepspage/limitwith class-validator decorators.PaginatedLeaderboardResponsenow includestotalPagesand asourcediscriminator ("contract" | "cache") so callers know where the data came from.5. getLeaderboard() service method
File:
src/creator-events/creator-events.service.tsNew
getLeaderboard(eventId, query)method with dual-path logic:Finalized path: looks up the
CreatorEventrow byon_chain_event_idand checksis_finalized. If true, queriesCreatorEventLeaderboardEntryrows ordered by rank with TypeORMfindAndCountfor efficient pagination, and returnssource: "cache".Live path: calls
contractService.getEventLeaderboard(eventId), paginates the result array in-process (the contract returns the full list), and returnssource: "contract".The
CreatorEventLeaderboardEntryrepository was injected alongside the existingCreatorEventrepository via@InjectRepository. Imports forLeaderboardQueryDto,LeaderboardEntryResponse, andPaginatedLeaderboardResponsewere added.6. New endpoint
File:
src/creator-events/creator-events.controller.tsGET /creator-events/:id/leaderboardadded toCreatorEventsController. UsesCacheInterceptorwith a 30-second TTL, aValidationPipewith transform/whitelist on the query, and Swagger annotations (@ApiOperation,@ApiQuery,@ApiResponse).7. Module update
File:
src/creator-events/creator-events.module.tsCreatorEventLeaderboardEntryadded toTypeOrmModule.forFeature. Addedexports: [CreatorEventsService]for future cross-module use.8. Deleted unused files
src/creator-events/dto/winners-query.dto.ts— removed entirely.WinnersQueryDto,WinnerResponse, andPaginatedWinnersResponseare no longer referenced anywhere in the codebase.How It Works End-to-End
A creator event is active →
GET /creator-events/:id/leaderboardcallscontractService.getEventLeaderboard()(live Soroban view), paginates the result, returnssource: "contract".The indexer's
EventFinalizedhandler (Issue 6) setsis_finalized = trueon theCreatorEventrow and populatescreator_event_leaderboard_entriesfrom the final contract state.Subsequent calls to the same endpoint detect
is_finalized = trueand read from the DB cache instead, returningsource: "cache". This avoids repeated Soroban RPC calls for settled events and enables fast, indexed pagination at scale.Acceptance Criteria Met