UNPKG

coindesk

Version:

Client for retrieving Bitcoin information from Coindesk API service

348 lines (297 loc) 10.4 kB
/* * Class-based Coindesk API client. */ const axios = require('axios'); const { Headers } = require('node-fetch'); const { _private } = require('./weakmap'); const { getLogger } = require('../logger/service'); const { CoindeskAPIClientError } = require('../errors'); const { CoindeskAPIHttpRequestError } = require('../errors'); const { CoindeskAPIHttpResponseError } = require('../errors'); const utils = require('./utils'); const settings = require('../settings'); const logger = getLogger(__filename); class CoindeskAPIHttpRequest { constructor(retries = 10, redirects = 5, timeout = 5000, backoff = true) { _private(this).retries = retries; _private(this).redirects = redirects; _private(this).timeout = timeout; _private(this).backoff = backoff; } toString() { return `Coindesk API Http Request - Class: ${ this.constructor.name }, Retries: ${ this.retries }, Redirects: ${ this.redirects }, Timeout: ${ this.timeout }, Backoff: ${ this.backoff }`; } static _validate(retries, redirects, timeout, backoff) { retries = utils.validateRetries(retries); redirects = utils.validateRedirects(redirects); timeout = utils.validateTimeout(timeout); utils.validateBackoff(backoff); return [retries, redirects, timeout, backoff]; } get retries() { return _private(this).retries; } set retries(retries) { retries = utils.validateRetries(retries); _private(this).retries = retries; } get redirects() { return _private(this).redirects; } set redirects(redirects) { redirects = utils.validateRedirects(redirects); _private(this).redirects = redirects; } get timeout() { return _private(this).timeout; } set timeout(timeout) { timeout = utils.validateTimeout(timeout); _private(this).timeout = timeout; } get backoff() { return _private(this).backoff; } set backoff(backoff) { utils.validateBackoff(backoff); _private(this).backoff = backoff; } async get(url, raw = false) { let response; const options = this._getRequestOptions(); try { response = await this._httpRequest(url, options); } catch (err) { const message = `Could not make request - ${ err.message }`; logger.error(`[CoindeskAPIHttpRequest] Request error: ${ message }`); throw new CoindeskAPIHttpRequestError(message); } this._checkResponseStatus(response); return raw ? response : response.data; } _getRequestOptions() { return { headers: this._getHeaders(), responseType: 'json', maxRedirects: this.redirects, timeout: this.timeout }; } _getHeaders() { const headers = settings.REQUEST_HEADERS; return new Headers(headers); } async _httpRequest(url, options) { for (let retry = 1; retry <= this.retries; retry++) { try { return await axios.get(url, options); } catch (err) { const timeout = this.backoff ? Math.pow(2, retry) : 0; logger.error(`[CoindeskAPIHttpRequest] Retry ${ retry } request: ${ err.message }`); logger.error(`[CoindeskAPIHttpRequest] Waiting ${ timeout } ms`); await this._waitExponentialBackoff(timeout); } } const message = `No response from Coindesk API url ${ url }`; logger.error(`[CoindeskAPIHttpRequest] Request error: ${ message }`); throw new CoindeskAPIHttpRequestError(message); } _waitExponentialBackoff(timeout) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), timeout); }); } _checkResponseStatus(response) { const { status: statusCode, statusText } = response; if (statusCode === 403 || statusCode === 404) { const message = `Response status code ${ statusCode } - ${ statusText }`; logger.error(`[CoindeskAPIHttpRequest] Request error: ${ message }`); throw new CoindeskAPIHttpRequestError(message); } else { const message = `Status code ${ statusCode }`; logger.info(`[CoindeskAPIHttpRequest] Request success: ${ message }`); } } } class CoindeskAPIClient extends CoindeskAPIHttpRequest { constructor(dataType = null, params = {}, retries = 10, redirects = 5, timeout = 5000, backoff = true) { super(retries, redirects, timeout, backoff); _private(this).dataType = dataType; _private(this).apiEndpoint = this._constructApiEndpoint(dataType, params); } toString() { const toString = super.toString().slice(super.toString().indexOf('Retries')); return `Coindesk API Client - Class: ${ this.constructor.name }, Url: ${ this.url }, ${ toString }`; } static start(dataType = null, params = {}, retries = 10, redirects = 5, timeout = 5000, backoff = true) { dataType = utils.validateDataType(dataType); params = utils.validateParams(dataType, params); [retries, redirects, timeout, backoff] = this._validate(retries, redirects, timeout, backoff); return new this(dataType, params, retries, redirects, timeout, backoff); } _constructApiEndpoint(dataType, params) { let resource = dataType ? settings.API_ENDPOINTS[dataType] : ''; if (dataType === settings.API_CURRENTPRICE_DATA_TYPE) { const currency = params.hasOwnProperty('currency') ? params.currency : ''; resource = currency !== '' ? resource.split('.').join(`/${ currency }.`) : resource; delete params.currency; } const apiPath = `${ this._getApiPath() }/${ resource }`; const apiEndpoint = this._getParsedUrl(apiPath, params); return apiEndpoint; } _getApiPath() { const { protocol, host, path } = settings.API_COINDESK_SETUP; const apiPath = `${ protocol }://${ host }${ path }`; return this._cleanApiPath(apiPath); } _cleanApiPath(apiPath) { return apiPath.endsWith('/') ? apiPath.replace(/\/+$/g, '') : apiPath; } _getParsedUrl(url, params) { const encodedParams = this._getEncodedParams(params); url = encodedParams !== '' ? `${ url }?${ encodedParams }` : url; utils.validateUrl(url); return new URL(url); } _getEncodedParams(params) { return Object.keys(params).map(key => { return `${ encodeURIComponent(key) }=${ encodeURIComponent(params[key]) }`; }).join('&'); } get dataType() { return _private(this).dataType; } set dataType(dataType) { dataType = utils.validateDataType(dataType); _private(this).dataType = dataType; if (dataType === settings.API_CURRENTPRICE_DATA_TYPE) this.params = {}; _private(this).apiEndpoint = this._constructApiEndpoint(dataType, this.params); } get url() { return _private(this).apiEndpoint; } get path() { return this.url.pathname; } set path(path) { this.url.pathname = path; this.deleteManyParams(Object.keys(this.params)); } get params() { const params = new Object(); for (let key of this.url.searchParams.keys()) { params[key] = this.getParam(key); } return params; } getParam(key) { return this.url.searchParams.get(key); } set params(params) { params = utils.validateParams(this.dataType, params); Object.keys(params).forEach(key => this.url.searchParams.set(key, params[key])); } deleteParam(key) { this.url.searchParams.delete(key); } deleteManyParams(keys) { keys.forEach(key => this.deleteParam(key)); } deleteAllParams() { Object.keys(this.params).forEach(key => this.deleteParam(key)); } get validParams() { switch (this.dataType) { case settings.API_CURRENTPRICE_DATA_TYPE: return settings.VALID_CURRENTPRICE_PARAMS; case settings.API_HISTORICAL_DATA_TYPE: return settings.VALID_HISTORICAL_PARAMS; default: const message = `Uncorrect data type setup for ${ this.dataType }`; logger.warn(`[CoindeskAPIClient] Data type error: ${ message }`); return null; } } async getSupportedCurrencies() { let currencies = null; const apiPath = this._getApiPath(); const resource = settings.API_ENDPOINTS[settings.API_SUPPORTED_CURRENCIES_DATA_TYPE]; const url = new URL(`${ apiPath }/${ resource }`); try { utils.validateUrl(url.href); currencies = await super.get(url.href, false); } catch (err) { const message = err.message; logger.warn(`[CoindeskAPIClient] Get currencies error: ${ message }`); } if (currencies !== null) await utils.validateSupportedCurrencies(currencies); return currencies !== null ? currencies : settings.SUPPORTED_CURRENCIES; } async get(raw = false) { try { return await super.get(this.url.href, raw); } catch (err) { const message = `Could not get response. ${ err.message }`; logger.error(`[CoindeskAPIClient] API call error: ${ message }`); throw new CoindeskAPIClientError(message); } } } class CoindeskAPIHttpResponse { constructor(response) { _private(this).response = response; } toString() { return `Coindesk API Http Response - Class: ${ this.constructor.name }`; } static parse(response, dataType, currency = null) { this._validate(response, dataType, currency); return new this(response); } static _validate(response, dataType, currency) { const schema = utils.getResponseSchema(dataType, currency); const { errors } = schema.validate(response); if (errors) { const message = error.message; logger.error(`[CoindeskAPIHttpResponse] Validation error: ${ message }`); throw new CoindeskAPIHttpResponseError(message); } } get response() { return _private(this).response; } get JSONresponse() { try { return JSON.stringify(this.response); } catch (err) { const message = `Unable to decode JSON response. ${ err.message }`; logger.error(`[CoindeskAPIHttpResponse] Response error: ${ message }`); throw new CoindeskAPIHttpResponseError(message); } } get responseItems() { return Object.keys(this.response); } getResponseItem(item) { if (!this.response.hasOwnProperty(item)) { const message = `Unvalid provided response item ${ item }`; logger.error(`[CoindeskAPIHttpResponse] Response item error: ${ message }`); return null; } return this.response[item]; } } module.exports = { CoindeskAPIClient, CoindeskAPIResponse: CoindeskAPIHttpResponse };