- Add Dockerfile.contracts for building WASM contracts - Add docker-compose.dex.yml for full DEX deployment - Add docker-compose.dex-services.yml for lightweight services - Add Node.js services for DEX ecosystem: - Oracle service (port 17500) - Price feeds with TWAP - Perps engine (port 17510) - Perpetual futures 2x-100x - Aggregator (port 17520) - Cross-chain liquidity routing - DEX API Gateway (port 17530) - Unified trading interface Services verified operational on Docker Desktop.
227 lines
7 KiB
JavaScript
227 lines
7 KiB
JavaScript
/**
|
|
* Synor Perpetuals Trading Engine
|
|
* Leveraged trading with 2x-100x positions
|
|
*
|
|
* Port: 17510 (REST), 17511 (WebSocket)
|
|
* Endpoints:
|
|
* GET /health - Health check
|
|
* GET /markets - Available markets
|
|
* GET /market/:id - Market details
|
|
* GET /positions/:address - User positions
|
|
* GET /funding - Current funding rates
|
|
* POST /order - Place order (simulated)
|
|
*/
|
|
|
|
const http = require('http');
|
|
|
|
const PORT = process.env.PORT || 17510;
|
|
const WS_PORT = process.env.WS_PORT || 17511;
|
|
const ORACLE_URL = process.env.ORACLE_URL || 'http://localhost:17500';
|
|
const MIN_LEVERAGE = parseInt(process.env.MIN_LEVERAGE) || 2;
|
|
const MAX_LEVERAGE = parseInt(process.env.MAX_LEVERAGE) || 100;
|
|
const MAINTENANCE_MARGIN_BPS = parseInt(process.env.MAINTENANCE_MARGIN_BPS) || 50;
|
|
const FUNDING_INTERVAL_HOURS = parseInt(process.env.FUNDING_INTERVAL_HOURS) || 8;
|
|
|
|
// Markets (in-memory state)
|
|
const markets = new Map([
|
|
[1, { id: 1, symbol: 'BTC/USD', tickSize: 0.1, lotSize: 0.001, maxLeverage: MAX_LEVERAGE }],
|
|
[2, { id: 2, symbol: 'ETH/USD', tickSize: 0.01, lotSize: 0.01, maxLeverage: MAX_LEVERAGE }],
|
|
[3, { id: 3, symbol: 'SYNOR/USD', tickSize: 0.0001, lotSize: 1.0, maxLeverage: 50 }],
|
|
[4, { id: 4, symbol: 'SOL/USD', tickSize: 0.01, lotSize: 0.1, maxLeverage: MAX_LEVERAGE }],
|
|
]);
|
|
|
|
// Open positions (in-memory)
|
|
const positions = new Map();
|
|
let positionIdCounter = 1;
|
|
|
|
// Funding rates (simulated)
|
|
const fundingRates = new Map([
|
|
[1, { marketId: 1, rate: 0.0001, nextFundingTime: Date.now() + 8 * 3600 * 1000 }],
|
|
[2, { marketId: 2, rate: 0.00012, nextFundingTime: Date.now() + 8 * 3600 * 1000 }],
|
|
[3, { marketId: 3, rate: 0.0002, nextFundingTime: Date.now() + 8 * 3600 * 1000 }],
|
|
[4, { marketId: 4, rate: 0.00008, nextFundingTime: Date.now() + 8 * 3600 * 1000 }],
|
|
]);
|
|
|
|
// Insurance fund (simulated)
|
|
let insuranceFund = 10000000; // $10M
|
|
|
|
// Calculate PnL
|
|
function calculatePnL(position, currentPrice) {
|
|
const priceDiff = currentPrice - position.entryPrice;
|
|
const multiplier = position.direction === 'long' ? 1 : -1;
|
|
return priceDiff * position.size * multiplier;
|
|
}
|
|
|
|
// Calculate margin ratio
|
|
function calculateMarginRatio(position, currentPrice) {
|
|
const pnl = calculatePnL(position, currentPrice);
|
|
const equity = position.collateral + pnl;
|
|
const notional = position.size * currentPrice;
|
|
return (equity / notional) * 10000; // in BPS
|
|
}
|
|
|
|
// Parse JSON body
|
|
function parseBody(req) {
|
|
return new Promise((resolve, reject) => {
|
|
let body = '';
|
|
req.on('data', chunk => body += chunk);
|
|
req.on('end', () => {
|
|
try {
|
|
resolve(body ? JSON.parse(body) : {});
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// HTTP Server
|
|
const server = http.createServer(async (req, res) => {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(200);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
const path = url.pathname;
|
|
|
|
try {
|
|
// Health check
|
|
if (path === '/health') {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
status: 'healthy',
|
|
markets: markets.size,
|
|
openPositions: positions.size,
|
|
insuranceFund,
|
|
timestamp: Date.now(),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// All markets
|
|
if (path === '/markets') {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
markets: Array.from(markets.values()),
|
|
config: { minLeverage: MIN_LEVERAGE, maxLeverage: MAX_LEVERAGE, maintenanceMarginBps: MAINTENANCE_MARGIN_BPS },
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Single market
|
|
if (path.startsWith('/market/')) {
|
|
const marketId = parseInt(path.slice(8));
|
|
const market = markets.get(marketId);
|
|
if (market) {
|
|
const funding = fundingRates.get(marketId);
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ ...market, funding }));
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: 'Market not found' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// User positions
|
|
if (path.startsWith('/positions/')) {
|
|
const address = path.slice(11);
|
|
const userPositions = Array.from(positions.values()).filter(p => p.owner === address);
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ address, positions: userPositions }));
|
|
return;
|
|
}
|
|
|
|
// Funding rates
|
|
if (path === '/funding') {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
rates: Array.from(fundingRates.values()),
|
|
interval: `${FUNDING_INTERVAL_HOURS}h`,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Insurance fund
|
|
if (path === '/insurance') {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ insuranceFund, timestamp: Date.now() }));
|
|
return;
|
|
}
|
|
|
|
// Place order (simulated)
|
|
if (path === '/order' && req.method === 'POST') {
|
|
const body = await parseBody(req);
|
|
const { marketId, direction, size, collateral, leverage, owner } = body;
|
|
|
|
// Validate
|
|
if (!marketId || !direction || !size || !collateral || !leverage || !owner) {
|
|
res.writeHead(400);
|
|
res.end(JSON.stringify({ error: 'Missing required fields' }));
|
|
return;
|
|
}
|
|
|
|
if (leverage < MIN_LEVERAGE || leverage > MAX_LEVERAGE) {
|
|
res.writeHead(400);
|
|
res.end(JSON.stringify({ error: `Leverage must be between ${MIN_LEVERAGE}x and ${MAX_LEVERAGE}x` }));
|
|
return;
|
|
}
|
|
|
|
const market = markets.get(marketId);
|
|
if (!market) {
|
|
res.writeHead(400);
|
|
res.end(JSON.stringify({ error: 'Invalid market' }));
|
|
return;
|
|
}
|
|
|
|
// Create position (simulated - in production would interact with smart contract)
|
|
const positionId = positionIdCounter++;
|
|
const position = {
|
|
id: positionId,
|
|
owner,
|
|
marketId,
|
|
direction,
|
|
size,
|
|
collateral,
|
|
entryPrice: 45000, // Would fetch from oracle
|
|
leverage,
|
|
openedAt: Date.now(),
|
|
isOpen: true,
|
|
};
|
|
|
|
positions.set(positionId, position);
|
|
|
|
res.writeHead(201);
|
|
res.end(JSON.stringify({ success: true, position }));
|
|
return;
|
|
}
|
|
|
|
// Default
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
} catch (err) {
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
});
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Perpetuals Engine running on port ${PORT}`);
|
|
console.log(`Leverage: ${MIN_LEVERAGE}x - ${MAX_LEVERAGE}x`);
|
|
console.log(`Maintenance Margin: ${MAINTENANCE_MARGIN_BPS / 100}%`);
|
|
console.log(`Markets: ${markets.size}`);
|
|
console.log('Endpoints:');
|
|
console.log(' GET /health - Health check');
|
|
console.log(' GET /markets - Available markets');
|
|
console.log(' GET /market/:id - Market details');
|
|
console.log(' GET /positions/:address - User positions');
|
|
console.log(' GET /funding - Current funding rates');
|
|
console.log(' POST /order - Place order');
|
|
});
|