- 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.
159 lines
4.5 KiB
JavaScript
159 lines
4.5 KiB
JavaScript
/**
|
|
* Synor Oracle Service
|
|
* Multi-source price aggregation with TWAP support
|
|
*
|
|
* Port: 17500
|
|
* Endpoints:
|
|
* GET /health - Health check
|
|
* GET /prices - All current prices
|
|
* GET /price/:symbol - Price for specific symbol
|
|
* GET /twap/:symbol - Time-weighted average price
|
|
*/
|
|
|
|
const http = require('http');
|
|
|
|
const PORT = process.env.PORT || 17500;
|
|
const UPDATE_INTERVAL = parseInt(process.env.UPDATE_INTERVAL_MS) || 1000;
|
|
const STALE_THRESHOLD = parseInt(process.env.STALE_THRESHOLD_MS) || 60000;
|
|
|
|
// Price state (in-memory cache)
|
|
const prices = new Map();
|
|
const priceHistory = new Map();
|
|
const MAX_HISTORY = 3600; // 1 hour of 1-second updates
|
|
|
|
// Initialize default prices (testnet)
|
|
const DEFAULT_PRICES = {
|
|
'BTC/USD': 45000,
|
|
'ETH/USD': 2500,
|
|
'SYNOR/USD': 1.50,
|
|
'SOL/USD': 95,
|
|
'ATOM/USD': 8.50,
|
|
'OSMO/USD': 0.75,
|
|
};
|
|
|
|
// Initialize prices
|
|
Object.entries(DEFAULT_PRICES).forEach(([symbol, price]) => {
|
|
prices.set(symbol, {
|
|
symbol,
|
|
price,
|
|
timestamp: Date.now(),
|
|
sources: ['simulator'],
|
|
confidence: 1.0,
|
|
});
|
|
priceHistory.set(symbol, [{ price, timestamp: Date.now() }]);
|
|
});
|
|
|
|
// Calculate TWAP
|
|
function calculateTWAP(symbol, windowMs = 300000) { // 5 minute default
|
|
const history = priceHistory.get(symbol) || [];
|
|
const cutoff = Date.now() - windowMs;
|
|
const relevantPrices = history.filter(p => p.timestamp >= cutoff);
|
|
|
|
if (relevantPrices.length === 0) return null;
|
|
|
|
const sum = relevantPrices.reduce((acc, p) => acc + p.price, 0);
|
|
return sum / relevantPrices.length;
|
|
}
|
|
|
|
// Update price with simulated volatility
|
|
function updatePrice(symbol, basePrice, volatility = 0.001) {
|
|
const change = (Math.random() - 0.5) * 2 * volatility * basePrice;
|
|
const newPrice = Math.max(basePrice + change, 0.01);
|
|
const timestamp = Date.now();
|
|
|
|
prices.set(symbol, {
|
|
symbol,
|
|
price: newPrice,
|
|
timestamp,
|
|
sources: ['simulator'],
|
|
confidence: 1.0,
|
|
});
|
|
|
|
// Update history
|
|
const history = priceHistory.get(symbol) || [];
|
|
history.push({ price: newPrice, timestamp });
|
|
if (history.length > MAX_HISTORY) history.shift();
|
|
priceHistory.set(symbol, history);
|
|
|
|
return newPrice;
|
|
}
|
|
|
|
// Periodic price updates
|
|
setInterval(() => {
|
|
Object.entries(DEFAULT_PRICES).forEach(([symbol, basePrice]) => {
|
|
const current = prices.get(symbol);
|
|
updatePrice(symbol, current?.price || basePrice);
|
|
});
|
|
}, UPDATE_INTERVAL);
|
|
|
|
// HTTP Server
|
|
const server = http.createServer((req, res) => {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
const path = url.pathname;
|
|
|
|
// Health check
|
|
if (path === '/health') {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ status: 'healthy', timestamp: Date.now() }));
|
|
return;
|
|
}
|
|
|
|
// All prices
|
|
if (path === '/prices') {
|
|
const allPrices = {};
|
|
prices.forEach((data, symbol) => {
|
|
const isStale = Date.now() - data.timestamp > STALE_THRESHOLD;
|
|
allPrices[symbol] = { ...data, stale: isStale };
|
|
});
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ prices: allPrices, timestamp: Date.now() }));
|
|
return;
|
|
}
|
|
|
|
// Single price
|
|
if (path.startsWith('/price/')) {
|
|
const symbol = decodeURIComponent(path.slice(7)).toUpperCase();
|
|
const data = prices.get(symbol);
|
|
if (data) {
|
|
const isStale = Date.now() - data.timestamp > STALE_THRESHOLD;
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ ...data, stale: isStale }));
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: 'Symbol not found', symbol }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// TWAP
|
|
if (path.startsWith('/twap/')) {
|
|
const symbol = decodeURIComponent(path.slice(6)).toUpperCase();
|
|
const window = parseInt(url.searchParams.get('window')) || 300000;
|
|
const twap = calculateTWAP(symbol, window);
|
|
if (twap !== null) {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ symbol, twap, windowMs: window, timestamp: Date.now() }));
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: 'No price history for symbol', symbol }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Default
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
});
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Oracle Service running on port ${PORT}`);
|
|
console.log(`Tracking ${prices.size} price feeds`);
|
|
console.log('Endpoints:');
|
|
console.log(' GET /health - Health check');
|
|
console.log(' GET /prices - All current prices');
|
|
console.log(' GET /price/:symbol - Price for specific symbol');
|
|
console.log(' GET /twap/:symbol - Time-weighted average price');
|
|
});
|