UNPKG

@hashgraph/sdk

Version:
1,447 lines (1,247 loc) 76.9 kB
// SPDX-License-Identifier: Apache-2.0 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 SignatureMapLegacy from "./SignatureMapLegacy.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 HieroProto 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"; import CustomFeeLimit from "./CustomFeeLimit.js"; import Key from "../Key.js"; import SignableNodeTransactionBodyBytes from "./SignableNodeTransactionBodyBytes.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"), ); /** * Node account ID used for batch transactions * @type {AccountId} */ // @ts-ignore const NODE_ACCOUNT_BATCH_ID = new AccountId(0, 0, 0); // 120 seconds const DEFAULT_TRANSACTION_VALID_DURATION = 120; // The default message chunk size in bytes when splitting a given message. // This value can be overriden using `setChunkSize` when preparing to submit a messsage via `TopicMessageSubmitTransaction`. export const CHUNK_SIZE = 1024; /** * @type {Map<NonNullable<HieroProto.proto.TransactionBody["data"]>, (transactions: HieroProto.proto.ITransaction[], signedTransactions: HieroProto.proto.ISignedTransaction[], transactionIds: TransactionId[], nodeIds: AccountId[], bodies: HieroProto.proto.TransactionBody[]) => Transaction>} */ export const TRANSACTION_REGISTRY = new Map(); /** * Base class for all transactions that may be submitted to Hedera. * * @abstract * @augments {Executable<HieroProto.proto.ITransaction, HieroProto.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<HieroProto.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<HieroProto.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 maximum custom fee that the user is willing to pay for the message. If left empty, the user is willing to pay any custom fee. * If used with a transaction type that does not support custom fee limits, the transaction will fail. * @type {CustomFeeLimit[]} */ this._customFeeLimits = []; /** * 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; /** * The key used to sign the batch transaction * * @private * @type {Key | null} */ this._batchKey = null; /** * Whether the transaction is throttled * * @private * @type {boolean} */ this._isThrottled = false; } /** * Deserialize a transaction from bytes. The bytes can either be a `proto.Transaction` or * `proto.TransactionList`. * * @param {Uint8Array} bytes * @returns {Transaction} */ static fromBytes(bytes) { /** @type {HieroProto.proto.ISignedTransaction[]} */ const signedTransactions = []; /** @type {TransactionId[]} */ const transactionIds = []; /** @type {AccountId[]} */ const nodeIds = []; /** @type {string[]} */ const transactionIdStrings = []; /** @type {string[]} */ const nodeIdStrings = []; /** @type {HieroProto.proto.TransactionBody[]} */ const bodies = []; const list = HieroProto.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 = HieroProto.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: HieroProto.proto.SignedTransaction.encode({ sigMap: transaction.sigMap, bodyBytes: transaction.bodyBytes, }).finish(), }); } } // This loop is responsible for fill out the `signedTransactions`, `transactionIds`, // `nodeIds`, and `bodies` variables. for (const transaction of list) { // The `bodyBytes` or `signedTransactionBytes` should not be null if ( transaction.bodyBytes == null && transaction.signedTransactionBytes == null ) { throw new Error( "bodyBytes and signedTransactionBytes are null", ); } if (transaction.bodyBytes && transaction.bodyBytes.length != 0) { // Decode a transaction const body = HieroProto.proto.TransactionBody.decode( transaction.bodyBytes, ); // Make sure the transaction ID within the body is set if (body.transactionID != null) { const transactionId = TransactionId._fromProtobuf( /** @type {HieroProto.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 {HieroProto.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()); } } // 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); } if ( transaction.signedTransactionBytes && transaction.signedTransactionBytes.length != 0 ) { // Decode a signed transaction const signedTransaction = HieroProto.proto.SignedTransaction.decode( transaction.signedTransactionBytes, ); signedTransactions.push(signedTransaction); // Decode a transaction body const body = HieroProto.proto.TransactionBody.decode( signedTransaction.bodyBytes, ); // Make sure the transaction ID within the body is set if (body.transactionID != null) { const transactionId = TransactionId._fromProtobuf( /** @type {HieroProto.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 {HieroProto.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()); } } // 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); } } // 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); } /** * @description Batchify method is used to mark a transaction as part of a batch transaction or make it so-called inner transaction. * The Transaction will be frozen and signed by the operator of the client. * * @param {import("../client/Client.js").default<Channel, *>} client * @param {Key} batchKey * @returns {Promise<this>} */ async batchify(client, batchKey) { this._requireNotFrozen(); this.setBatchKey(batchKey); return await this.signWithOperator(client); } /** * 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 {HieroProto.proto.ITransaction[]} transactions * @param {HieroProto.proto.ISignedTransaction[]} signedTransactions * @param {TransactionId[]} transactionIds * @param {AccountId[]} nodeIds * @param {HieroProto.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. Although, they // can be manipulated if for instance more signatures are added transaction._signedTransactions.setList(signedTransactions); // Set the transaction IDs accordingly transaction._transactionIds.setList(transactionIds); // Set the node account IDs accordingly transaction._nodeAccountIds.setList(nodeIds); // 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 && body.transactionFee > new Long(0, 0, true) ? Hbar.fromTinybars(body.transactionFee) : null; transaction._customFeeLimits = body.maxCustomFees != null ? body.maxCustomFees?.map((fee) => CustomFeeLimit._fromProtobuf(fee), ) : []; transaction._batchKey = body.batchKey != null ? Key._fromProtobufKey(body?.batchKey) : null; 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 tx = signedTransactions[i] || transactions[i]; if (tx.sigMap != null && tx.sigMap.sigPair != null) { for (const sigPair of tx.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; } /** * Protobuf encoding has specific rules about how data is serialized * Different fields take different amounts of space depending on their values * The actual wire format size can only be determined after encoding * * @returns {Promise<number>} */ get size() { this._requireFrozen(); return this._makeRequestAsync().then( (request) => HieroProto.proto.Transaction.encode(request).finish().length, ); } /** * Get the transaction body size * Protobuf encoding has specific rules about how data is serialized * Different fields take different amounts of space depending on their values * The actual wire format size can only be determined after encoding * * @returns {number} */ get bodySize() { const body = this._makeTransactionBody(AccountId.fromString("0.0.0")); return HieroProto.proto.TransactionBody.encode(body).finish().length; } /** * 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; } /** * How many chunk sizes are expected * @abstract * @internal * @returns {number} */ getRequiredChunks() { return 1; } /** * Get the body sizes for all chunks in a Chunked transaction. * For transactions with multiple chunks (like large topic message submissions), * this returns an array containing the size of each chunk's transaction body. * The size is calculated by encoding the transaction body to protobuf format. * * @returns {number[]} An array of body sizes, where each element represents * the size in bytes of a chunk's transaction body * */ get bodySizeAllChunks() { const bodySizes = []; // Store sizes for each chunk for (let i = 0; i < this.getRequiredChunks(); i++) { // Set index directly this._transactionIds.index = i; // Use super.bodySize to access the base class implementation bodySizes.push(this.bodySize); } // Restore to initial index this._transactionIds.index = 0; return bodySizes; } /** * 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); this._publicKeys.push(publicKey); this._transactionSigners.push(transactionSigner); if (this._signOnDemand) { 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); } /** * Resets the transaction to its initial state * @param {Client} client */ _resetTransaction(client) { if (!client.operatorAccountId) { throw new Error("Client must have an operator account ID"); } this.logger?.info("Resetting transaction id and resigning"); const newTxId = TransactionId.generate(client.operatorAccountId); this._transactionIds.clear(); this._signedTransactions.clear(); this._transactionIds.setList([newTxId]); this._isThrottled = true; } /** * @deprecated - Using uint8array and uint8array[] as signaturemap is deprecated, * use SignatureMap insted. * @overload * @param { PublicKey } publicKey * @param { Uint8Array | Uint8Array[] } signatureMap * @returns {this} */ /** * @overload * @param {PublicKey} publicKey * @param { SignatureMap } signatureMap * @returns {this} */ /** * Add a signature explicitly * * @param {PublicKey} publicKey * @param {SignatureMap | Uint8Array |Uint8Array[]} signatureMap * @returns {this} */ addSignature(publicKey, signatureMap) { if (!(signatureMap instanceof SignatureMap)) { return this._addSignatureLegacy(publicKey, signatureMap); } // 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; } // If we add a new signer, then we need to re-create all transactions 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 for (let index = 0; index < this._signedTransactions.length; index++) { const signedTransaction = this._signedTransactions.get(index); if (signedTransaction.sigMap == null) { signedTransaction.sigMap = {}; } if (signedTransaction.sigMap.sigPair == null) { signedTransaction.sigMap.sigPair = []; } if (signedTransaction.bodyBytes) { const { transactionID, nodeAccountID } = HieroProto.proto.TransactionBody.decode( signedTransaction.bodyBytes, ); if (!transactionID || !nodeAccountID) { throw new Error( "Transaction ID or Node Account ID not found in the signed transaction", ); } const transactionId = TransactionId._fromProtobuf(transactionID); const nodeAccountId = AccountId._fromProtobuf(nodeAccountID); const nodeSignatures = signatureMap.get(nodeAccountId); const transactionSignatures = nodeSignatures?.get(transactionId); const signature = transactionSignatures?.get(publicKey); if (!signature) { throw new Error( "Signature not found for the transaction and public key", ); } const sigPair = publicKey._toProtobufSignature(signature); signedTransaction.sigMap?.sigPair?.push(sigPair); } } this._signerPublicKeys.add(publicKeyHex); this._publicKeys.push(publicKey); this._transactionSigners.push(null); return this; } /** * Add a signature explicitly * This method supports both single and multiple signatures. A single signature will be applied to all transactions, * * While an array of signatures must correspond to each transaction individually. * * @param {PublicKey} publicKey * @param {Uint8Array | Uint8Array[]} signature * @returns {this} */ _addSignatureLegacy(publicKey, signature) { const isSingleSignature = signature instanceof Uint8Array; const isArraySignature = Array.isArray(signature); if (this.getRequiredChunks() > 1) { throw new Error( "Add signature is not supported for chunked transactions", ); } // Check if it is a single signature with NOT exactly one transaction if (isSingleSignature && this._signedTransactions.length !== 1) { throw new Error( "Signature array must match the number of transactions", ); } // Check if it's an array but the array length doesn't match the number of transactions if ( isArraySignature && signature.length !== this._signedTransactions.length ) { throw new Error( "Signature array must match the number of transactions", ); } // 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; } // If we add a new signer, then we need to re-create all transactions 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(); const signatureArray = isSingleSignature ? [signature] : signature; // Add the signature to the signed transaction list for (let index = 0; index < this._signedTransactions.length; index++) { const signedTransaction = this._signedTransactions.get(index); if (signedTransaction.sigMap == null) { signedTransaction.sigMap = {}; } if (signedTransaction.sigMap.sigPair == null) { signedTransaction.sigMap.sigPair = []; } signedTransaction.sigMap.sigPair.push( publicKey._toProtobufSignature(signatureArray[index]), ); } 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 {SignatureMapLegacy} */ getSignaturesLegacy() { // 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 // eslint-disable-next-line deprecation/deprecation return SignatureMapLegacy._fromTransaction(this); } /** * This method removes all signatures from the transaction based on the public key provided. * * @param {PublicKey} publicKey - The public key associated with the signature to remove. * @returns {Uint8Array[]} The removed signatures. */ removeSignature(publicKey) { if (!this.isFrozen()) { this.freeze(); } const publicKeyData = publicKey.toBytesRaw(); const publicKeyHex = hex.encode(publicKeyData); if (!this._signerPublicKeys.has(publicKeyHex)) { throw new Error("The public key has not signed this transaction"); } /** @type {Uint8Array[]} */ const removedSignatures = []; // Iterate over the signed transactions and remove matching signatures for (const transaction of this._signedTransactions.list) { const removedSignaturesFromTransaction = this._removeSignaturesFromTransaction( transaction, publicKeyHex, ); removedSignatures.push(...removedSignaturesFromTransaction); } // Remove the public key from internal tracking if no signatures remain this._signerPublicKeys.delete(publicKeyHex); this._publicKeys = this._publicKeys.filter( (key) => !key.equals(publicKey), ); // Update transaction signers array this._transactionSigners.pop(); return removedSignatures; } /** * This method clears all signatures from the transaction and returns them in a specific format. * * It will call collectSignatures to get the removed signatures, then clear all signatures * from the internal tracking. * * @returns { Map<PublicKey, Uint8Array[] | Uint8Array> } The removed signatures in the specified format. */ removeAllSignatures() { if (!this.isFrozen()) { this.freeze(); } const removedSignatures = this._collectSignaturesByPublicKey(); // Iterate over the signed transactions and clear all signatures for (const transaction of this._signedTransactions.list) { if (transaction.sigMap && transaction.sigMap.sigPair) { // Clear all signature pairs from the transaction's signature map transaction.sigMap.sigPair = []; } } // Clear the internal tracking of signer public keys and other relevant arrays this._signerPublicKeys.clear(); this._publicKeys = []; this._transactionSigners = []; return removedSignatures; } /** * @deprecated - Use the legacy=flag instead to use the modern approach * @overload * @param {true} legacy * @returns {SignatureMapLegacy} */ /** * @overload * @param {false} [legacy] * @returns {SignatureMap} */ /** * Get the current signatures on the request * * **NOTE**: Does NOT support sign on demand * @param {boolean} [legacy] * @returns {SignatureMap | SignatureMapLegacy} */ getSignatures(legacy) { if (legacy) { return this.getSignaturesLegacy(); } // 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(), ); } /** * @description Set the key that will sign the batch of which this Transaction is a part of. * @param {Key} batchKey * @returns {this} */ setBatchKey(batchKey) { this._requireNotFrozen(); this._batchKey = batchKey; return this; } /** * @description Get the key that will sign the batch of which this Transaction is a part of. * @returns {Key | null | undefined} */ get batchKey() { return this._batchKey; } /** * Returns a List of SignableNodeTransactionBodyBytes for each node the transaction is intended for. * These are the canonical bytes that must be signed externally (e.g., via HSM). * * @returns {SignableNodeTransactionBodyBytes[]} */ get signableNodeBodyBytesList() { this._requireFrozen(); return this._signedTransactions.list.map((signedTransaction) => { if (!signedTransaction.bodyBytes) { throw new Error("Missing bodyBytes in signed transaction."); } const body = HieroProto.proto.TransactionBody.decode( signedTransaction.bodyBytes, ); if (!body.nodeAccountID) { throw new Error("Missing nodeAccountID in transaction body."); } const nodeAccountId = AccountId._fromProtobuf(body.nodeAccountID); if (!body.transactionID) { throw new Error("Missing transactionID in transaction body."); } const transactionId = TransactionId._fromProtobuf( body.transactionID, ); return new SignableNodeTransactionBodyBytes( nodeAccountId, transactionId, signedTransaction.bodyBytes, ); }); } /** * 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), ), ); } /** * Build all the signed transactions from the node account IDs * * @internal */ _buildIncompleteTransactions() { if (this._nodeAccountIds.length == 0) { this._transactions.setList([this._makeSignedTransaction(null)]); } else { // In case the node account ids are set this._transactions.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 if (this.batchKey) { this._nodeAccountIds.setList([NODE_ACCOUNT_BATCH_ID]); } else {