UNPKG

@signumjs/core

Version:

Principal package with functions and models for building Signum Network applications.

180 lines 7.95 kB
"use strict"; /** * Copyright (c) 2019 Burst Apps Team * Modified (c) 2023 Signum Network */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ChainService = void 0; const http_1 = require("@signumjs/http"); const util_1 = require("@signumjs/util"); const constants_1 = require("../constants"); const verifyTransaction_1 = require("../internal/verifyTransaction"); class SettingsImpl { constructor(settings) { this.apiRootUrl = settings.apiRootUrl || constants_1.DefaultApiEndpoint; this.nodeHost = settings.nodeHost; this.httpClient = settings.httpClient || http_1.HttpClientFactory.createHttpClient(settings.nodeHost, settings.httpClientOptions); this.reliableNodeHosts = settings.reliableNodeHosts || []; } apiRootUrl; httpClient; nodeHost; reliableNodeHosts; } /** * Generic Chain Service class. * * This class can be used to call the chain api directly, in case a function is * not supported yet by SignumJS. Usually, you won't need to do it. * * * * */ class ChainService { /** * Creates Service instance * @param settings The settings for the service */ constructor(settings) { this.settings = new SettingsImpl(settings); const { apiRootUrl } = this.settings; if (apiRootUrl) { this._relPath = apiRootUrl.endsWith('/') ? apiRootUrl.substr(0, apiRootUrl.length - 1) : apiRootUrl; } } settings; _relPath = constants_1.DefaultApiEndpoint; static throwAsHttpError(url, apiError) { const errorCode = apiError.errorCode && ` (Code: ${apiError.errorCode})` || ''; throw new http_1.HttpError(url, 400, `${apiError.errorDescription || apiError.error}${errorCode}`, apiError); } /** * Mounts an API conformant endpoint of format `<host>?requestType=getBlock&height=123` * * @see https://docs.signum.network/signum/node-http-api * * @param {string} method The method name for `requestType` * @param {any} data A JSON object which will be mapped to url params * @return {string} The mounted url (without host) */ toApiEndpoint(method, data = {}) { const request = `${this._relPath}?requestType=${method}`; const params = Object.keys(data) .filter(k => data[k] !== undefined && k !== 'skipAdditionalSecurityCheck') .map(k => `${k}=${encodeURIComponent(data[k])}`) .join('&'); return params ? `${request}&${params}` : request; } /** * Requests a query to the configured chain node * @param {string} method The method according https://europe.signum.network/api-doc/ * @param {any} args A JSON object which will be mapped to url params * @param {any} options The optional request configuration for the passed Http client * (default is [AxiosRequestConfig](https://axios-http.com/docs/req_config) ) * @return {Promise<T>} The response data of success * @throws HttpError in case of failure */ async query(method, args = {}, options) { const endpoint = this.toApiEndpoint(method, args); const { response } = await this.faultTolerantRequest(() => this.settings.httpClient.get(endpoint, options)); if (response.errorCode || response.error || response.errorDescription) { ChainService.throwAsHttpError(endpoint, response); } return response; } /** * Send data to chain node * @param {string} method The method according https://europe.signum.network/api-doc/. * Note that there are only a few POST methods * @param {SendArgs} args A JSON object which will be mapped to url params * @param {any} body An object with key value pairs to submit as post body * @param {any} options The optional request configuration for the passed Http client * (default is [AxiosRequestConfig](https://axios-http.com/docs/req_config) ) * @return {Promise<T>} The response data of success * @throws HttpError in case of failure */ async send(method, args = {}, body, options) { const endpoint = this.toApiEndpoint(method, args); const { response } = await this.faultTolerantRequest(() => this.settings.httpClient.post(endpoint, body, options)); if (response.errorCode || response.error || response.errorDescription) { ChainService.throwAsHttpError(endpoint, response); } if (!args.skipAdditionalSecurityCheck) { (0, verifyTransaction_1.verifyTransaction)(method, args, response); } return response; } async faultTolerantRequest(requestFn) { const onFailureAsync = async (e, retrialCount) => { const shouldRetry = this.settings.reliableNodeHosts.length && retrialCount < this.settings.reliableNodeHosts.length; if (shouldRetry) { await this.selectBestHost(true); } return shouldRetry; }; return (0, util_1.asyncRetry)({ asyncFn: requestFn, onFailureAsync }); } /** * Selects the fastest responding host from the configured reliable node hosts. * @param reconfigure An optional flag to set automatic reconfiguration. Default is `false` * Attention: Reconfiguration works only, if you use the default http client. Otherwise, you need to reconfigure manually! * @param checkMethod The optional API method to be called. This applies only for GET methods. Default is `getBlockchainStatus` * @param timeout The optional amount of time in milliseconds to check. Default is 10_000 * @returns Promise resolving to the selected host * @throws Error if no reliable hosts are configured or if all hosts fail */ async selectBestHost(reconfigure = false, timeout = 10_000, checkMethod = 'getBlockchainStatus') { const { reliableNodeHosts } = this.settings; if (!reliableNodeHosts.length) { throw new Error('No reliable node hosts configured'); } const checkEndpoint = this.toApiEndpoint(checkMethod); // Create a function to check a single host with timeout const checkHost = async (host) => { const start = Date.now(); const absoluteUrl = `${host}${checkEndpoint}`; try { await Promise.race([ this.settings.httpClient.get(absoluteUrl), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout)) ]); return { host, responseTime: Date.now() - start }; } catch (error) { throw error; } }; try { // Check all hosts concurrently and get the fastest responding one const results = await Promise.allSettled(reliableNodeHosts.map(host => checkHost(host))); const successfulHosts = results .filter((result) => result.status === 'fulfilled') .map(result => result.value) .sort((a, b) => a.responseTime - b.responseTime); if (!successfulHosts.length) { throw new Error('All reliable node hosts failed to respond'); } const bestHost = successfulHosts[0].host; if (reconfigure) { this.settings = new SettingsImpl({ ...this.settings, httpClient: http_1.HttpClientFactory.createHttpClient(bestHost, this.settings.httpClientOptions), nodeHost: bestHost, }); } return bestHost; } catch (error) { throw new Error(`Failed to select best host: ${error.message}`); } } } exports.ChainService = ChainService; //# sourceMappingURL=chainService.js.map