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
11 changes: 8 additions & 3 deletions backend/src/common/filters/http-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

Expand Down
16 changes: 10 additions & 6 deletions backend/src/common/guards/rpc-throttle.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 () => {
Expand Down
7 changes: 5 additions & 2 deletions backend/src/common/guards/rpc-throttle.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
);
}
}
Original file line number Diff line number Diff line change
@@ -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<void> {
await queryRunner.query(`
DO $$
Expand Down Expand Up @@ -133,4 +131,4 @@ export class AlignTransactionsEntity1775000000000
$$;
`);
}
}
}
114 changes: 114 additions & 0 deletions backend/src/migrations/1775100000000-CreateSavingsGoalsTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
TableIndex,
} from 'typeorm';

export class CreateSavingsGoalsTable1775100000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.dropTable('savings_goals');
}
}
2 changes: 1 addition & 1 deletion backend/src/modules/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ import { LedgerTransaction } from '../blockchain/entities/transaction.entity';
providers: [AnalyticsService],
exports: [AnalyticsService],
})
export class AnalyticsModule {}
export class AnalyticsModule {}
8 changes: 6 additions & 2 deletions backend/src/modules/blockchain/indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -200,4 +204,4 @@ export class IndexerService implements OnModuleInit {
getMonitoredContracts(): string[] {
return Array.from(this.contractIds);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion backend/src/modules/governance/governance.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ describe('GovernanceService', () => {
);
});
});
});
});
2 changes: 1 addition & 1 deletion backend/src/modules/governance/governance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ export class GovernanceService {
});
return { votingPower: `${votingPower} NST` };
}
}
}
3 changes: 2 additions & 1 deletion backend/src/modules/savings/dto/subscribe.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,4 @@ export class Transaction extends BaseEntity {
set transactionHash(value: string | null | undefined) {
this.txHash = value;
}
}
}
10 changes: 7 additions & 3 deletions backend/src/modules/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class UserService {

async create(data: Partial<User>) {
const newEntity = this.userRepository.create(data);

try {
const savedUser = await this.userRepository.save(newEntity);
// Return with only selected fields to match old behavior
Expand All @@ -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');
}
Expand Down
7 changes: 5 additions & 2 deletions backend/test/throttling.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -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);
});
});
Expand Down
Loading