- 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.
278 lines
8.4 KiB
JavaScript
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');
|
|
});
|