From e99ff5975f56e9e6f48979b8f57208de2395ad3d Mon Sep 17 00:00:00 2001 From: Goodnessukaigwe Date: Fri, 27 Mar 2026 02:13:05 +0100 Subject: [PATCH] configure strict stellar validation pipes and decorators - enforce Stellar key regex as ^G[A-Z2-7]{55}$ for 56-char Ed25519 public keys - align Soroban contract regex length to 56-char format consistency - apply IsStellarPublicKey to SubscribeDto.walletAddress (optional field) - keep strict global ValidationPipe (whitelist + forbidNonWhitelisted) - add focused validator unit tests for valid/invalid key cases Closes #393 --- .../is-stellar-key.validator.spec.ts | 49 +++++++++++++++++++ .../validators/is-stellar-key.validator.ts | 4 +- .../src/modules/savings/dto/subscribe.dto.ts | 13 ++++- 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 backend/src/common/validators/is-stellar-key.validator.spec.ts diff --git a/backend/src/common/validators/is-stellar-key.validator.spec.ts b/backend/src/common/validators/is-stellar-key.validator.spec.ts new file mode 100644 index 000000000..733648bd6 --- /dev/null +++ b/backend/src/common/validators/is-stellar-key.validator.spec.ts @@ -0,0 +1,49 @@ +import { validate } from 'class-validator'; +import { IsStellarPublicKey } from './is-stellar-key.validator'; + +class TestDto { + @IsStellarPublicKey() + publicKey: string; +} + +describe('IsStellarPublicKey', () => { + it('accepts valid 56-char Stellar public keys starting with G', async () => { + const dto = new TestDto(); + dto.publicKey = `G${'A'.repeat(55)}`; + + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it('rejects keys with invalid prefix', async () => { + const dto = new TestDto(); + dto.publicKey = `S${'A'.repeat(55)}`; + + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + }); + + it('rejects keys shorter than 56 chars', async () => { + const dto = new TestDto(); + dto.publicKey = `G${'A'.repeat(54)}`; + + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + }); + + it('rejects keys longer than 56 chars', async () => { + const dto = new TestDto(); + dto.publicKey = `G${'A'.repeat(56)}`; + + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + }); + + it('rejects non-base32 characters', async () => { + const dto = new TestDto(); + dto.publicKey = `G${'A'.repeat(54)}!`; + + const errors = await validate(dto); + expect(errors.length).toBeGreaterThan(0); + }); +}); diff --git a/backend/src/common/validators/is-stellar-key.validator.ts b/backend/src/common/validators/is-stellar-key.validator.ts index ce0527210..fe3308582 100644 --- a/backend/src/common/validators/is-stellar-key.validator.ts +++ b/backend/src/common/validators/is-stellar-key.validator.ts @@ -28,7 +28,7 @@ export function IsStellarPublicKey(validationOptions?: ValidationOptions) { } // Stellar public keys: start with G, exactly 56 chars, Base32 (A-Z, 2-7) - const stellarKeyPattern = /^G[A-Z2-7]{54}$/; + const stellarKeyPattern = /^G[A-Z2-7]{55}$/; return stellarKeyPattern.test(value); }, defaultMessage(args: ValidationArguments) { @@ -63,7 +63,7 @@ export function IsSorobanContractId(validationOptions?: ValidationOptions) { } // Soroban contract IDs: start with C, exactly 56 chars, Base32 (A-Z, 2-7) - const sorobanContractPattern = /^C[A-Z2-7]{54}$/; + const sorobanContractPattern = /^C[A-Z2-7]{55}$/; return sorobanContractPattern.test(value); }, defaultMessage(args: ValidationArguments) { diff --git a/backend/src/modules/savings/dto/subscribe.dto.ts b/backend/src/modules/savings/dto/subscribe.dto.ts index c108d2d33..f2f4e318a 100644 --- a/backend/src/modules/savings/dto/subscribe.dto.ts +++ b/backend/src/modules/savings/dto/subscribe.dto.ts @@ -1,5 +1,6 @@ -import { IsUUID, IsNumber, Min } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID, IsNumber, Min, IsOptional } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsStellarPublicKey } from '../../../common/validators/is-stellar-key.validator'; export class SubscribeDto { @ApiProperty({ description: 'Savings product ID to subscribe to' }) @@ -10,4 +11,12 @@ export class SubscribeDto { @IsNumber() @Min(0.01) amount: number; + + @ApiPropertyOptional({ + example: 'GABCDEF234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHJKLMN', + description: 'Optional Stellar wallet address associated with this subscription', + }) + @IsOptional() + @IsStellarPublicKey() + walletAddress?: string; }