Skip to content
Open
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
56 changes: 56 additions & 0 deletions backend/src/controllers/trading.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,60 @@ export class TradingController {
}
}

/**
* POST /api/trading/sell — requires auth (issue #16)
* Body: { marketId, outcomeId, sharesAmount, minCollateralOut }
*/
async sellSharesDirect(req: Request, res: Response): Promise<void> {
try {
const userId = (req as AuthenticatedRequest).user?.userId;
if (!userId) {
res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } });
return;
}

const { marketId, outcomeId, sharesAmount, minCollateralOut } = req.body;

const result = await tradingService.sellShares({
userId,
marketId,
outcome: outcomeId,
shares: Number(sharesAmount),
minPayout: minCollateralOut ? Number(minCollateralOut) : undefined,
});

res.status(200).json({
success: true,
data: {
sharesSold: result.sharesSold,
pricePerUnit: result.pricePerUnit,
payout: result.payout,
feeAmount: result.feeAmount,
txHash: result.txHash,
tradeId: result.tradeId,
remainingShares: result.remainingShares,
},
});
} catch (error: any) {
logger.error('TradingController.sellSharesDirect error', { error });

if (
error.message?.includes('Insufficient') ||
error.message?.includes('No shares') ||
error.message?.includes('Invalid outcome') ||
error.message?.includes('Slippage')
) {
res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: error.message } });
return;
}
if (error.message?.includes('not found')) {
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: error.message } });
return;
}

res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Failed to sell shares' } });
}
}
}

export const tradingController = new TradingController();
55 changes: 54 additions & 1 deletion backend/src/routes/submit-tx.routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// backend/src/routes/submit-tx.routes.ts
// POST /api/trading/submit-tx — user-signed transaction submission
// POST /api/trading/sell — direct sell shares (issue #16)

import { Router } from 'express';
import { submitTxController } from '../controllers/submit-tx.controller.js';
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 { submitTxBody } from '../schemas/validation.schemas.js';
import { submitTxBody, sellSharesDirectBody } from '../schemas/validation.schemas.js';

const router: Router = Router();

Expand Down Expand Up @@ -69,4 +71,55 @@ router.post(
(req, res, next) => submitTxController.submitTx(req as any, res, next)
);

/**
* @swagger
* /api/trading/sell:
* post:
* summary: Sell outcome shares (issue #16)
* description: Sell shares back to the AMM before market resolution. Validates user holds enough shares, calls Stellar sell_shares, updates trade record and share position.
* tags: [Trading]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - marketId
* - outcomeId
* - sharesAmount
* properties:
* marketId:
* type: string
* format: uuid
* outcomeId:
* type: integer
* enum: [0, 1]
* description: 0 for NO, 1 for YES
* sharesAmount:
* type: string
* description: Number of shares to sell (base units, numeric string)
* minCollateralOut:
* type: string
* description: Minimum collateral to receive (slippage protection, numeric string)
* responses:
* 200:
* description: Shares sold — returns TradeReceipt
* 400:
* description: Insufficient shares or slippage exceeded
* 401:
* description: Unauthorized
* 404:
* description: Market not found
*/
router.post(
'/sell',
requireAuth,
tradeRateLimiter,
validate({ body: sellSharesDirectBody }),
(req, res) => tradingController.sellSharesDirect(req, res)
);

export default router;
20 changes: 20 additions & 0 deletions backend/src/schemas/validation.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,23 @@ export const getTransactionsQuery = z.object({
.datetime()
.optional(),
});

// --- Trading: POST /trading/sell schema (issue #16) ---

export const sellSharesDirectBody = z.object({
marketId: z.string().uuid('marketId must be a valid UUID'),
outcomeId: z.number().int().min(0).max(1, 'outcomeId must be 0 (NO) or 1 (YES)'),
sharesAmount: z
.string()
.regex(/^\d+$/, 'sharesAmount must be a numeric string (base units)')
.refine(
(val) => {
try { return BigInt(val) > 0n; } catch { return false; }
},
{ message: 'sharesAmount must be greater than 0' }
),
minCollateralOut: z
.string()
.regex(/^\d+$/, 'minCollateralOut must be a numeric string')
.optional(),
});