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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('CreatorEventsService predictions and stats', () => {
| 'getPredictionDistribution'
>
>;
let creatorEventRepository: { createQueryBuilder: jest.Mock; findOne: jest.Mock };

const mockEvent = {
eventId: '1',
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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);
Expand All @@ -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 () => {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/creator-events/creator-events.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ describe('CreatorEventsController', () => {
winnerCount: 0,
averagePredictionsPerUser: 4,
completionRate: 60,
prizePool: '0',
totalEntryFeesCollected: '0',
};

service.getEventStats.mockResolvedValue(mockStats);
Expand Down
8 changes: 7 additions & 1 deletion backend/src/creator-events/creator-events.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
};
}

Expand Down
12 changes: 12 additions & 0 deletions backend/src/creator-events/dto/event-stats-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
7 changes: 7 additions & 0 deletions backend/src/creator-events/entities/creator-event.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
73 changes: 73 additions & 0 deletions backend/src/indexer/indexer.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
(event: unknown) => event as CreatorEvent,
);
(creatorEventRepository.save as jest.Mock).mockImplementation(
async (event: unknown) => event as CreatorEvent,

Check warning on line 242 in backend/src/indexer/indexer.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Async arrow function has no 'await' expression
);
});

Expand All @@ -266,7 +266,7 @@
});
const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ result: { events: [], latestLedger: 100 } }),

Check warning on line 269 in backend/src/indexer/indexer.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Async method 'json' has no 'await' expression
} as unknown as Response);

try {
Expand Down Expand Up @@ -516,6 +516,79 @@
});
});

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 = {
Expand Down
12 changes: 12 additions & 0 deletions backend/src/indexer/indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions backend/src/matches/entities/creator-event.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
33 changes: 33 additions & 0 deletions backend/src/migrations/1775900000000-AddTotalEntryFeesCollected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddTotalEntryFeesCollected1775900000000
implements MigrationInterface
{
name = 'AddTotalEntryFeesCollected1775900000000';

public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`
ALTER TABLE "creator_events"
DROP COLUMN IF EXISTS "total_entry_fees_collected"
`);
}
}
Loading