UNPKG

@hiero-ledger/sdk

Version:
929 lines (805 loc) 23.4 kB
// SPDX-License-Identifier: Apache-2.0 import AccountId from "../account/AccountId.js"; import AccountBalanceQuery from "../account/AccountBalanceQuery.js"; import Hbar from "../Hbar.js"; import Network from "./Network.js"; import MirrorNetwork from "./MirrorNetwork.js"; import PublicKey from "../PublicKey.js"; import PrivateKey from "../PrivateKey.js"; import LedgerId from "../LedgerId.js"; import FileId from "../file/FileId.js"; import Logger from "../logger/Logger.js"; // eslint-disable-line import { convertToNumber } from "../util.js"; import AddressBookQuery from "../network/AddressBookQuery.js"; /** * @typedef {import("../channel/Channel.js").default} Channel * @typedef {import("../channel/MirrorChannel.js").default} MirrorChannel * @typedef {import("../address_book/NodeAddressBook.js").default} NodeAddressBook */ /** * @typedef {object} Operator * @property {string | PrivateKey} privateKey * @property {string | AccountId} accountId */ /** * @typedef {object} ClientOperator * @property {PublicKey} publicKey * @property {AccountId} accountId * @property {(message: Uint8Array) => Promise<Uint8Array>} transactionSigner */ /** * @typedef {object} ClientConfiguration * @property {{[key: string]: (string | AccountId)} | string} [network] * @property {string[] | string} [mirrorNetwork] * @property {Operator} [operator] * @property {boolean} [scheduleNetworkUpdate] * @property {number} [shard] * @property {number} [realm] */ /** * @typedef {"mainnet" | "testnet" | "previewnet"} NetworkName */ /** * The `Client` class is the main entry point for interacting with the Hedera Hashgraph network. * It provides methods for managing network connections, setting operators, handling transactions * and queries, and configuring various client settings. * * @abstract * @template {Channel} ChannelT * @template {MirrorChannel} MirrorChannelT */ export default class Client { /** * @protected * @hideconstructor * @param {ClientConfiguration} [props] */ constructor(props) { /** * List of mirror network URLs. * * @internal * @type {MirrorNetwork} */ this._mirrorNetwork = new MirrorNetwork( this._createMirrorNetworkChannel(), ); /** * Map of node account ID (as a string) * to the node URL. * * @internal * @type {Network} */ this._network = new Network(this._createNetworkChannel()); /** * @internal * @type {?ClientOperator} */ this._operator = null; /** * @private * @type {?Hbar} */ this._defaultMaxTransactionFee = null; /** * @private * @type {Hbar} */ this._defaultMaxQueryPayment = new Hbar(1); if (props != null) { if (props.operator != null) { this.setOperator( props.operator.accountId, props.operator.privateKey, ); } } /** @type {number | null} */ this._maxAttempts = null; /** @private */ this._signOnDemand = false; /** @private */ this._autoValidateChecksums = false; /** @private */ this._minBackoff = 250; /** @private */ this._maxBackoff = 8000; /** @private */ this._defaultRegenerateTransactionId = true; /** @private */ this._requestTimeout = null; /** * @type {boolean} */ this._isUpdatingNetwork = false; /** @private */ this._networkUpdatePeriod = 24 * 60 * 60 * 1000; /** @private */ this._isShutdown = false; this._shard = 0; this._realm = 0; if (props != null && props.scheduleNetworkUpdate !== false) { this._scheduleNetworkUpdate(); } if (props != null && props.shard != null) { this._shard = props.shard; } if (props != null && props.realm != null) { this._realm = props.realm; } /** @internal */ /** @type {NodeJS.Timeout} */ this._timer; /** * Logger * * @external * @type {Logger | null} */ this._logger = null; } /** * @deprecated * @param {NetworkName} networkName * @returns {this} */ setNetworkName(networkName) { // uses custom NetworkName type // remove if phasing out set|get NetworkName console.warn("Deprecated: Use `setLedgerId` instead"); return this.setLedgerId(networkName); } /** * @deprecated * @returns {string | null} */ get networkName() { console.warn("Deprecated: Use `ledgerId` instead"); return this.ledgerId != null ? this.ledgerId.toString() : null; } /** * @param {string|LedgerId} ledgerId * @returns {this} */ setLedgerId(ledgerId) { this._network.setLedgerId( typeof ledgerId === "string" ? LedgerId.fromString(ledgerId) : ledgerId, ); return this; } /** * @returns {LedgerId | null} */ get ledgerId() { return this._network._ledgerId != null ? this._network.ledgerId : null; } /** * @param {{[key: string]: (string | AccountId)} | string} network * @returns {void} */ // eslint-disable-next-line @typescript-eslint/no-unused-vars setNetwork(network) { // TODO: This logic _can_ be de-duplicated and likely should throw new Error("not implemented"); } /** * @param {NodeAddressBook} addressBook * @returns {this} */ setNetworkFromAddressBook(addressBook) { this._network.setNetworkFromAddressBook(addressBook); return this; } /** * @returns {{[key: string]: (string | AccountId)}} */ get network() { return this._network.network; } /** * @returns {number} */ get shard() { return this._shard; } /** * @returns {number} */ get realm() { return this._realm; } /** * @param {string[] | string} mirrorNetwork * @returns {void} */ // eslint-disable-next-line @typescript-eslint/no-unused-vars setMirrorNetwork(mirrorNetwork) { throw new Error("not implemented"); } /** * @returns {string[]} */ get mirrorNetwork() { return this._mirrorNetwork.network; } /** * @returns {string} * @throws {Error} When no mirror network is configured or available */ get mirrorRestApiBaseUrl() { try { const mirrorNode = this._mirrorNetwork.getNextMirrorNode(); const host = mirrorNode.address.address; const port = mirrorNode.address.port; if (!host || !port) { throw new Error( "Mirror node has invalid address configuration", ); } const scheme = this._getSchemeFromHostAndPort(host, port); return `${scheme}://${host}:${port}/api/v1`; } catch (error) { // Re-throw with a more descriptive error message throw new Error( "Client has no mirror network configured or no healthy mirror nodes are available", ); } } /** * @returns {boolean} */ get signOnDemand() { return this._signOnDemand; } /** * @param {boolean} signOnDemand */ setSignOnDemand(signOnDemand) { this._signOnDemand = signOnDemand; } /** * @returns {boolean} */ isTransportSecurity() { return this._network.isTransportSecurity(); } /** * @param {boolean} transportSecurity * @returns {this} */ setTransportSecurity(transportSecurity) { this._network.setTransportSecurity(transportSecurity); return this; } /** * Set the account that will, by default, pay for transactions and queries built with this client. * NOTE: When using string for private key, the string needs to contain DER headers * * @param {AccountId | string} accountId * @param {PrivateKey | string} privateKey * @returns {this} */ setOperator(accountId, privateKey) { const key = typeof privateKey === "string" ? PrivateKey.fromStringDer(privateKey) : privateKey; return this.setOperatorWith(accountId, key.publicKey, (message) => Promise.resolve(key.sign(message)), ); } /** * @returns {?ClientOperator} */ getOperator() { return this._operator; } /** * Sets the account that will, by default, pay for transactions and queries built with * this client. * * @param {AccountId | string} accountId * @param {PublicKey | string} publicKey * @param {(message: Uint8Array) => Promise<Uint8Array>} transactionSigner * @returns {this} */ setOperatorWith(accountId, publicKey, transactionSigner) { const accountId_ = accountId instanceof AccountId ? accountId : AccountId.fromString(accountId); if (this._network._ledgerId != null) { accountId_.validateChecksum(this); } this._operator = { transactionSigner, accountId: accountId_, publicKey: publicKey instanceof PublicKey ? publicKey : PublicKey.fromString(publicKey), }; return this; } /** * @param {boolean} value * @returns {this} */ setAutoValidateChecksums(value) { this._autoValidateChecksums = value; return this; } /** * @returns {boolean} */ isAutoValidateChecksumsEnabled() { return this._autoValidateChecksums; } /** * @returns {?AccountId} */ get operatorAccountId() { return this._operator != null ? this._operator.accountId : null; } /** * @returns {?PublicKey} */ get operatorPublicKey() { return this._operator != null ? this._operator.publicKey : null; } /** * @returns {?Hbar} */ get defaultMaxTransactionFee() { return this._defaultMaxTransactionFee; } /** * @deprecated - Use `defaultMaxTransactionFee` instead * @returns {?Hbar} */ get maxTransactionFee() { return this.defaultMaxTransactionFee; } /** * Set the defaultimum fee to be paid for transactions * executed by this client. * * @param {Hbar} defaultMaxTransactionFee * @returns {this} */ setDefaultMaxTransactionFee(defaultMaxTransactionFee) { if (defaultMaxTransactionFee.toTinybars().toInt() < 0) { throw new Error("defaultMaxTransactionFee must be non-negative"); } this._defaultMaxTransactionFee = defaultMaxTransactionFee; return this; } /** * @deprecated - Use `setDefaultMaxTransactionFee()` instead * Set the maximum fee to be paid for transactions * executed by this client. * @param {Hbar} maxTransactionFee * @returns {this} */ setMaxTransactionFee(maxTransactionFee) { return this.setDefaultMaxTransactionFee(maxTransactionFee); } /** * @returns {boolean} */ get defaultRegenerateTransactionId() { return this._defaultRegenerateTransactionId; } /** * Set if a new transaction ID should be generated when a `TRANSACTION_EXPIRED` status * is returned. * * @param {boolean} defaultRegenerateTransactionId * @returns {this} */ setDefaultRegenerateTransactionId(defaultRegenerateTransactionId) { this._defaultRegenerateTransactionId = defaultRegenerateTransactionId; return this; } /** * @returns {Hbar} */ get defaultMaxQueryPayment() { return this._defaultMaxQueryPayment; } /** * @deprecated in a favor of defaultMaxQueryPayment * @returns {Hbar} */ get maxQueryPayment() { return this.defaultMaxQueryPayment; } /** * Set the maximum payment allowable for queries. * * @param {Hbar} defaultMaxQueryPayment * @returns {Client<ChannelT, MirrorChannelT>} */ setDefaultMaxQueryPayment(defaultMaxQueryPayment) { const isMaxQueryPaymentNegative = convertToNumber(defaultMaxQueryPayment.toTinybars()) < 0; if (isMaxQueryPaymentNegative) { throw new Error("defaultMaxQueryPayment must be non-negative"); } this._defaultMaxQueryPayment = defaultMaxQueryPayment; return this; } /** * @deprecated in a favor of setDefaultMaxQueryPayment() * Set the maximum payment allowable for queries. * @param {Hbar} maxQueryPayment * @returns {Client<ChannelT, MirrorChannelT>} */ setMaxQueryPayment(maxQueryPayment) { return this.setDefaultMaxQueryPayment(maxQueryPayment); } /** * @returns {number} */ get maxAttempts() { return this._maxAttempts != null ? this._maxAttempts : 10; } /** * @param {number} maxAttempts * @returns {this} */ setMaxAttempts(maxAttempts) { this._maxAttempts = maxAttempts; return this; } /** * @returns {number} */ get maxNodeAttempts() { return this._network.maxNodeAttempts; } /** * @param {number} maxNodeAttempts * @returns {this} */ setMaxNodeAttempts(maxNodeAttempts) { this._network.setMaxNodeAttempts(maxNodeAttempts); return this; } /** * @returns {number} */ get nodeWaitTime() { return this._network.minBackoff; } /** * @param {number} nodeWaitTime * @returns {this} */ setNodeWaitTime(nodeWaitTime) { this._network.setMinBackoff(nodeWaitTime); return this; } /** * @returns {number} */ get maxNodesPerTransaction() { return this._network.maxNodesPerTransaction; } /** * @param {number} maxNodesPerTransaction * @returns {this} */ setMaxNodesPerTransaction(maxNodesPerTransaction) { this._network.setMaxNodesPerTransaction(maxNodesPerTransaction); return this; } /** * @param {?number} minBackoff * @returns {this} */ setMinBackoff(minBackoff) { if (minBackoff == null) { throw new Error("minBackoff cannot be null."); } if (minBackoff > this._maxBackoff) { throw new Error("minBackoff cannot be larger than maxBackoff."); } this._minBackoff = minBackoff; return this; } /** * @returns {number} */ get minBackoff() { return this._minBackoff; } /** * @param {?number} maxBackoff * @returns {this} */ setMaxBackoff(maxBackoff) { if (maxBackoff == null) { throw new Error("maxBackoff cannot be null."); } else if (maxBackoff < this._minBackoff) { throw new Error("maxBackoff cannot be smaller than minBackoff."); } this._maxBackoff = maxBackoff; return this; } /** * @returns {number} */ get maxBackoff() { return this._maxBackoff; } /** * @param {number} nodeMinBackoff * @returns {this} */ setNodeMinBackoff(nodeMinBackoff) { this._network.setMinBackoff(nodeMinBackoff); return this; } /** * @returns {number} */ get nodeMinBackoff() { return this._network.minBackoff; } /** * @param {number} nodeMaxBackoff * @returns {this} */ setNodeMaxBackoff(nodeMaxBackoff) { this._network.setMaxBackoff(nodeMaxBackoff); return this; } /** * @returns {number} */ get nodeMaxBackoff() { return this._network.maxBackoff; } /** * @param {number} nodeMinReadmitPeriod * @returns {this} */ setNodeMinReadmitPeriod(nodeMinReadmitPeriod) { this._network.setNodeMinReadmitPeriod(nodeMinReadmitPeriod); return this; } /** * @returns {number} */ get nodeMinReadmitPeriod() { return this._network.nodeMinReadmitPeriod; } /** * @param {number} nodeMaxReadmitPeriod * @returns {this} */ setNodeMaxReadmitPeriod(nodeMaxReadmitPeriod) { this._network.setNodeMaxReadmitPeriod(nodeMaxReadmitPeriod); return this; } /** * @returns {number} */ get nodeMaxReadmitPeriod() { return this._network.nodeMaxReadmitPeriod; } /** * @param {number} requestTimeout - Number of milliseconds * @returns {this} */ setRequestTimeout(requestTimeout) { this._requestTimeout = requestTimeout; return this; } /** * @returns {?number} */ get requestTimeout() { return this._requestTimeout; } /** * @returns {number} */ get networkUpdatePeriod() { return this._networkUpdatePeriod; } /** * @param {number} networkUpdatePeriod * @returns {this} */ setNetworkUpdatePeriod(networkUpdatePeriod) { clearTimeout(this._timer); this._networkUpdatePeriod = networkUpdatePeriod; this._scheduleNetworkUpdate(); return this; } /** * Set logger * * @param {Logger} logger * @returns {this} */ setLogger(logger) { this._logger = logger; return this; } /** * Get logger if set * * @returns {?Logger} */ get logger() { return this._logger; } /** * @param {AccountId | string} accountId */ async ping(accountId) { await new AccountBalanceQuery({ accountId }) .setNodeAccountIds([ accountId instanceof AccountId ? accountId : AccountId.fromString(accountId), ]) .execute(this); } async pingAll() { for (const nodeAccountId of Object.values(this._network.network)) { await this.ping(nodeAccountId); } } /** * Update the network address book. * @returns {Promise<this>} */ async updateNetwork() { if (this._isUpdatingNetwork) { return this; } this._isUpdatingNetwork = true; try { const addressBook = await new AddressBookQuery() .setFileId( FileId.getAddressBookFileIdFor(this._shard, this._realm), ) .execute(this); this.setNetworkFromAddressBook(addressBook); } catch (error) { if (this._logger) { this._logger.trace( `failed to update client address book: ${ /** @type {Error} */ (error).toString() }`, ); } } finally { this._isUpdatingNetwork = false; } return this; } /** * @returns {void} */ close() { this._network.close(); this._mirrorNetwork.close(); this._isShutdown = true; clearTimeout(this._timer); } /** * @abstract * @returns {(address: string) => ChannelT} */ _createNetworkChannel() { throw new Error("not implemented"); } /** * @abstract * @returns {(address: string) => MirrorChannelT} */ _createMirrorNetworkChannel() { throw new Error("not implemented"); } /** * @private */ _scheduleNetworkUpdate() { // This is the automatic network update promise that _eventually_ completes // eslint-disable-next-line @typescript-eslint/no-floating-promises,@typescript-eslint/no-misused-promises this._timer = setTimeout(async () => { await this.updateNetwork(); if (!this._isShutdown) { // Recall this method to continuously update the network // every `networkUpdatePeriod` amount of itme this._scheduleNetworkUpdate(); } }, this._networkUpdatePeriod); } /** * Determines the appropriate scheme (http/https) based on the host and port. * * @private * @param {string} host - The host address * @param {number} port - The port number * @returns {string} - The scheme ('http' or 'https') */ _getSchemeFromHostAndPort(host, port) { // For localhost and 127.0.0.1, use HTTP scheme if (host === "localhost" || host === "127.0.0.1") { return "http"; } // Standard HTTPS ports if (port === 443) { return "https"; } // Standard HTTP ports if (port === 80) { return "http"; } // For other ports, assume HTTPS for security return "https"; } /** * @returns {boolean} */ get isClientShutDown() { return this._isShutdown; } /** * Validates that all nodes in a network are in the same shard and realm. * * @param {{[key: string]: (string | AccountId)}} network */ static _validateNetworkConsistency(network) { if (Object.keys(network).length === 0) { return; } const [, nodeAccountId] = Object.entries(network)[0]; const accountIdStr = nodeAccountId.toString(); const [firstNodeShard, firstNodeRealm] = accountIdStr .split(".") .map(Number); const isNetworkValid = Object.values(network).every((accountId) => { const accountIdStr = accountId.toString(); const [currentShard, currentRealm] = accountIdStr .split(".") .map(Number); return ( currentShard === firstNodeShard && currentRealm === firstNodeRealm ); }); if (!isNetworkValid) { throw new Error( "Network is not valid, all nodes must be in the same shard and realm", ); } } /** * Extracts shard and realm values from a network configuration. * Note: This method assumes the network is consistent (all nodes in same shard/realm). * Use validateNetworkConsistency() first to ensure this. * * @param {{[key: string]: (string | AccountId)}} network * @returns {{shard: number, realm: number}} */ static _extractShardRealm(network) { const entries = Object.entries(network); if (entries.length === 0) { return { shard: 0, realm: 0 }; } const [, firstNodeAccountId] = entries[0]; const accountIdStr = firstNodeAccountId.toString(); const [shard, realm] = accountIdStr.split(".").map(Number); return { shard, realm }; } }