Skip to content

Commit 0d5dd27

Browse files
Merge branch 'main' into feat/BE-020-notification-api
2 parents 4397b8c + c87f8f5 commit 0d5dd27

9 files changed

Lines changed: 1022 additions & 3 deletions

src/app.module.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ import { MetricsModule } from './metrics/metrics.module';
3535
import { NotificationsModule } from './notifications/notifications.module';
3636
import { Notification } from './notifications/entities/notification.entity';
3737
import { NotificationPreference } from './notifications/entities/notification-preference.entity';
38+
import { GovernanceModule } from './governance/governance.module';
3839

3940
// In-memory storage for development (no Redis needed)
4041
class ThrottlerMemoryStorage {
41-
private storage = new Map<
42+
private storage = new Map
4243
string,
4344
{
4445
totalHits: number;
@@ -287,6 +288,7 @@ async function createThrottlerStorage(configService: ConfigService): Promise<any
287288
ThemeModule,
288289
MetricsModule,
289290
NotificationsModule,
291+
GovernanceModule,
290292
],
291293
controllers: [AppController],
292294
providers: [
@@ -309,5 +311,4 @@ async function createThrottlerStorage(configService: ConfigService): Promise<any
309311
},
310312
],
311313
})
312-
export class AppModule { }
313-
314+
export class AppModule { }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import {
3+
IsBoolean,
4+
IsNotEmpty,
5+
IsNumber,
6+
IsObject,
7+
IsOptional,
8+
IsString,
9+
Max,
10+
Min,
11+
} from 'class-validator';
12+
13+
export class CastVoteDto {
14+
@ApiProperty({ description: 'Voter wallet address' })
15+
@IsString()
16+
@IsNotEmpty()
17+
voter: string;
18+
19+
@ApiProperty({ description: 'Whether the voter supports the proposal' })
20+
@IsBoolean()
21+
support: boolean;
22+
23+
@ApiPropertyOptional({
24+
description: 'Voting weight',
25+
minimum: 0,
26+
maximum: 1000000,
27+
})
28+
@IsOptional()
29+
@IsNumber()
30+
@Min(0)
31+
@Max(1000000)
32+
weight?: number;
33+
34+
@ApiPropertyOptional({ description: 'Additional metadata' })
35+
@IsOptional()
36+
@IsObject()
37+
metadata?: Record<string, unknown>;
38+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import {
3+
IsEnum,
4+
IsNotEmpty,
5+
IsNumber,
6+
IsObject,
7+
IsOptional,
8+
IsString,
9+
Max,
10+
Min,
11+
} from 'class-validator';
12+
import { ProposalCategory } from '../entities/proposal.entity';
13+
14+
export class CreateProposalDto {
15+
@ApiProperty({ description: 'Proposal title' })
16+
@IsString()
17+
@IsNotEmpty()
18+
title: string;
19+
20+
@ApiProperty({ description: 'Proposal description' })
21+
@IsString()
22+
@IsNotEmpty()
23+
description: string;
24+
25+
@ApiProperty({ description: 'Proposer wallet address' })
26+
@IsString()
27+
@IsNotEmpty()
28+
proposer: string;
29+
30+
@ApiPropertyOptional({
31+
enum: ProposalCategory,
32+
description: 'Proposal category',
33+
})
34+
@IsOptional()
35+
@IsEnum(ProposalCategory)
36+
category?: ProposalCategory;
37+
38+
@ApiPropertyOptional({
39+
description: 'Blockchain transaction hash',
40+
})
41+
@IsOptional()
42+
@IsString()
43+
blockchainTxHash?: string;
44+
45+
@ApiPropertyOptional({ description: 'Additional metadata' })
46+
@IsOptional()
47+
@IsObject()
48+
metadata?: Record<string, unknown>;
49+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
UpdateDateColumn,
7+
Index,
8+
} from 'typeorm';
9+
10+
export enum ProposalStatus {
11+
ACTIVE = 'ACTIVE',
12+
PASSED = 'PASSED',
13+
REJECTED = 'REJECTED',
14+
EXECUTED = 'EXECUTED',
15+
CANCELLED = 'CANCELLED',
16+
PENDING = 'PENDING',
17+
}
18+
19+
export enum ProposalCategory {
20+
PROTOCOL_UPGRADE = 'PROTOCOL_UPGRADE',
21+
TREASURY = 'TREASURY',
22+
PARAMETER_CHANGE = 'PARAMETER_CHANGE',
23+
GOVERNANCE = 'GOVERNANCE',
24+
COMMUNITY = 'COMMUNITY',
25+
}
26+
27+
@Entity('proposals')
28+
@Index(['status'])
29+
@Index(['category'])
30+
@Index(['proposer'])
31+
@Index(['createdAt'])
32+
@Index(['status', 'category'])
33+
export class Proposal {
34+
@PrimaryGeneratedColumn('uuid')
35+
id: string;
36+
37+
@Column({ type: 'varchar', length: 500 })
38+
title: string;
39+
40+
@Column({ type: 'text' })
41+
description: string;
42+
43+
@Column({ type: 'varchar' })
44+
proposer: string;
45+
46+
@Column({
47+
type: 'varchar',
48+
default: ProposalStatus.PENDING,
49+
})
50+
status: ProposalStatus;
51+
52+
@Column({
53+
type: 'varchar',
54+
default: ProposalCategory.PROTOCOL_UPGRADE,
55+
})
56+
category: ProposalCategory;
57+
58+
@Column({ type: 'varchar', nullable: true })
59+
blockchainTxHash: string | null;
60+
61+
@Column({ type: 'int', default: 0 })
62+
totalVotes: number;
63+
64+
@Column({ type: 'int', default: 0 })
65+
votesFor: number;
66+
67+
@Column({ type: 'int', default: 0 })
68+
votesAgainst: number;
69+
70+
@Column({ type: 'decimal', precision: 5, scale: 4, default: 0 })
71+
participationRate: number;
72+
73+
@Column({ type: 'decimal', precision: 5, scale: 4, default: 0 })
74+
quorumProgress: number;
75+
76+
@Column({ type: 'datetime', nullable: true })
77+
votingStartsAt: Date | null;
78+
79+
@Column({ type: 'datetime', nullable: true })
80+
votingEndsAt: Date | null;
81+
82+
@Column({ type: 'datetime', nullable: true })
83+
executedAt: Date | null;
84+
85+
@Column({ type: 'simple-json', default: {} })
86+
metadata: Record<string, any>;
87+
88+
@CreateDateColumn()
89+
createdAt: Date;
90+
91+
@UpdateDateColumn()
92+
updatedAt: Date;
93+
}
94+
95+
@Entity('votes')
96+
@Index(['proposalId'])
97+
@Index(['voter'])
98+
@Index(['proposalId', 'voter'], { unique: true })
99+
export class Vote {
100+
@PrimaryGeneratedColumn('uuid')
101+
id: string;
102+
103+
@Column()
104+
proposalId: string;
105+
106+
@Column()
107+
voter: string;
108+
109+
@Column({ type: 'boolean' })
110+
support: boolean;
111+
112+
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
113+
weight: number;
114+
115+
@Column({ type: 'simple-json', default: {} })
116+
metadata: Record<string, any>;
117+
118+
@CreateDateColumn()
119+
createdAt: Date;
120+
}

src/governance/governance.cache.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { RedisService } from '../redis/redis.service';
3+
import { ConfigService } from '@nestjs/config';
4+
5+
@Injectable()
6+
export class GovernanceCache {
7+
private readonly logger = new Logger(GovernanceCache.name);
8+
private readonly ttl: number;
9+
10+
constructor(
11+
private readonly redisService: RedisService,
12+
private readonly configService: ConfigService,
13+
) {
14+
this.ttl = this.configService.get<number>('CACHE_GOVERNANCE_TTL', 3600);
15+
}
16+
17+
private getProposalKey(id: string): string {
18+
return `governance:proposal:${id}`;
19+
}
20+
21+
private getActiveProposalsKey(): string {
22+
return 'governance:proposals:active';
23+
}
24+
25+
private getAllProposalsKey(): string {
26+
return 'governance:proposals:all';
27+
}
28+
29+
private getStatsKey(): string {
30+
return 'governance:stats';
31+
}
32+
33+
private getVotesKey(proposalId: string): string {
34+
return `governance:votes:${proposalId}`;
35+
}
36+
37+
async getProposal(id: string): Promise<any | null> {
38+
const data = await this.redisService.get(this.getProposalKey(id));
39+
if (data) {
40+
this.logger.debug(`Cache hit for governance proposal:${id}`);
41+
try {
42+
return JSON.parse(data);
43+
} catch {
44+
return null;
45+
}
46+
}
47+
this.logger.debug(`Cache miss for governance proposal:${id}`);
48+
return null;
49+
}
50+
51+
async setProposal(id: string, proposal: any): Promise<void> {
52+
await this.redisService.set(
53+
this.getProposalKey(id),
54+
JSON.stringify(proposal),
55+
this.ttl,
56+
);
57+
}
58+
59+
async getActiveProposals(): Promise<any[] | null> {
60+
const data = await this.redisService.get(this.getActiveProposalsKey());
61+
if (data) {
62+
this.logger.debug('Cache hit for governance:proposals:active');
63+
try {
64+
return JSON.parse(data);
65+
} catch {
66+
return null;
67+
}
68+
}
69+
return null;
70+
}
71+
72+
async setActiveProposals(proposals: any[]): Promise<void> {
73+
await this.redisService.set(
74+
this.getActiveProposalsKey(),
75+
JSON.stringify(proposals),
76+
this.ttl,
77+
);
78+
}
79+
80+
async getStats(): Promise<any | null> {
81+
const data = await this.redisService.get(this.getStatsKey());
82+
if (data) {
83+
this.logger.debug('Cache hit for governance:stats');
84+
try {
85+
return JSON.parse(data);
86+
} catch {
87+
return null;
88+
}
89+
}
90+
return null;
91+
}
92+
93+
async setStats(stats: any): Promise<void> {
94+
await this.redisService.set(
95+
this.getStatsKey(),
96+
JSON.stringify(stats),
97+
this.ttl,
98+
);
99+
}
100+
101+
async getVotes(proposalId: string): Promise<any[] | null> {
102+
const data = await this.redisService.get(this.getVotesKey(proposalId));
103+
if (data) {
104+
this.logger.debug(`Cache hit for governance:votes:${proposalId}`);
105+
try {
106+
return JSON.parse(data);
107+
} catch {
108+
return null;
109+
}
110+
}
111+
return null;
112+
}
113+
114+
async setVotes(proposalId: string, votes: any[]): Promise<void> {
115+
await this.redisService.set(
116+
this.getVotesKey(proposalId),
117+
JSON.stringify(votes),
118+
this.ttl,
119+
);
120+
}
121+
122+
async invalidateProposal(id: string): Promise<void> {
123+
const promises = [
124+
this.redisService.del(this.getProposalKey(id)),
125+
this.redisService.del(this.getActiveProposalsKey()),
126+
this.redisService.del(this.getAllProposalsKey()),
127+
this.redisService.del(this.getStatsKey()),
128+
this.redisService.del(this.getVotesKey(id)),
129+
];
130+
await Promise.all(promises);
131+
this.logger.debug(`Invalidated cache for governance proposal:${id}`);
132+
}
133+
134+
async invalidateAll(): Promise<void> {
135+
const promises = [
136+
this.redisService.del(this.getActiveProposalsKey()),
137+
this.redisService.del(this.getAllProposalsKey()),
138+
this.redisService.del(this.getStatsKey()),
139+
];
140+
await Promise.all(promises);
141+
this.logger.debug('Invalidated all governance caches');
142+
}
143+
}

0 commit comments

Comments
 (0)