From e52e9c501061a8768d8c9c6c66cc3b982569f318 Mon Sep 17 00:00:00 2001 From: "Chris (Someguy123)" Date: Sun, 22 Mar 2020 02:50:59 +0000 Subject: [PATCH] v3.0.0 - Major updates inc. Hive Support Not everything is listed below, since there has been a lot of refactoring to make `steemfeed-js` more flexible, and there has also been lots of small changes such as cleaning up syntax to make lines more readable. Overall, this is a major update to `steemfeed-js`, making the system more flexible, potentially usable across forks of Steem, and cleaner code making it easier to understand and develop `steemfeed-js`. **New Exchange Adapter** - Added `IonomyAdapter.js`, allowing price feeds to include prices from ionomy.com **Various Improvements** - Refactored handling of config defaults and parsing CLI options into `lib/settings.py` - `publish_feed` now uses `config.base_symbol` and `config.quote_symbol` instead of hardcoded `STEEM` / `SBD` - Refactored the hardcoded `steem` / `usd` default exchange pair lookup into config options `config.ex_symbol` and `config.ex_compare` - Tons of refactoring to `lib/exchange.js` to improve the adapter initialisation process. **New Additions** - New config options - `ex_symbol` - The symbol we're obtaining the price of. Default: `steem` - `ex_compare` - The symbol we're pricing `ex_symbol` with (i.e. the other half of the exchange pair). Default: `usd` - `base_symbol` - The symbol used for the `"base": "0.512 SBD"` part of the feed. Default: `SBD` - `quote_symbol` - The symbol used for the `"quote": "1.000 STEEM"` part of the feed. Default: `STEEM` - `disable_exchanges` which allows the user to disable one or more exchanges if they're down, or returning bad data. - `exchanges_no_provide` - allowing the user to disable individual coin pairs from being used, per each exchange. - `exchanges_provide` - works similarly to `exchanges_no_provide`, except it allows new coin pairs to be added to existing exchanges, instead of removing them. - `network` - allows Steemfeed-JS to work with forks, such as [Hive](https://hive.io) - Created `lib/adapters/base.js`, which contains `BaseAdapter` - an example adapter with comment blocks explaining how you should layout a new exchange adapter. - Added `has_pair` method to all exchange adapters, and added `has_pair` check in all `get_pair` methods. - Added `code` attribute to all exchange adapters, a unique short string used as the "ID" for the adapter. - Added `available_adapters` list to `lib/exchange.js`, allowing adapters to be programatically enabled/disabled etc. **Using Steemfeed-JS with Hive?** Open up your `config.json` and remove any existing Steem `"node"` config line. (or change it to `https://anyx.io`) Add the config line `"network": "hive"` Stop, re-build, and start steemfeed-js, and you'll be broadcasting a Hive feed :) --- .jshintrc | 3 + README.md | 103 +++++++++++++++++++++++++++++--- app.js | 72 ++++++++++------------ config.advanced.json | 42 +++++++++++++ config.example.json | 9 ++- lib/adapters/BTCEAdapter.js | 11 +++- lib/adapters/BinanceAdapter.js | 13 +++- lib/adapters/BittrexAdapter.js | 17 ++++-- lib/adapters/IonomyAdapter.js | 39 ++++++++++++ lib/adapters/KrakenAdapter.js | 15 ++++- lib/adapters/PoloniexAdapter.js | 15 +++-- lib/adapters/base.js | 80 +++++++++++++++++++++++++ lib/cache.js | 4 +- lib/exchange.js | 93 ++++++++++++++++++++++++---- lib/settings.js | 66 ++++++++++++++++++++ tools/get_rate.js | 73 ++++++++++++++++++++++ 16 files changed, 580 insertions(+), 75 deletions(-) create mode 100644 .jshintrc mode change 100644 => 100755 app.js create mode 100644 config.advanced.json create mode 100644 lib/adapters/IonomyAdapter.js create mode 100644 lib/adapters/base.js create mode 100644 lib/settings.js create mode 100755 tools/get_rate.js diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..021cf75 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 9 +} \ No newline at end of file diff --git a/README.md b/README.md index 3acf9cb..e1bbd1f 100644 --- a/README.md +++ b/README.md @@ -68,18 +68,105 @@ Configuration =========== ``` { - "node": "https://steemd.privex.io/", - "name": "your steem name", + "name": "your steem/hive name", "wif": "your active private key", + "network": "steem", "interval": 60, "peg": false, "peg_multi": 1 } ``` -- **node** - The URL of the steem node to use. -- **name** - The name of the steem account that will publish the feed -- **wif** - The active private key for the steem account -- **interval** - The number of minutes between publishing the feed -- **peg** - Set to true only if you want to adjust your price feed bias -- **peg_multi** - If "peg" is set to true, then this will change the "quote" to 1 / peg_multi. If you set "peg_multi" to 2 it will show a 100% bias on your feed. +- **name** (REQUIRED) - The name of the steem account that will publish the feed + +- **wif** (REQUIRED) - The active private key for the steem account + +- **node** (default: `https://api.steemit.com`) - The HTTP(S) URL of the steem node to use, e.g. `https://steemd.privex.io` + +- **interval** (default: `60`) - The number of minutes between publishing the feed + +- **network** (default: `steem`) - The network (chain) you're using this for. Options are: `steem` and `hive` + +- **peg** (default: `false`) - Set to true only if you want to adjust your price feed bias + +- **peg_multi** (default: `1`) - If "peg" is set to true, then this will change the "quote" to 1 / peg_multi. If you set "peg_multi" to 2 it will show a 100% bias on your feed. + + +Advanced Configuration Options +============================== + +**NOTE:** The settings `ex_symbol`, `ex_compare`, `base_symbol` and `quote_symbol` normally do not need to be adjusted. + +Just set the correct `network`, and those settings will be automatically updated to the correct values. + + +`ex_symbol` - The symbol we're obtaining the price of. Default: `steem` + +`ex_compare` - The symbol we're pricing `ex_symbol` with (i.e. the other half of the exchange pair). Default: `usd` + +`base_symbol` - The symbol used for the `"base": "0.512 SBD"` part of the feed. Default: `SBD` + +`quote_symbol` - The symbol used for the `"quote": "1.000 STEEM"` part of the feed. Default: `STEEM` + + +`disable_exchanges` - A list of exchange `code` 's to disable. Exchanges listed here will not be used +directly (i.e. get price for A/B), nor indirectly (i.e. get price for A/D by converting A/C then C/D). + +Example (disable all exchanges...): + +```json +{ + "disable_exchanges": ["bittrex", "poloniex", "kraken", "ionomy", "binance"] +} +``` + +`exchanges_no_provide` - Disable use of specific coin pairs per exchange, for example, you might want +to temporarily ban the usage of STEEM/BTC from Poloniex. + +Example (block STEEM/BTC from poloniex, block BTC/USDT on Kraken): + +```json +{ + "exchanges_no_provide": { + "poloniex": [ + ["steem", "btc"] + ], + "kraken": [ + ["btc", "usdt"] + ] + }, +} +``` + +`exchanges_provide` - Add new coin pairs to exchanges, allowing the user to inform steemfeed-js of +new pairs supported by a given exchange. + +By default, most exchange adapters have the following pairs enabled (if the exchange supports them): + + - BTC/USD + - BTC/USDT + - USDT/USD + - STEEM/BTC + - HIVE/BTC + - STEEM/USD (preferred over STEEM/BTC) + - HIVE/USD (preferred over HIVE/BTC) + +Example (add BTC/DASH, EOS/USD, EOS/BTC to bittrex - add EOS/BTC to kraken): + +```json +{ + "exchanges_provide": { + "bittrex": [ + ["btc","dash"], + ["eos","usd"], + ["eos","btc"] + ], + "kraken": [ + ["eos", "btc"] + ] + } +} +``` + + + diff --git a/app.js b/app.js old mode 100644 new mode 100755 index 05074b1..7be0f0b --- a/app.js +++ b/app.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node /** * * Node.JS pricefeed script for steem @@ -7,22 +8,20 @@ * Requires Node v8.11.4 */ -var config = require('./config.json'); +var settings = require('./lib/settings'); var exchange = require('./lib/exchange'); var request = require('request'); var steem = require('steem'); -if(!('node' in config)) { config['node'] = 'https://steemd.privex.io/'; } -// disable peg by default. 0% peg (bias) -if(!('peg' in config)) { config['peg'] = false; } -if(!('peg_multi' in config)) { config['peg_multi'] = 1; } +var config = settings.config, + retry_conf = settings.retry_conf; -console.log('-------------') +console.log('-------------'); log(`Loaded configuration: Username: ${config.name} Bias: ${config.peg ? config.peg_multi : 'Disabled'} -RPC Node: ${config.node}`) -console.log('-------------') +RPC Node: ${config.node}`); +console.log('-------------'); global.verbose = false; for(var t_arg of process.argv) { @@ -31,22 +30,11 @@ for(var t_arg of process.argv) { } } -steem.api.setOptions({ url: config['node'] }); +steem.api.setOptions({ url: config.node }); // used for re-trying failed promises function delay(t) { - return new Promise((r_resolve) => { - setTimeout(r_resolve, t); - }); -} - -// Attempts = how many times to allow an RPC problem before giving up -// Delay = how long before a retry -var retry_conf = { - feed_attempts: 10, - feed_delay: 60, - login_attempts: 6, - login_delay: 10 + return new Promise((r_resolve) => setTimeout(r_resolve, t) ); } class SteemAcc { @@ -90,9 +78,9 @@ class SteemAcc { console.error('Most likely the RPC node is down.'); var msg = ('message' in err) ? err.message : err; console.error('The error returned was:', msg); - if(tries < retry_conf['login_attempts']) { - console.error(`Will retry in ${retry_conf['login_delay']} seconds`); - return delay(retry_conf['login_delay'] * 1000) + if(tries < retry_conf.login_attempts) { + console.error(`Will retry in ${retry_conf.login_delay} seconds`); + return delay(retry_conf.login_delay * 1000) .then(() => resolve(this.loadAccount(reload, tries+1))) .catch((e) => reject(e)); } @@ -116,7 +104,7 @@ class SteemAcc { }; this.user_data = ud; return resolve(ud); - }) + }); }); } @@ -143,16 +131,16 @@ class SteemAcc { publish_feed(rate, tries=0) { try { // var tr = new TransactionBuilder(); - var ex_data = rate.toFixed(3) + " SBD"; + var ex_data = rate.toFixed(3) + ` ${config.base_symbol}`; var quote = 1; if(config.peg) { - var pcnt = ((1 - config['peg_multi']) * 100).toFixed(2) + var pcnt = ((1 - config.peg_multi) * 100).toFixed(2); log('Pegging is enabled. Reducing price by '+pcnt+'% (set config.peg to false to disable)'); log('Original price (pre-peg):', ex_data); - quote = 1 / config['peg_multi']; + quote = 1 / config.peg_multi; } - var exchangeRate = {base: ex_data, quote: quote.toFixed(3) + " STEEM"} + var exchangeRate = {base: ex_data, quote: quote.toFixed(3) + ` ${config.quote_symbol}`}; var {username, active_wif} = this.user_data; steem.broadcast.feedPublish(active_wif, username, exchangeRate, (err, r) => { @@ -160,18 +148,18 @@ class SteemAcc { console.error('Failed to publish feed...'); var msg = ('message' in err) ? err.message : err; console.error('reason:', msg); - if(tries < retry_conf['feed_attempts']) { - console.error(`Will retry in ${retry_conf['feed_delay']} seconds`); - return delay(retry_conf['feed_delay'] * 1000) + if(tries < retry_conf.feed_attempts) { + console.error(`Will retry in ${retry_conf.feed_delay} seconds`); + return delay(retry_conf.feed_delay * 1000) .then(() => this.publish_feed(rate, tries+1)) .catch(console.error); } - console.error(`Giving up. Tried ${tries} times`) + console.error(`Giving up. Tried ${tries} times`); return reject(err); } - log('Data published at: ', ""+new Date()) + log('Data published at: ', ""+new Date()); log('Successfully published feed.'); - log(`TXID: ${r.id} TXNUM: ${r.trx_num}`) + log(`TXID: ${r.id} TXNUM: ${r.trx_num}`); }); } catch(e) { console.error(e); @@ -190,8 +178,11 @@ try { var shouldPublish = process.argv.length > 2 && process.argv[2] == "publishnow"; var dryRun = process.argv.length > 2 && process.argv[2] == "dry"; +var cap_sym = config.ex_symbol.toUpperCase(), + cap_comp = config.ex_compare.toUpperCase(); + function get_price(callback) { - exchange.get_pair('steem','usd', + exchange.get_pair( config.ex_symbol, config.ex_compare, (err, price) => callback(err, parseFloat(price)) ); } @@ -201,13 +192,12 @@ function main() { if(err) { return console.error('error loading prices, will retry later'); } - log('STEEM/USD is ', price.toFixed(3)); - log('Attempting to publish feed...') + log(`${cap_sym}/${cap_comp} is: ${price.toFixed(3)} ${cap_comp} per ${cap_sym}`); + log('Attempting to publish feed...'); if(!dryRun) { accountmgr.publish_feed(price); } else { - console.log('Dry Run. Not actually publishing.') - + console.log('Dry Run. Not actually publishing.'); } }); } @@ -229,7 +219,7 @@ accountmgr.login().then((user_data) => { var interval = parseInt(config.interval) * 1000 * 60; setInterval(() => main(), interval); }).catch((e) => { - console.error(`An error occurred attempting to log into ${config.name}... Exiting`) + console.error(`An error occurred attempting to log into ${config.name}... Exiting`); console.error('Reason:', e); process.exit(1); }); diff --git a/config.advanced.json b/config.advanced.json new file mode 100644 index 0000000..d1f6268 --- /dev/null +++ b/config.advanced.json @@ -0,0 +1,42 @@ +{ + "name": "your steem name", + "wif": "your active private key", + "interval": 60, + "peg": false, + "peg_multi": 1.0, + "node": "https://steemd.privex.io", + "ex_symbol": "steem", + "ex_compare": "usd", + "base_symbol": "SBD", + "quote_symbol": "STEEM", + "read_this_disable": [ + "If an exchange is down, or has issues resulting in inaccurate prices,", + "then you can add exchanges to the disable_exchanges list, resulting in those ", + "exchanges never being used for any price data." + ], + "disable_exchanges": [ + "poloniex", "ionomy" + ], + "exchanges_no_provide": { + "read_this": [ + "The below example, disables the use of the BTC:STEEM pair for Poloniex.", + "This feature allows you to disable specific pairs per exchange, if they're", + "unstable, broken, or reporting bad data." + ], + "poloniex": [ + ["btc", "steem"] + ] + }, + "exchanges_provide": { + "read_this": [ + "This feature allows you to enable additional pairs for existing exchanges, which", + "aren't enabled by default", + "The below example, would add the pairs BTC/DASH and USD/EOS to the 'pairs provided' list", + "of the Bittrex adapter." + ], + "bittrex": [ + ["btc","dash"], + ["usd","eos"] + ] + } +} diff --git a/config.example.json b/config.example.json index c843173..89e8012 100644 --- a/config.example.json +++ b/config.example.json @@ -1,7 +1,14 @@ { "name": "your steem name", "wif": "your active private key", + "network": "steem", + "interval": 60, + "peg": false, - "peg_multi": 1.0 + "peg_multi": 1.0, + + "disable_exchanges": [], + "exchanges_no_provide": {}, + "exchanges_provide": {} } diff --git a/lib/adapters/BTCEAdapter.js b/lib/adapters/BTCEAdapter.js index 0708719..f7faa9e 100644 --- a/lib/adapters/BTCEAdapter.js +++ b/lib/adapters/BTCEAdapter.js @@ -1,12 +1,17 @@ -var request = require('request'); +var request = require('request'), + BaseAdapter = require('./base'); var BTCEAdapter = { name: 'BTC-e', + code: 'btce', provides: [ ['btc','usd'], ['btc','eur'], ['btc','ltc'] ], + + has_pair: (from, to) => BaseAdapter.has_pair_ext(from, to, BTCEAdapter.provides), + get_pair: function(from,to,callback) { if(['usd', 'eur', 'gbp', 'rub'].indexOf(to) == -1) { // btc-e is backwards for altcoins, so flip the pair! @@ -15,6 +20,10 @@ var BTCEAdapter = { from = tmp; } + if (!BTCEAdapter.has_pair(from, to)) { + return callback(`Pair ${from}/${to} is not supported by this adapter.`, null); + } + var pair = [from,to].join('_'), ticker_url = 'https://btc-e.com/api/2/'+pair+'/ticker'; diff --git a/lib/adapters/BinanceAdapter.js b/lib/adapters/BinanceAdapter.js index 56cab79..68c3800 100644 --- a/lib/adapters/BinanceAdapter.js +++ b/lib/adapters/BinanceAdapter.js @@ -1,14 +1,24 @@ -var request = require('request'); +var request = require('request'), + BaseAdapter = require('./base'); var BinanceAdapter = { name: 'Binance', + code: 'binance', provides: [ ['btc','usdt'], ['steem','btc'] ], + + has_pair: (from, to) => BaseAdapter.has_pair_ext(from, to, BinanceAdapter.provides), + get_pair: function(from, to, callback) { if(from == 'usd') from = 'usdt'; if(to == 'usd') to = 'usdt'; + + if (!BinanceAdapter.has_pair(from, to)) { + return callback(`Pair ${from}/${to} is not supported by this adapter.`, null); + } + var p_cached_data = cache.get('binance_data'); if(p_cached_data === null) return BinanceAdapter.load_data(function(err) { if(err) return callback(true,null) @@ -16,6 +26,7 @@ var BinanceAdapter = { }); return BinanceAdapter._get_pair(from,to,callback); }, + load_data: function(callback) { try { request('https://api.binance.com/api/v1/ticker/24hr', function(error,response,body) { diff --git a/lib/adapters/BittrexAdapter.js b/lib/adapters/BittrexAdapter.js index 9a41465..97cf6c9 100644 --- a/lib/adapters/BittrexAdapter.js +++ b/lib/adapters/BittrexAdapter.js @@ -1,22 +1,31 @@ -var request = require('request'); +var request = require('request'), + BaseAdapter = require('./base'); var BittrexAdapter = { name: 'Bittrex', + code: 'bittrex', provides: [ ['steem','btc'], ['sbd','btc'], + ['hive','btc'], ['btc','usdt'], // ['btc','usd'], ['usdt','usd'], + ['usd','usdt'], ['ltc','btc'] ], + has_pair: (from, to) => BaseAdapter.has_pair_ext(from, to, BittrexAdapter.provides), + get_pair: function(from,to,callback) { + if (!BittrexAdapter.has_pair(from, to)) { + return callback(`Pair ${from}/${to} is not supported by this adapter.`, null); + } + // bittrex pairs are all backwards, so flip the pair! - var tmp = to; - to = from; - from = tmp; + var tmp = to; to = from; from = tmp; var pair = [from,to].join('-'), ticker_url = 'https://bittrex.com/api/v1.1/public/getticker?market='+pair; + request(ticker_url, function(error,response,body) { if(error || response.statusCode != 200) { console.error('Invalid response code or server error:',error,response.statusCode); diff --git a/lib/adapters/IonomyAdapter.js b/lib/adapters/IonomyAdapter.js new file mode 100644 index 0000000..5723dee --- /dev/null +++ b/lib/adapters/IonomyAdapter.js @@ -0,0 +1,39 @@ +var request = require('request'), + BaseAdapter = require('./base'); + +var IonomyAdapter = { + name: 'Ionomy', + code: 'ionomy', + provides: [ + ['hive','btc'], + ['steem','btc'], + ], + + has_pair: (from, to) => BaseAdapter.has_pair_ext(from, to, IonomyAdapter.provides), + + get_pair: function(from,to,callback) { + if (!IonomyAdapter.has_pair(from, to)) { + return callback(`Pair ${from}/${to} is not supported by this adapter.`, null); + } + // Ionomy pairs are all backwards, so flip the pair! + var tmp = to; to = from; from = tmp; + + var pair = [from,to].join('-'), + ticker_url = 'https://ionomy.com/api/v1/public/market-summary?market='+pair; + request(ticker_url, function(error,response,body) { + if(error || response.statusCode != 200) { + console.error('Invalid response code or server error:',error,response.statusCode); + return callback(true,null); + } + var ticker_data = JSON.parse(body); + var success = ('success' in ticker_data) && ticker_data['success'] == true; + if(!success || !('data' in ticker_data) || !('price' in ticker_data['data'])) { + return callback(true,null); + } + return callback(false, ticker_data['data']['price']); + + }); + }, +} + +module.exports = IonomyAdapter; diff --git a/lib/adapters/KrakenAdapter.js b/lib/adapters/KrakenAdapter.js index 27b02d3..55a148c 100644 --- a/lib/adapters/KrakenAdapter.js +++ b/lib/adapters/KrakenAdapter.js @@ -1,15 +1,24 @@ -var request = require('request'); +var request = require('request'), + BaseAdapter = require('./base'); var KrakenAdapter = { name: 'Kraken', + code: 'kraken', provides: [ ['usdt', 'usd'], - // ['btc', 'usd'] + ['btc', 'usdt'] ], + + has_pair: (from, to) => BaseAdapter.has_pair_ext(from, to, KrakenAdapter.provides), + + get_pair: function(from,to,callback) { + if (!KrakenAdapter.has_pair(from, to)) { + return callback(`Pair ${from}/${to} is not supported by this adapter.`, null); + } if(from == 'btc') from = 'xbt'; if(to == 'btc') to = 'xbt'; - + var pair = [from,to].join('').toUpperCase(), ticker_url = 'https://api.kraken.com/0/public/Ticker?pair='+pair; request(ticker_url, function(error,response,body) { diff --git a/lib/adapters/PoloniexAdapter.js b/lib/adapters/PoloniexAdapter.js index 3bc291d..832deac 100644 --- a/lib/adapters/PoloniexAdapter.js +++ b/lib/adapters/PoloniexAdapter.js @@ -1,17 +1,24 @@ -var request = require('request'); +var request = require('request'), + BaseAdapter = require('./base'); var PoloniexAdapter = { name: 'Poloniex', + code: 'poloniex', provides: [ ['btc','usdt'], ['steem', 'btc'], ['btc','ltc'] ], + + has_pair: (from, to) => BaseAdapter.has_pair_ext(from, to, PoloniexAdapter.provides), + get_pair: function(from, to, callback) { + if (!PoloniexAdapter.has_pair(from, to)) { + return callback(`Pair ${from}/${to} is not supported by this adapter.`, null); + } // poloniex is backwards, so flip the pair! - var tmp = to; - to = from; - from = tmp; + var tmp = to; to = from; from = tmp; + var p_cached_data = cache.get('poloniex_data'); if(p_cached_data === null) return PoloniexAdapter.load_data(function(err) { if(err) return callback(true,null) diff --git a/lib/adapters/base.js b/lib/adapters/base.js new file mode 100644 index 0000000..58f3e97 --- /dev/null +++ b/lib/adapters/base.js @@ -0,0 +1,80 @@ + +var BaseAdapter = { + /** + * Human name for exchange / adapter + */ + name: 'Base Adapter', + /** + * A short, simple, unique "code" for referring to this adapter/exchange. + * No spaces / uppercase / special characters + */ + code: 'baseadapter', + + /** + * The 'provides' list should contain a list of pairs - written as 2-item lists. + * These should be lowercase and entered in ``['from', 'to']`` format. + */ + provides: [ + ['fake','btc'], + ['unreal','eth'], + ], + /** + * Returns ``true`` if the given ``from`` / ``to`` pair is found in ``provides`` + * This method is designed to be called by external methods / functions: + * + * prov = [['btc', 'ltc'], ['ltc', 'usd']] + * BaseAdapter.has_pair_ext('btc', 'ltc', prov) + * // true + * BaseAdapter.has_pair_ext('btc', 'usd', prov) + * // false + * + * @param {string} from + * @param {string} to + * @param {Array} provides + */ + has_pair_ext: function(from, to, provides) { + from = from.toLowerCase(); to = to.toLowerCase(); + for (var p of provides) { + if (p[0] == from && p[1] == to) return true; + } + return false; + }, + + has_pair: (from, to) => BaseAdapter.has_pair_ext(from, to, BaseAdapter.provides), + + /** + * get_pair - Return the price for a given asset pair + * In this method, your adapter should attempt to validate the given from/to pair, + * then query the exchange for the price per coin. + * + * Be warned! Some exchanges do not understand that there's a standard for from/to pairs, + * for example 'BTC/USD' should be priced in "amount of USD per BTC", while 'USD/BTC' means + * "amount of BTC per USD". + * + * Many exchanges don't respect this standard, and may have either **all** pairs in the wrong + * direction, or specific pairs such as USD pairs in the wrong direction. + * + * This function should attempt to correct any pairs which don't follow the standard on the + * exchange used. + * + * Example (swap from/to for exchanges that have backwards pairs): + * + * var tmp = to; to = from; from = tmp; + * + * @param {string} from + * @param {string} to + * @param {Function} callback + */ + get_pair: function(from,to,callback) { + if (!BaseAdapter.has_pair(from, to)) { + return callback(`Pair ${from}/${to} is not supported by this adapter.`, null); + } + ticker_data = { + result: { last: 0.00012321 } + }; + + return callback(false, ticker_data.result.Last); + } +}; + +module.exports = BaseAdapter; diff --git a/lib/cache.js b/lib/cache.js index b6ee961..62bbf89 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -24,10 +24,10 @@ var cache = { } var data = cache_data[key]; - if(data['expires'] < now()) + if(data.expires < now()) return null; - return data['value']; + return data.value; }, get_or_set: function(key,value,timeout,callback) { var data = cache.get(key); diff --git a/lib/exchange.js b/lib/exchange.js index 3f780f7..abd020c 100644 --- a/lib/exchange.js +++ b/lib/exchange.js @@ -8,7 +8,8 @@ */ var request = require('request'), - cache = require('./cache'); + cache = require('./cache') + settings = require('./settings'); global.cache = cache; require('./helpers'); @@ -16,9 +17,20 @@ require('./helpers'); //var BTCEAdapter = require('./adapters/BTCEAdapter'), var PoloniexAdapter = require('./adapters/PoloniexAdapter'), BittrexAdapter = require('./adapters/BittrexAdapter'), + IonomyAdapter = require('./adapters/IonomyAdapter'), KrakenAdapter = require('./adapters/KrakenAdapter'), BinanceAdapter = require('./adapters/BinanceAdapter') ; + +var available_adapters = [ + PoloniexAdapter, + BittrexAdapter, + BinanceAdapter, + KrakenAdapter, + IonomyAdapter +]; + + /** * This file handles communication * with various exchanges @@ -291,22 +303,81 @@ function get_pair(from,to,callback) { } var avg = get_avg(output); dbg_log(green(`[get_pair] (${from}/${to}) - Exchange Prices:`), output) - dbg_log(green(`[get_pair] (${from}/${to}) - Median average price: ${avg.toFixed(5)} ${to.toUpperCase()} per ${from.toUpperCase()}`)) + dbg_log(green(`[get_pair] (${from}/${to}) - Median average price: ${avg.toFixed(8)} ${to.toUpperCase()} per ${from.toUpperCase()}`)) return callback(false, avg); }, function(e) { log(red('Problem loading prices... Will try to continue'), e); return callback(true, null) } ); } -add_adapter(PoloniexAdapter); -// add_adapter(BTCEAdapter); -add_adapter(BittrexAdapter); -add_adapter(BinanceAdapter); -add_adapter(KrakenAdapter); +/** + * Convert a list of ``provides`` pairs, into a readable string + * e.g. ``steem:btc, sbd:btc, hive:btc, btc:usdt, usdt:usd`` + * + * @param {Array>} data + */ +function readable_provides(data) { + var out = ""; + + for (var p of data) { + out = out + `${p[0]}:${p[1]}, ` + } + // Trim trailing space/comma + if (out[out.length - 1] == ' ') out = out.substr(0, out.length - 1); + if (out[out.length - 1] == ',') out = out.substr(0, out.length - 1); + + return out; +} + +var ex_provide_block = settings.config.exchanges_no_provide, + ex_provide_add = settings.config.exchanges_provide; + +_get_blocked_pairs = (code) => (code in ex_provide_block) ? ex_provide_block[code] : []; +_get_added_pairs = (code) => (code in ex_provide_add) ? ex_provide_add[code] : []; + +/** + * Remove the coin pair ``pair`` from the ``provides`` list of the adapter ``adapter``. + * + * @param {*} adapter - An exchange adapter such as BittrexAdapter / BinanceAdapter + * @param {Array} pair - A coin pair, e.g. ``['steem', 'btc']`` + */ +function _rm_pair(adapter, pair) { + var a = adapter; + if (adapter.has_pair(pair[0], pair[1])) { + dbg_log(`Removing pair ${pair[0]}/${pair[1]} from adapter '${adapter.code}'`); + adapter.provides = adapter.provides.filter(pv => pv[0] != pair[0] && pv[1] != pair[1]); + dbg_log(`New provides for '${adapter.code}': ${readable_provides(adapter.provides)} `); + } +} + +// Iterate through ``available_adapter``, skipping disabled adapters, and handling +// added/blocked pairs from the user config - then initialise enabled adapters. +for (var a of available_adapters) { + + if(settings.config.disable_exchanges.indexOf(a.code) > -1) { + log(`Exchange adapter for '${a.name}' is disabled. Not initialising adapter.`); + } else { + // Remove any disabled pairs specified in config.exchanges_no_provide + for (var pair of _get_blocked_pairs(a.code)) { + _rm_pair(a, pair); + } + // Add any additional pairs specified in config.exchanges_provide + for (var pair of _get_added_pairs(a.code)) { + dbg_log(`Adding extra pair ${pair[0]}/${pair[1]} to adapter '${a.code}'`); + a.provides.push(pair); + } + + var rp = readable_provides(a.provides); + dbg_log(`Initialising adapter for exchange '${a.name}' - provides pairs: ${rp}`); + add_adapter(a); + } +} + // Simple usage of this file: +// var exchange = require('./lib/exchange.js'); // -// get_pair('steem','usd', function(err, price) { +// exchange.get_pair('steem','usd', function(err, price) { // log('Median price is: ', parseFloat(price).toFixed(3)); // }); @@ -314,5 +385,7 @@ module.exports = { get_pair: get_pair, get_avg: get_avg, add_adapter: add_adapter, - get_adapters: get_adapters -} + get_adapters: get_adapters, + available_adapters: available_adapters, +}; + diff --git a/lib/settings.js b/lib/settings.js new file mode 100644 index 0000000..5d0faa9 --- /dev/null +++ b/lib/settings.js @@ -0,0 +1,66 @@ +var config = require('../config.json'); + +var settings = { + shouldPublish: false, + dryRun: false, + // Attempts = how many times to allow an RPC problem before giving up + // Delay = how long before a retry + retry_conf: { + feed_attempts: 10, + feed_delay: 60, + login_attempts: 6, + login_delay: 10 + } +}; + + +var defaults = { + steem: { + node: 'https://api.steemit.com/', + ex_symbol: 'steem', ex_compare: 'usd', + base_symbol: 'SBD', quote_symbol: 'STEEM', + }, + // steem-js requires we reference STEEM / SBD instead of HIVE / HBD + hive: { + node: 'https://anyx.io/', + ex_symbol: 'hive', ex_compare: 'usd', + base_symbol: 'SBD', quote_symbol: 'STEEM', + } +}; + +if(!('network' in config)) { config.network = 'steem'; } + +// Contains defaults for the network selected by the user +var ndef = defaults[config.network]; + +if(!('node' in config)) { config.node = ndef.node; } +// disable peg by default. 0% peg (bias) +if(!('peg' in config)) { config.peg = false; } +if(!('peg_multi' in config)) { config.peg_multi = 1; } +if(!('interval' in config)) { config.interval = 60; } + +if(!('ex_symbol' in config)) { config.ex_symbol = ndef.ex_symbol; } +if(!('ex_compare' in config)) { config.ex_compare = ndef.ex_compare; } +if(!('base_symbol' in config)) { config.base_symbol = ndef.base_symbol; } +if(!('quote_symbol' in config)) { config.quote_symbol = ndef.quote_symbol; } + +if(!('disable_exchanges' in config)) { config.disable_exchanges = []; } +if(!('exchanges_no_provide' in config)) { config.exchanges_no_provide = {}; } +if(!('exchanges_provide' in config)) { config.exchanges_provide = {}; } + +config.ex_symbol = config.ex_symbol.toLowerCase(); +config.ex_compare = config.ex_compare.toLowerCase(); +config.quote_symbol = config.quote_symbol.toUpperCase(); +config.base_symbol = config.base_symbol.toUpperCase(); + +// Parse any command line arguments +global.verbose = false; +for(var t_arg of process.argv) { + if(t_arg == '-v') global.verbose = true; + if (t_arg == "publishnow") settings.shouldPublish = true; + if (t_arg == "dry") settings.dryRun = true; +} + +settings.config = config; + +module.exports = settings; diff --git a/tools/get_rate.js b/tools/get_rate.js new file mode 100755 index 0000000..0affbdd --- /dev/null +++ b/tools/get_rate.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * get_rate.js + * A small tool for quickly checking the calculated exchange rate for a given + * coin pair. + * + * Usage: + * # With no arguments, gets the STEEM/USD exchange rate + * ./tools/get_rate.js + * ./tools/get_rate.js -v + * # Get the HIVE/USD exchange rate via any exchange possible (including via proxy rates) + * ./tools/get_rate.js hive usd + * + */ +var exchange = require('../lib/exchange'); + +var args_processed = 0, + pair_base = "steem", + pair_quote = "usd", + pair_exchange = null; + + +for(var t_arg of process.argv) { + if(t_arg == '-v') { + global.verbose = true; + continue; + } + if (args_processed == 2) pair_base = t_arg; + if (args_processed == 3) pair_quote = t_arg; + if (args_processed == 4) pair_exchange = t_arg; + args_processed += 1; +} + +pair_base = pair_base.toLowerCase(); +pair_quote = pair_quote.toLowerCase(); +var pb = pair_base.toUpperCase(), pq = pair_quote.toUpperCase(); + +if (pair_exchange == null) { + console.log(`Getting last price for pair: ${pb}/${pq}`); + exchange.get_pair( + pair_base, pair_quote, (err, price) => { + if(err) { + console.error("get_pair returned an error: " + err.toString()); + return console.error('error loading prices, please retry later'); + } + console.log(`${pb}/${pq} is ${price.toFixed(6)} ${pq} per 1 ${pb}`); + } + ); +} else { + console.log(`Getting last price for pair: ${pb}/${pq} - using only exchange '${pair_exchange}'`); + var adapter = null; + for (var a of exchange.available_adapters) { + if (a.code == pair_exchange.toLowerCase() || a.name == pair_exchange) { + adapter = a; + } + } + + if (adapter == null) { + console.error(`ERROR: Could not find adapter with name/code '${pair_exchange}'`) + process.exit(1); + } + + adapter.get_pair(pair_base, pair_quote, (err, price) => { + if(err) { + console.error("get_pair returned an error: " + err.toString()); + return console.error('error loading prices, please retry later'); + } + console.log(`Using specific exchange ${pair_exchange} for price:`); + console.log(`${pb}/${pq} is ${price.toFixed(6)} ${pq} per 1 ${pb}`); + }); + +} +