UNPKG

@abcpros/bitcore-wallet-service

Version:
429 lines (395 loc) 13.6 kB
import * as async from 'async'; import _, { countBy, reject } from 'lodash'; import * as request from 'request'; import { Storage } from './storage'; const $ = require('preconditions').singleton(); const Common = require('./common'); const Defaults = Common.Defaults; const Constants = Common.Constants; const config = require('../config'); const Bitcore = require('@abcpros/bitcore-lib'); const ELECTRICITY_RATE = config.fiatRateServiceOpts.lotusFormula.ELECTRICITY_RATE; const MINER_MARGIN = config.fiatRateServiceOpts.lotusFormula.MINER_MARGIN; const RIG_HASHRATE = config.fiatRateServiceOpts.lotusFormula.RIG_HASHRATE; const RIG_POWER = config.fiatRateServiceOpts.lotusFormula.RIG_POWER; const MINING_EFFICIENCY = RIG_HASHRATE / RIG_POWER; import { BlockChainExplorer } from './blockchainexplorer'; import logger from './logger'; import { EtokenSupportPrice } from './model/config-model'; export class FiatRateService { request: request.RequestAPI<any, any, any>; defaultProvider: any; cryptoCompareApiKey: string = ''; providers: any[]; storage: Storage; init(opts, cb) { opts = opts || {}; this.request = opts.request || request; this.defaultProvider = opts.defaultProvider || Defaults.FIAT_RATE_PROVIDER; this.cryptoCompareApiKey = opts.cryptoCompareApiKey; async.parallel( [ done => { if (opts.storage) { this.storage = opts.storage; done(); } else { this.storage = new Storage(); this.storage.connect(opts.storageOpts, done); } } ], err => { if (err) { logger.error(err); } return cb(err); } ); } startCron(opts, cb) { opts = opts || {}; this.providers = _.values(require('./fiatrateproviders')); const interval = opts.fetchInterval || Defaults.FIAT_RATE_FETCH_INTERVAL; if (interval) { this._fetch(); setInterval(() => { this._fetch(); }, interval * 60 * 1000); } return cb(); } async handleRateCurrencyCoin(res, listRate) { let newData = []; const valueUsd = _.get( _.find(res, item => item.code == 'USD'), 'value', 0 ); return new Promise((resolve, reject) => { _.forEach(listRate, (rate: any) => { newData.push({ code: rate.code, value: valueUsd * rate.value }); }); return resolve(newData); }); } _getProviderRate(coin) { let nameProvider = this.defaultProvider; const etoken = this._getEtokenSupportPrice(); if (coin == 'xpi') { nameProvider = 'LotusExbitron'; } else if (_.includes(etoken, coin)) { nameProvider = 'EtokenPrice'; } else { nameProvider = this.defaultProvider; } return _.find(this.providers, provider => provider.name === nameProvider); } getLatestCurrencyRates(opts): Promise<any> { return new Promise((resolve, reject) => { const now = Date.now(); const ts = opts.ts ? opts.ts : now; let fiatFiltered = []; let rates = []; if (opts.code) { fiatFiltered = _.filter(Defaults.FIAT_CURRENCIES, ['code', opts.code]); if (!fiatFiltered.length) return reject(opts.code + ' is not supported'); } const currencies: { code: string; name: string }[] = fiatFiltered.length ? fiatFiltered : Defaults.SUPPORT_FIAT_CURRENCIES; const promiseList = []; _.forEach(currencies, currency => { promiseList.push(this._getCurrencyRate(currency.code, ts)); }); Promise.all(promiseList).then(listRate => { return resolve(listRate); }); }); } _getCurrencyRate(code, ts): Promise<any> { return new Promise((resolve, reject) => { this.storage.fetchCurrencyRates(code, ts, async (err, res) => { if (err) { logger.warn('Error fetching data for ' + code, err); } return resolve(res); }); }); } _getEtokenSupportPrice() { const etokenSupportPrice = _.get(config, 'etoken.etokenSupportPrice', undefined); if (!etokenSupportPrice) return []; return _.map(etokenSupportPrice, 'coin'); } async _fetch(cb?) { cb = cb || function() {}; let coinsData = ['btc', 'bch', 'xec', 'eth', 'xrp', 'doge', 'xpi', 'ltc']; const etoken = this._getEtokenSupportPrice(); const coins = _.concat(coinsData, etoken); const listRate = await this.getLatestCurrencyRates({}); if (listRate) { async.eachSeries( coins, async (coin, next2) => { const provider = this._getProviderRate(coin); this._retrieve(provider, coin, async (err, res) => { if (err) { logger.warn('Error retrieving data for ' + provider.name + coin, err); return next2(); } res = await this.handleRateCurrencyCoin(res, listRate); this.storage.storeFiatRate(coin, res, err => { if (err) { logger.warn('Error storing data for ' + provider.name, err); } return next2(); }); }); }, cb ); } } async _retrieve(provider, coin, cb) { if (coin === 'xpi') { return this._retrieveLotus(cb); } logger.debug(`Fetching data for ${provider.name} / ${coin} `); let params = []; let appendString = ''; let headers = provider.headers ?? ''; if (provider.name === 'CryptoCompare') { params = provider.params; params['fsym'] = coin.toUpperCase(); } else if (provider.name === 'Coingecko') { params = provider.params; params['ids'] = provider.coinMapping[coin]; } else if (provider.name === 'LotusExplorer') { appendString = ''; } else if (provider.name === 'LotusExbitron') { appendString = ''; } else if (provider.name === 'EtokenPrice') { try { const etokenSupportPrice: EtokenSupportPrice[] = _.get(config, 'etoken.etokenSupportPrice', []); if (!etokenSupportPrice) return cb('no etoken supported'); let currencyRate = null; if (coin.toLowerCase() === 'elps') { currencyRate = await this.getLatestCurrencyRates({ code: 'HNL' }); } const body = await provider.getRate( coin, etokenSupportPrice, currencyRate && currencyRate[0] ? currencyRate[0] : null ); const rates = _.filter(body, x => _.some(Defaults.FIAT_CURRENCIES, ['code', x.code])); return cb(null, rates); } catch (e) { return cb(e); } } else { appendString = coin.toUpperCase(); } this.request.get( { url: provider.url + appendString, qs: params, useQuerystring: true, headers, json: true }, (err, res, body) => { if (err || !body) { return cb(err); } logger.debug(`Data for ${provider.name} / ${coin} fetched successfully`); if (!provider.parseFn) { return cb(new Error('No parse function for provider ' + provider.name)); } try { const rates = _.filter(provider.parseFn(body), x => _.some(Defaults.FIAT_CURRENCIES, ['code', x.code])); return cb(null, rates); } catch (e) { return cb(e); } } ); } _retrieveLotus(cb) { logger.debug('Fetching data for lotus'); const bc = BlockChainExplorer({ coin: 'xpi', network: 'livenet', url: config.blockchainExplorerOpts.xpi.livenet.url }); bc.getBlockBits((err, bits) => { if (err) return cb(err); const currentDiff = Bitcore.BlockHeader({ bits }).getDifficulty(); let lotusPrice = 0; const networkHashRate = ((2 ** 48 / 65535 / (2 * 60)) * currentDiff) / 1000 / 1000 / 1000; const currentMinerReward = Math.round((Math.log2(currentDiff / 16) + 1) * 130); const dailyElectricityCost = (((networkHashRate / MINING_EFFICIENCY) * 24) / 1000) * ELECTRICITY_RATE; const lotusCost = dailyElectricityCost / currentMinerReward / 30 / 24; lotusPrice = lotusCost * (1 + MINER_MARGIN); return cb(null, [{ code: 'USD', value: lotusPrice }]); }); } getRate(opts, cb) { $.shouldBeFunction(cb, 'Failed state: type error (cb not a function) at <getRate()>'); opts = opts || {}; const now = Date.now(); let coin = opts.coin || 'btc'; // const provider = opts.provider || this.defaultProvider; const ts = _.isNumber(opts.ts) || _.isArray(opts.ts) ? opts.ts : now; async.map( [].concat(ts), (ts, cb) => { if (coin === 'wbtc') { logger.info('Using btc for wbtc rate.'); coin = 'btc'; } this.storage.fetchFiatRate(coin, opts.code, ts, (err, rate) => { if (err) return cb(err); if (rate && ts - rate.ts > Defaults.FIAT_RATE_MAX_LOOK_BACK_TIME * 60 * 1000) rate = null; return cb(null, { ts: +ts, rate: rate ? rate.value : undefined, fetchedOn: rate ? rate.ts : undefined }); }); }, (err, res: any) => { if (err) return cb(err); if (!_.isArray(ts)) res = res[0]; return cb(null, res); } ); } getRates(opts, cb) { $.shouldBeFunction(cb, 'Failed state: type error (cb not a function) at <getRates()>'); opts = opts || {}; const now = Date.now(); const ts = opts.ts ? opts.ts : now; let fiatFiltered = []; let rates = []; if (opts.code) { fiatFiltered = _.filter(Defaults.FIAT_CURRENCIES, ['code', opts.code]); if (!fiatFiltered.length) return cb(opts.code + ' is not supported'); } const currencies: { code: string; name: string }[] = fiatFiltered.length ? fiatFiltered : Defaults.SUPPORT_FIAT_CURRENCIES; const etoken = this._getEtokenSupportPrice(); const coins = _.concat(_.values(Constants.COINS), etoken); async.map( coins, (coin, cb) => { rates[coin] = []; async.map( currencies, (currency, cb) => { let c = coin; if (coin === 'wbtc') { logger.info('Using btc for wbtc rate.'); c = 'btc'; } this.storage.fetchFiatRate(c, currency.code, ts, (err, rate) => { if (err) return cb(err); if (rate && ts - rate.ts > Defaults.FIAT_RATE_MAX_LOOK_BACK_TIME * 60 * 1000) rate = null; return cb(null, { ts: +ts, rate: rate ? rate.value : undefined, fetchedOn: rate ? rate.ts : undefined, code: currency.code, name: currency.name }); }); }, (err, res: any) => { if (err) return cb(err); var obj = {}; obj[coin] = res; return cb(null, obj); } ); }, (err, res: any) => { if (err) return cb(err); return cb(null, Object.assign({}, ...res)); } ); } public getRatesByCoin(opts, cb) { $.shouldBeFunction(cb, 'Failed state: type error (cb not a function) at <getRatesByCoin()>'); opts = opts || {}; const rates = []; const now = Date.now(); let coin = opts.coin; const ts = opts.ts ? opts.ts : now; let fiatFiltered = []; if (opts.code) { fiatFiltered = _.filter(Defaults.FIAT_CURRENCIES, ['code', opts.code]); if (!fiatFiltered.length) return cb(opts.code + ' is not supported'); } const currencies: { code: string; name: string }[] = fiatFiltered.length ? fiatFiltered : Defaults.FIAT_CURRENCIES; async.map( currencies, (currency, cb) => { if (coin === 'wbtc') { logger.info('Using btc for wbtc rate.'); coin = 'btc'; } this.storage.fetchFiatRate(coin, currency.code, ts, (err, rate) => { if (err) return cb(err); if (rate && ts - rate.ts > Defaults.FIAT_RATE_MAX_LOOK_BACK_TIME * 60 * 1000) rate = null; rates.push({ ts: +ts, rate: rate ? rate.value : undefined, fetchedOn: rate ? rate.ts : undefined, code: currency.code, name: currency.name }); return cb(null, rates); }); }, (err, res: any) => { if (err) return cb(err); return cb(null, res[0]); } ); } getHistoricalRates(opts, cb) { $.shouldBeFunction(cb); opts = opts || {}; const historicalRates = {}; // Oldest date in timestamp range in epoch number ex. 24 hours ago const now = Date.now() - Defaults.FIAT_RATE_FETCH_INTERVAL * 60 * 1000; const ts = _.isNumber(opts.ts) ? opts.ts : now; const coins = ['btc', 'bch', 'xec', 'eth', 'xrp', 'doge', 'xpi', 'ltc']; async.map( coins, (coin: string, cb) => { this.storage.fetchHistoricalRates(coin, opts.code, ts, (err, rates) => { if (err) return cb(err); if (!rates) return cb(); for (const rate of rates) { rate.rate = rate.value; delete rate['_id']; delete rate['code']; delete rate['value']; delete rate['coin']; } historicalRates[coin] = rates; return cb(null, historicalRates); }); }, (err, res: any) => { if (err) return cb(err); return cb(null, res[0]); } ); } }