Files
peya-nodejs-pool/lib/market.js
Codex Bot aa404d185e
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 39s
Update Zephyr pool symbols and examples
2026-03-23 03:14:44 +01:00

528 lines
16 KiB
JavaScript

/**
* 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;