UNPKG

@tatumio/tatum

Version:

Tatum JS SDK

551 lines 23.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var LoadBalancer_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.TronLoadBalancer = exports.LoadBalancer = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const typedi_1 = require("typedi"); const connector_1 = require("../../../connector"); const util_1 = require("../../../util"); const tatum_1 = require("../../tatum"); const NODE_TYPE_LABEL = { [tatum_1.RpcNodeType.NORMAL]: 'normal', [tatum_1.RpcNodeType.ARCHIVE]: 'archive', }; var RequestType; (function (RequestType) { RequestType["RPC"] = "RPC"; RequestType["POST"] = "POST"; RequestType["GET"] = "GET"; RequestType["BATCH"] = "BATCH"; RequestType["PUT"] = "PUT"; RequestType["DELETE"] = "DELETE"; })(RequestType || (RequestType = {})); let LoadBalancer = LoadBalancer_1 = class LoadBalancer { constructor(id) { this.id = id; this.rpcUrls = { [tatum_1.RpcNodeType.NORMAL]: [], [tatum_1.RpcNodeType.ARCHIVE]: [], }; this.activeUrl = { [tatum_1.RpcNodeType.NORMAL]: {}, [tatum_1.RpcNodeType.ARCHIVE]: {}, }; this.noActiveNode = false; this.connector = typedi_1.Container.of(this.id).get(connector_1.TatumConnector); const config = typedi_1.Container.of(this.id).get(util_1.CONFIG); this.network = config.network; this.evictNodesOnFailure = !!config.rpc?.evictNodesOnFailure; this.logger = typedi_1.Container.of(this.id).get(util_1.LOGGER); } async init() { const config = typedi_1.Container.of(this.id).get(util_1.CONFIG); const nodes = config.rpc?.nodes; if (nodes) { util_1.Utils.log({ id: this.id, message: 'Initializing RPC module from static URLs' }); this.initCustomNodes(nodes); } else { util_1.Utils.log({ id: this.id, message: 'Initializing RPC module from remote hosts' }); await this.initRemoteHostsUrls(); } if (util_1.EnvUtils.isProcessAvailable() && process.release && process.release.name === 'node') { process.on('exit', () => this.destroy()); } if (nodes && nodes.length > 1) { if (config.rpc?.oneTimeLoadBalancing) { util_1.Utils.log({ id: this.id, message: 'oneTimeLoadBalancing enabled' }); await this.checkStatuses(); } else { this.interval = setInterval(() => this.checkStatuses(), util_1.Constant.OPEN_RPC.LB_INTERVAL); } } } destroy() { util_1.Utils.log({ id: this.id, message: 'Destroying LoadBalancer instance' }); clearInterval(this.interval); process.off('exit', () => this.destroy()); } initCustomNodes(nodes) { this.initRemoteHosts({ nodeType: tatum_1.RpcNodeType.NORMAL, nodes: nodes, noSSRFCheck: true }); this.initRemoteHosts({ nodeType: tatum_1.RpcNodeType.ARCHIVE, nodes: nodes, noSSRFCheck: true }); if (nodes?.length) { for (const node of nodes) { if (node.type === tatum_1.RpcNodeType.NORMAL) { this.rpcUrls[tatum_1.RpcNodeType.NORMAL].push({ node: { url: util_1.Utils.removeLastSlash(node.url) }, lastBlock: 0, lastResponseTime: 0, failed: false, }); } if (node.type === tatum_1.RpcNodeType.ARCHIVE) { this.rpcUrls[tatum_1.RpcNodeType.ARCHIVE].push({ node: { url: util_1.Utils.removeLastSlash(node.url) }, lastBlock: 0, lastResponseTime: 0, failed: false, }); } } } else { util_1.Utils.log({ id: this.id, message: 'No RPC URLs provided' }); } } resetFailedStatuses(nodeType) { for (const server of this.rpcUrls[nodeType]) { server.failed = false; } } async checkStatuses() { try { await this.checkStatus(tatum_1.RpcNodeType.NORMAL); await this.checkStatus(tatum_1.RpcNodeType.ARCHIVE); this.checkIfNoActiveNodes(); } catch (e) { util_1.Utils.log({ id: this.id, message: `LoadBalancing failed to check statuses. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); } } checkIfNoActiveNodes() { if (!this.activeUrl[tatum_1.RpcNodeType.NORMAL].url && !this.activeUrl[tatum_1.RpcNodeType.ARCHIVE].url) { util_1.Utils.log({ id: this.id, message: 'No active node found, please set node urls manually.' }); this.noActiveNode = true; } else { this.noActiveNode = false; } } async checkStatus(nodeType) { const { rpc, network } = typedi_1.Container.of(this.id).get(util_1.CONFIG); /** * Check status of all nodes. * If the node is not responding, it will be marked as failed. * If the node is responding, it will be marked as not failed and the last block will be updated. */ const statusPayload = util_1.Utils.getStatusPayload(network); for (const server of this.rpcUrls[nodeType]) { util_1.Utils.log({ id: this.id, message: `Checking status of ${server.node.url}` }); await util_1.Utils.fetchWithTimeoutAndRetry(util_1.Utils.getStatusUrl(network, server.node.url), this.id, { method: util_1.Utils.getStatusMethod(network), // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // body: statusPayload ? JSON.stringify(statusPayload) : undefined, // add body only if is defined ...(statusPayload && { body: JSON.stringify(statusPayload) }), }) .then(async ({ response: res, responseTime }) => { server.lastResponseTime = responseTime; const response = await res.json(); util_1.Utils.log({ id: this.id, message: `Response time of ${server.node.url} is ${server.lastResponseTime}ms with response: `, data: response, }); if (res.ok && util_1.Utils.isResponseOk(network, response)) { server.failed = false; server.lastBlock = util_1.Utils.parseStatusPayload(network, response); } else { util_1.Utils.log({ id: this.id, message: `Failed to check status of ${server.node.url}. Error: ${JSON.stringify(response, Object.getOwnPropertyNames(response))}`, }); server.failed = true; } }) .catch((e) => { util_1.Utils.log({ id: this.id, message: `Failed to check status of ${server.node.url}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); util_1.Utils.log({ id: this.id, message: `Server ${server.node.url} will be marked as failed and will be removed from the pool.`, }); server.failed = true; }); } /** * The fastest node will be selected and will be used. */ const { fastestServer, index } = LoadBalancer_1.getFastestServer(this.rpcUrls[nodeType], rpc?.allowedBlocksBehind); util_1.Utils.log({ id: this.id, data: this.rpcUrls[nodeType], mode: 'table', }); if (fastestServer && index !== -1) { util_1.Utils.log({ id: this.id, message: `Server ${fastestServer.node.url} is selected as active server for ${tatum_1.RpcNodeType[nodeType]}.`, data: { url: fastestServer.node.url, index }, }); this.activeUrl[nodeType] = { url: fastestServer.node.url, index }; } } static getFastestServer(servers, allowedBlocksBehind) { const { fastestServer, index } = servers.reduce((result, item, index) => { const isNotFailed = !item.failed; const isFasterBlock = item.lastBlock - allowedBlocksBehind > result.fastestServer.lastBlock; const isSameBlockFasterResponse = item.lastBlock === result.fastestServer.lastBlock && item.lastResponseTime < result.fastestServer.lastResponseTime; if (isNotFailed && (isFasterBlock || isSameBlockFasterResponse)) { return { fastestServer: item, index: index }; } else { return result; } }, { fastestServer: { lastBlock: -Infinity, lastResponseTime: Infinity, node: { url: '' } }, index: -1 }); return { fastestServer, index }; } getActiveArchiveUrlWithFallback() { const activeArchiveUrl = this.getActiveUrl(tatum_1.RpcNodeType.ARCHIVE); if (activeArchiveUrl?.url) { return { url: activeArchiveUrl.url, type: tatum_1.RpcNodeType.ARCHIVE }; } if (this.getActiveUrl(tatum_1.RpcNodeType.NORMAL)?.url) { return { url: this.getActiveUrl(tatum_1.RpcNodeType.NORMAL).url, type: tatum_1.RpcNodeType.NORMAL }; } if (this.noActiveNode) { util_1.Utils.log({ id: this.id, data: this.rpcUrls[tatum_1.RpcNodeType.NORMAL], mode: 'table', }); util_1.Utils.log({ id: this.id, data: this.rpcUrls[tatum_1.RpcNodeType.ARCHIVE], mode: 'table', }); throw new Error('No active ARCHIVE node found, fallback failed, please set node urls manually.'); } throw new Error('No active ARCHIVE node found.'); } getActiveNormalUrlWithFallback() { const activeNormalUrl = this.getActiveUrl(tatum_1.RpcNodeType.NORMAL); if (activeNormalUrl?.url) { return { url: activeNormalUrl.url, type: tatum_1.RpcNodeType.NORMAL }; } if (this.getActiveUrl(tatum_1.RpcNodeType.ARCHIVE)?.url) { return { url: this.getActiveUrl(tatum_1.RpcNodeType.ARCHIVE).url, type: tatum_1.RpcNodeType.ARCHIVE }; } if (this.noActiveNode) { util_1.Utils.log({ id: this.id, data: this.rpcUrls[tatum_1.RpcNodeType.NORMAL], mode: 'table', }); util_1.Utils.log({ id: this.id, data: this.rpcUrls[tatum_1.RpcNodeType.ARCHIVE], mode: 'table', }); throw new Error('No active NORMAL node found, fallback failed, please set node urls manually.'); } throw new Error('No active NORMAL node found.'); } getActiveUrl(nodeType) { return { url: this.activeUrl[nodeType]?.url, type: nodeType }; } getActiveIndex(nodeType) { return this.activeUrl[nodeType]?.index; } checkSSRF(url) { try { const parsedUrl = new URL(url); return parsedUrl.hostname.endsWith('tatum.io'); } catch (e) { util_1.Utils.log({ id: this.id, message: `Failed to parse URL ${url}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); return false; } } initRemoteHosts({ nodeType, nodes, noSSRFCheck }) { const filteredNodes = nodes.filter((node) => { // Check if the node type matches. const typeMatch = node.type === nodeType; // If noSSRFCheck is true, skip the SSRF check. if (noSSRFCheck) { return typeMatch; } // If noSSRFCheck is false or undefined, check if the URL ends with 'tatum.io'. const ssrfCheckPassed = this.checkSSRF(node.url); // Log if the URL doesn't pass the SSRF check if (!ssrfCheckPassed) { util_1.Utils.log({ id: this.id, message: `Skipping URL ${node.url} as it doesn't pass the SSRF check.`, }); } return typeMatch && ssrfCheckPassed; }); if (filteredNodes.length === 0) { return; } if (!this.rpcUrls[nodeType]) { this.rpcUrls[nodeType] = []; } this.rpcUrls[nodeType] = [ ...this.rpcUrls[nodeType], ...filteredNodes.map((s) => ({ node: { url: util_1.Utils.removeLastSlash(s.url) }, lastBlock: 0, lastResponseTime: 0, failed: false, })), ]; util_1.Utils.log({ id: this.id, message: `Added ${filteredNodes.length} nodes (${filteredNodes.map((s) => s.url).join(', ')}) for ${this.network} blockchain during the initialization for node ${NODE_TYPE_LABEL[nodeType]}.`, }); const randomIndex = Math.floor(Math.random() * this.rpcUrls[nodeType].length); util_1.Utils.log({ id: this.id, message: `Using random URL ${this.rpcUrls[nodeType][randomIndex].node.url} for ${this.network} blockchain during the initialization for node ${NODE_TYPE_LABEL[nodeType]}.`, }); this.activeUrl[nodeType] = { url: this.rpcUrls[nodeType][randomIndex].node.url, index: randomIndex }; } async initRemoteHostsUrls() { const network = this.network; const rpcList = util_1.Utils.getRpcListUrl(network); util_1.Utils.log({ id: this.id, message: `Fetching response from ${rpcList}` }); try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const [normal, archive] = await Promise.all(rpcList.map((url) => fetch(url))); await this.initRemoteHostsFromResponse(normal, tatum_1.RpcNodeType.NORMAL); await this.initRemoteHostsFromResponse(archive, tatum_1.RpcNodeType.ARCHIVE); } catch (e) { this.logger.error(new Date().toISOString(), `Failed to initialize RPC module. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`); } } async initRemoteHostsFromResponse(response, nodeType) { if (response.ok) { const nodes = await response.json(); this.initRemoteHosts({ nodeType: tatum_1.RpcNodeType.NORMAL, nodes: nodes }); this.initRemoteHosts({ nodeType: tatum_1.RpcNodeType.ARCHIVE, nodes: nodes }); } else { util_1.Utils.log({ id: this.id, message: `Failed to fetch RPC configuration for ${this.network} blockchain for ${tatum_1.RpcNodeType[nodeType]} nodes`, }); } } async handleFailedRpcCall({ rpcCall, e, nodeType, requestType, url }) { const { rpc: rpcConfig } = typedi_1.Container.of(this.id).get(util_1.CONFIG); const activeIndex = this.getActiveIndex(nodeType); if (requestType === RequestType.RPC && 'method' in rpcCall) { util_1.Utils.log({ id: this.id, message: `Failed to call RPC ${rpcCall.method} on ${url}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); } else if (requestType === RequestType.BATCH && Array.isArray(rpcCall)) { const methods = rpcCall.map((item) => item.method).join(', '); util_1.Utils.log({ id: this.id, message: `Failed to call RPC methods [${methods}] on ${url}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); } else if (requestType === RequestType.POST && 'path' in rpcCall && 'body' in rpcCall) { util_1.Utils.log({ id: this.id, message: `Failed to call request on url ${url}${rpcCall.path} with body ${JSON.stringify(rpcCall.body)}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); } else if (requestType === RequestType.GET && 'path' in rpcCall) { util_1.Utils.log({ id: this.id, message: `Failed to call request on url ${url}${rpcCall.path}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); } else { // Handle other cases util_1.Utils.log({ id: this.id, message: `Failed to call request on url ${url}. Error: ${JSON.stringify(e, Object.getOwnPropertyNames(e))}`, }); } util_1.Utils.log({ id: this.id, message: `Switching to another server, marking ${url} as unstable.`, }); if (activeIndex == null) { this.logger.error(`No active node found for node type ${tatum_1.RpcNodeType[nodeType]}. Looks like your request is malformed or all nodes are down. Turn on verbose mode to see more details and check status pages.`); throw e; } const servers = this.rpcUrls[nodeType]; /** * If the node is not responding, it will be marked as failed. * New node will be selected and will be used for the given blockchain. */ servers[activeIndex].failed = true; const { index, fastestServer } = LoadBalancer_1.getFastestServer(servers, rpcConfig?.allowedBlocksBehind); if (index === -1) { if (this.evictNodesOnFailure) { this.logger.error(`Looks like your request is malformed or all RPC nodes are down. Turn on verbose mode to see more details and check status pages.`); } else { // Recover failed nodes, prepare them for the next round of requests this.resetFailedStatuses(nodeType); } throw e; } util_1.Utils.log({ id: this.id, message: `Server ${fastestServer.node.url} is selected as active server, because ${url} failed.`, }); this.activeUrl[nodeType] = { url: fastestServer.node.url, index }; } async rawRpcCall(rpcCall, archive) { const { url, type } = archive ? this.getActiveArchiveUrlWithFallback() : this.getActiveNormalUrlWithFallback(); try { util_1.Utils.log({ id: this.id, message: `Sending RPC ${rpcCall.method} to ${url} for ${this.network} blockchain node type ${tatum_1.RpcNodeType[type]}.`, }); return await this.connector.rpcCall(url, rpcCall); } catch (e) { await this.handleFailedRpcCall({ rpcCall, e, nodeType: type, requestType: RequestType.RPC, url }); return await this.rawRpcCall(rpcCall, archive); } } async rawBatchRpcCall(rpcCall) { const { url, type } = this.getActiveArchiveUrlWithFallback(); try { return await this.connector.rpcCall(url, rpcCall); } catch (e) { await this.handleFailedRpcCall({ rpcCall, e, nodeType: type, requestType: RequestType.BATCH, url }); return await this.rawBatchRpcCall(rpcCall); } } modifyNodeUrl(url) { return url; } getUrlForHttpMethod(prefix) { const { url, type } = this.getActiveNormalUrlWithFallback(); const modifiedUrl = this.modifyNodeUrl(url); return { url: prefix ? `${modifiedUrl}${prefix}` : modifiedUrl, type }; } async post({ path, body, prefix }) { const { url, type } = this.getUrlForHttpMethod(prefix); try { return await this.connector.post({ basePath: url, path, body }); } catch (e) { await this.handleFailedRpcCall({ rpcCall: { path, body }, e, nodeType: type, requestType: RequestType.POST, url, }); return await this.post({ path, body, prefix }); } } async put({ path, body, prefix }) { const { url, type } = this.getUrlForHttpMethod(prefix); try { return await this.connector.put({ basePath: url, path, body }); } catch (e) { await this.handleFailedRpcCall({ rpcCall: { path, body }, e, nodeType: type, requestType: RequestType.PUT, url, }); return await this.put({ path, body, prefix }); } } async delete({ path, prefix }) { const { url, type } = this.getUrlForHttpMethod(prefix); try { return await this.connector.delete({ basePath: url, path }); } catch (e) { await this.handleFailedRpcCall({ rpcCall: { path }, e, nodeType: type, requestType: RequestType.DELETE, url, }); return await this.delete({ path, prefix }); } } async get({ path, prefix }) { const { url, type } = this.getUrlForHttpMethod(prefix); try { return await this.connector.get({ basePath: url, path }); } catch (e) { await this.handleFailedRpcCall({ rpcCall: { path }, e, nodeType: type, requestType: RequestType.GET, url, }); return await this.get({ path, prefix }); } } getRpcNodeUrl() { return this.getActiveNormalUrlWithFallback().url; } }; exports.LoadBalancer = LoadBalancer; exports.LoadBalancer = LoadBalancer = LoadBalancer_1 = __decorate([ (0, typedi_1.Service)({ factory: (data) => { return new LoadBalancer(data.id); }, transient: true, }), __metadata("design:paramtypes", [String]) ], LoadBalancer); let TronLoadBalancer = class TronLoadBalancer extends LoadBalancer { constructor(id) { super(id); } // remove jsonrpc from end of the url modifyNodeUrl(url) { return url.replace(/\/jsonrpc$/, ''); } }; exports.TronLoadBalancer = TronLoadBalancer; exports.TronLoadBalancer = TronLoadBalancer = __decorate([ (0, typedi_1.Service)({ factory: (data) => { return new TronLoadBalancer(data.id); }, transient: true, }), __metadata("design:paramtypes", [String]) ], TronLoadBalancer); //# sourceMappingURL=LoadBalancer.js.map