UNPKG

@hashgraph/sdk

Version:
449 lines (410 loc) 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.QUERY_REGISTRY = void 0; var _Status = _interopRequireDefault(require("../Status.cjs")); var _AccountId = _interopRequireDefault(require("../account/AccountId.cjs")); var _Hbar = _interopRequireDefault(require("../Hbar.cjs")); var _Executable = require("../Executable.cjs"); var _TransactionId = _interopRequireDefault(require("../transaction/TransactionId.cjs")); var HieroProto = _interopRequireWildcard(require("@hashgraph/proto")); var _PrecheckStatusError = _interopRequireDefault(require("../PrecheckStatusError.cjs")); var _MaxQueryPaymentExceeded = _interopRequireDefault(require("../MaxQueryPaymentExceeded.cjs")); var _QueryBase = _interopRequireDefault(require("./QueryBase.cjs")); var _CostQuery = _interopRequireDefault(require("./CostQuery.cjs")); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } 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("../PublicKey.js").default} PublicKey * @typedef {import("../client/Client.js").ClientOperator} ClientOperator * @typedef {import("../client/Client.js").default<*, *>} Client * @typedef {import("../logger/Logger.js").default} Logger */ /** * This registry holds a bunch of callbacks for `fromProtobuf()` implementations * Since this is essentially aa cache, perhaps we should move this variable into the `Cache` * type for consistency? * * @type {Map<HieroProto.proto.Query["query"], (query: HieroProto.proto.IQuery) => Query<*>>} */ const QUERY_REGISTRY = exports.QUERY_REGISTRY = new Map(); /** * Base class for all queries that can be submitted to Hedera. * * @abstract * @template OutputT * @augments {QueryBase<HieroProto.proto.IQuery, HieroProto.proto.IResponse, OutputT>} */ class Query extends _QueryBase.default { constructor() { super(); /** * The payment transaction ID * * @type {?TransactionId} */ this._paymentTransactionId = null; /** * The payment transactions list where each index points to a different node * * @type {HieroProto.proto.ITransaction[]} */ this._paymentTransactions = []; /** * The amount being paid to the node for this query. * A user can set this field explicitly, or we'll query the value during execution. * * @type {?Hbar} */ this._queryPayment = null; /** * The maximum query payment a user is willing to pay. Unlike `Transaction.maxTransactionFee` * this field only exists in the SDK; there is no protobuf field equivalent. If and when * we query the actual cost of the query and the cost is greater than the max query payment * we'll throw a `MaxQueryPaymentExceeded` error. * * @type {?Hbar} */ this._maxQueryPayment = null; /** * This is strictly used for `_getLogId()` which requires a timestamp. The timestamp it typically * uses comes from the payment transaction ID, but that field is not set if this query is free. * For those occasions we use this timestamp field generated at query construction instead. * * @type {number} */ this._timestamp = Date.now(); } /** * Deserialize a query from bytes. The bytes should be a `proto.Query`. * * @template T * @param {Uint8Array} bytes * @returns {Query<T>} */ static fromBytes(bytes) { const query = HieroProto.proto.Query.decode(bytes); if (query.query == null) { throw new Error("(BUG) query.query was not set in the protobuf"); } const fromProtobuf = /** @type {(query: HieroProto.proto.IQuery) => Query<T>} */ QUERY_REGISTRY.get(query.query); if (fromProtobuf == null) { throw new Error(`(BUG) Query.fromBytes() not implemented for type ${query.query}`); } return fromProtobuf(query); } /** * Serialize the query into bytes. * * **NOTE**: Does not preserve payment transactions * * @returns {Uint8Array} */ toBytes() { return HieroProto.proto.Query.encode(this._makeRequest()).finish(); } /** * Set an explicit payment amount for this query. * * The client will submit exactly this amount for the payment of this query. Hedera * will not return any remainder. * * @param {Hbar} queryPayment * @returns {this} */ setQueryPayment(queryPayment) { this._queryPayment = queryPayment; return this; } /** * Set the maximum payment allowable for this query. * * @param {Hbar} maxQueryPayment * @returns {this} */ setMaxQueryPayment(maxQueryPayment) { this._maxQueryPayment = maxQueryPayment; return this; } /** * Fetch the cost of this query from a consensus node * * @param {import("../client/Client.js").default<Channel, *>} client * @returns {Promise<Hbar>} */ async getCost(client) { // The node account IDs must be set to execute a cost query if (this._nodeAccountIds.isEmpty) { this._nodeAccountIds.setList(client._network.getNodeAccountIdsForExecute()); } // Change the timestamp. Should we be doing this? this._timestamp = Date.now(); const cost = await new _CostQuery.default(this).execute(client); return _Hbar.default.fromTinybars(cost._valueInTinybar.multipliedBy(1.1).toFixed(0)); } /** * Set he payment transaction explicitly * * @param {TransactionId} paymentTransactionId * @returns {this} */ setPaymentTransactionId(paymentTransactionId) { this._paymentTransactionId = paymentTransactionId; return this; } /** * Get the payment transaction ID * * @returns {?TransactionId} */ get paymentTransactionId() { return this._paymentTransactionId; } /** * Get the current transaction ID, and make sure it's not null * * @returns {TransactionId} */ _getTransactionId() { if (this._paymentTransactionId == null) { throw new Error("Query.PaymentTransactionId was not set duration execution"); } return this._paymentTransactionId; } /** * Is payment required for this query. By default most queries require payment * so the default implementation returns true. * * @protected * @returns {boolean} */ _isPaymentRequired() { return true; } /** * Validate checksums of the query. * * @param {Client} client */ // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function _validateChecksums(client) { // Shouldn't we be checking `paymentTransactionId` here sine it contains an `accountId`? // Do nothing } /** * Before we proceed exeuction, we need to do a couple checks * * @template {MirrorChannel} MirrorChannelT * @param {import("../client/Client.js").default<Channel, MirrorChannelT>} client * @returns {Promise<void>} */ async _beforeExecute(client) { // If we're executing this query multiple times the the payment transaction ID list // will already be set if (this._paymentTransactions.length > 0) { return; } // Check checksums if enabled if (client.isAutoValidateChecksumsEnabled()) { this._validateChecksums(client); } // If the nodes aren't set, set them. if (this._nodeAccountIds.isEmpty) { this._nodeAccountIds.setList(client._network.getNodeAccountIdsForExecute()); } // Save the operator this._operator = this._operator != null ? this._operator : client._operator; // And payment is required if (this._isPaymentRequired()) { // Assign the account IDs to which the transaction should be sent. this.transactionNodeIds = Object.values(client.network).map(accountNodeId => accountNodeId.toString()); // And the client has an operator if (this._operator != null) { // Generate the payment transaction ID this._paymentTransactionId = _TransactionId.default.generate(this._operator.accountId); } else { // If payment is required, but an operator did not exist, throw an error throw new Error("`client` must have an `operator` or an explicit payment transaction must be provided"); } } else { // If the payment transaction ID is not set, but this query doesn't require a payment // set the payment transaction ID to an empty transaction ID. // FIXME: Should use `TransactionId.withValidStart()` instead this._paymentTransactionId = _TransactionId.default.generate(new _AccountId.default(0)); } let cost = new _Hbar.default(0); const maxQueryPayment = this._maxQueryPayment != null ? this._maxQueryPayment : client.defaultMaxQueryPayment; if (this._queryPayment != null) { cost = this._queryPayment; } else if (this._paymentTransactions.length === 0 && this._isPaymentRequired()) { // If the query payment was not explictly set, fetch the actual cost. const actualCost = await this.getCost(client); // Confirm it's less than max query payment if (maxQueryPayment.toTinybars().toInt() < actualCost.toTinybars().toInt()) { throw new _MaxQueryPaymentExceeded.default(actualCost, maxQueryPayment); } cost = actualCost; if (this._logger) { this._logger.debug(`[${this._getLogId()}] received cost for query ${cost.toString()}`); } } // Set the either queried cost, or the original value back into `queryPayment` // in case a user executes same query multiple times. However, users should // really not be executing the same query multiple times meaning this is // typically not needed. this._queryPayment = cost; // Not sure if we should be overwritting this field tbh. this._timestamp = Date.now(); this._nodeAccountIds.setLocked(); // Generate the payment transactions for (const nodeId of this._nodeAccountIds.list) { const logId = this._getLogId(); const paymentTransactionId = /** @type {import("../transaction/TransactionId.js").default} */ this._paymentTransactionId; const paymentAmount = /** @type {Hbar} */this._queryPayment; if (this._logger) { this._logger.debug(`[${logId}] making a payment transaction for node ${nodeId.toString()} and transaction ID ${paymentTransactionId.toString()} with amount ${paymentAmount.toString()}`); } this._paymentTransactions.push(await this._makePaymentTransaction(paymentTransactionId, nodeId, this._isPaymentRequired() ? this._operator : null, paymentAmount)); } } /** * @abstract * @internal * @param {HieroProto.proto.IResponse} response * @returns {HieroProto.proto.IResponseHeader} */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _mapResponseHeader(response) { throw new Error("not implemented"); } /** * @protected * @returns {HieroProto.proto.IQueryHeader} */ _makeRequestHeader() { /** @type {HieroProto.proto.IQueryHeader} */ let header = {}; if (this._isPaymentRequired() && this._paymentTransactions.length > 0) { header = { responseType: HieroProto.proto.ResponseType.ANSWER_ONLY, payment: this._paymentTransactions[this._nodeAccountIds.index] }; } return header; } /** * @abstract * @internal * @param {HieroProto.proto.IQueryHeader} header * @returns {HieroProto.proto.IQuery} */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _onMakeRequest(header) { throw new Error("not implemented"); } /** * @internal * @returns {HieroProto.proto.IQuery} */ _makeRequest() { /** @type {HieroProto.proto.IQueryHeader} */ let header = {}; if (this._isPaymentRequired() && this._paymentTransactions != null) { header = { payment: this._paymentTransactions[this._nodeAccountIds.index], responseType: HieroProto.proto.ResponseType.ANSWER_ONLY }; } return this._onMakeRequest(header); } /** * @override * @internal * @returns {Promise<HieroProto.proto.IQuery>} */ async _makeRequestAsync() { /** @type {HieroProto.proto.IQueryHeader} */ let header = { responseType: HieroProto.proto.ResponseType.ANSWER_ONLY }; const logId = this._getLogId(); const nodeId = this._nodeAccountIds.current; const paymentTransactionId = _TransactionId.default.generate(this._operator ? this._operator.accountId : new _AccountId.default(0)); const paymentAmount = /** @type {Hbar} */this._queryPayment; if (this._logger) { this._logger.debug(`[${logId}] making a payment transaction for node ${nodeId.toString()} and transaction ID ${paymentTransactionId.toString()} with amount ${paymentAmount.toString()}`); } header.payment = await this._makePaymentTransaction(paymentTransactionId, nodeId, this._isPaymentRequired() ? this._operator : null, paymentAmount); return this._onMakeRequest(header); } /** * @override * @internal * @param {HieroProto.proto.IQuery} request * @param {HieroProto.proto.IResponse} response * @returns {[Status, ExecutionState]} */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _shouldRetry(request, response) { const { nodeTransactionPrecheckCode } = this._mapResponseHeader(response); const status = _Status.default._fromCode(nodeTransactionPrecheckCode != null ? nodeTransactionPrecheckCode : HieroProto.proto.ResponseCodeEnum.OK); if (this._logger) { this._logger.debug(`[${this._getLogId()}] received status ${status.toString()}`); } switch (status) { case _Status.default.Busy: case _Status.default.Unknown: case _Status.default.PlatformTransactionNotCreated: case _Status.default.PlatformNotActive: return [status, _Executable.ExecutionState.Retry]; case _Status.default.Ok: return [status, _Executable.ExecutionState.Finished]; default: return [status, _Executable.ExecutionState.Error]; } } /** * @override * @internal * @param {HieroProto.proto.IQuery} request * @param {HieroProto.proto.IResponse} response * @param {AccountId} nodeId * @returns {Error} */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _mapStatusError(request, response, nodeId) { const { nodeTransactionPrecheckCode } = this._mapResponseHeader(response); const status = _Status.default._fromCode(nodeTransactionPrecheckCode != null ? nodeTransactionPrecheckCode : HieroProto.proto.ResponseCodeEnum.OK); return new _PrecheckStatusError.default({ nodeId, status, transactionId: this._getTransactionId(), contractFunctionResult: null }); } /** * @param {HieroProto.proto.Query} request * @returns {Uint8Array} */ _requestToBytes(request) { return HieroProto.proto.Query.encode(request).finish(); } /** * @param {HieroProto.proto.Response} response * @returns {Uint8Array} */ _responseToBytes(response) { return HieroProto.proto.Response.encode(response).finish(); } } exports.default = Query;