diff --git a/backend/src/controllers/trading.controller.ts b/backend/src/controllers/trading.controller.ts
index 422e38dd..e3c69ba3 100644
--- a/backend/src/controllers/trading.controller.ts
+++ b/backend/src/controllers/trading.controller.ts
@@ -25,19 +25,11 @@ export class TradingController {
const marketId = req.params.marketId as string;
const { outcome, amount, minShares } = req.body;
- // Validation
- if (outcome === undefined || !amount) {
- res
- .status(400)
- .json({ success: false, error: 'Missing outcome or amount' });
- return;
- }
-
const xdr = await tradingService.buildBuySharesTx(
userId,
userPublicKey,
marketId,
- Number(outcome),
+ outcome,
BigInt(amount),
BigInt(minShares || 0)
);
@@ -71,47 +63,13 @@ export class TradingController {
const marketId = req.params.marketId as string;
const { outcome, amount, minShares } = req.body;
- // Validate input
- if (outcome === undefined || outcome === null) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'outcome is required (0 for NO, 1 for YES)',
- },
- });
- return;
- }
-
- if (!amount || amount <= 0) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'amount must be greater than 0',
- },
- });
- return;
- }
-
- if (![0, 1].includes(outcome)) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'outcome must be 0 (NO) or 1 (YES)',
- },
- });
- return;
- }
-
// Call service
const result = await tradingService.buyShares({
userId,
marketId,
outcome,
- amount,
- minShares,
+ amount: Number(amount),
+ minShares: minShares ? Number(minShares) : undefined,
});
res.status(201).json({
@@ -179,18 +137,11 @@ export class TradingController {
const marketId = req.params.marketId as string;
const { outcome, shares, minPayout } = req.body;
- if (outcome === undefined || !shares) {
- res
- .status(400)
- .json({ success: false, error: 'Missing outcome or shares' });
- return;
- }
-
const xdr = await tradingService.buildSellSharesTx(
userId,
userPublicKey,
marketId,
- Number(outcome),
+ outcome,
BigInt(shares),
BigInt(minPayout || 0)
);
@@ -224,47 +175,13 @@ export class TradingController {
const marketId = req.params.marketId as string;
const { outcome, shares, minPayout } = req.body;
- // Validate input
- if (outcome === undefined || outcome === null) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'outcome is required (0 for NO, 1 for YES)',
- },
- });
- return;
- }
-
- if (!shares || shares <= 0) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'shares must be greater than 0',
- },
- });
- return;
- }
-
- if (![0, 1].includes(outcome)) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'outcome must be 0 (NO) or 1 (YES)',
- },
- });
- return;
- }
-
// Call service
const result = await tradingService.sellShares({
userId,
marketId,
outcome,
- shares,
- minPayout,
+ shares: Number(shares),
+ minPayout: minPayout ? Number(minPayout) : undefined,
});
res.status(200).json({
@@ -425,46 +342,10 @@ export class TradingController {
const marketId = req.params.marketId as string;
const { usdcAmount } = req.body;
- if (!usdcAmount) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'usdcAmount is required',
- },
- });
- return;
- }
-
- let parsedAmount: bigint;
- try {
- parsedAmount = BigInt(usdcAmount);
- } catch {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'usdcAmount must be a valid integer string',
- },
- });
- return;
- }
-
- if (parsedAmount <= BigInt(0)) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'usdcAmount must be greater than 0',
- },
- });
- return;
- }
-
const result = await tradingService.addLiquidity(
userId,
marketId,
- parsedAmount
+ BigInt(usdcAmount)
);
res.status(200).json({
@@ -521,46 +402,10 @@ export class TradingController {
const marketId = req.params.marketId as string;
const { lpTokens } = req.body;
- if (!lpTokens) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'lpTokens is required',
- },
- });
- return;
- }
-
- let parsedTokens: bigint;
- try {
- parsedTokens = BigInt(lpTokens);
- } catch {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'lpTokens must be a valid integer string',
- },
- });
- return;
- }
-
- if (parsedTokens <= BigInt(0)) {
- res.status(400).json({
- success: false,
- error: {
- code: 'VALIDATION_ERROR',
- message: 'lpTokens must be greater than 0',
- },
- });
- return;
- }
-
const result = await tradingService.removeLiquidity(
userId,
marketId,
- parsedTokens
+ BigInt(lpTokens)
);
res.status(200).json({
diff --git a/backend/src/routes/trading.ts b/backend/src/routes/trading.ts
index b3267d6e..ccbdd62e 100644
--- a/backend/src/routes/trading.ts
+++ b/backend/src/routes/trading.ts
@@ -5,6 +5,14 @@ import { Router } from 'express';
import { tradingController } from '../controllers/trading.controller.js';
import { requireAuth } from '../middleware/auth.middleware.js';
import { tradeRateLimiter } from '../middleware/rateLimit.middleware.js';
+import { validate } from '../middleware/validation.middleware.js';
+import {
+ marketIdParam,
+ buySharesBody,
+ sellSharesBody,
+ addLiquidityBody,
+ removeLiquidityBody,
+} from '../schemas/validation.schemas.js';
const router: Router = Router();
@@ -84,8 +92,11 @@ const router: Router = Router();
* 404:
* $ref: '#/components/responses/NotFound'
*/
-router.post('/:marketId/buy', requireAuth, (req, res) =>
- tradingController.buyShares(req, res)
+router.post(
+ '/:marketId/buy',
+ requireAuth,
+ validate({ params: marketIdParam, body: buySharesBody }),
+ (req, res) => tradingController.buyShares(req, res)
);
/**
@@ -156,8 +167,11 @@ router.post('/:marketId/buy', requireAuth, (req, res) =>
* 404:
* $ref: '#/components/responses/NotFound'
*/
-router.post('/:marketId/sell', requireAuth, (req, res) =>
- tradingController.sellShares(req, res)
+router.post(
+ '/:marketId/sell',
+ requireAuth,
+ validate({ params: marketIdParam, body: sellSharesBody }),
+ (req, res) => tradingController.sellShares(req, res)
);
/**
@@ -225,15 +239,21 @@ router.get('/:marketId/odds', (req, res) =>
/**
* POST /api/markets/:marketId/liquidity/add - Add USDC Liquidity to Pool
*/
-router.post('/:marketId/liquidity/add', requireAuth, (req, res) =>
- tradingController.addLiquidity(req, res)
+router.post(
+ '/:marketId/liquidity/add',
+ requireAuth,
+ validate({ params: marketIdParam, body: addLiquidityBody }),
+ (req, res) => tradingController.addLiquidity(req, res)
);
/**
* POST /api/markets/:marketId/liquidity/remove - Remove Liquidity from Pool
*/
-router.post('/:marketId/liquidity/remove', requireAuth, (req, res) =>
- tradingController.removeLiquidity(req, res)
+router.post(
+ '/:marketId/liquidity/remove',
+ requireAuth,
+ validate({ params: marketIdParam, body: removeLiquidityBody }),
+ (req, res) => tradingController.removeLiquidity(req, res)
);
// ─── User-signed Transaction Routes ──────────────────────────────────────────
diff --git a/backend/src/schemas/validation.schemas.ts b/backend/src/schemas/validation.schemas.ts
index 537d2fdd..40a7c70c 100644
--- a/backend/src/schemas/validation.schemas.ts
+++ b/backend/src/schemas/validation.schemas.ts
@@ -1,5 +1,6 @@
import { z } from 'zod';
import { MarketCategory } from '@prisma/client';
+import { stellarService } from '../services/stellar.service.js';
// --- Sanitization helper ---
@@ -10,9 +11,16 @@ import { MarketCategory } from '@prisma/client';
export function stripHtml(val: string): string {
// Strip script tags and their content
val = val.replace(/';
+ expect(stripHtml(input)).toBe('Check out this market! ');
+ });
+
+ it('should strip inline event handlers', () => {
+ const input = ' Click me';
+ // Our stripHtml replaces on\w+="[^"]*" with empty string
+ const result = stripHtml(input);
+ expect(result).not.toContain('onerror');
+ expect(result).not.toContain('alert');
+ });
+
+ it('should strip style tags', () => {
+ const input = 'Normal text ';
+ expect(stripHtml(input)).toBe('Normal text ');
+ });
+
+ it('should strip javascript: pseudo-protocol', () => {
+ const input = 'Click';
+ const result = stripHtml(input);
+ expect(result).not.toContain('javascript:');
+ expect(result).toBe('Click');
+ });
+
+ it('should strip multiple tags and handle nested-like structures', () => {
+ const input = '
Message