diff --git a/backend/src/creator-events/creator-events-predictions-stats.spec.ts b/backend/src/creator-events/creator-events-predictions-stats.spec.ts index 8586dc5f..9b164dd3 100644 --- a/backend/src/creator-events/creator-events-predictions-stats.spec.ts +++ b/backend/src/creator-events/creator-events-predictions-stats.spec.ts @@ -22,6 +22,7 @@ describe('CreatorEventsService predictions and stats', () => { | 'getPredictionDistribution' > >; + let creatorEventRepository: { createQueryBuilder: jest.Mock; findOne: jest.Mock }; const mockEvent = { eventId: '1', @@ -67,13 +68,18 @@ describe('CreatorEventsService predictions and stats', () => { getPredictionDistribution: jest.fn(), }; + creatorEventRepository = { + createQueryBuilder: jest.fn(), + findOne: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ providers: [ CreatorEventsService, { provide: ContractService, useValue: contractService }, { provide: getRepositoryToken(CreatorEvent), - useValue: { createQueryBuilder: jest.fn() }, + useValue: creatorEventRepository, }, { provide: getRepositoryToken(CreatorEventLeaderboardEntry), @@ -158,6 +164,11 @@ describe('CreatorEventsService predictions and stats', () => { }); it('calculates event statistics with distribution and completion rate', async () => { + creatorEventRepository.findOne.mockResolvedValue({ + prize_pool: '5010000000', + total_entry_fees_collected: '10000000', + }); + const result = await service.getEventStats('1'); expect(result.totalParticipants).toBe(3); @@ -170,6 +181,17 @@ describe('CreatorEventsService predictions and stats', () => { expect(result.averagePredictionsPerUser).toBe(1.67); expect(result.completionRate).toBe(67); expect(result.winnersVerified).toBe(false); + expect(result.prizePool).toBe('5010000000'); + expect(result.totalEntryFeesCollected).toBe('10000000'); + }); + + it('defaults prize pool and entry fees to "0" when no cached event exists', async () => { + creatorEventRepository.findOne.mockResolvedValue(null); + + const result = await service.getEventStats('1'); + + expect(result.prizePool).toBe('0'); + expect(result.totalEntryFeesCollected).toBe('0'); }); it('throws when event is not found', async () => { diff --git a/backend/src/creator-events/creator-events.controller.spec.ts b/backend/src/creator-events/creator-events.controller.spec.ts index 84f47bb4..4e7e496f 100644 --- a/backend/src/creator-events/creator-events.controller.spec.ts +++ b/backend/src/creator-events/creator-events.controller.spec.ts @@ -126,6 +126,8 @@ describe('CreatorEventsController', () => { winnerCount: 0, averagePredictionsPerUser: 4, completionRate: 60, + prizePool: '0', + totalEntryFeesCollected: '0', }; service.getEventStats.mockResolvedValue(mockStats); diff --git a/backend/src/creator-events/creator-events.service.ts b/backend/src/creator-events/creator-events.service.ts index bca46b2d..1525e9c1 100644 --- a/backend/src/creator-events/creator-events.service.ts +++ b/backend/src/creator-events/creator-events.service.ts @@ -410,10 +410,14 @@ export class CreatorEventsService { throw new NotFoundException(`Event ${eventId} not found`); } - const [statistics, matches, participants] = await Promise.all([ + const [statistics, matches, participants, cachedEvent] = await Promise.all([ this.contractService.getEventStatistics(eventId), this.contractService.getEventMatches(eventId), this.contractService.getEventParticipants(eventId), + this.creatorEventRepository.findOne({ + where: { on_chain_event_id: Number(eventId) as unknown as number }, + select: ['prize_pool', 'total_entry_fees_collected'], + }), ]); const matchesResolved = matches.filter((m) => m.resolved).length; @@ -470,6 +474,8 @@ export class CreatorEventsService { winnerCount: statistics?.winnerCount ?? 0, averagePredictionsPerUser, completionRate, + prizePool: cachedEvent?.prize_pool ?? '0', + totalEntryFeesCollected: cachedEvent?.total_entry_fees_collected ?? '0', }; } diff --git a/backend/src/creator-events/dto/event-stats-response.dto.ts b/backend/src/creator-events/dto/event-stats-response.dto.ts index 7897173f..2f689937 100644 --- a/backend/src/creator-events/dto/event-stats-response.dto.ts +++ b/backend/src/creator-events/dto/event-stats-response.dto.ts @@ -65,4 +65,16 @@ export class EventStatsResponseDto { description: 'Percentage of participants who predicted all matches (0-100)', }) completionRate: number; + + @ApiProperty({ + description: + 'Total prize pool in stroops, including entry fees collected from participants', + }) + prizePool: string; + + @ApiProperty({ + description: + 'Running total of entry fees collected from participants in stroops', + }) + totalEntryFeesCollected: string; } diff --git a/backend/src/creator-events/entities/creator-event.entity.ts b/backend/src/creator-events/entities/creator-event.entity.ts index c1c59d15..e869a401 100644 --- a/backend/src/creator-events/entities/creator-event.entity.ts +++ b/backend/src/creator-events/entities/creator-event.entity.ts @@ -80,6 +80,13 @@ export class CreatorEvent { @ApiProperty({ description: 'Entry fee in stroops' }) entry_fee: string; + @Column({ type: 'bigint', default: '0' }) + @ApiProperty({ + description: + 'Running total of entry fees collected from participants in stroops', + }) + total_entry_fees_collected: string; + @Column({ type: 'varchar', length: 100, default: 'general' }) @ApiProperty({ description: 'Normalized campaign category slug' }) category: string; diff --git a/backend/src/indexer/indexer.service.spec.ts b/backend/src/indexer/indexer.service.spec.ts index 798fd8ad..bb35d582 100644 --- a/backend/src/indexer/indexer.service.spec.ts +++ b/backend/src/indexer/indexer.service.spec.ts @@ -516,6 +516,79 @@ describe('IndexerService', () => { }); }); + describe('handleUserJoinedEvent', () => { + it('extracts entry_fee_paid in the UserJoinedEvent payload', () => { + const data = (service as any).extractEventData('UserJoinedEvent', { + user_address: 'GUSER', + event_id: '7', + joined_at: 1710000000, + entry_fee_paid: '10000000', + }); + + expect(data).toMatchObject({ + user_address: 'GUSER', + event_id: '7', + joined_at: 1710000000, + entry_fee_paid: '10000000', + }); + }); + + it('defaults entry_fee_paid to "0" for free events', () => { + const data = (service as any).extractEventData('UserJoinedEvent', { + user_address: 'GUSER', + event_id: '7', + joined_at: 1710000000, + }); + + expect(data).toMatchObject({ entry_fee_paid: '0' }); + }); + + it('adds the entry fee paid to prize_pool and total_entry_fees_collected', async () => { + const event = { + on_chain_event_id: 7, + participant_count: 2, + prize_pool: '5000000000', + total_entry_fees_collected: '20000000', + } as CreatorEvent; + creatorEventRepository.findOne.mockResolvedValue(event); + creatorEventRepository.save.mockResolvedValue(event); + + await (service as any).handleUserJoinedEvent({ + user_address: 'GUSER', + event_id: '7', + joined_at: 1710000000, + entry_fee_paid: '10000000', + }); + + expect(event.participant_count).toBe(3); + expect(event.prize_pool).toBe('5010000000'); + expect(event.total_entry_fees_collected).toBe('30000000'); + expect(creatorEventRepository.save).toHaveBeenCalledWith(event); + }); + + it('leaves prize_pool and total_entry_fees_collected unchanged for free events', async () => { + const event = { + on_chain_event_id: 7, + participant_count: 0, + prize_pool: '5000000000', + total_entry_fees_collected: '0', + } as CreatorEvent; + creatorEventRepository.findOne.mockResolvedValue(event); + creatorEventRepository.save.mockResolvedValue(event); + + await (service as any).handleUserJoinedEvent({ + user_address: 'GUSER', + event_id: '7', + joined_at: 1710000000, + entry_fee_paid: '0', + }); + + expect(event.participant_count).toBe(1); + expect(event.prize_pool).toBe('5000000000'); + expect(event.total_entry_fees_collected).toBe('0'); + }); + }); + describe('retryFailedEvents', () => { it('should retry failed events', async () => { const failedEvent = { diff --git a/backend/src/indexer/indexer.service.ts b/backend/src/indexer/indexer.service.ts index 89513782..7362f2b8 100644 --- a/backend/src/indexer/indexer.service.ts +++ b/backend/src/indexer/indexer.service.ts @@ -406,6 +406,7 @@ export class IndexerService implements OnModuleInit { user_address: this.readStr(base, 'user_address'), event_id: this.readBigInt(base, 'event_id'), joined_at: this.readNum(base, 'joined_at'), + entry_fee_paid: this.readUnsignedBigInt(base, 'entry_fee_paid'), }; case 'PredictionSubmitted': return { @@ -691,6 +692,17 @@ export class IndexerService implements OnModuleInit { } event.participant_count += 1; + + const entryFeePaid = this.readUnsignedBigInt(data, 'entry_fee_paid'); + if (BigInt(entryFeePaid) > 0n) { + event.prize_pool = ( + BigInt(event.prize_pool ?? '0') + BigInt(entryFeePaid) + ).toString(); + event.total_entry_fees_collected = ( + BigInt(event.total_entry_fees_collected ?? '0') + BigInt(entryFeePaid) + ).toString(); + } + await this.creatorEventRepository.save(event); // Trigger notification diff --git a/backend/src/matches/entities/creator-event.entity.ts b/backend/src/matches/entities/creator-event.entity.ts index 286ff77f..e57beb76 100644 --- a/backend/src/matches/entities/creator-event.entity.ts +++ b/backend/src/matches/entities/creator-event.entity.ts @@ -62,6 +62,9 @@ export class CreatorEvent { @Column({ type: 'bigint', default: '0' }) entry_fee: string; + @Column({ type: 'bigint', default: '0' }) + total_entry_fees_collected: string; + @Column({ type: 'varchar', length: 100, default: 'general' }) category: string; diff --git a/backend/src/migrations/1775900000000-AddTotalEntryFeesCollected.ts b/backend/src/migrations/1775900000000-AddTotalEntryFeesCollected.ts new file mode 100644 index 00000000..8c739df9 --- /dev/null +++ b/backend/src/migrations/1775900000000-AddTotalEntryFeesCollected.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTotalEntryFeesCollected1775900000000 + implements MigrationInterface +{ + name = 'AddTotalEntryFeesCollected1775900000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "creator_events" + ADD COLUMN IF NOT EXISTS "total_entry_fees_collected" BIGINT DEFAULT '0' + `); + + await queryRunner.query(` + UPDATE "creator_events" + SET "total_entry_fees_collected" = 0 + WHERE "total_entry_fees_collected" IS NULL + `); + + await queryRunner.query(` + ALTER TABLE "creator_events" + ALTER COLUMN "total_entry_fees_collected" SET DEFAULT '0', + ALTER COLUMN "total_entry_fees_collected" SET NOT NULL + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "creator_events" + DROP COLUMN IF EXISTS "total_entry_fees_collected" + `); + } +}