@abcpros/bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
429 lines (395 loc) • 13.6 kB
text/typescript
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]);
}
);
}
}