PHP WebShell
Текущая директория: /usr/lib/node_modules/bitgo/node_modules/@bitgo/sdk-coin-hbar/node_modules/@hashgraph/sdk/src
Просмотр файла: Executable.js
/*-
*
* Hedera JavaScript SDK
*
* Copyright (C) 2020 - 2022 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import GrpcServiceError from "./grpc/GrpcServiceError.js";
import GrpcStatus from "./grpc/GrpcStatus.js";
import List from "./transaction/List.js";
import * as hex from "./encoding/hex.js";
import HttpError from "./http/HttpError.js";
/**
* @typedef {import("./account/AccountId.js").default} AccountId
* @typedef {import("./Status.js").default} Status
* @typedef {import("./channel/Channel.js").default} Channel
* @typedef {import("./channel/MirrorChannel.js").default} MirrorChannel
* @typedef {import("./transaction/TransactionId.js").default} TransactionId
* @typedef {import("./client/Client.js").ClientOperator} ClientOperator
* @typedef {import("./Signer.js").Signer} Signer
* @typedef {import("./PublicKey.js").default} PublicKey
* @typedef {import("./logger/Logger.js").default} Logger
*/
/**
* @enum {string}
*/
export const ExecutionState = {
Finished: "Finished",
Retry: "Retry",
Error: "Error",
};
export const RST_STREAM = /\brst[^0-9a-zA-Z]stream\b/i;
/**
* @abstract
* @internal
* @template RequestT
* @template ResponseT
* @template OutputT
*/
export default class Executable {
constructor() {
/**
* The number of times we can retry the grpc call
*
* @internal
* @type {number}
*/
this._maxAttempts = 10;
/**
* List of node account IDs for each transaction that has been
* built.
*
* @internal
* @type {List<AccountId>}
*/
this._nodeAccountIds = new List();
/**
* @internal
*/
this._signOnDemand = false;
/**
* This is the request's min backoff
*
* @internal
* @type {number | null}
*/
this._minBackoff = null;
/**
* This is the request's max backoff
*
* @internal
* @type {number}
*/
this._maxBackoff = 8000;
/**
* The operator that was used to execute this request.
* The reason we save the operator in the request is because of the signing on
* demand feature. This feature requires us to sign new request on each attempt
* meaning if a client with an operator was used we'd need to sign with the operator
* on each attempt.
*
* @internal
* @type {ClientOperator | null}
*/
this._operator = null;
/**
* The complete timeout for running the `execute()` method
*
* @internal
* @type {number | null}
*/
this._requestTimeout = null;
/**
* The grpc request timeout aka deadline.
*
* The reason we have this is because there were times that consensus nodes held the grpc
* connection, but didn't return anything; not error nor regular response. This resulted
* in some weird behavior in the SDKs. To fix this we've added a grpc deadline to prevent
* nodes from stalling the executing of a request.
*
* @internal
* @type {number | null}
*/
this._grpcDeadline = null;
/**
* Logger
*
* @protected
* @type {Logger | null}
*/
this._logger = null;
}
/**
* Get the list of node account IDs on the request. If no nodes are set, then null is returned.
* The reasoning for this is simply "legacy behavior".
*
* @returns {?AccountId[]}
*/
get nodeAccountIds() {
if (this._nodeAccountIds.isEmpty) {
return null;
} else {
this._nodeAccountIds.setLocked();
return this._nodeAccountIds.list;
}
}
/**
* Set the node account IDs on the request
*
* @param {AccountId[]} nodeIds
* @returns {this}
*/
setNodeAccountIds(nodeIds) {
// Set the node account IDs, and lock the list. This will require `execute`
// to use these nodes instead of random nodes from the network.
this._nodeAccountIds.setList(nodeIds).setLocked();
return this;
}
/**
* @deprecated
* @returns {number}
*/
get maxRetries() {
console.warn("Deprecated: use maxAttempts instead");
return this.maxAttempts;
}
/**
* @param {number} maxRetries
* @returns {this}
*/
setMaxRetries(maxRetries) {
console.warn("Deprecated: use setMaxAttempts() instead");
return this.setMaxAttempts(maxRetries);
}
/**
* Get the max attempts on the request
*
* @returns {number}
*/
get maxAttempts() {
return this._maxAttempts;
}
/**
* Set the max attempts on the request
*
* @param {number} maxAttempts
* @returns {this}
*/
setMaxAttempts(maxAttempts) {
this._maxAttempts = maxAttempts;
return this;
}
/**
* Get the grpc deadline
*
* @returns {?number}
*/
get grpcDeadline() {
return this._grpcDeadline;
}
/**
* Set the grpc deadline
*
* @param {number} grpcDeadline
* @returns {this}
*/
setGrpcDeadline(grpcDeadline) {
this._grpcDeadline = grpcDeadline;
return this;
}
/**
* Set the min backoff for the request
*
* @param {number} minBackoff
* @returns {this}
*/
setMinBackoff(minBackoff) {
// Honestly we shouldn't be checking for null since that should be TypeScript's job.
// Also verify that min backoff is not greater than max backoff.
if (minBackoff == null) {
throw new Error("minBackoff cannot be null.");
} else if (this._maxBackoff != null && minBackoff > this._maxBackoff) {
throw new Error("minBackoff cannot be larger than maxBackoff.");
}
this._minBackoff = minBackoff;
return this;
}
/**
* Get the min backoff
*
* @returns {number | null}
*/
get minBackoff() {
return this._minBackoff;
}
/**
* Set the max backoff for the request
*
* @param {?number} maxBackoff
* @returns {this}
*/
setMaxBackoff(maxBackoff) {
// Honestly we shouldn't be checking for null since that should be TypeScript's job.
// Also verify that max backoff is not less than min backoff.
if (maxBackoff == null) {
throw new Error("maxBackoff cannot be null.");
} else if (this._minBackoff != null && maxBackoff < this._minBackoff) {
throw new Error("maxBackoff cannot be smaller than minBackoff.");
}
this._maxBackoff = maxBackoff;
return this;
}
/**
* Get the max backoff
*
* @returns {number}
*/
get maxBackoff() {
return this._maxBackoff;
}
/**
* This method is responsible for doing any work before the executing process begins.
* For paid queries this will result in executing a cost query, for transactions this
* will make sure we save the operator and sign any requests that need to be signed
* in case signing on demand is disabled.
*
* @abstract
* @protected
* @param {import("./client/Client.js").default<Channel, *>} client
* @returns {Promise<void>}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_beforeExecute(client) {
throw new Error("not implemented");
}
/**
* Create a protobuf request which will be passed into the `_execute()` method
*
* @abstract
* @protected
* @returns {Promise<RequestT>}
*/
_makeRequestAsync() {
throw new Error("not implemented");
}
/**
* This name is a bit wrong now, but the purpose of this method is to map the
* request and response into an error. This method will only be called when
* `_shouldRetry` returned `ExecutionState.Error`
*
* @abstract
* @internal
* @param {RequestT} request
* @param {ResponseT} response
* @returns {Error}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_mapStatusError(request, response) {
throw new Error("not implemented");
}
/**
* Map the request, response, and the node account ID used for this attempt into a response.
* This method will only be called when `_shouldRetry` returned `ExecutionState.Finished`
*
* @abstract
* @protected
* @param {ResponseT} response
* @param {AccountId} nodeAccountId
* @param {RequestT} request
* @returns {Promise<OutputT>}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_mapResponse(response, nodeAccountId, request) {
throw new Error("not implemented");
}
/**
* Perform a single grpc call with the given request. Each request has it's own
* required service so we just pass in channel, and it'$ the request's responsiblity
* to use the right service and call the right grpc method.
*
* @abstract
* @internal
* @param {Channel} channel
* @param {RequestT} request
* @returns {Promise<ResponseT>}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_execute(channel, request) {
throw new Error("not implemented");
}
/**
* Return the current transaction ID for the request. All requests which are
* use the same transaction ID for each node, but the catch is that `Transaction`
* implicitly supports chunked transactions. Meaning there could be multiple
* transaction IDs stored in the request, and a different transaction ID will be used
* on subsequent calls to `execute()`
*
* FIXME: This method can most likely be removed, although some further inspection
* is required.
*
* @abstract
* @protected
* @returns {TransactionId}
*/
_getTransactionId() {
throw new Error("not implemented");
}
/**
* Return the log ID for this particular request
*
* Log IDs are simply a string constructed to make it easy to track each request's
* execution even when mulitple requests are executing in parallel. Typically, this
* method returns the format of `[<request type>.<timestamp of the transaction ID>]`
*
* Maybe we should deduplicate this using ${this.consturtor.name}
*
* @abstract
* @internal
* @returns {string}
*/
_getLogId() {
throw new Error("not implemented");
}
/**
* Serialize the request into bytes
*
* @abstract
* @param {RequestT} request
* @returns {Uint8Array}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_requestToBytes(request) {
throw new Error("not implemented");
}
/**
* Serialize the response into bytes
*
* @abstract
* @param {ResponseT} response
* @returns {Uint8Array}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_responseToBytes(response) {
throw new Error("not implemented");
}
/**
* Advance the request to the next node
*
* FIXME: This method used to perform different code depending on if we're
* executing a query or transaction, but that is no longer the case
* and hence could be removed.
*
* @protected
* @returns {void}
*/
_advanceRequest() {
this._nodeAccountIds.advance();
}
/**
* Determine if we should continue the execution process, error, or finish.
*
* FIXME: This method should really be called something else. Initially it returned
* a boolean so `shouldRetry` made sense, but now it returns an enum, so the name
* no longer makes sense.
*
* @abstract
* @protected
* @param {RequestT} request
* @param {ResponseT} response
* @returns {[Status, ExecutionState]}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_shouldRetry(request, response) {
throw new Error("not implemented");
}
/**
* Determine if we should error based on the gRPC status
*
* Unlike `shouldRetry` this method does in fact still return a boolean
*
* @protected
* @param {Error} error
* @returns {boolean}
*/
_shouldRetryExceptionally(error) {
if (error instanceof GrpcServiceError) {
return (
error.status._code === GrpcStatus.Timeout._code ||
error.status._code === GrpcStatus.Unavailable._code ||
error.status._code === GrpcStatus.ResourceExhausted._code ||
(error.status._code === GrpcStatus.Internal._code &&
RST_STREAM.test(error.message))
);
} else {
// if we get to the 'else' statement, the 'error' is instanceof 'HttpError'
// and in this case, we have to retry always
return true;
}
}
/**
* A helper method for setting the operator on the request
*
* @internal
* @param {AccountId} accountId
* @param {PublicKey} publicKey
* @param {(message: Uint8Array) => Promise<Uint8Array>} transactionSigner
* @returns {this}
*/
_setOperatorWith(accountId, publicKey, transactionSigner) {
this._operator = {
transactionSigner,
accountId,
publicKey,
};
return this;
}
/**
* Execute this request using the signer
*
* This method is part of the signature providers feature
* https://hips.hedera.com/hip/hip-338
*
* @param {Signer} signer
* @returns {Promise<OutputT>}
*/
async executeWithSigner(signer) {
return signer.call(this);
}
/**
* Execute the request using a client and an optional request timeout
*
* @template {Channel} ChannelT
* @template {MirrorChannel} MirrorChannelT
* @param {import("./client/Client.js").default<ChannelT, MirrorChannelT>} client
* @param {number=} requestTimeout
* @returns {Promise<OutputT>}
*/
async execute(client, requestTimeout) {
// If the logger on the request is not set, use the logger in client
// (if set, otherwise do not use logger)
this._logger =
this._logger == null
? client._logger != null
? client._logger
: null
: this._logger;
// If the request timeout is set on the request we'll prioritize that instead
// of the parameter provided, and if the parameter isn't provided we'll
// use the default request timeout on client
if (this._requestTimeout == null) {
this._requestTimeout =
requestTimeout != null ? requestTimeout : client.requestTimeout;
}
// Some request need to perform additional requests before the executing
// such as paid queries need to fetch the cost of the query before
// finally executing the actual query.
await this._beforeExecute(client);
// If the max backoff on the request is not set, use the default value in client
if (this._maxBackoff == null) {
this._maxBackoff = client.maxBackoff;
}
// If the min backoff on the request is not set, use the default value in client
if (this._minBackoff == null) {
this._minBackoff = client.minBackoff;
}
// If the max attempts on the request is not set, use the default value in client
// If the default value in client is not set, use a default of 10.
//
// FIXME: current implementation is wrong, update to follow comment above.
const maxAttempts =
client._maxAttempts != null
? client._maxAttempts
: this._maxAttempts;
// Save the start time to be used later with request timeout
const startTime = Date.now();
// Saves each error we get so when we err due to max attempts exceeded we'll have
// the last error that was returned by the consensus node
let persistentError = null;
// The retry loop
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
// Determine if we've exceeded request timeout
if (
this._requestTimeout != null &&
startTime + this._requestTimeout <= Date.now()
) {
throw new Error("timeout exceeded");
}
let nodeAccountId;
let node;
// If node account IDs is locked then use the node account IDs
// from the list, otherwise build a new list of one node account ID
// using the entire network
if (this._nodeAccountIds.locked) {
nodeAccountId = this._nodeAccountIds.current;
node = client._network.getNode(nodeAccountId);
} else {
node = client._network.getNode();
nodeAccountId = node.accountId;
this._nodeAccountIds.setList([nodeAccountId]);
}
if (node == null) {
throw new Error(
`NodeAccountId not recognized: ${nodeAccountId.toString()}`
);
}
// Get the log ID for the request.
const logId = this._getLogId();
if (this._logger) {
this._logger.debug(
`[${logId}] Node AccountID: ${node.accountId.toString()}, IP: ${node.address.toString()}`
);
}
const channel = node.getChannel();
const request = await this._makeRequestAsync();
// advance the internal index
// non-free queries and transactions map to more than 1 actual transaction and this will cause
// the next invocation of makeRequest to return the _next_ transaction
// FIXME: This is likely no longer relavent after we've transitioned to using our `List` type
// can be replaced with `this._nodeAccountIds.advance();`
this._advanceRequest();
let response;
// If the node is unhealthy, wait for it to be healthy
// FIXME: This is wrong, we should skip to the next node, and only perform
// a request backoff after we've tried all nodes in the current list.
if (!node.isHealthy()) {
if (this._logger) {
this._logger.debug(
`[${logId}] node is not healthy, skipping waiting ${node.getRemainingTime()}`
);
}
// We don't need to wait, we can proceed to the next attempt.
continue;
}
try {
// Race the execution promise against the grpc timeout to prevent grpc connections
// from blocking this request
const promises = [];
// If a grpc deadline is est, we should race it, otherwise the only thing in the
// list of promises will be the execution promise.
if (this._grpcDeadline != null) {
promises.push(
// eslint-disable-next-line ie11/no-loop-func
new Promise((_, reject) =>
setTimeout(
// eslint-disable-next-line ie11/no-loop-func
() =>
reject(new Error("grpc deadline exceeded")),
/** @type {number=} */ (this._grpcDeadline)
)
)
);
}
if (this._logger) {
this._logger.trace(
`[${this._getLogId()}] sending protobuf ${hex.encode(
this._requestToBytes(request)
)}`
);
}
promises.push(this._execute(channel, request));
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
response = /** @type {ResponseT} */ (
await Promise.race(promises)
);
} catch (err) {
// If we received a grpc status error we need to determine if
// we should retry on this error, or err from the request entirely.
const error = GrpcServiceError._fromResponse(
/** @type {Error} */ (err)
);
// Save the error in case we retry
persistentError = error;
if (this._logger) {
this._logger.debug(
`[${logId}] received error ${JSON.stringify(error)}`
);
}
if (
(error instanceof GrpcServiceError ||
error instanceof HttpError) &&
this._shouldRetryExceptionally(error) &&
attempt <= maxAttempts
) {
// Increase the backoff for the particular node and remove it from
// the healthy node list
if (this._logger) {
this._logger.debug(
`[${this._getLogId()}] node with accountId: ${node.accountId.toString()} and proxy IP: ${node.address.toString()} is unhealthy`
);
}
client._network.increaseBackoff(node);
continue;
}
throw err;
}
if (this._logger) {
this._logger.trace(
`[${this._getLogId()}] sending protobuf ${hex.encode(
this._responseToBytes(response)
)}`
);
}
// If we didn't receive an error we should decrease the current nodes backoff
// in case it is a recovering node
client._network.decreaseBackoff(node);
// Determine what execution state we're in by the response
// For transactions this would be as simple as checking the response status is `OK`
// while for _most_ queries it would check if the response status is `SUCCESS`
// The only odd balls are `TransactionReceiptQuery` and `TransactionRecordQuery`
const [err, shouldRetry] = this._shouldRetry(request, response);
if (err != null) {
persistentError = err;
}
// Determine by the executing state what we should do
switch (shouldRetry) {
case ExecutionState.Retry:
await delayForAttempt(
attempt,
this._minBackoff,
this._maxBackoff
);
continue;
case ExecutionState.Finished:
return this._mapResponse(response, nodeAccountId, request);
case ExecutionState.Error:
throw this._mapStatusError(request, response);
default:
throw new Error(
"(BUG) non-exhuastive switch statement for `ExecutionState`"
);
}
}
// We'll only get here if we've run out of attempts, so we return an error wrapping the
// persistent error we saved before.
throw new Error(
`max attempts of ${maxAttempts.toString()} was reached for request with last error being: ${
persistentError != null ? persistentError.toString() : ""
}`
);
}
/**
* The current purpose of this method is to easily support signature providers since
* signature providers need to serialize _any_ request into bytes. `Query` and `Transaction`
* already implement `toBytes()` so it only made sense to make it avaiable here too.
*
* @abstract
* @returns {Uint8Array}
*/
toBytes() {
throw new Error("not implemented");
}
/**
* 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;
}
}
/**
* A simple function that returns a promise timeout for a specific period of time
*
* @param {number} attempt
* @param {number} minBackoff
* @param {number} maxBackoff
* @returns {Promise<void>}
*/
function delayForAttempt(attempt, minBackoff, maxBackoff) {
// 0.1s, 0.2s, 0.4s, 0.8s, ...
const ms = Math.min(
Math.floor(minBackoff * Math.pow(2, attempt)),
maxBackoff
);
return new Promise((resolve) => setTimeout(resolve, ms));
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!