@tatumio/tatum
Version:
Tatum JS SDK
551 lines • 23.7 kB
JavaScript
;
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