synor/docker/dex/aggregator/index.js
Gulshan Yadav e2ce0022e5 feat(dex): add Docker deployment for DEX ecosystem services
- 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.
2026-01-19 19:59:30 +05:30

278 lines
8.4 KiB
JavaScript

/**
* Synor Liquidity Aggregator
* Cross-chain DEX routing with optimal pricing
*
* Port: 17520
* Endpoints:
* GET /health - Health check
* GET /sources - Available liquidity sources
* GET /quote - Get best price quote
* GET /routes - Get optimal routing
* POST /swap - Execute swap (simulated)
*/
const http = require('http');
const PORT = process.env.PORT || 17520;
const AGGREGATION_FEE_BPS = parseInt(process.env.AGGREGATION_FEE_BPS) || 5;
const MAX_SLIPPAGE_BPS = parseInt(process.env.MAX_SLIPPAGE_BPS) || 100;
// Liquidity sources (simulated)
const sources = new Map([
['synor', { id: 'synor', name: 'Synor DEX', type: 'amm', chain: 'synor', active: true, liquidity: 5000000 }],
['osmosis', { id: 'osmosis', name: 'Osmosis', type: 'amm', chain: 'cosmos', active: true, liquidity: 50000000 }],
['dydx', { id: 'dydx', name: 'dYdX', type: 'orderbook', chain: 'cosmos', active: true, liquidity: 100000000 }],
['uniswap', { id: 'uniswap', name: 'Uniswap V3', type: 'amm', chain: 'ethereum', active: true, liquidity: 500000000 }],
]);
// Simulated price quotes
const baseQuotes = {
'BTC/USDT': 45000,
'ETH/USDT': 2500,
'SYNOR/USDT': 1.50,
'SOL/USDT': 95,
'ATOM/USDT': 8.50,
};
// Get quote from source with slippage simulation
function getQuoteFromSource(sourceId, tokenIn, tokenOut, amountIn) {
const source = sources.get(sourceId);
if (!source || !source.active) return null;
const pair = `${tokenIn}/${tokenOut}`;
const basePrice = baseQuotes[pair] || baseQuotes[`${tokenOut}/${tokenIn}`];
if (!basePrice) return null;
// Add random spread based on source
const spreadMultiplier = source.type === 'orderbook' ? 0.9995 : 0.999; // Orderbooks typically tighter
const slippageImpact = (amountIn / source.liquidity) * 0.01; // Price impact
const effectivePrice = basePrice * spreadMultiplier * (1 - slippageImpact);
const amountOut = (amountIn / effectivePrice) * (1 - AGGREGATION_FEE_BPS / 10000);
return {
source: sourceId,
sourceName: source.name,
tokenIn,
tokenOut,
amountIn,
amountOut,
effectivePrice,
priceImpact: slippageImpact * 100,
fee: amountIn * (AGGREGATION_FEE_BPS / 10000),
timestamp: Date.now(),
};
}
// Find best route
function findBestRoute(tokenIn, tokenOut, amountIn) {
const quotes = [];
for (const [sourceId] of sources) {
const quote = getQuoteFromSource(sourceId, tokenIn, tokenOut, amountIn);
if (quote) quotes.push(quote);
}
if (quotes.length === 0) return null;
// Sort by best output amount
quotes.sort((a, b) => b.amountOut - a.amountOut);
// For large amounts, consider split routing
const bestSingle = quotes[0];
// Simple split route: 60% to best, 40% to second best
if (quotes.length >= 2 && amountIn > 10000) {
const splitRoute = {
type: 'split',
routes: [
{ ...getQuoteFromSource(quotes[0].source, tokenIn, tokenOut, amountIn * 0.6), percentage: 60 },
{ ...getQuoteFromSource(quotes[1].source, tokenIn, tokenOut, amountIn * 0.4), percentage: 40 },
],
totalAmountOut: 0,
};
splitRoute.routes.forEach(r => splitRoute.totalAmountOut += r.amountOut);
// Use split if better
if (splitRoute.totalAmountOut > bestSingle.amountOut) {
return { best: splitRoute, alternatives: quotes.slice(0, 3) };
}
}
return { best: bestSingle, alternatives: quotes.slice(1, 4) };
}
// 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') {
const activeSources = Array.from(sources.values()).filter(s => s.active).length;
res.writeHead(200);
res.end(JSON.stringify({
status: 'healthy',
sources: activeSources,
feeBps: AGGREGATION_FEE_BPS,
timestamp: Date.now(),
}));
return;
}
// Available sources
if (path === '/sources') {
res.writeHead(200);
res.end(JSON.stringify({
sources: Array.from(sources.values()),
count: sources.size,
}));
return;
}
// Get quote
if (path === '/quote') {
const tokenIn = url.searchParams.get('tokenIn')?.toUpperCase();
const tokenOut = url.searchParams.get('tokenOut')?.toUpperCase();
const amountIn = parseFloat(url.searchParams.get('amount'));
if (!tokenIn || !tokenOut || !amountIn) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Missing tokenIn, tokenOut, or amount' }));
return;
}
const result = findBestRoute(tokenIn, tokenOut, amountIn);
if (!result) {
res.writeHead(404);
res.end(JSON.stringify({ error: 'No route found', tokenIn, tokenOut }));
return;
}
res.writeHead(200);
res.end(JSON.stringify(result));
return;
}
// Get routes (same as quote but more detailed)
if (path === '/routes') {
const tokenIn = url.searchParams.get('tokenIn')?.toUpperCase();
const tokenOut = url.searchParams.get('tokenOut')?.toUpperCase();
const amountIn = parseFloat(url.searchParams.get('amount'));
if (!tokenIn || !tokenOut || !amountIn) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Missing parameters' }));
return;
}
const quotes = [];
for (const [sourceId] of sources) {
const quote = getQuoteFromSource(sourceId, tokenIn, tokenOut, amountIn);
if (quote) quotes.push(quote);
}
quotes.sort((a, b) => b.amountOut - a.amountOut);
res.writeHead(200);
res.end(JSON.stringify({
tokenIn,
tokenOut,
amountIn,
routes: quotes,
bestSource: quotes[0]?.source,
}));
return;
}
// Execute swap (simulated)
if (path === '/swap' && req.method === 'POST') {
const body = await parseBody(req);
const { tokenIn, tokenOut, amountIn, minAmountOut, route, sender } = body;
if (!tokenIn || !tokenOut || !amountIn || !sender) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Missing required fields' }));
return;
}
const result = findBestRoute(tokenIn, tokenOut, amountIn);
if (!result) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'No route available' }));
return;
}
const amountOut = result.best.type === 'split' ? result.best.totalAmountOut : result.best.amountOut;
if (minAmountOut && amountOut < minAmountOut) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Slippage too high', expected: minAmountOut, actual: amountOut }));
return;
}
// Simulate transaction
const txId = `0x${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`;
res.writeHead(200);
res.end(JSON.stringify({
success: true,
txId,
tokenIn,
tokenOut,
amountIn,
amountOut,
route: result.best,
sender,
timestamp: Date.now(),
}));
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(`Liquidity Aggregator running on port ${PORT}`);
console.log(`Sources: ${Array.from(sources.values()).map(s => s.name).join(', ')}`);
console.log(`Aggregation Fee: ${AGGREGATION_FEE_BPS / 100}%`);
console.log('Endpoints:');
console.log(' GET /health - Health check');
console.log(' GET /sources - Available liquidity sources');
console.log(' GET /quote?tokenIn=X&tokenOut=Y&amount=Z - Get best quote');
console.log(' GET /routes?tokenIn=X&tokenOut=Y&amount=Z - Get all routes');
console.log(' POST /swap - Execute swap');
});