diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index 2f242027e..1b5c2ab7f 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -41,9 +41,14 @@ function isDatabaseConnectionError(exception: unknown): exception is Error { return ( DB_CONNECTION_PATTERNS.some((pattern) => pattern.test(message)) || - ['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', '57P01', '08001', '08006'].includes( - code, - ) + [ + 'ECONNREFUSED', + 'ENOTFOUND', + 'ETIMEDOUT', + '57P01', + '08001', + '08006', + ].includes(code) ); } diff --git a/backend/src/common/guards/rpc-throttle.guard.spec.ts b/backend/src/common/guards/rpc-throttle.guard.spec.ts index b0b27c29d..098c28c68 100644 --- a/backend/src/common/guards/rpc-throttle.guard.spec.ts +++ b/backend/src/common/guards/rpc-throttle.guard.spec.ts @@ -1,6 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ExecutionContext, HttpException } from '@nestjs/common'; -import { ThrottlerException, ThrottlerModuleOptions, ThrottlerStorage } from '@nestjs/throttler'; +import { + ThrottlerException, + ThrottlerModuleOptions, + ThrottlerStorage, +} from '@nestjs/throttler'; import { Reflector } from '@nestjs/core'; import { RpcThrottleGuard } from './rpc-throttle.guard'; @@ -32,8 +36,8 @@ describe('RpcThrottleGuard', () => { // Initialize the guard with mocked dependencies guard = new RpcThrottleGuard( mockOptions as any, - mockStorageService as any, - mockReflector as any, + mockStorageService, + mockReflector, ); // Mock response object @@ -110,9 +114,9 @@ describe('RpcThrottleGuard', () => { const limit = 10; const ttl = 60000; // 1 minute - await expect( - guard.onLimitExceeded(context, limit, ttl), - ).rejects.toThrow('Too many RPC requests'); + await expect(guard.onLimitExceeded(context, limit, ttl)).rejects.toThrow( + 'Too many RPC requests', + ); }); it('should set Retry-After header', async () => { diff --git a/backend/src/common/guards/rpc-throttle.guard.ts b/backend/src/common/guards/rpc-throttle.guard.ts index efc631924..7e8aeca13 100644 --- a/backend/src/common/guards/rpc-throttle.guard.ts +++ b/backend/src/common/guards/rpc-throttle.guard.ts @@ -78,11 +78,14 @@ export class RpcThrottleGuard extends ThrottlerGuard { response.setHeader('Retry-After', Math.ceil(ttl / 1000)); response.setHeader('X-RateLimit-Limit', limit); response.setHeader('X-RateLimit-Remaining', 0); - response.setHeader('X-RateLimit-Reset', new Date(Date.now() + ttl).toISOString()); + response.setHeader( + 'X-RateLimit-Reset', + new Date(Date.now() + ttl).toISOString(), + ); // Throw ThrottlerException which results in HTTP 429 throw new ThrottlerException( - `Too many RPC requests. Maximum ${limit} requests per ${Math.round(ttl / 1000)} seconds allowed.` + `Too many RPC requests. Maximum ${limit} requests per ${Math.round(ttl / 1000)} seconds allowed.`, ); } } diff --git a/backend/src/migrations/1775000000000-AlignTransactionsEntity.ts b/backend/src/migrations/1775000000000-AlignTransactionsEntity.ts index fea05a0e1..96bf49ab6 100644 --- a/backend/src/migrations/1775000000000-AlignTransactionsEntity.ts +++ b/backend/src/migrations/1775000000000-AlignTransactionsEntity.ts @@ -1,8 +1,6 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AlignTransactionsEntity1775000000000 - implements MigrationInterface -{ +export class AlignTransactionsEntity1775000000000 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` DO $$ @@ -133,4 +131,4 @@ export class AlignTransactionsEntity1775000000000 $$; `); } -} \ No newline at end of file +} diff --git a/backend/src/migrations/1775100000000-CreateSavingsGoalsTable.ts b/backend/src/migrations/1775100000000-CreateSavingsGoalsTable.ts new file mode 100644 index 000000000..70db2bb9f --- /dev/null +++ b/backend/src/migrations/1775100000000-CreateSavingsGoalsTable.ts @@ -0,0 +1,114 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, + TableIndex, +} from 'typeorm'; + +export class CreateSavingsGoalsTable1775100000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'savings_goals', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'uuid_generate_v4()', + }, + { + name: 'userId', + type: 'uuid', + isNullable: false, + }, + { + name: 'goalName', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'targetAmount', + type: 'decimal', + precision: 14, + scale: 2, + isNullable: false, + }, + { + name: 'targetDate', + type: 'date', + isNullable: false, + }, + { + name: 'status', + type: 'enum', + enum: ['IN_PROGRESS', 'COMPLETED'], + default: "'IN_PROGRESS'", + isNullable: false, + }, + { + name: 'metadata', + type: 'jsonb', + isNullable: true, + }, + { + name: 'createdAt', + type: 'timestamp', + default: 'now()', + }, + { + name: 'updatedAt', + type: 'timestamp', + default: 'now()', + }, + ], + }), + true, + ); + + // Create foreign key to users table + await queryRunner.createForeignKey( + 'savings_goals', + new TableForeignKey({ + columnNames: ['userId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + // Create index on userId for faster lookups + await queryRunner.createIndex( + 'savings_goals', + new TableIndex({ + name: 'IDX_SAVINGS_GOALS_USER_ID', + columnNames: ['userId'], + }), + ); + + // Create index on status for filtering active/completed goals + await queryRunner.createIndex( + 'savings_goals', + new TableIndex({ + name: 'IDX_SAVINGS_GOALS_STATUS', + columnNames: ['status'], + }), + ); + + // Create index on targetDate for date-based queries + await queryRunner.createIndex( + 'savings_goals', + new TableIndex({ + name: 'IDX_SAVINGS_GOALS_TARGET_DATE', + columnNames: ['targetDate'], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('savings_goals'); + } +} diff --git a/backend/src/modules/analytics/analytics.module.ts b/backend/src/modules/analytics/analytics.module.ts index 6123d5ec5..6c9abdf94 100644 --- a/backend/src/modules/analytics/analytics.module.ts +++ b/backend/src/modules/analytics/analytics.module.ts @@ -13,4 +13,4 @@ import { LedgerTransaction } from '../blockchain/entities/transaction.entity'; providers: [AnalyticsService], exports: [AnalyticsService], }) -export class AnalyticsModule {} \ No newline at end of file +export class AnalyticsModule {} diff --git a/backend/src/modules/blockchain/indexer.service.ts b/backend/src/modules/blockchain/indexer.service.ts index d5b157543..604c29890 100644 --- a/backend/src/modules/blockchain/indexer.service.ts +++ b/backend/src/modules/blockchain/indexer.service.ts @@ -80,7 +80,11 @@ export class IndexerService implements OnModuleInit { for (const event of events) { const ok = await this.processEvent(event); - ok ? processed++ : failed++; + if (ok) { + processed++; + } else { + failed++; + } } this.indexerState.totalEventsProcessed += processed; @@ -200,4 +204,4 @@ export class IndexerService implements OnModuleInit { getMonitoredContracts(): string[] { return Array.from(this.contractIds); } -} \ No newline at end of file +} diff --git a/backend/src/modules/governance/dto/voting-power-response.dto.ts b/backend/src/modules/governance/dto/voting-power-response.dto.ts index 207a9537f..fc53a8afd 100644 --- a/backend/src/modules/governance/dto/voting-power-response.dto.ts +++ b/backend/src/modules/governance/dto/voting-power-response.dto.ts @@ -2,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; export class VotingPowerResponseDto { @ApiProperty({ - description: 'The user\'s voting power as a formatted string', + description: "The user's voting power as a formatted string", example: '12,500 NST', }) votingPower: string; -} \ No newline at end of file +} diff --git a/backend/src/modules/governance/governance.service.spec.ts b/backend/src/modules/governance/governance.service.spec.ts index 403f9a7c9..6d3443575 100644 --- a/backend/src/modules/governance/governance.service.spec.ts +++ b/backend/src/modules/governance/governance.service.spec.ts @@ -108,4 +108,4 @@ describe('GovernanceService', () => { ); }); }); -}); \ No newline at end of file +}); diff --git a/backend/src/modules/governance/governance.service.ts b/backend/src/modules/governance/governance.service.ts index 11c224780..3269c1aa8 100644 --- a/backend/src/modules/governance/governance.service.ts +++ b/backend/src/modules/governance/governance.service.ts @@ -46,4 +46,4 @@ export class GovernanceService { }); return { votingPower: `${votingPower} NST` }; } -} \ No newline at end of file +} diff --git a/backend/src/modules/savings/dto/subscribe.dto.ts b/backend/src/modules/savings/dto/subscribe.dto.ts index f2f4e318a..7c3d1bb15 100644 --- a/backend/src/modules/savings/dto/subscribe.dto.ts +++ b/backend/src/modules/savings/dto/subscribe.dto.ts @@ -14,7 +14,8 @@ export class SubscribeDto { @ApiPropertyOptional({ example: 'GABCDEF234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHJKLMN', - description: 'Optional Stellar wallet address associated with this subscription', + description: + 'Optional Stellar wallet address associated with this subscription', }) @IsOptional() @IsStellarPublicKey() diff --git a/backend/src/modules/transactions/entities/transaction.entity.ts b/backend/src/modules/transactions/entities/transaction.entity.ts index 57e3514fd..901def7e4 100644 --- a/backend/src/modules/transactions/entities/transaction.entity.ts +++ b/backend/src/modules/transactions/entities/transaction.entity.ts @@ -76,4 +76,4 @@ export class Transaction extends BaseEntity { set transactionHash(value: string | null | undefined) { this.txHash = value; } -} \ No newline at end of file +} diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index 014ed5486..056afba18 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -73,7 +73,7 @@ export class UserService { async create(data: Partial) { const newEntity = this.userRepository.create(data); - + try { const savedUser = await this.userRepository.save(newEntity); // Return with only selected fields to match old behavior @@ -86,9 +86,13 @@ export class UserService { if (error.detail?.includes('email')) { throw new ConflictException('Email already exists'); } else if (error.detail?.includes('walletAddress')) { - throw new ConflictException('This wallet address is already linked to another account'); + throw new ConflictException( + 'This wallet address is already linked to another account', + ); } else if (error.detail?.includes('publicKey')) { - throw new ConflictException('This public key is already linked to another account'); + throw new ConflictException( + 'This public key is already linked to another account', + ); } throw new ConflictException('This record already exists'); } diff --git a/backend/test/throttling.e2e-spec.ts b/backend/test/throttling.e2e-spec.ts index 76d807d13..5cbbeb596 100644 --- a/backend/test/throttling.e2e-spec.ts +++ b/backend/test/throttling.e2e-spec.ts @@ -36,7 +36,8 @@ describe('Throttler Guard (e2e)', () => { * Expected: 6th request should return 429 Too Many Requests */ it('should return 429 after exceeding auth rate limit (5 req/15min)', async () => { - const authNonceUrl = '/auth/nonce?publicKey=GBUQWP3BOUZX34ULNQG23RQ6F4BFXEUVS2YB5YKTVQ63XVXVYXSX'; + const authNonceUrl = + '/auth/nonce?publicKey=GBUQWP3BOUZX34ULNQG23RQ6F4BFXEUVS2YB5YKTVQ63XVXVYXSX'; // Make 5 requests (within limit) for (let i = 0; i < 5; i++) { @@ -101,7 +102,9 @@ describe('Throttler Guard (e2e)', () => { } // All responses should be 2xx, no 429 errors - const allSuccess = responses.every((status) => status >= 200 && status < 300); + const allSuccess = responses.every( + (status) => status >= 200 && status < 300, + ); expect(allSuccess).toBe(true); }); });