/** * Cryptonote Node.JS Pool * https://github.com/dvandal/cryptonote-nodejs-pool * * Market Exchanges **/ // Load required modules let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet); // Initialize log system let logSystem = 'market'; require('./exceptionWriter.js')(logSystem); /** * Get market prices **/ exports.get = function (exchange, tickers, callback) { if (!exchange) { callback('No exchange specified', null); } exchange = exchange.toLowerCase(); if (!tickers || tickers.length === 0) { callback('No tickers specified', null); } let marketPrices = []; let numTickers = tickers.length; let completedFetches = 0; getExchangeMarkets(exchange, function (error, marketData) { if (!marketData || marketData.length === 0) { callback({}); return; } for (let i in tickers) { (function (i) { let pairName = tickers[i]; let pairParts = pairName.split('-'); let base = pairParts[0] || null; let target = pairParts[1] || null; if (!marketData[base]) { completedFetches++; if (completedFetches === numTickers) callback(marketPrices); } else { let price = marketData[base][target] || null; if (!price || price === 0) { let cryptonatorBase; if (marketData[base]['BTC']) cryptonatorBase = 'BTC'; else if (marketData[base]['ETH']) cryptonatorBase = 'ETH'; else if (marketData[base]['LTC']) cryptonatorBase = 'LTC'; if (!cryptonatorBase) { completedFetches++; if (completedFetches === numTickers) callback(marketPrices); } else { getExchangePrice("cryptonator", cryptonatorBase, target, function (error, tickerData) { completedFetches++; if (tickerData && tickerData.price) { marketPrices[i] = { ticker: pairName, price: tickerData.price * marketData[base][cryptonatorBase], source: tickerData.source }; } if (completedFetches === numTickers) callback(marketPrices); }); } } else { completedFetches++; marketPrices[i] = { ticker: pairName, price: price, source: exchange }; if (completedFetches === numTickers) callback(marketPrices); } } })(i); } }); } /** * Get Exchange Market Prices **/ let marketRequestsCache = {}; function getConfiguredPoolSymbol() { const cfgSymRaw = (config && config.symbol); return cfgSymRaw ? String(cfgSymRaw).toUpperCase() : null; } function getConfiguredQuotes(exchangeName) { const exCfg = (config && config.exchanges && config.exchanges[exchangeName]) || {}; const quotesRaw = exCfg.quotes ?? exCfg.quote ?? 'USDT'; const quotes = Array.isArray(quotesRaw) ? quotesRaw : [quotesRaw]; return quotes.length > 0 ? quotes.map(q => String(q).toUpperCase()) : ['USDT']; } function getCoinPaprikaId() { const exCfg = (config && config.exchanges && config.exchanges.coinpaprika) || {}; if (exCfg.coinId || exCfg.id) return exCfg.coinId || exCfg.id; const poolSymbol = getConfiguredPoolSymbol(); switch (poolSymbol) { case 'XMR': return 'xmr-monero'; case 'ZPH': case 'ZEPH': return 'zeph-zephyr-protocol'; case 'SAL': return 'sal-salvium'; default: return null; } } function getExchangeMarkets (exchange, callback) { callback = callback || function () {}; if (!exchange) { callback('No exchange specified', null); } exchange = exchange.toLowerCase(); // Return cache if available let cacheKey = exchange; let currentTimestamp = Date.now() / 1000; if (marketRequestsCache[cacheKey] && marketRequestsCache[cacheKey].ts > (currentTimestamp - 60)) { callback(null, marketRequestsCache[cacheKey].data); return; } let target = null; let symbol = null; let price = 0.0; let data = {}; // CoinEx if (exchange === "coinex") { const poolSymbol = getConfiguredPoolSymbol(); if (!poolSymbol) { log('error', logSystem, 'CoinEx: config.symbol not set; skipping'); return callback(null, data); } const exchangeBase = poolSymbol; const quotes = getConfiguredQuotes('coinex'); let pending = quotes.length; const finish = () => { marketRequestsCache[cacheKey] = { ts: currentTimestamp, data }; callback(null, data); }; quotes.forEach((q) => { const quote = String(q).toUpperCase(); const pair = `${exchangeBase}${quote}`; const path = `/v2/spot/ticker?market=${pair}`; apiInterfaces.jsonHttpRequest('api.coinex.com', 443, '', function (error, response) { if (error || !response) { log('error', logSystem, 'CoinEx %s failed: %s', [pair, error || 'no response']); } else { try { // { code, data: [ { market: "BASEQUOTE", last: "..." , ... } ] } const items = (response && response.data) || []; const item = Array.isArray(items) ? items[0] : null; const price = parseFloat(item?.last ?? item?.ticker?.last); if (price > 0) { const rspBase = exchangeBase; // request is single-market const rspQuote = quote; if (!data[rspBase]) data[rspBase] = {}; data[rspBase][rspQuote] = price; // mirror under pool symbol if exchange base differs (e.g., SAL vs SAL1) if (rspBase !== poolSymbol) { if (!data[poolSymbol]) data[poolSymbol] = {}; data[poolSymbol][rspQuote] = price; } } else { log('warn', logSystem, 'CoinEx %s non-positive or missing price: %j', [pair, response]); } } catch (e) { log('error', logSystem, 'CoinEx parse error for %s: %s', [pair, e && e.stack || e]); } } if (--pending === 0) finish(); }, path); }); } // MEXC (single-market request; unified pattern) else if (exchange === "mexc") { const poolSymbol = getConfiguredPoolSymbol(); if (!poolSymbol) { log('error', logSystem, 'MEXC: config.symbol not set; skipping'); return callback(null, data); } const exchangeBase = poolSymbol; const quotes = getConfiguredQuotes('mexc'); let pending = quotes.length; const finish = () => { marketRequestsCache[cacheKey] = { ts: currentTimestamp, data }; callback(null, data); }; quotes.forEach((q) => { const quote = String(q).toUpperCase(); const pair = `${exchangeBase}${quote}`; // e.g., SALUSDT const path = `/api/v3/ticker/price?symbol=${pair}`; // single-symbol apiInterfaces.jsonHttpRequest('api.mexc.com', 443, '', function (error, response) { if (error || !response) { log('error', logSystem, 'MEXC %s failed: %s', [pair, error || 'no response']); } else { try { // single: { symbol: "BASEQUOTE", price: "..." } const price = parseFloat(response.price ?? response.last ?? response.lastPrice); if (price > 0) { const rspBase = exchangeBase; const rspQuote = quote; if (!data[rspBase]) data[rspBase] = {}; data[rspBase][rspQuote] = price; if (rspBase !== poolSymbol) { if (!data[poolSymbol]) data[poolSymbol] = {}; data[poolSymbol][rspQuote] = price; } } else { log('warn', logSystem, 'MEXC %s non-positive or missing price: %j', [pair, response]); } } catch (e) { log('error', logSystem, 'MEXC parse error for %s: %s', [pair, e && e.stack || e]); } } if (--pending === 0) finish(); }, path); }); } // NonKYC.io else if (exchange === "nonkyc") { const poolSymbol = getConfiguredPoolSymbol(); if (!poolSymbol) { log('error', logSystem, 'NonKYC: config.symbol not set; skipping price fetch'); return callback(null, data); } const exchangeBase = poolSymbol; const quotes = getConfiguredQuotes('nonkyc'); let pending = quotes.length; const finish = () => { marketRequestsCache[cacheKey] = { ts: currentTimestamp, data }; callback(null, data); }; quotes.forEach((q) => { const quote = String(q).toUpperCase(); const pair = `${exchangeBase}_${quote}`; const path = `/api/v2/ticker/${pair}`; apiInterfaces.jsonHttpRequest('api.nonkyc.io', 443, '', function (error, response) { if (error || !response) { log('error', logSystem, 'NonKYC API %s failed: %s', [pair, error || 'no response']); } else { try { // Example: // {"ticker_id":"SAL_USDT","base_currency":"SAL","target_currency":"USDT","last_price":"0.069706",...} const rspBase = String(response.base_currency || exchangeBase).toUpperCase(); const rspQuote = String(response.target_currency || quote).toUpperCase(); const price = parseFloat(response.last_price || response.last || response.price); if (price > 0) { if (!data[rspBase]) data[rspBase] = {}; data[rspBase][rspQuote] = price; // Mirror under pool symbol iff exchange base differs (e.g., pool SAL1 vs exchange SAL) if (rspBase !== poolSymbol) { if (!data[poolSymbol]) data[poolSymbol] = {}; data[poolSymbol][rspQuote] = price; } } else { log('warn', logSystem, 'NonKYC %s returned non-positive price: %j', [pair, response]); } } catch (e) { log('error', logSystem, 'NonKYC parse error for %s: %s', [pair, e && e.stack || e]); } } if (--pending === 0) finish(); }, path); }); } // KlingEx else if (exchange === "klingex") { apiInterfaces.jsonHttpRequest('api.klingex.io', 443, '', function (error, response) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); if (!error && Array.isArray(response)) { response.forEach((item) => { const symbol = String(item.base_currency || '').toUpperCase(); const target = String(item.target_currency || '').toUpperCase(); const price = parseFloat(item.last_price); if (!symbol || !target || !price) return; if (!data[symbol]) data[symbol] = {}; data[symbol][target] = price; }); } if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; callback(null, data); }, '/api/tickers'); } // AnonEx else if (exchange === "anonex") { apiInterfaces.jsonHttpRequest('api.anonex.io', 443, '', function (error, response) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); if (!error && Array.isArray(response)) { response.forEach((item) => { const symbol = String(item.base_currency || '').toUpperCase(); const target = String(item.target_currency || '').toUpperCase(); const price = parseFloat(item.last_price); if (!symbol || !target || !price) return; if (!data[symbol]) data[symbol] = {}; data[symbol][target] = price; }); } if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; callback(null, data); }, '/api/v2/tickers'); } // CoinPaprika else if (exchange === "coinpaprika") { const coinId = getCoinPaprikaId(); if (!coinId) { log('warn', logSystem, 'CoinPaprika: no coin id configured or known for symbol %s', [getConfiguredPoolSymbol()]); marketRequestsCache[cacheKey] = { ts: currentTimestamp, data }; return callback(null, data); } apiInterfaces.jsonHttpRequest('api.coinpaprika.com', 443, '', function (error, response) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); if (!error && response && response.symbol && response.quotes) { const symbol = String(response.symbol).toUpperCase(); Object.keys(response.quotes).forEach((target) => { const price = parseFloat(response.quotes[target] && response.quotes[target].price); if (!price) return; if (!data[symbol]) data[symbol] = {}; data[symbol][String(target).toUpperCase()] = price; }); } if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; callback(null, data); }, '/v1/tickers/' + coinId); } // Exbitron else if (exchange == "exbitron") { apiInterfaces.jsonHttpRequest('api.exbitron.com', 443, '', function (error, response) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); let data = {}; if (!error && response && response.status === "OK" && response.data) { let market = response.data.market; let pairParts = market.id.split('-'); let symbol = pairParts[0]; let target = pairParts[1]; let price = +market.marketDynamics.lastPrice; if (price !== 0) { if (!data[symbol]) data[symbol] = {}; data[symbol][target] = price; } } if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; callback(null, data); }, '/api/v1/trading/info/' + config.symbol + '-USDT'); } else if (exchange == "coingecko") { apiInterfaces.jsonHttpRequest('api.coingecko.com', 443, '', function (error, response) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); if (!error && response) { let matchingCoin = response.filter(coin => { return coin.symbol === config.symbol.toLowerCase() ? coin.name.toLowerCase() : '' }) apiInterfaces.jsonHttpRequest('api.coingecko.com', 443, '', function (error, response) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); let data = {}; if (!error && response.tickers) { for (let model in response.tickers) { target = response.tickers[model].target symbol = response.tickers[model].base price = +response.tickers[model].last if (price === 0) continue; if (!data[symbol]) data[symbol] = {}; data[symbol][target] = price; } } if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; callback(null, data); }, `/api/v3/coins/${matchingCoin[0].id}/tickers`); } }, `/api/v3/coins/list`); } // Unknown else { callback('Exchange not supported: ' + exchange); } } exports.getExchangeMarkets = getExchangeMarkets; /** * Get Exchange Market Price **/ let priceRequestsCache = {}; function getExchangePrice (exchange, base, target, callback) { callback = callback || function () {}; if (!exchange) { callback('No exchange specified'); } else if (!base) { callback('No base specified'); } else if (!target) { callback('No target specified'); } exchange = exchange.toLowerCase(); base = base.toUpperCase(); target = target.toUpperCase(); // Return cache if available let cacheKey = exchange + '-' + base + '-' + target; let currentTimestamp = Date.now() / 1000; let error = null; let price = 0.0; let data = {}; let ticker = null; if (priceRequestsCache[cacheKey] && priceRequestsCache[cacheKey].ts > (currentTimestamp - 60)) { callback(null, priceRequestsCache[cacheKey].data); return; } // Cryptonator if (exchange == "cryptonator") { ticker = base + '-' + target; apiInterfaces.jsonHttpRequest('api.cryptonator.com', 443, '', function (error, response) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); if (response.error) log('warn', logSystem, 'Cryptonator API error: %s', [response.error]); error = response.error ? response.error : error; price = response.success ? +response.ticker.price : null; if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); data = { ticker: ticker, price: price, source: exchange }; if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; callback(error, data); }, '/api/ticker/' + ticker); } else if (exchange == "coinex" || exchange == "mexc" || exchange == "nonkyc" || exchange == "klingex" || exchange == "anonex" || exchange == "exbitron" || exchange == "coingecko" || exchange == "coinpaprika") { getExchangeMarkets(exchange, function (error, marketData) { if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); price = null; if (!error && marketData[base] && marketData[base][target]) { price = marketData[base][target]; } if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); data = { ticker: ticker, price: price, source: exchange }; if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; callback(error, data); }); } // Unknown else { callback('Exchange not supported: ' + exchange); } } exports.getExchangePrice = getExchangePrice;