From 798521bac10145fe43d801cd7176f4a63defd68a Mon Sep 17 00:00:00 2001 From: Shodipo Micheal Date: Wed, 21 Oct 2026 10:26:07 +0100 Subject: [PATCH 1/6] feat(validation): add Stellar asset code schema --- src/common/validators/stellar-asset.schema.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/common/validators/stellar-asset.schema.ts diff --git a/src/common/validators/stellar-asset.schema.ts b/src/common/validators/stellar-asset.schema.ts new file mode 100644 index 00000000..ebad1133 --- /dev/null +++ b/src/common/validators/stellar-asset.schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +/** + * Stellar asset codes are 1–12 ASCII alphanumeric characters. The native asset + * is represented by XLM; issued assets use the same code constraints and carry + * their issuer separately in the Stellar SDK layer. + */ +export const stellarAssetCodeSchema = z + .string({ invalid_type_error: 'Asset code must be a string' }) + .min(1, 'Asset code is required') + .max(12, 'Asset code must be at most 12 characters') + .regex(/^[a-zA-Z0-9]+$/, 'Asset code must contain only ASCII letters and digits'); + +export type StellarAssetCode = z.infer; From f46334f4469b998ef9d6afae9402ff04d2331897 Mon Sep 17 00:00:00 2001 From: Shodipo Micheal Date: Wed, 21 Oct 2026 10:26:09 +0100 Subject: [PATCH 2/6] feat(validation): export Stellar asset schema --- src/common/validators/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/validators/index.ts b/src/common/validators/index.ts index 8d84142a..2d656f0e 100644 --- a/src/common/validators/index.ts +++ b/src/common/validators/index.ts @@ -4,3 +4,4 @@ export * from './stellar-memo.pipe'; export * from './stellar-address.schema'; export * from './stellar-address.pipe'; export * from './zod-error'; +export * from './stellar-asset.schema'; From a1c9e42f2009680b707f29f346c05c4c64ba8053 Mon Sep 17 00:00:00 2001 From: Shodipo Micheal Date: Wed, 21 Oct 2026 10:26:12 +0100 Subject: [PATCH 3/6] fix(validation): validate Stellar assets and payment destinations --- src/modules/transactions/transaction.dto.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/transactions/transaction.dto.ts b/src/modules/transactions/transaction.dto.ts index 12555d8b..9ccd4b2d 100644 --- a/src/modules/transactions/transaction.dto.ts +++ b/src/modules/transactions/transaction.dto.ts @@ -1,7 +1,8 @@ import { z } from 'zod'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { stellarMemoTypeSchema } from '../../common/validators/stellar-memo.schema'; -import { stellarAddressSchema } from '../../common/validators/stellar-address.schema'; +import { stellarPublicKeySchema } from '../../common/validators/stellar-address.schema'; +import { stellarAssetCodeSchema } from '../../common/validators/stellar-asset.schema'; import { ValidationException } from '../../common/exceptions/domain.exception'; const idempotencyKeyPattern = /^[A-Za-z0-9._~-]{1,128}$/; @@ -39,9 +40,9 @@ export const createTransactionSchema = z walletId: z.string().uuid(), agentId: z.string().uuid().optional(), budgetId: z.string().uuid().optional(), - asset: z.string().min(1).max(24).default('XLM'), + asset: stellarAssetCodeSchema.default('XLM'), amount: amountString, - recipientAddress: stellarAddressSchema, + recipientAddress: stellarPublicKeySchema, memo: z.string().max(128).optional(), memoType: stellarMemoTypeSchema.optional(), memoValue: z.string().optional(), From d1688cfb48fd0b3445d18cb360e95902e6fa8810 Mon Sep 17 00:00:00 2001 From: Shodipo Micheal Date: Wed, 21 Oct 2026 10:26:14 +0100 Subject: [PATCH 4/6] test(validation): cover Stellar transaction payload edge cases --- .../stellar-transaction-validation.spec.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/modules/transactions/stellar-transaction-validation.spec.ts diff --git a/src/modules/transactions/stellar-transaction-validation.spec.ts b/src/modules/transactions/stellar-transaction-validation.spec.ts new file mode 100644 index 00000000..236747ed --- /dev/null +++ b/src/modules/transactions/stellar-transaction-validation.spec.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest'; +import { ArgumentMetadata, BadRequestException } from '@nestjs/common'; +import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe'; +import { stellarAssetCodeSchema } from '../../common/validators/stellar-asset.schema'; +import { createTransactionSchema } from './transaction.dto'; + +const metadata: ArgumentMetadata = { type: 'body', metatype: Object }; +const basePayload = { + walletId: '550e8400-e29b-41d4-a716-446655440000', + amount: '10.0000000', + recipientAddress: 'GDEGSXLGANKHK7QFOV63XCBHBTZ3YRKUJV7ZB7JMSJQB5CNBRLL5QIG5', +}; + +describe('Stellar transaction payload validation', () => { + it.each(['XLM', 'USDC', 'USD', 'A1', '123456789012'])( + 'accepts valid Stellar asset code %s', + (asset) => { + expect(stellarAssetCodeSchema.parse(asset)).toBe(asset); + }, + ); + + it.each([ + ['', 'Asset code is required'], + ['ASSET-WITH-DASH', 'Asset code must contain only ASCII letters and digits'], + ['ASSET_WITH_UNDERSCORE', 'Asset code must be at most 12 characters'], + ['TOOLONGASSET123', 'Asset code must be at most 12 characters'], + ['USD$', 'Asset code must contain only ASCII letters and digits'], + ])('rejects invalid asset code %s with a useful validation message', (asset, message) => { + const result = stellarAssetCodeSchema.safeParse(asset); + expect(result.success).toBe(false); + if (!result.success) expect(result.error.issues[0].message).toBe(message); + }); + + it('rejects malformed G... public keys and invalid checksums', () => { + const pipe = new ZodValidationPipe(createTransactionSchema); + for (const recipientAddress of [ + 'G-not-a-valid-key', + 'gdegsxlgankhk7qfov63xcbhbtz3yrkujv7zb7jmsjqb5cnbrll5qig5', + 'GDEGSXLGANKHK7QFOV63XCBHBTZ3YRKUJV7ZB7JMSJQB5CNBRLL5QIG4', + 'CDEGSXLGANKHK7QFOV63XCBHBTZ3YRKUJV7ZB7JMSJQB5CNBRLL5QIG5', + ]) { + expect(() => pipe.transform({ ...basePayload, recipientAddress }, metadata)).toThrow( + BadRequestException, + ); + } + }); + + it('returns HTTP 400 with field-level details for invalid Stellar transfer payloads', () => { + const pipe = new ZodValidationPipe(createTransactionSchema); + try { + pipe.transform({ ...basePayload, asset: 'USD$', amount: '-1' }, metadata); + expect.fail('Expected validation to fail'); + } catch (error) { + expect(error).toBeInstanceOf(BadRequestException); + const exception = error as BadRequestException; + expect(exception.getStatus()).toBe(400); + expect(exception.getResponse()).toMatchObject({ + code: 'VALIDATION_ERROR', + details: expect.arrayContaining([ + expect.objectContaining({ path: 'asset' }), + expect.objectContaining({ path: 'amount' }), + ]), + }); + } + }); + + it('rejects non-positive and over-precision amounts', () => { + const pipe = new ZodValidationPipe(createTransactionSchema); + for (const amount of ['0', '-0.01', '1.12345678', 'NaN', 'Infinity']) { + expect(() => pipe.transform({ ...basePayload, amount }, metadata)).toThrow( + BadRequestException, + ); + } + }); + + it('parses a valid transfer and applies the default XLM asset', () => { + const pipe = new ZodValidationPipe(createTransactionSchema); + expect(pipe.transform(basePayload, metadata)).toMatchObject({ + asset: 'XLM', + amount: '10.0000000', + recipientAddress: basePayload.recipientAddress, + }); + }); +}); From 2d5d8164538b774006b99b69d6b53baeddea7c03 Mon Sep 17 00:00:00 2001 From: Shodipo Micheal Date: Wed, 21 Oct 2026 11:05:21 +0100 Subject: [PATCH 5/6] fix(validation): prioritize invalid asset characters --- src/common/validators/stellar-asset.schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/validators/stellar-asset.schema.ts b/src/common/validators/stellar-asset.schema.ts index ebad1133..3b5a45a5 100644 --- a/src/common/validators/stellar-asset.schema.ts +++ b/src/common/validators/stellar-asset.schema.ts @@ -8,7 +8,7 @@ import { z } from 'zod'; export const stellarAssetCodeSchema = z .string({ invalid_type_error: 'Asset code must be a string' }) .min(1, 'Asset code is required') - .max(12, 'Asset code must be at most 12 characters') - .regex(/^[a-zA-Z0-9]+$/, 'Asset code must contain only ASCII letters and digits'); + .regex(/^[a-zA-Z0-9]+$/, 'Asset code must contain only ASCII letters and digits') + .max(12, 'Asset code must be at most 12 characters'); export type StellarAssetCode = z.infer; From 4c5c8cc34b32c5858a28921c31d9a2d92d5c3821 Mon Sep 17 00:00:00 2001 From: Shodipo Micheal Date: Wed, 21 Oct 2026 11:05:29 +0100 Subject: [PATCH 6/6] test(validation): expect character error for invalid asset code --- src/modules/transactions/stellar-transaction-validation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/transactions/stellar-transaction-validation.spec.ts b/src/modules/transactions/stellar-transaction-validation.spec.ts index 236747ed..e84e6c2b 100644 --- a/src/modules/transactions/stellar-transaction-validation.spec.ts +++ b/src/modules/transactions/stellar-transaction-validation.spec.ts @@ -22,7 +22,7 @@ describe('Stellar transaction payload validation', () => { it.each([ ['', 'Asset code is required'], ['ASSET-WITH-DASH', 'Asset code must contain only ASCII letters and digits'], - ['ASSET_WITH_UNDERSCORE', 'Asset code must be at most 12 characters'], + ['ASSET_WITH_UNDERSCORE', 'Asset code must contain only ASCII letters and digits'], ['TOOLONGASSET123', 'Asset code must be at most 12 characters'], ['USD$', 'Asset code must contain only ASCII letters and digits'], ])('rejects invalid asset code %s with a useful validation message', (asset, message) => {