527 lines
16 KiB
JavaScript
527 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 '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;
|