PHP WebShell
Текущая директория: /opt/BitGoJS/node_modules/@hashgraph/sdk/src/transaction
Просмотр файла: Transaction.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 Hbar from "../Hbar.js";
import TransactionResponse from "./TransactionResponse.js";
import TransactionId from "./TransactionId.js";
import TransactionHashMap from "./TransactionHashMap.js";
import SignatureMap from "./SignatureMap.js";
import Executable, { ExecutionState } from "../Executable.js";
import Status from "../Status.js";
import Long from "long";
import * as sha384 from "../cryptography/sha384.js";
import * as hex from "../encoding/hex.js";
import * as HashgraphProto from "@hashgraph/proto";
import PrecheckStatusError from "../PrecheckStatusError.js";
import AccountId from "../account/AccountId.js";
import PublicKey from "../PublicKey.js";
import List from "./List.js";
import Timestamp from "../Timestamp.js";
import * as util from "../util.js";
/**
* @typedef {import("bignumber.js").default} BigNumber
*/
/**
* @typedef {import("../schedule/ScheduleCreateTransaction.js").default} ScheduleCreateTransaction
* @typedef {import("../PrivateKey.js").default} PrivateKey
* @typedef {import("../channel/Channel.js").default} Channel
* @typedef {import("../client/Client.js").default<*, *>} Client
* @typedef {import("../Signer.js").Signer} Signer
*/
// 90 days (in seconds)
export const DEFAULT_AUTO_RENEW_PERIOD = Long.fromValue(7776000);
// maximum value of i64 (so there is never a record generated)
export const DEFAULT_RECORD_THRESHOLD = Hbar.fromTinybars(
Long.fromString("9223372036854775807")
);
// 120 seconds
const DEFAULT_TRANSACTION_VALID_DURATION = 120;
export const CHUNK_SIZE = 1024;
/**
* @type {Map<NonNullable<HashgraphProto.proto.TransactionBody["data"]>, (transactions: HashgraphProto.proto.ITransaction[], signedTransactions: HashgraphProto.proto.ISignedTransaction[], transactionIds: TransactionId[], nodeIds: AccountId[], bodies: HashgraphProto.proto.TransactionBody[]) => Transaction>}
*/
export const TRANSACTION_REGISTRY = new Map();
/**
* Base class for all transactions that may be submitted to Hedera.
*
* @abstract
* @augments {Executable<HashgraphProto.proto.ITransaction, HashgraphProto.proto.ITransactionResponse, TransactionResponse>}
*/
export default class Transaction extends Executable {
// A SDK transaction is composed of multiple, raw protobuf transactions.
// These should be functionally identical, with the exception of pointing to
// different nodes.
// When retrying a transaction after a network error or retry-able
// status response, we try a different transaction and thus a different node.
constructor() {
super();
/**
* List of proto transactions that have been built from this SDK
* transaction.
*
* This is a 2-D array built into one, meaning to
* get to the next row you'd index into this array `row * rowLength + column`
* where `rowLength` is `nodeAccountIds.length`
*
* @internal
* @type {List<HashgraphProto.proto.ITransaction | null>}
*/
this._transactions = new List();
/**
* List of proto transactions that have been built from this SDK
* transaction.
*
* This is a 2-D array built into one, meaning to
* get to the next row you'd index into this array `row * rowLength + column`
* where `rowLength` is `nodeAccountIds.length`
*
* @internal
* @type {List<HashgraphProto.proto.ISignedTransaction>}
*/
this._signedTransactions = new List();
/**
* Set of public keys (as string) who have signed this transaction so
* we do not allow them to sign it again.
*
* @internal
* @type {Set<string>}
*/
this._signerPublicKeys = new Set();
/**
* The transaction valid duration
*
* @private
* @type {number}
*/
this._transactionValidDuration = DEFAULT_TRANSACTION_VALID_DURATION;
/**
* The default max transaction fee for this particular transaction type.
* Most transactions use the default of 2 Hbars, but some requests such
* as `TokenCreateTransaction` need to use a different default value.
*
* @protected
* @type {Hbar}
*/
this._defaultMaxTransactionFee = new Hbar(2);
/**
* The max transaction fee on the request. This field is what users are able
* to set, not the `defaultMaxTransactionFee`. The purpose of this field is
* to allow us to determine if the user set the field explicitly, or if we're
* using the default max transation fee for the request.
*
* @private
* @type {Hbar | null}
*/
this._maxTransactionFee = null;
/**
* The transaction's memo
*
* @private
* @type {string}
*/
this._transactionMemo = "";
/**
* The list of transaction IDs. This list will almost always be of length 1.
* The only time this list will be a different length is for chunked transactions.
* The only two chunked transactions supported right now are `FileAppendTransaction`
* and `TopicMessageSubmitTransaction`
*
* @protected
* @type {List<TransactionId>}
*/
this._transactionIds = new List();
/**
* A list of public keys that will be added to the requests signatures
*
* @private
* @type {PublicKey[]}
*/
this._publicKeys = [];
/**
* The list of signing function 1-1 with `_publicKeys` which sign the request.
* The reason this list allows `null` is because if we go from bytes into
* a transaction, then we know the public key, but we don't have the signing function.
*
* @private
* @type {(((message: Uint8Array) => Promise<Uint8Array>) | null)[]}
*/
this._transactionSigners = [];
/**
* Determine if we should regenerate transaction IDs when we receive `TRANSACITON_EXPIRED`
*
* @private
* @type {?boolean}
*/
this._regenerateTransactionId = null;
}
/**
* Deserialize a transaction from bytes. The bytes can either be a `proto.Transaction` or
* `proto.TransactionList`.
*
* @param {Uint8Array} bytes
* @returns {Transaction}
*/
static fromBytes(bytes) {
const signedTransactions = [];
const transactionIds = [];
const nodeIds = [];
/** @type {string[]} */
const transactionIdStrings = [];
/** @type {string[]} */
const nodeIdStrings = [];
const bodies = [];
const list =
HashgraphProto.proto.TransactionList.decode(bytes).transactionList;
// If the list is of length 0, then teh bytes provided were not a
// `proto.TransactionList`
//
// FIXME: We should also check to make sure the bytes length is greater than
// 0 otherwise this check is wrong?
if (list.length === 0) {
const transaction = HashgraphProto.proto.Transaction.decode(bytes);
// We support `Transaction.signedTransactionBytes` and
// `Transaction.bodyBytes` + `Transaction.sigMap`. If the bytes represent the
// latter, convert them into `signedTransactionBytes`
if (transaction.signedTransactionBytes.length !== 0) {
list.push(transaction);
} else {
list.push({
signedTransactionBytes:
HashgraphProto.proto.SignedTransaction.encode({
bodyBytes: transaction.bodyBytes,
sigMap: transaction.sigMap,
}).finish(),
});
}
}
// This loop is responsible for fill out the `signedTransactions`, `transactionIds`,
// `nodeIds`, and `bodies` variables.
for (const transaction of list) {
// The `signedTransactionBytes` should not be null
if (transaction.signedTransactionBytes == null) {
throw new Error("Transaction.signedTransactionBytes are null");
}
// Decode a signed transaction
const signedTransaction =
HashgraphProto.proto.SignedTransaction.decode(
transaction.signedTransactionBytes
);
signedTransactions.push(signedTransaction);
// Decode a transaction body
const body = HashgraphProto.proto.TransactionBody.decode(
signedTransaction.bodyBytes
);
// Make sure the body is set
if (body.data == null) {
throw new Error("(BUG) body.data was not set in the protobuf");
}
bodies.push(body);
// Make sure the transaction ID within the body is set
if (body.transactionID != null) {
const transactionId = TransactionId._fromProtobuf(
/** @type {HashgraphProto.proto.ITransactionID} */ (
body.transactionID
)
);
// If we haven't already seen this transaction ID in the list, add it
if (!transactionIdStrings.includes(transactionId.toString())) {
transactionIds.push(transactionId);
transactionIdStrings.push(transactionId.toString());
}
}
// Make sure the node account ID within the body is set
if (body.nodeAccountID != null) {
const nodeAccountId = AccountId._fromProtobuf(
/** @type {HashgraphProto.proto.IAccountID} */ (
body.nodeAccountID
)
);
// If we haven't already seen this node account ID in the list, add it
if (!nodeIdStrings.includes(nodeAccountId.toString())) {
nodeIds.push(nodeAccountId);
nodeIdStrings.push(nodeAccountId.toString());
}
}
}
// FIXME: We should have a length check before we access `0` since that would error
const body = bodies[0];
// We should have at least more than one body
if (body == null || body.data == null) {
throw new Error(
"No transaction found in bytes or failed to decode TransactionBody"
);
}
// Use the registry to call the right transaction's `fromProtobuf` method based
// on the `body.data` string
const fromProtobuf = TRANSACTION_REGISTRY.get(body.data); //NOSONAR
// If we forgot to update the registry we should error
if (fromProtobuf == null) {
throw new Error(
`(BUG) Transaction.fromBytes() not implemented for type ${body.data}`
);
}
// That the specific transaction type from protobuf implementation and pass in all the
// information we've gathered.
return fromProtobuf(
list,
signedTransactions,
transactionIds,
nodeIds,
bodies
);
}
/**
* Convert this transaction a `ScheduleCreateTransaction`
*
* @returns {ScheduleCreateTransaction}
*/
schedule() {
this._requireNotFrozen();
if (SCHEDULE_CREATE_TRANSACTION.length != 1) {
throw new Error(
"ScheduleCreateTransaction has not been loaded yet"
);
}
return SCHEDULE_CREATE_TRANSACTION[0]()._setScheduledTransaction(this);
}
/**
* This method is called by each `*Transaction._fromProtobuf()` method. It does
* all the finalization before the user gets hold of a complete `Transaction`
*
* @template {Transaction} TransactionT
* @param {TransactionT} transaction
* @param {HashgraphProto.proto.ITransaction[]} transactions
* @param {HashgraphProto.proto.ISignedTransaction[]} signedTransactions
* @param {TransactionId[]} transactionIds
* @param {AccountId[]} nodeIds
* @param {HashgraphProto.proto.ITransactionBody[]} bodies
* @returns {TransactionT}
*/
static _fromProtobufTransactions(
transaction,
transactions,
signedTransactions,
transactionIds,
nodeIds,
bodies
) {
const body = bodies[0];
// "row" of the 2-D `bodies` array has all the same contents except for `nodeAccountID`
for (let i = 0; i < transactionIds.length; i++) {
for (let j = 0; j < nodeIds.length - 1; j++) {
if (
!util.compare(
bodies[i * nodeIds.length + j],
bodies[i * nodeIds.length + j + 1],
// eslint-disable-next-line ie11/no-collection-args
new Set(["nodeAccountID"])
)
) {
throw new Error("failed to validate transaction bodies");
}
}
}
// Remove node account IDs of 0
// _IIRC_ this was initial due to some funny behavior with `ScheduleCreateTransaction`
// We may be able to remove this.
const zero = new AccountId(0);
for (let i = 0; i < nodeIds.length; i++) {
if (nodeIds[i].equals(zero)) {
nodeIds.splice(i--, 1);
}
}
// Set the transactions accordingly, but don't lock the list because transactions can
// be regenerated if more signatures are added
transaction._transactions.setList(transactions);
// Set the signed transactions accordingly, and lock the list since signed transaction
// will not be regenerated. Although, they can be manipulated if for instance more
// signatures are added
transaction._signedTransactions.setList(signedTransactions).setLocked();
// Set the transaction IDs accordingly, and lock the list. Transaction IDs should not
// be regenerated if we're deserializing a request from bytes
transaction._transactionIds.setList(transactionIds).setLocked();
// Set the node account IDs accordingly, and lock the list. Node account IDs should
// never be changed if we're deserializing a request from bytes
transaction._nodeAccountIds.setList(nodeIds).setLocked();
// Make sure to update the rest of the fields
transaction._transactionValidDuration =
body.transactionValidDuration != null &&
body.transactionValidDuration.seconds != null
? Long.fromValue(body.transactionValidDuration.seconds).toInt()
: DEFAULT_TRANSACTION_VALID_DURATION;
transaction._maxTransactionFee =
body.transactionFee != null
? Hbar.fromTinybars(body.transactionFee)
: new Hbar(0);
transaction._transactionMemo = body.memo != null ? body.memo : "";
// Loop over a single row of `signedTransactions` and add all the public
// keys to the `signerPublicKeys` set, and `publicKeys` list with
// `null` in the `transactionSigners` at the same index.
for (let i = 0; i < nodeIds.length; i++) {
const signedTransaction = signedTransactions[i];
if (
signedTransaction.sigMap != null &&
signedTransaction.sigMap.sigPair != null
) {
for (const sigPair of signedTransaction.sigMap.sigPair) {
transaction._signerPublicKeys.add(
hex.encode(
/** @type {Uint8Array} */ (sigPair.pubKeyPrefix)
)
);
transaction._publicKeys.push(
PublicKey.fromBytes(
/** @type {Uint8Array} */ (sigPair.pubKeyPrefix)
)
);
transaction._transactionSigners.push(null);
}
}
}
return transaction;
}
/**
* Set the node account IDs
*
* @override
* @param {AccountId[]} nodeIds
* @returns {this}
*/
setNodeAccountIds(nodeIds) {
// The reason we overwrite this method is simply because we need to call `requireNotFrozen()`
// Now that I think of it, we could just add an abstract method `setterPrerequiest()` which
// by default does nothing, and `Executable` can call. Then we'd only need to overwrite that
// method once.
this._requireNotFrozen();
super.setNodeAccountIds(nodeIds);
return this;
}
/**
* Get the transaction valid duration
*
* @returns {number}
*/
get transactionValidDuration() {
return this._transactionValidDuration;
}
/**
* Sets the duration (in seconds) that this transaction is valid for.
*
* This is defaulted to 120 seconds (from the time its executed).
*
* @param {number} validDuration
* @returns {this}
*/
setTransactionValidDuration(validDuration) {
this._requireNotFrozen();
this._transactionValidDuration = validDuration;
return this;
}
/**
* Get the max transaction fee
*
* @returns {?Hbar}
*/
get maxTransactionFee() {
return this._maxTransactionFee;
}
/**
* Set the maximum transaction fee the operator (paying account)
* is willing to pay.
*
* @param {number | string | Long | BigNumber | Hbar} maxTransactionFee
* @returns {this}
*/
setMaxTransactionFee(maxTransactionFee) {
this._requireNotFrozen();
this._maxTransactionFee =
maxTransactionFee instanceof Hbar
? maxTransactionFee
: new Hbar(maxTransactionFee);
return this;
}
/**
* Is transaction ID regeneration enabled
*
* @returns {?boolean}
*/
get regenerateTransactionId() {
return this._regenerateTransactionId;
}
/**
* Set the maximum transaction fee the operator (paying account)
* is willing to pay.
*
* @param {boolean} regenerateTransactionId
* @returns {this}
*/
setRegenerateTransactionId(regenerateTransactionId) {
this._requireNotFrozen();
this._regenerateTransactionId = regenerateTransactionId;
return this;
}
/**
* Get the transaction memo
*
* @returns {string}
*/
get transactionMemo() {
return this._transactionMemo;
}
/**
* Set a note or description to be recorded in the transaction
* record (maximum length of 100 bytes).
*
* @param {string} transactionMemo
* @returns {this}
*/
setTransactionMemo(transactionMemo) {
this._requireNotFrozen();
this._transactionMemo = transactionMemo;
return this;
}
/**
* Get the curent transaction ID
*
* @returns {?TransactionId}
*/
get transactionId() {
if (this._transactionIds.isEmpty) {
return null;
}
// If a user calls `.transactionId` that means we need to use that transaction ID
// and **not** regenerate it. To do this, we simply lock the transaction ID list.
//
// This may be a little conffusing since a user can enable transaction ID regenration
// explicity, but if they call `.transactionId` then we will not regenerate transaction
// IDs.
this._transactionIds.setLocked();
return this._transactionIds.current;
}
/**
* Set the ID for this transaction.
*
* The transaction ID includes the operator's account ( the account paying the transaction
* fee). If two transactions have the same transaction ID, they won't both have an effect. One
* will complete normally and the other will fail with a duplicate transaction status.
*
* Normally, you should not use this method. Just before a transaction is executed, a
* transaction ID will be generated from the operator on the client.
*
* @param {TransactionId} transactionId
* @returns {this}
*/
setTransactionId(transactionId) {
this._requireNotFrozen();
this._transactionIds.setList([transactionId]).setLocked();
return this;
}
/**
* Sign the transaction with the private key
* **NOTE**: This is a thin wrapper around `.signWith()`
*
* @param {PrivateKey} privateKey
* @returns {Promise<this>}
*/
sign(privateKey) {
return this.signWith(privateKey.publicKey, (message) =>
Promise.resolve(privateKey.sign(message))
);
}
/**
* Sign the transaction with the public key and signer function
*
* If sign on demand is enabled no signing will be done immediately, instead
* the private key signing function and public key are saved to be used when
* a user calls an exit condition method (not sure what a better name for this is)
* such as `toBytes[Async]()`, `getTransactionHash[PerNode]()` or `execute()`.
*
* @param {PublicKey} publicKey
* @param {(message: Uint8Array) => Promise<Uint8Array>} transactionSigner
* @returns {Promise<this>}
*/
async signWith(publicKey, transactionSigner) {
// If signing on demand is disabled, we need to make sure
// the request is frozen
if (!this._signOnDemand) {
this._requireFrozen();
}
const publicKeyData = publicKey.toBytesRaw();
// note: this omits the DER prefix on purpose because Hedera doesn't
// support that in the protobuf. this means that we would fail
// to re-inflate [this._signerPublicKeys] during [fromBytes] if we used DER
// prefixes here
const publicKeyHex = hex.encode(publicKeyData);
if (this._signerPublicKeys.has(publicKeyHex)) {
// this public key has already signed this transaction
return this;
}
// If we add a new signer, then we need to re-create all transactions
this._transactions.clear();
// Save the current public key so we don't attempt to sign twice
this._signerPublicKeys.add(publicKeyHex);
// If signing on demand is enabled we will save the public key and signer and return
if (this._signOnDemand) {
this._publicKeys.push(publicKey);
this._transactionSigners.push(transactionSigner);
return this;
}
// If we get here, signing on demand is disabled, this means the transaction
// is frozen and we need to sign all the transactions immediately. If we're
// signing all the transactions immediately, we need to lock the node account IDs
// and transaction IDs.
// Now that I think of it, this code should likely exist in `freezeWith()`?
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
// Sign each signed transatcion
for (const signedTransaction of this._signedTransactions.list) {
const bodyBytes = /** @type {Uint8Array} */ (
signedTransaction.bodyBytes
);
const signature = await transactionSigner(bodyBytes);
if (signedTransaction.sigMap == null) {
signedTransaction.sigMap = {};
}
if (signedTransaction.sigMap.sigPair == null) {
signedTransaction.sigMap.sigPair = [];
}
signedTransaction.sigMap.sigPair.push(
publicKey._toProtobufSignature(signature)
);
}
return this;
}
/**
* Sign the transaction with the client operator. This is a thin wrapper
* around `.signWith()`
*
* **NOTE**: If client does not have an operator set, this method will throw
*
* @param {import("../client/Client.js").default<Channel, *>} client
* @returns {Promise<this>}
*/
signWithOperator(client) {
const operator = client._operator;
if (operator == null) {
throw new Error(
"`client` must have an operator to sign with the operator"
);
}
if (!this._isFrozen()) {
this.freezeWith(client);
}
return this.signWith(operator.publicKey, operator.transactionSigner);
}
/**
* Add a signature explicitly
*
* This method requires the transaction to have exactly 1 node account ID set
* since different node account IDs have different byte representations and
* hence the same signature would not work for all transactions that are the same
* except for node account ID being different.
*
* @param {PublicKey} publicKey
* @param {Uint8Array} signature
* @returns {this}
*/
addSignature(publicKey, signature) {
// Require that only one node is set on this transaction
// FIXME: This doesn't consider if we have one node account ID set, but we're
// also a chunked transaction. We should also check transaction IDs is of length 1
this._requireOneNodeAccountId();
// If the transaction isn't frozen, freeze it.
if (!this.isFrozen()) {
this.freeze();
}
const publicKeyData = publicKey.toBytesRaw();
const publicKeyHex = hex.encode(publicKeyData);
if (this._signerPublicKeys.has(publicKeyHex)) {
// this public key has already signed this transaction
return this;
}
// Transactions will have to be regenerated
this._transactions.clear();
// Locking the transaction IDs and node account IDs is necessary for consistency
// between before and after execution
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
this._signedTransactions.setLocked();
// Add the signature to the signed transaction list. This is a copy paste
// of `.signWith()`, but it really shouldn't be if `_signedTransactions.list`
// must be a length of one.
// FIXME: Remove unnecessary for loop.
for (const transaction of this._signedTransactions.list) {
if (transaction.sigMap == null) {
transaction.sigMap = {};
}
if (transaction.sigMap.sigPair == null) {
transaction.sigMap.sigPair = [];
}
transaction.sigMap.sigPair.push(
publicKey._toProtobufSignature(signature)
);
}
this._signerPublicKeys.add(publicKeyHex);
this._publicKeys.push(publicKey);
this._transactionSigners.push(null);
return this;
}
/**
* Get the current signatures on the request
*
* **NOTE**: Does NOT support sign on demand
*
* @returns {SignatureMap}
*/
getSignatures() {
// If a user is attempting to get signatures for a transaction, then the
// transaction must be frozen.
this._requireFrozen();
// Sign on demand must be disabled because this is the non-async version and
// signing requires awaiting callbacks.
this._requireNotSignOnDemand();
// Build all the transactions
this._buildAllTransactions();
// Lock transaction IDs, and node account IDs
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
// Construct a signature map from this transaction
return SignatureMap._fromTransaction(this);
}
/**
* Get the current signatures on the request
*
* **NOTE**: Supports sign on demand
*
* @returns {Promise<SignatureMap>}
*/
async getSignaturesAsync() {
// If sign on demand is enabled, we don't need to care about being frozen
// since we can just regenerate and resign later if some field of the transaction
// changes.
// Locking the transaction IDs and node account IDs is necessary for consistency
// between before and after execution
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
// Build all transactions, and sign them
await this._buildAllTransactionsAsync();
// Lock transaction IDs, and node account IDs
this._transactions.setLocked();
this._signedTransactions.setLocked();
// Construct a signature map from this transaction
return SignatureMap._fromTransaction(this);
}
/**
* Not sure why this is called `setTransactionId()` when it doesn't set anything...
* FIXME: Remove this?
*/
_setTransactionId() {
if (this._operatorAccountId == null && this._transactionIds.isEmpty) {
throw new Error(
"`transactionId` must be set or `client` must be provided with `freezeWith`"
);
}
}
/**
* Set the node account IDs using the client
*
* @param {?import("../client/Client.js").default<Channel, *>} client
*/
_setNodeAccountIds(client) {
if (!this._nodeAccountIds.isEmpty) {
return;
}
if (client == null) {
throw new Error(
"`nodeAccountId` must be set or `client` must be provided with `freezeWith`"
);
}
this._nodeAccountIds.setList(
client._network.getNodeAccountIdsForExecute()
);
}
/**
* Build all the signed transactions from the node account IDs
*
* @private
*/
_buildSignedTransactions() {
if (this._signedTransactions.locked) {
return;
}
this._signedTransactions.setList(
this._nodeAccountIds.list.map((nodeId) =>
this._makeSignedTransaction(nodeId)
)
);
}
/**
* Freeze this transaction from future modification to prepare for
* signing or serialization.
*
* @returns {this}
*/
freeze() {
return this.freezeWith(null);
}
/**
* @param {?AccountId} accountId
*/
_freezeWithAccountId(accountId) {
if (this._operatorAccountId == null) {
this._operatorAccountId = accountId;
}
}
/**
* Freeze this transaction from further modification to prepare for
* signing or serialization.
*
* Will use the `Client`, if available, to generate a default Transaction ID and select 1/3
* nodes to prepare this transaction for.
*
* @param {?import("../client/Client.js").default<Channel, *>} client
* @returns {this}
*/
freezeWith(client) {
// Set sign on demand based on client
this._signOnDemand = client != null ? client.signOnDemand : false;
// Save the operator
this._operator = client != null ? client._operator : null;
this._freezeWithAccountId(
client != null ? client.operatorAccountId : null
);
// Set max transaction fee to either `this._maxTransactionFee`,
// `client._defaultMaxTransactionFee`, or `this._defaultMaxTransactionFee`
// in that priority order depending on if `this._maxTransactionFee` has
// been set or if `client._defaultMaxTransactionFee` has been set.
this._maxTransactionFee =
this._maxTransactionFee == null
? client != null && client.defaultMaxTransactionFee != null
? client.defaultMaxTransactionFee
: this._defaultMaxTransactionFee
: this._maxTransactionFee;
// Determine if transaction ID generation should be enabled.
this._regenerateTransactionId =
client != null && this._regenerateTransactionId == null
? client.defaultRegenerateTransactionId
: this._regenerateTransactionId;
// Set the node account IDs via client
this._setNodeAccountIds(client);
// Make sure a transaction ID or operator is set.
this._setTransactionId();
// If a client was not provided, we need to make sure the transaction ID already set
// validates aginst the client.
if (client != null) {
for (const transactionId of this._transactionIds.list) {
if (transactionId.accountId != null) {
transactionId.accountId.validateChecksum(client);
}
}
}
// Build a list of transaction IDs so that if a user calls `.transactionId` they'll
// get a value, but if they dont' we'll just regenerate transaction IDs during execution
this._buildNewTransactionIdList();
// If sign on demand is disabled we need to build out all the signed transactions
if (!this._signOnDemand) {
this._buildSignedTransactions();
}
return this;
}
/**
* Sign the transaction using a signer
*
* This is part of the signature provider feature
*
* @param {Signer} signer
* @returns {Promise<this>}
*/
async signWithSigner(signer) {
await signer.signTransaction(this);
return this;
}
/**
* Freeze the transaction using a signer
*
* This is part of the signature provider feature.
*
* @param {Signer} signer
* @returns {Promise<this>}
*/
async freezeWithSigner(signer) {
await signer.populateTransaction(this);
this.freeze();
return this;
}
/**
* Serialize the request into bytes. This will encode all the transactions
* into a `proto.TransactionList` and return the encoded protobuf.
*
* **NOTE**: Does not support sign on demand
*
* @returns {Uint8Array}
*/
toBytes() {
// If a user is attempting to serialize a transaction into bytes, then the
// transaction must be frozen.
this._requireFrozen();
// Sign on demand must be disabled because this is the non-async version and
// signing requires awaiting callbacks.
this._requireNotSignOnDemand();
// Locking the transaction IDs and node account IDs is necessary for consistency
// between before and after execution
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
// Build all the transactions withot signing
this._buildAllTransactions();
// Construct and encode the transaction list
return HashgraphProto.proto.TransactionList.encode({
transactionList:
/** @type {HashgraphProto.proto.ITransaction[]} */ (
this._transactions.list
),
}).finish();
}
/**
* Serialize the transaction into bytes
*
* **NOTE**: Supports sign on demand
*
* @returns {Promise<Uint8Array>}
*/
async toBytesAsync() {
// If sign on demand is enabled, we don't need to care about being frozen
// since we can just regenerate and resign later if some field of the transaction
// changes.
// Locking the transaction IDs and node account IDs is necessary for consistency
// between before and after execution
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
// Build all transactions, and sign them
await this._buildAllTransactionsAsync();
// Lock transaction IDs, and node account IDs
this._transactions.setLocked();
this._signedTransactions.setLocked();
// Construct and encode the transaction list
return HashgraphProto.proto.TransactionList.encode({
transactionList:
/** @type {HashgraphProto.proto.ITransaction[]} */ (
this._transactions.list
),
}).finish();
}
/**
* Get the transaction hash
*
* @returns {Promise<Uint8Array>}
*/
async getTransactionHash() {
this._requireFrozen();
// Locking the transaction IDs and node account IDs is necessary for consistency
// between before and after execution
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
await this._buildAllTransactionsAsync();
this._transactions.setLocked();
this._signedTransactions.setLocked();
return sha384.digest(
/** @type {Uint8Array} */ (
/** @type {HashgraphProto.proto.ITransaction} */ (
this._transactions.get(0)
).signedTransactionBytes
)
);
}
/**
* Get all the transaction hashes
*
* @returns {Promise<TransactionHashMap>}
*/
async getTransactionHashPerNode() {
this._requireFrozen();
// Locking the transaction IDs and node account IDs is necessary for consistency
// between before and after execution
this._transactionIds.setLocked();
this._nodeAccountIds.setLocked();
await this._buildAllTransactionsAsync();
return await TransactionHashMap._fromTransaction(this);
}
/**
* Is transaction frozen
*
* @returns {boolean}
*/
isFrozen() {
return this._signedTransactions.length > 0;
}
/**
* Get the current transaction ID, and make sure it's not null
*
* @protected
* @returns {TransactionId}
*/
_getTransactionId() {
const transactionId = this.transactionId;
if (transactionId == null) {
throw new Error(
"transaction must have been frozen before getting the transaction ID, try calling `freeze`"
);
}
return transactionId;
}
/**
* @param {Client} client
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
_validateChecksums(client) {
// Do nothing
}
/**
* Before we proceed exeuction, we need to do a couple checks
*
* @override
* @protected
* @param {import("../client/Client.js").default<Channel, *>} client
* @returns {Promise<void>}
*/
async _beforeExecute(client) {
if (this._logger) {
this._logger.info(
`Network used: ${client._network.networkName}` // eslint-disable-line @typescript-eslint/restrict-template-expressions
);
}
// Make sure we're frozen
if (!this._isFrozen()) {
this.freezeWith(client);
}
// Valid checksums if the option is enabled
if (client.isAutoValidateChecksumsEnabled()) {
this._validateChecksums(client);
}
// Set the operator if the client has one and the current operator is nullish
if (this._operator == null || this._operator == undefined) {
this._operator = client != null ? client._operator : null;
}
if (
this._operatorAccountId == null ||
this._operatorAccountId == undefined
) {
this._operatorAccountId =
client != null && client._operator != null
? client._operator.accountId
: null;
}
// If the client has an operator, sign this request with the operator
if (this._operator != null) {
await this.signWith(
this._operator.publicKey,
this._operator.transactionSigner
);
}
}
/**
* Construct a protobuf transaction
*
* @override
* @internal
* @returns {Promise<HashgraphProto.proto.ITransaction>}
*/
async _makeRequestAsync() {
// The index for the transaction
const index =
this._transactionIds.index * this._nodeAccountIds.length +
this._nodeAccountIds.index;
// If sign on demand is disabled we need to simply build that transaction
// and return the result, without signing
if (!this._signOnDemand) {
this._buildTransaction(index);
return /** @type {HashgraphProto.proto.ITransaction} */ (
this._transactions.get(index)
);
}
// Build and sign a transaction
return await this._buildTransactionAsync();
}
/**
* Sign a `proto.SignedTransaction` with all the keys
*
* @private
* @returns {Promise<HashgraphProto.proto.ISignedTransaction>}
*/
async _signTransaction() {
const signedTransaction = this._makeSignedTransaction(
this._nodeAccountIds.next
);
const bodyBytes = /** @type {Uint8Array} */ (
signedTransaction.bodyBytes
);
for (let j = 0; j < this._publicKeys.length; j++) {
const publicKey = this._publicKeys[j];
const transactionSigner = this._transactionSigners[j];
if (transactionSigner == null) {
continue;
}
const signature = await transactionSigner(bodyBytes);
if (signedTransaction.sigMap == null) {
signedTransaction.sigMap = {};
}
if (signedTransaction.sigMap.sigPair == null) {
signedTransaction.sigMap.sigPair = [];
}
signedTransaction.sigMap.sigPair.push(
publicKey._toProtobufSignature(signature)
);
}
return signedTransaction;
}
/**
* Construct a new transaction ID at the current index
*
* @private
*/
_buildNewTransactionIdList() {
if (this._transactionIds.locked || this._operatorAccountId == null) {
return;
}
const transactionId = TransactionId.withValidStart(
this._operatorAccountId,
Timestamp.generate()
);
this._transactionIds.set(this._transactionIds.index, transactionId);
}
/**
* Build each transaction in a loop
*
* @private
*/
_buildAllTransactions() {
for (let i = 0; i < this._signedTransactions.length; i++) {
this._buildTransaction(i);
}
}
/**
* Build and and sign each transaction in a loop
*
* This method is primary used in the exist condition methods
* which are not `execute()`, e.g. `toBytesAsync()` and `getSignaturesAsync()`
*
* @private
*/
async _buildAllTransactionsAsync() {
if (!this._signOnDemand) {
this._buildAllTransactions();
return;
}
this._buildSignedTransactions();
if (this._transactions.locked) {
return;
}
for (let i = 0; i < this._signedTransactions.length; i++) {
this._transactions.push(await this._buildTransactionAsync());
}
}
/**
* Build a transaction at a particular index
*
* @private
* @param {number} index
*/
_buildTransaction(index) {
if (this._transactions.length < index) {
for (let i = this._transactions.length; i < index; i++) {
this._transactions.push(null);
}
}
this._transactions.setIfAbsent(index, () => {
return {
signedTransactionBytes:
HashgraphProto.proto.SignedTransaction.encode(
this._signedTransactions.get(index)
).finish(),
};
});
}
/**
* Build a trransaction using the current index, where the current
* index is determined by `this._nodeAccountIds.index` and
* `this._transactionIds.index`
*
* @private
* @returns {Promise<HashgraphProto.proto.ITransaction>}
*/
async _buildTransactionAsync() {
return {
signedTransactionBytes:
HashgraphProto.proto.SignedTransaction.encode(
await this._signTransaction()
).finish(),
};
}
/**
* Determine what execution state we're in.
*
* @override
* @internal
* @param {HashgraphProto.proto.ITransaction} request
* @param {HashgraphProto.proto.ITransactionResponse} response
* @returns {[Status, ExecutionState]}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_shouldRetry(request, response) {
const { nodeTransactionPrecheckCode } = response;
// Get the node precheck code, and convert it into an SDK `Status`
const status = Status._fromCode(
nodeTransactionPrecheckCode != null
? nodeTransactionPrecheckCode
: HashgraphProto.proto.ResponseCodeEnum.OK
);
if (this._logger) {
this._logger.debug(
`[${this._getLogId()}] received status ${status.toString()}`
);
this._logger.info(
`SDK Transaction Status Response: ${status.toString()}`
);
}
// Based on the status what execution state are we in
switch (status) {
case Status.Busy:
case Status.Unknown:
case Status.PlatformTransactionNotCreated:
case Status.PlatformNotActive:
return [status, ExecutionState.Retry];
case Status.Ok:
return [status, ExecutionState.Finished];
case Status.TransactionExpired:
if (
this._regenerateTransactionId == null ||
this._regenerateTransactionId
) {
this._buildNewTransactionIdList();
return [status, ExecutionState.Retry];
} else {
return [status, ExecutionState.Error];
}
default:
return [status, ExecutionState.Error];
}
}
/**
* Map the request and response into a precheck status error
*
* @override
* @internal
* @param {HashgraphProto.proto.ITransaction} request
* @param {HashgraphProto.proto.ITransactionResponse} response
* @returns {Error}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_mapStatusError(request, response) {
const { nodeTransactionPrecheckCode } = response;
const status = Status._fromCode(
nodeTransactionPrecheckCode != null
? nodeTransactionPrecheckCode
: HashgraphProto.proto.ResponseCodeEnum.OK
);
if (this._logger) {
this._logger.info(
// @ts-ignore
`Transaction Error Info: ${status.toString()}, ${this.transactionId.toString()}` // eslint-disable-line @typescript-eslint/restrict-template-expressions
);
}
return new PrecheckStatusError({
status,
transactionId: this._getTransactionId(),
contractFunctionResult: null,
});
}
/**
* Map the request, response, and node account ID into a `TransactionResponse`
*
* @override
* @protected
* @param {HashgraphProto.proto.ITransactionResponse} response
* @param {AccountId} nodeId
* @param {HashgraphProto.proto.ITransaction} request
* @returns {Promise<TransactionResponse>}
*/
async _mapResponse(response, nodeId, request) {
const transactionHash = await sha384.digest(
/** @type {Uint8Array} */ (request.signedTransactionBytes)
);
const transactionId = this._getTransactionId();
this._transactionIds.advance();
if (this._logger) {
this._logger.info(
`Transaction Info: ${JSON.stringify(
new TransactionResponse({
nodeId,
transactionHash,
transactionId,
}).toJSON()
)}`
);
}
return new TransactionResponse({
nodeId,
transactionHash,
transactionId,
});
}
/**
* Make a signed transaction given a node account ID
*
* @internal
* @param {?AccountId} nodeId
* @returns {HashgraphProto.proto.ISignedTransaction}
*/
_makeSignedTransaction(nodeId) {
const body = this._makeTransactionBody(nodeId);
if (this._logger) {
this._logger.info(`Transaction Body: ${JSON.stringify(body)}`);
}
const bodyBytes =
HashgraphProto.proto.TransactionBody.encode(body).finish();
return {
bodyBytes,
sigMap: {
sigPair: [],
},
};
}
/**
* Make a protobuf transaction body
*
* @private
* @param {?AccountId} nodeId
* @returns {HashgraphProto.proto.ITransactionBody}
*/
_makeTransactionBody(nodeId) {
return {
[this._getTransactionDataCase()]: this._makeTransactionData(),
transactionFee:
this._maxTransactionFee != null
? this._maxTransactionFee.toTinybars()
: null,
memo: this._transactionMemo,
transactionID: this._transactionIds.current._toProtobuf(),
nodeAccountID: nodeId != null ? nodeId._toProtobuf() : null,
transactionValidDuration: {
seconds: Long.fromNumber(this._transactionValidDuration),
},
};
}
/**
* This method returns a key for the `data` field in a transaction body.
* Each transaction overwrite this to make sure when we build the transaction body
* we set the right data field.
*
* @abstract
* @protected
* @returns {NonNullable<HashgraphProto.proto.TransactionBody["data"]>}
*/
_getTransactionDataCase() {
throw new Error("not implemented");
}
/**
* Make a scheduled transaction body
* FIXME: Should really call this `makeScheduledTransactionBody` to be consistent
*
* @internal
* @returns {HashgraphProto.proto.ISchedulableTransactionBody}
*/
_getScheduledTransactionBody() {
return {
memo: this.transactionMemo,
transactionFee:
this._maxTransactionFee == null
? this._defaultMaxTransactionFee.toTinybars()
: this._maxTransactionFee.toTinybars(),
[this._getTransactionDataCase()]: this._makeTransactionData(),
};
}
/**
* Make the transaction body data.
*
* @abstract
* @protected
* @returns {object}
*/
_makeTransactionData() {
throw new Error("not implemented");
}
/**
* FIXME: Why do we have `isFrozen` and `_isFrozen()`?
*
* @protected
* @returns {boolean}
*/
_isFrozen() {
return (
this._signOnDemand ||
this._signedTransactions.length > 0 ||
this._transactions.length > 0
);
}
/**
* Require the transaction to NOT be frozen
*
* @internal
*/
_requireNotFrozen() {
if (this._isFrozen()) {
throw new Error(
"transaction is immutable; it has at least one signature or has been explicitly frozen"
);
}
}
/**
* Require the transaction to have sign on demand disabled
*
* @internal
*/
_requireNotSignOnDemand() {
if (this._signOnDemand) {
throw new Error(
"Please use `toBytesAsync()` if `signOnDemand` is enabled"
);
}
}
/**
* Require the transaction to be frozen
*
* @internal
*/
_requireFrozen() {
if (!this._isFrozen()) {
throw new Error(
"transaction must have been frozen before calculating the hash will be stable, try calling `freeze`"
);
}
}
/**
* Require the transaction to have a single node account ID set
*
* @internal
* @protected
*/
_requireOneNodeAccountId() {
if (this._nodeAccountIds.length != 1) {
throw "transaction did not have exactly one node ID set";
}
}
/**
* @param {HashgraphProto.proto.Transaction} request
* @returns {Uint8Array}
*/
_requestToBytes(request) {
return HashgraphProto.proto.Transaction.encode(request).finish();
}
/**
* @param {HashgraphProto.proto.TransactionResponse} response
* @returns {Uint8Array}
*/
_responseToBytes(response) {
return HashgraphProto.proto.TransactionResponse.encode(
response
).finish();
}
}
/**
* This is essentially a registry/cache for a callback that creates a `ScheduleCreateTransaction`
*
* @type {(() => ScheduleCreateTransaction)[]}
*/
export const SCHEDULE_CREATE_TRANSACTION = [];
Выполнить команду
Для локальной разработки. Не используйте в интернете!