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
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { LoggingInterceptor } from './logger/logging.interceptor';
import { AuthModule } from './auth/auth.module';
import { GlobalAuthGuard } from './auth/global-auth.guard';
import { MetricsModule } from './metrics/metrics.module';
import { ReputationModule } from './reputation/reputation.module';
import { GovernanceModule } from './governance/governance.module';

// In-memory storage for development (no Redis needed)
Expand Down Expand Up @@ -284,6 +285,7 @@ async function createThrottlerStorage(configService: ConfigService): Promise<any
AuditModule,
ThemeModule,
MetricsModule,
ReputationModule,
GovernanceModule,
],
controllers: [AppController],
Expand Down
40 changes: 40 additions & 0 deletions src/reputation/dto/query-reputation.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import {
IsNumber,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';

export class QueryReputationDto {
@ApiPropertyOptional({ description: 'Wallet address to query' })
@IsOptional()
@IsString()
walletAddress?: string;

@ApiPropertyOptional({ description: 'Minimum reputation score', minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
minScore?: number;

@ApiPropertyOptional({ description: 'Maximum reputation score', maximum: 10000 })
@IsOptional()
@IsNumber()
@Max(10000)
maxScore?: number;

@ApiPropertyOptional({ description: 'Page number', minimum: 1 })
@IsOptional()
@IsNumber()
@Min(1)
page?: number;

@ApiPropertyOptional({ description: 'Page size', minimum: 1, maximum: 100 })
@IsOptional()
@IsNumber()
@Min(1)
@Max(100)
pageSize?: number;
}
91 changes: 91 additions & 0 deletions src/reputation/entities/reputation.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';

export enum ReputationEventType {
VERIFICATION = 'VERIFICATION',
DISPUTE = 'DISPUTE',
REWARD = 'REWARD',
GOVERNANCE = 'GOVERNANCE',
STAKING = 'STAKING',
MILESTONE = 'MILESTONE',
}

@Entity('reputation_records')
@Index(['walletAddress'])
@Index(['score'])
@Index(['createdAt'])
@Index(['walletAddress', 'createdAt'])
export class ReputationRecord {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar' })
walletAddress: string;

@Column({ type: 'int', default: 0 })
score: number;

@Column({ type: 'int', default: 0 })
verificationCount: number;

@Column({ type: 'int', default: 0 })
disputeCount: number;

@Column({ type: 'int', default: 0 })
rewardTotal: number;

@Column({ type: 'int', default: 0 })
governanceParticipation: number;

@Column({ type: 'int', default: 0 })
stakingAmount: number;

@Column({ type: 'simple-json', default: {} })
metadata: Record<string, any>;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}

@Entity('reputation_events')
@Index(['walletAddress'])
@Index(['eventType'])
@Index(['walletAddress', 'eventType'])
@Index(['walletAddress', 'createdAt'])
export class ReputationEvent {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar' })
walletAddress: string;

@Column({
type: 'varchar',
default: ReputationEventType.VERIFICATION,
})
eventType: ReputationEventType;

@Column({ type: 'int', default: 0 })
scoreChange: number;

@Column({ type: 'int', default: 0 })
scoreAfter: number;

@Column({ type: 'text', nullable: true })
description: string | null;

@Column({ type: 'simple-json', default: {} })
metadata: Record<string, any>;

@CreateDateColumn()
createdAt: Date;
}
129 changes: 129 additions & 0 deletions src/reputation/reputation.cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Injectable, Logger } from '@nestjs/common';
import { RedisService } from '../redis/redis.service';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class ReputationCache {
private readonly logger = new Logger(ReputationCache.name);
private readonly ttl: number;

constructor(
private readonly redisService: RedisService,
private readonly configService: ConfigService,
) {
this.ttl = this.configService.get<number>('CACHE_REPUTATION_TTL', 3600);
}

private getUserKey(wallet: string): string {
return `reputation:user:${wallet.toLowerCase()}`;
}

private getLeaderboardKey(type: string): string {
return `reputation:leaderboard:${type}`;
}

private getEventsKey(wallet: string): string {
return `reputation:events:${wallet.toLowerCase()}`;
}

private getStatsKey(): string {
return 'reputation:stats';
}

async getUserReputation(wallet: string): Promise<any | null> {
const data = await this.redisService.get(this.getUserKey(wallet));
if (data) {
this.logger.debug(`Cache hit for reputation:user:${wallet}`);
try {
return JSON.parse(data);
} catch {
return null;
}
}
return null;
}

async setUserReputation(wallet: string, reputation: any): Promise<void> {
await this.redisService.set(
this.getUserKey(wallet),
JSON.stringify(reputation),
this.ttl,
);
}

async getLeaderboard(type: string): Promise<any[] | null> {
const data = await this.redisService.get(this.getLeaderboardKey(type));
if (data) {
this.logger.debug(`Cache hit for reputation:leaderboard:${type}`);
try {
return JSON.parse(data);
} catch {
return null;
}
}
return null;
}

async setLeaderboard(type: string, leaderboard: any[]): Promise<void> {
await this.redisService.set(
this.getLeaderboardKey(type),
JSON.stringify(leaderboard),
this.ttl,
);
}

async getEvents(wallet: string): Promise<any[] | null> {
const data = await this.redisService.get(this.getEventsKey(wallet));
if (data) {
this.logger.debug(`Cache hit for reputation:events:${wallet}`);
try {
return JSON.parse(data);
} catch {
return null;
}
}
return null;
}

async setEvents(wallet: string, events: any[]): Promise<void> {
await this.redisService.set(
this.getEventsKey(wallet),
JSON.stringify(events),
this.ttl,
);
}

async getStats(): Promise<any | null> {
const data = await this.redisService.get(this.getStatsKey());
if (data) {
this.logger.debug('Cache hit for reputation:stats');
try {
return JSON.parse(data);
} catch {
return null;
}
}
return null;
}

async setStats(stats: any): Promise<void> {
await this.redisService.set(
this.getStatsKey(),
JSON.stringify(stats),
this.ttl,
);
}

async invalidateUser(wallet: string): Promise<void> {
const promises = [
this.redisService.del(this.getUserKey(wallet)),
this.redisService.del(this.getEventsKey(wallet)),
this.redisService.del(this.getLeaderboardKey('highest')),
this.redisService.del(this.getLeaderboardKey('fastest_growing')),
this.redisService.del(this.getLeaderboardKey('most_active')),
this.redisService.del(this.getStatsKey()),
];
await Promise.all(promises);
this.logger.debug(`Invalidated cache for reputation:user:${wallet}`);
}
}
Loading