Skip to content
Open
11 changes: 11 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@types/helmet": "^0.0.48",
"@types/jsonwebtoken": "^9.0.7",
"@types/morgan": "^1.9.9",
"@types/ms": "^2.1.0",
"@types/node": "^22.10.5",
"@types/supertest": "^6.0.3",
"@types/swagger-jsdoc": "^6.0.4",
Expand All @@ -83,5 +84,15 @@
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"pnpm": {
"onlyBuiltDependencies": [
"@prisma/client",
"@prisma/engines",
"@scarf/scarf",
"bcrypt",
"esbuild",
"prisma"
]
}
}
281 changes: 281 additions & 0 deletions backend/src/controllers/trading.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
// backend/src/controllers/trading.controller.ts
// Trading controller - handles trading HTTP requests

import { Request, Response } from 'express';
import { tradingService } from '../services/trading.service.js';
import { AuthenticatedRequest } from '../types/auth.types.js';

class TradingController {
/**
* POST /api/markets/:marketId/buy - Buy outcome shares
*/
async buyShares(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 = 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,
});

res.status(201).json({
success: true,
data: {
sharesBought: result.sharesBought,
pricePerUnit: result.pricePerUnit,
totalCost: result.totalCost,
feeAmount: result.feeAmount,
txHash: result.txHash,
tradeId: result.tradeId,
position: result.newSharePosition,
},
});
} catch (error: any) {
console.error('Error buying shares:', error);

// Determine appropriate status code
let statusCode = 500;
let errorCode = 'INTERNAL_ERROR';

if (error.message.includes('not found')) {
statusCode = 404;
errorCode = 'NOT_FOUND';
} else if (
error.message.includes('Insufficient') ||
error.message.includes('Invalid') ||
error.message.includes('only allowed')
) {
statusCode = 400;
errorCode = 'BAD_REQUEST';
} else if (error.message.includes('Slippage')) {
statusCode = 400;
errorCode = 'SLIPPAGE_EXCEEDED';
} else if (error.message.includes('blockchain')) {
statusCode = 503;
errorCode = 'BLOCKCHAIN_ERROR';
}

res.status(statusCode).json({
success: false,
error: {
code: errorCode,
message: error.message || 'Failed to buy shares',
},
});
}
}

/**
* POST /api/markets/:marketId/sell - Sell outcome shares
*/
async sellShares(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 = 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,
});

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) {
console.error('Error selling shares:', error);

// Determine appropriate status code
let statusCode = 500;
let errorCode = 'INTERNAL_ERROR';

if (error.message.includes('not found')) {
statusCode = 404;
errorCode = 'NOT_FOUND';
} else if (
error.message.includes('Insufficient') ||
error.message.includes('Invalid') ||
error.message.includes('No shares')
) {
statusCode = 400;
errorCode = 'BAD_REQUEST';
} else if (error.message.includes('Slippage')) {
statusCode = 400;
errorCode = 'SLIPPAGE_EXCEEDED';
} else if (error.message.includes('blockchain')) {
statusCode = 503;
errorCode = 'BLOCKCHAIN_ERROR';
}

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

/**
* GET /api/markets/:marketId/odds - Get current market odds
*/
async getOdds(req: Request, res: Response): Promise<void> {
try {
const marketId = req.params.marketId as string;

// Call service
const result = await tradingService.getMarketOdds(marketId);

res.status(200).json({
success: true,
data: {
yes: {
odds: result.yesOdds,
percentage: result.yesPercentage,
liquidity: result.yesLiquidity,
},
no: {
odds: result.noOdds,
percentage: result.noPercentage,
liquidity: result.noLiquidity,
},
totalLiquidity: result.totalLiquidity,
},
});
} catch (error: any) {
console.error('Error getting odds:', error);

// Determine appropriate status code
let statusCode = 500;
let errorCode = 'INTERNAL_ERROR';

if (error.message.includes('not found')) {
statusCode = 404;
errorCode = 'NOT_FOUND';
} else if (error.message.includes('blockchain')) {
statusCode = 503;
errorCode = 'BLOCKCHAIN_ERROR';
}

res.status(statusCode).json({
success: false,
error: {
code: errorCode,
message: error.message || 'Failed to get odds',
},
});
}
}
}

export const tradingController = new TradingController();
5 changes: 4 additions & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import authRoutes from './routes/auth.routes.js';
import marketRoutes from './routes/markets.routes.js';
import oracleRoutes from './routes/oracle.js';
import predictionRoutes from './routes/predictions.js';
import tradingRoutes from './routes/trading.js';
import treasuryRoutes from './routes/treasury.routes.js';

// Import Redis initialization
Expand Down Expand Up @@ -46,7 +47,7 @@ import {
import { setupSwagger } from './config/swagger.js';

// Initialize Express app
const app = express();
const app: express.Express = express();
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';

Expand Down Expand Up @@ -207,6 +208,8 @@ app.use('/api/markets', oracleRoutes);
// Prediction routes (commit-reveal flow)
app.use('/api/markets', predictionRoutes);

// Trading routes (buy/sell shares, odds)
app.use('/api/markets', tradingRoutes);
// Treasury routes
app.use('/api/treasury', treasuryRoutes);

Expand Down
Loading