UNPKG

@hashgraph/sdk

Version:
326 lines (301 loc) 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _Query = _interopRequireDefault(require("../query/Query.cjs")); var _NodeAddressBook = _interopRequireDefault(require("../address_book/NodeAddressBook.cjs")); var _FileId = _interopRequireDefault(require("../file/FileId.cjs")); var _Executable = require("../Executable.cjs"); var _NodeAddress = _interopRequireDefault(require("../address_book/NodeAddress.cjs")); var _ClientConstants = require("../constants/ClientConstants.cjs"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } // SPDX-License-Identifier: Apache-2.0 /** * @typedef {import("../channel/Channel.js").default} Channel * @typedef {import("../channel/MirrorChannel.js").default} MirrorChannel * @typedef {import("../channel/MirrorChannel.js").MirrorError} MirrorError */ /** * @template {Channel} ChannelT * @typedef {import("../client/Client.js").default<ChannelT, MirrorChannel>} Client<ChannelT, MirrorChannel> */ /** * @typedef {object} EndpointWebResponse * @property {string} domain_name * @property {string} ip_address_v4 * @property {number} port */ /** * @typedef {object} AddressBookQueryWebResponse * @property {Array<{ * admin_key: { * key: string, * _type: string, * }, * decline_reward: boolean, * grpc_proxy_endpoint: EndpointWebResponse, * file_id: string, * memo: string, * public_key: string, * node_id: number, * node_account_id: string, * node_cert_hash: string, * address: string, * service_endpoints: EndpointWebResponse[], * description: string, * stake: number * }>} nodes */ /** * Web-compatible query to get a list of Hedera network node addresses from a mirror node. * Uses fetch API instead of gRPC for web environments. * * This query can be used to retrieve node addresses either from a specific file ID * or from the most recent address book if no file ID is specified. The response * contains node metadata including IP addresses and ports for both node and mirror * node services. * @augments {Query<NodeAddressBook>} */ class AddressBookQueryWeb extends _Query.default { /** * @param {object} props * @param {FileId | string} [props.fileId] * @param {number} [props.limit] */ constructor(props = {}) { super(); /** * @private * @type {?FileId} */ this._fileId = null; if (props.fileId != null) { this.setFileId(props.fileId); } /** * Page limit for the query * @private * @type {?number} */ this._limit = null; if (props.limit != null) { this.setLimit(props.limit); } /** * @private * @type {(error: MirrorError | Error | null) => boolean} */ this._retryHandler = error => { if (error != null) { if (error instanceof Error) { // Retry on all errors which are not `MirrorError` because they're // likely lower level HTTP errors return true; } else { // Retry on `NOT_FOUND`, `RESOURCE_EXHAUSTED`, `UNAVAILABLE`, and conditionally on `INTERNAL` // if the message matches the right regex. switch (error.code) { // INTERNAL // eslint-disable-next-line no-fallthrough case 13: return _Executable.RST_STREAM.test(error.details.toString()); // NOT_FOUND // eslint-disable-next-line no-fallthrough case 5: // RESOURCE_EXHAUSTED // eslint-disable-next-line no-fallthrough case 8: // UNAVAILABLE // eslint-disable-next-line no-fallthrough case 14: case 17: return true; default: return false; } } } return false; }; /** @type {NodeAddress[]} */ this._addresses = []; } /** * @returns {?FileId} */ get fileId() { return this._fileId; } /** * @param {FileId | string} fileId * @returns {AddressBookQueryWeb} */ setFileId(fileId) { this._fileId = typeof fileId === "string" ? _FileId.default.fromString(fileId) : fileId.clone(); return this; } /** * Page limit for the query * @returns {?number} */ get limit() { return this._limit; } /** * Set the page limit for the query * @param {number} limit * @returns {AddressBookQueryWeb} */ setLimit(limit) { this._limit = limit; return this; } /** * @param {number} attempts * @returns {this} */ setMaxAttempts(attempts) { this._maxAttempts = attempts; return this; } /** * @param {number} backoff * @returns {this} */ setMaxBackoff(backoff) { this._maxBackoff = backoff; return this; } /** * @param {Client<Channel>} client * @param {number=} requestTimeout * @returns {Promise<NodeAddressBook>} */ execute(client, requestTimeout) { // Extra validation when initializing the client with only a mirror network if (client._network._network.size === 0 && !client._timer) { throw new Error("The client's network update period is required. Please set it using the setNetworkUpdatePeriod method."); } return new Promise((resolve, reject) => { void this._makeFetchRequest(client, resolve, reject, requestTimeout); }); } /** * @private * @param {Client<Channel>} client * @param {(value: NodeAddressBook) => void} resolve * @param {(error: Error) => void} reject * @param {number=} requestTimeout */ async _makeFetchRequest(client, resolve, reject, requestTimeout) { const { port, address } = client._mirrorNetwork.getNextMirrorNode().address; let baseUrl = `${address.includes("127.0.0.1") || address.includes("localhost") ? "http" : "https"}://${address}`; if (port) { baseUrl = `${baseUrl}:${port}`; } const url = new URL(`${baseUrl}/api/v1/network/nodes`); if (this._fileId != null) { url.searchParams.append("file.id", this._fileId.toString()); } if (this._limit != null) { url.searchParams.append("limit", this._limit.toString()); } for (let attempt = 0; attempt <= this._maxAttempts; attempt++) { try { // eslint-disable-next-line n/no-unsupported-features/node-builtins const response = await fetch(url.toString(), { method: "GET", headers: { Accept: "application/json" }, signal: requestTimeout ? AbortSignal.timeout(requestTimeout) : undefined }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const data = /** @type {AddressBookQueryWebResponse} */ await response.json(); const nodes = data.nodes || []; // eslint-disable-next-line ie11/no-loop-func this._addresses = nodes.map(node => _NodeAddress.default.fromJSON({ nodeId: node.node_id.toString(), accountId: node.node_account_id, addresses: this._handleAddressesFromGrpcProxyEndpoint(node, client), certHash: node.node_cert_hash, publicKey: node.public_key, description: node.description, stake: node.stake.toString() })); const addressBook = new _NodeAddressBook.default({ nodeAddresses: this._addresses }); resolve(addressBook); return; } catch (error) { console.error("Error in _makeFetchRequest:", error); const message = error instanceof Error ? error.message : String(error); // Check if we should retry if (attempt < this._maxAttempts && !client.isClientShutDown && this._retryHandler(/** @type {MirrorError | Error | null} */error)) { const delay = Math.min(250 * 2 ** attempt, this._maxBackoff); if (this._logger) { this._logger.debug(`Error getting nodes from mirror for file ${this._fileId != null ? this._fileId.toString() : "UNKNOWN"} during attempt ${attempt + 1}. Waiting ${delay} ms before next attempt: ${message}`); } // Wait before next attempt // eslint-disable-next-line ie11/no-loop-func await new Promise(resolve => setTimeout(resolve, delay)); continue; } // If we shouldn't retry or have exhausted attempts, reject const maxAttemptsReached = attempt >= this._maxAttempts; const errorMessage = maxAttemptsReached ? `Failed to query address book after ${this._maxAttempts + 1} attempts. Last error: ${message}` : `Failed to query address book: ${message}`; reject(new Error(errorMessage)); return; } } // This should never be reached, but just in case reject(new Error("failed to query address book")); } /** * Handles the grpc_proxy_endpoint fallback logic for a node. * @param {AddressBookQueryWebResponse['nodes'][number]} node - The node object from the mirror node response. * @param {Client<Channel>} client - The client instance. * @returns {Array<{address: string, port: string}>} */ _handleAddressesFromGrpcProxyEndpoint(node, client) { const grpcProxyEndpoint = node.grpc_proxy_endpoint; if (grpcProxyEndpoint && grpcProxyEndpoint.domain_name && grpcProxyEndpoint.port) { return [{ address: grpcProxyEndpoint.domain_name, port: grpcProxyEndpoint.port.toString() }]; } let networkConstant; const ledgerId = client._network.ledgerId; if (ledgerId && ledgerId.isMainnet()) { networkConstant = _ClientConstants.MAINNET; } else if (ledgerId && ledgerId.isTestnet()) { networkConstant = _ClientConstants.WEB_TESTNET; } else if (ledgerId && ledgerId.isPreviewnet()) { networkConstant = _ClientConstants.WEB_PREVIEWNET; } else { return []; } const nodeAccountId = node.node_account_id; for (const [address, accountIdObj] of Object.entries(networkConstant)) { if (accountIdObj.toString() === nodeAccountId) { const [domain_name, port] = address.split(":"); return [{ address: domain_name, port }]; } } return []; } } exports.default = AddressBookQueryWeb;