com.hedera.hashgraph.sdk.Transaction Maven / Gradle / Ivy
Show all versions of sdk-full Show documentation
/*-
*
* Hedera Java SDK
*
* Copyright (C) 2020 - 2024 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.
*
*/
package com.hedera.hashgraph.sdk;
import com.google.errorprone.annotations.Var;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
import com.hedera.hashgraph.sdk.proto.SignatureMap;
import com.hedera.hashgraph.sdk.proto.SignaturePair;
import com.hedera.hashgraph.sdk.proto.SignedTransaction;
import com.hedera.hashgraph.sdk.proto.TransactionBody;
import com.hedera.hashgraph.sdk.proto.TransactionList;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.reflect.Modifier;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import javax.annotation.Nullable;
import org.bouncycastle.crypto.digests.SHA384Digest;
/**
* Base class for all transactions that may be built and submitted to Hedera.
*
* @param The type of the transaction. Used to enable chaining.
*/
public abstract class Transaction>
extends
Executable {
/**
* Default auto renew duration for accounts, contracts, topics, and files (entities)
*/
static final Duration DEFAULT_AUTO_RENEW_PERIOD = Duration.ofDays(90);
/**
* Dummy account ID used to assist in deserializing incomplete Transactions.
*/
protected static final AccountId DUMMY_ACCOUNT_ID = new AccountId(0L, 0L, 0L);
/**
* Dummy transaction ID used to assist in deserializing incomplete Transactions.
*/
protected static final TransactionId DUMMY_TRANSACTION_ID = TransactionId.withValidStart(DUMMY_ACCOUNT_ID, Instant.EPOCH);
/**
* Default transaction duration
*/
private static final Duration DEFAULT_TRANSACTION_VALID_DURATION = Duration.ofSeconds(120);
/**
* Transaction constructors end their work by setting sourceTransactionBody. The expectation is that the Transaction
* subclass constructor will pick up where the Transaction superclass constructor left off, and will unpack the data
* in the transaction body.
*/
protected final TransactionBody sourceTransactionBody;
/**
* The builder that gets re-used to build each outer transaction. freezeWith() will create the frozenBodyBuilder.
* The presence of frozenBodyBuilder indicates that this transaction is frozen.
*/
@Nullable
protected TransactionBody.Builder frozenBodyBuilder = null;
/**
* An SDK [Transaction] is composed of multiple, raw protobuf transactions. These should be functionally identical,
* except 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.
*/
protected List outerTransactions = Collections.emptyList();
/**
* An SDK [Transaction] is composed of multiple, raw protobuf transactions. These should be functionally identical,
* except 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.
*/
protected List innerSignedTransactions = Collections.emptyList();
/**
* A set of signatures corresponding to every unique public key used to sign the transaction.
*/
protected List sigPairLists = Collections.emptyList();
/**
* List of IDs for the transaction based on the operator because the transaction ID includes the operator's account
*/
protected LockableList transactionIds = new LockableList<>();
/**
* publicKeys and signers are parallel arrays. If the signer associated with a public key is null, that means that
* the private key associated with that public key has already contributed a signature to sigPairListBuilders, but
* the signer is not available (likely because this came from fromBytes())
*/
protected List publicKeys = new ArrayList<>();
/**
* publicKeys and signers are parallel arrays. If the signer associated with a public key is null, that means that
* the private key associated with that public key has already contributed a signature to sigPairListBuilders, but
* the signer is not available (likely because this came from fromBytes())
*/
protected List> signers = new ArrayList<>();
/**
* The maximum transaction fee the client is willing to pay
*/
protected Hbar defaultMaxTransactionFee = new Hbar(2);
/**
* Should the transaction id be regenerated
*/
protected Boolean regenerateTransactionId = null;
private Duration transactionValidDuration;
@Nullable
private Hbar maxTransactionFee = null;
private String memo = "";
/**
* Constructor.
*/
Transaction() {
setTransactionValidDuration(DEFAULT_TRANSACTION_VALID_DURATION);
sourceTransactionBody = TransactionBody.getDefaultInstance();
}
// This constructor is used to construct from a scheduled transaction body
/**
* Constructor.
*
* @param txBody protobuf TransactionBody
*/
Transaction(com.hedera.hashgraph.sdk.proto.TransactionBody txBody) {
setTransactionValidDuration(DEFAULT_TRANSACTION_VALID_DURATION);
setMaxTransactionFee(Hbar.fromTinybars(txBody.getTransactionFee()));
setTransactionMemo(txBody.getMemo());
sourceTransactionBody = txBody;
}
// This constructor is used to construct via fromBytes
/**
* Constructor.
*
* @param txs Compound list of transaction id's list of (AccountId, Transaction) records
* @throws InvalidProtocolBufferException when there is an issue with the protobuf
*/
Transaction(LinkedHashMap> txs)
throws InvalidProtocolBufferException {
LinkedHashMap transactionMap = txs.values().iterator().next();
if (!transactionMap.isEmpty() && transactionMap.keySet().iterator().next().equals(DUMMY_ACCOUNT_ID)) {
// If the first account ID is a dummy account ID, then only the source TransactionBody needs to be copied.
var signedTransaction = SignedTransaction.parseFrom(transactionMap.values().iterator().next().getSignedTransactionBytes());
sourceTransactionBody = TransactionBody.parseFrom(signedTransaction.getBodyBytes());
} else {
var txCount = txs.keySet().size();
var nodeCount = txs.values().iterator().next().size();
nodeAccountIds.ensureCapacity(nodeCount);
sigPairLists = new ArrayList<>(nodeCount * txCount);
outerTransactions = new ArrayList<>(nodeCount * txCount);
innerSignedTransactions = new ArrayList<>(nodeCount * txCount);
transactionIds.ensureCapacity(txCount);
for (var transactionEntry : txs.entrySet()) {
if (!transactionEntry.getKey().equals(DUMMY_TRANSACTION_ID)) {
transactionIds.add(transactionEntry.getKey());
}
for (var nodeEntry : transactionEntry.getValue().entrySet()) {
if (nodeAccountIds.size() != nodeCount) {
nodeAccountIds.add(nodeEntry.getKey());
}
var transaction = SignedTransaction.parseFrom(nodeEntry.getValue().getSignedTransactionBytes());
outerTransactions.add(nodeEntry.getValue());
sigPairLists.add(transaction.getSigMap().toBuilder());
innerSignedTransactions.add(transaction.toBuilder());
if (publicKeys.isEmpty()) {
for (var sigPair : transaction.getSigMap().getSigPairList()) {
publicKeys.add(PublicKey.fromBytes(sigPair.getPubKeyPrefix().toByteArray()));
signers.add(null);
}
}
}
}
nodeAccountIds.remove(new AccountId(0));
// Verify that transaction bodies match
for (@Var int i = 0; i < txCount; i++) {
@Var TransactionBody firstTxBody = null;
for (@Var int j = 0; j < nodeCount; j++) {
int k = i * nodeCount + j;
var txBody = TransactionBody.parseFrom(innerSignedTransactions.get(k).getBodyBytes());
if (firstTxBody == null) {
firstTxBody = txBody;
} else {
requireProtoMatches(
firstTxBody,
txBody,
new HashSet<>(List.of("NodeAccountID")),
"TransactionBody"
);
}
}
}
sourceTransactionBody = TransactionBody.parseFrom(innerSignedTransactions.get(0).getBodyBytes());
}
setTransactionValidDuration(
DurationConverter.fromProtobuf(sourceTransactionBody.getTransactionValidDuration()));
setMaxTransactionFee(Hbar.fromTinybars(sourceTransactionBody.getTransactionFee()));
setTransactionMemo(sourceTransactionBody.getMemo());
// The presence of signatures implies the Transaction should be frozen.
if (!publicKeys.isEmpty()) {
frozenBodyBuilder = sourceTransactionBody.toBuilder();
}
}
/**
* Create the correct transaction from a byte array.
*
* @param bytes the byte array
* @return the new transaction
* @throws InvalidProtocolBufferException when there is an issue with the protobuf
*/
public static Transaction> fromBytes(byte[] bytes) throws InvalidProtocolBufferException {
var txs = new LinkedHashMap>();
@Var TransactionBody.DataCase dataCase = TransactionBody.DataCase.DATA_NOT_SET;
var list = TransactionList.parseFrom(bytes);
if (list.getTransactionListList().isEmpty()) {
var transaction = com.hedera.hashgraph.sdk.proto.Transaction.parseFrom(bytes).toBuilder();
TransactionBody txBody;
if (transaction.getSignedTransactionBytes().isEmpty()) {
txBody = TransactionBody.parseFrom(transaction.getBodyBytes());
transaction.setSignedTransactionBytes(SignedTransaction.newBuilder()
.setBodyBytes(transaction.getBodyBytes())
.setSigMap(transaction.getSigMap())
.build()
.toByteString())
.clearBodyBytes()
.clearSigMap();
} else {
var signedTransaction = SignedTransaction.parseFrom(transaction.getSignedTransactionBytes());
txBody = TransactionBody.parseFrom(signedTransaction.getBodyBytes());
}
dataCase = txBody.getDataCase();
var account = txBody.hasNodeAccountID() ? AccountId.fromProtobuf(txBody.getNodeAccountID())
: DUMMY_ACCOUNT_ID;
var transactionId = txBody.hasTransactionID() ? TransactionId.fromProtobuf(txBody.getTransactionID())
: DUMMY_TRANSACTION_ID;
var linked = new LinkedHashMap();
linked.put(account, transaction.build());
txs.put(transactionId, linked);
} else {
for (var transaction : list.getTransactionListList()) {
var signedTransaction = SignedTransaction.parseFrom(transaction.getSignedTransactionBytes());
var txBody = TransactionBody.parseFrom(signedTransaction.getBodyBytes());
if (dataCase.getNumber() == TransactionBody.DataCase.DATA_NOT_SET.getNumber()) {
dataCase = txBody.getDataCase();
}
var account = txBody.hasNodeAccountID() ? AccountId.fromProtobuf(txBody.getNodeAccountID())
: DUMMY_ACCOUNT_ID;
var transactionId = txBody.hasTransactionID() ? TransactionId.fromProtobuf(txBody.getTransactionID())
: DUMMY_TRANSACTION_ID;
var linked = txs.containsKey(transactionId) ?
Objects.requireNonNull(txs.get(transactionId)) :
new LinkedHashMap();
linked.put(account, transaction);
txs.put(transactionId, linked);
}
}
return switch (dataCase) {
case CONTRACTCALL -> new ContractExecuteTransaction(txs);
case CONTRACTCREATEINSTANCE -> new ContractCreateTransaction(txs);
case CONTRACTUPDATEINSTANCE -> new ContractUpdateTransaction(txs);
case CONTRACTDELETEINSTANCE -> new ContractDeleteTransaction(txs);
case ETHEREUMTRANSACTION -> new EthereumTransaction(txs);
case CRYPTOADDLIVEHASH -> new LiveHashAddTransaction(txs);
case CRYPTOCREATEACCOUNT -> new AccountCreateTransaction(txs);
case CRYPTODELETE -> new AccountDeleteTransaction(txs);
case CRYPTODELETELIVEHASH -> new LiveHashDeleteTransaction(txs);
case CRYPTOTRANSFER -> new TransferTransaction(txs);
case CRYPTOUPDATEACCOUNT -> new AccountUpdateTransaction(txs);
case FILEAPPEND -> new FileAppendTransaction(txs);
case FILECREATE -> new FileCreateTransaction(txs);
case FILEDELETE -> new FileDeleteTransaction(txs);
case FILEUPDATE -> new FileUpdateTransaction(txs);
case NODECREATE -> new NodeCreateTransaction(txs);
case NODEUPDATE -> new NodeUpdateTransaction(txs);
case NODEDELETE -> new NodeDeleteTransaction(txs);
case SYSTEMDELETE -> new SystemDeleteTransaction(txs);
case SYSTEMUNDELETE -> new SystemUndeleteTransaction(txs);
case FREEZE -> new FreezeTransaction(txs);
case CONSENSUSCREATETOPIC -> new TopicCreateTransaction(txs);
case CONSENSUSUPDATETOPIC -> new TopicUpdateTransaction(txs);
case CONSENSUSDELETETOPIC -> new TopicDeleteTransaction(txs);
case CONSENSUSSUBMITMESSAGE -> new TopicMessageSubmitTransaction(txs);
case TOKENASSOCIATE -> new TokenAssociateTransaction(txs);
case TOKENBURN -> new TokenBurnTransaction(txs);
case TOKENCREATION -> new TokenCreateTransaction(txs);
case TOKENDELETION -> new TokenDeleteTransaction(txs);
case TOKENDISSOCIATE -> new TokenDissociateTransaction(txs);
case TOKENFREEZE -> new TokenFreezeTransaction(txs);
case TOKENGRANTKYC -> new TokenGrantKycTransaction(txs);
case TOKENMINT -> new TokenMintTransaction(txs);
case TOKENREVOKEKYC -> new TokenRevokeKycTransaction(txs);
case TOKENUNFREEZE -> new TokenUnfreezeTransaction(txs);
case TOKENUPDATE -> new TokenUpdateTransaction(txs);
case TOKEN_UPDATE_NFTS -> new TokenUpdateNftsTransaction(txs);
case TOKENWIPE -> new TokenWipeTransaction(txs);
case TOKEN_FEE_SCHEDULE_UPDATE -> new TokenFeeScheduleUpdateTransaction(txs);
case SCHEDULECREATE -> new ScheduleCreateTransaction(txs);
case SCHEDULEDELETE -> new ScheduleDeleteTransaction(txs);
case SCHEDULESIGN -> new ScheduleSignTransaction(txs);
case TOKEN_PAUSE -> new TokenPauseTransaction(txs);
case TOKEN_UNPAUSE -> new TokenUnpauseTransaction(txs);
case TOKENREJECT -> new TokenRejectTransaction(txs);
case TOKENAIRDROP -> new TokenAirdropTransaction(txs);
case TOKENCANCELAIRDROP -> new TokenCancelAirdropTransaction(txs);
case TOKENCLAIMAIRDROP -> new TokenClaimAirdropTransaction(txs);
case CRYPTOAPPROVEALLOWANCE -> new AccountAllowanceApproveTransaction(txs);
case CRYPTODELETEALLOWANCE -> new AccountAllowanceDeleteTransaction(txs);
default -> throw new IllegalArgumentException("parsed transaction body has no data");
};
}
/**
* Create the correct transaction from a scheduled transaction.
*
* @param scheduled the scheduled transaction
* @return the new transaction
*/
public static Transaction> fromScheduledTransaction(
com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody scheduled) {
var body = TransactionBody.newBuilder()
.setMemo(scheduled.getMemo())
.setTransactionFee(scheduled.getTransactionFee());
return switch (scheduled.getDataCase()) {
case CONTRACTCALL ->
new ContractExecuteTransaction(body.setContractCall(scheduled.getContractCall()).build());
case CONTRACTCREATEINSTANCE -> new ContractCreateTransaction(
body.setContractCreateInstance(scheduled.getContractCreateInstance()).build());
case CONTRACTUPDATEINSTANCE -> new ContractUpdateTransaction(
body.setContractUpdateInstance(scheduled.getContractUpdateInstance()).build());
case CONTRACTDELETEINSTANCE -> new ContractDeleteTransaction(
body.setContractDeleteInstance(scheduled.getContractDeleteInstance()).build());
case CRYPTOAPPROVEALLOWANCE -> new AccountAllowanceApproveTransaction(
body.setCryptoApproveAllowance(scheduled.getCryptoApproveAllowance()).build());
case CRYPTODELETEALLOWANCE -> new AccountAllowanceDeleteTransaction(
body.setCryptoDeleteAllowance(scheduled.getCryptoDeleteAllowance()).build());
case CRYPTOCREATEACCOUNT -> new AccountCreateTransaction(
body.setCryptoCreateAccount(scheduled.getCryptoCreateAccount()).build());
case CRYPTODELETE ->
new AccountDeleteTransaction(body.setCryptoDelete(scheduled.getCryptoDelete()).build());
case CRYPTOTRANSFER ->
new TransferTransaction(body.setCryptoTransfer(scheduled.getCryptoTransfer()).build());
case CRYPTOUPDATEACCOUNT -> new AccountUpdateTransaction(
body.setCryptoUpdateAccount(scheduled.getCryptoUpdateAccount()).build());
case FILEAPPEND -> new FileAppendTransaction(body.setFileAppend(scheduled.getFileAppend()).build());
case FILECREATE -> new FileCreateTransaction(body.setFileCreate(scheduled.getFileCreate()).build());
case FILEDELETE -> new FileDeleteTransaction(body.setFileDelete(scheduled.getFileDelete()).build());
case FILEUPDATE -> new FileUpdateTransaction(body.setFileUpdate(scheduled.getFileUpdate()).build());
case NODECREATE -> new NodeCreateTransaction(body.setNodeCreate(scheduled.getNodeCreate()).build());
case NODEUPDATE -> new NodeUpdateTransaction(body.setNodeUpdate(scheduled.getNodeUpdate()).build());
case NODEDELETE -> new NodeDeleteTransaction(body.setNodeDelete(scheduled.getNodeDelete()).build());
case SYSTEMDELETE -> new SystemDeleteTransaction(body.setSystemDelete(scheduled.getSystemDelete()).build());
case SYSTEMUNDELETE ->
new SystemUndeleteTransaction(body.setSystemUndelete(scheduled.getSystemUndelete()).build());
case FREEZE -> new FreezeTransaction(body.setFreeze(scheduled.getFreeze()).build());
case CONSENSUSCREATETOPIC -> new TopicCreateTransaction(
body.setConsensusCreateTopic(scheduled.getConsensusCreateTopic()).build());
case CONSENSUSUPDATETOPIC -> new TopicUpdateTransaction(
body.setConsensusUpdateTopic(scheduled.getConsensusUpdateTopic()).build());
case CONSENSUSDELETETOPIC -> new TopicDeleteTransaction(
body.setConsensusDeleteTopic(scheduled.getConsensusDeleteTopic()).build());
case CONSENSUSSUBMITMESSAGE -> new TopicMessageSubmitTransaction(
body.setConsensusSubmitMessage(scheduled.getConsensusSubmitMessage()).build());
case TOKENCREATION ->
new TokenCreateTransaction(body.setTokenCreation(scheduled.getTokenCreation()).build());
case TOKENFREEZE -> new TokenFreezeTransaction(body.setTokenFreeze(scheduled.getTokenFreeze()).build());
case TOKENUNFREEZE ->
new TokenUnfreezeTransaction(body.setTokenUnfreeze(scheduled.getTokenUnfreeze()).build());
case TOKENGRANTKYC ->
new TokenGrantKycTransaction(body.setTokenGrantKyc(scheduled.getTokenGrantKyc()).build());
case TOKENREVOKEKYC ->
new TokenRevokeKycTransaction(body.setTokenRevokeKyc(scheduled.getTokenRevokeKyc()).build());
case TOKENDELETION ->
new TokenDeleteTransaction(body.setTokenDeletion(scheduled.getTokenDeletion()).build());
case TOKENUPDATE -> new TokenUpdateTransaction(body.setTokenUpdate(scheduled.getTokenUpdate()).build());
case TOKEN_UPDATE_NFTS -> new TokenUpdateNftsTransaction(body.setTokenUpdateNfts(scheduled.getTokenUpdateNfts()).build());
case TOKENMINT -> new TokenMintTransaction(body.setTokenMint(scheduled.getTokenMint()).build());
case TOKENBURN -> new TokenBurnTransaction(body.setTokenBurn(scheduled.getTokenBurn()).build());
case TOKENWIPE -> new TokenWipeTransaction(body.setTokenWipe(scheduled.getTokenWipe()).build());
case TOKENASSOCIATE ->
new TokenAssociateTransaction(body.setTokenAssociate(scheduled.getTokenAssociate()).build());
case TOKENDISSOCIATE ->
new TokenDissociateTransaction(body.setTokenDissociate(scheduled.getTokenDissociate()).build());
case TOKEN_FEE_SCHEDULE_UPDATE -> new TokenFeeScheduleUpdateTransaction(
body.setTokenFeeScheduleUpdate(scheduled.getTokenFeeScheduleUpdate()).build());
case TOKEN_PAUSE -> new TokenPauseTransaction(body.setTokenPause(scheduled.getTokenPause()).build());
case TOKEN_UNPAUSE ->
new TokenUnpauseTransaction(body.setTokenUnpause(scheduled.getTokenUnpause()).build());
case TOKENREJECT ->
new TokenRejectTransaction(body.setTokenReject(scheduled.getTokenReject()).build());
case TOKENAIRDROP -> new TokenAirdropTransaction(body.setTokenAirdrop(scheduled.getTokenAirdrop()).build());
case TOKENCANCELAIRDROP -> new TokenCancelAirdropTransaction(body.setTokenCancelAirdrop(scheduled.getTokenCancelAirdrop()).build());
case TOKENCLAIMAIRDROP -> new TokenClaimAirdropTransaction(body.setTokenCancelAirdrop(scheduled.getTokenCancelAirdrop()).build());
case SCHEDULEDELETE ->
new ScheduleDeleteTransaction(body.setScheduleDelete(scheduled.getScheduleDelete()).build());
default -> throw new IllegalStateException("schedulable transaction did not have a transaction set");
};
}
private static void throwProtoMatchException(String fieldName, String aWas, String bWas) {
throw new IllegalArgumentException(
"fromBytes() failed because " + fieldName +
" fields in TransactionBody protobuf messages in the TransactionList did not match: A was " +
aWas + ", B was " + bWas
);
}
private static void requireProtoMatches(Object protoA, Object protoB, Set ignoreSet, String thisFieldName) {
var aIsNull = protoA == null;
var bIsNull = protoB == null;
if (aIsNull != bIsNull) {
throwProtoMatchException(thisFieldName, aIsNull ? "null" : "not null", bIsNull ? "null" : "not null");
}
if (aIsNull) {
return;
}
var protoAClass = protoA.getClass();
var protoBClass = protoB.getClass();
if (!protoAClass.equals(protoBClass)) {
throwProtoMatchException(thisFieldName, "of class " + protoAClass, "of class " + protoBClass);
}
if (protoA instanceof Boolean ||
protoA instanceof Integer ||
protoA instanceof Long ||
protoA instanceof Float ||
protoA instanceof Double ||
protoA instanceof String ||
protoA instanceof ByteString
) {
// System.out.println("values A = " + protoA.toString() + ", B = " + protoB.toString());
if (!protoA.equals(protoB)) {
throwProtoMatchException(thisFieldName, protoA.toString(), protoB.toString());
}
}
for (var method : protoAClass.getDeclaredMethods()) {
if (method.getParameterCount() != 0) {
continue;
}
int methodModifiers = method.getModifiers();
if ((!Modifier.isPublic(methodModifiers)) || Modifier.isStatic(methodModifiers)) {
continue;
}
var methodName = method.getName();
if (!methodName.startsWith("get")) {
continue;
}
var isList = methodName.endsWith("List") && List.class.isAssignableFrom(method.getReturnType());
var methodFieldName = methodName.substring(3, methodName.length() - (isList ? 4 : 0));
if (ignoreSet.contains(methodFieldName) || methodFieldName.equals("DefaultInstance")) {
continue;
}
if (!isList) {
try {
var hasMethod = protoAClass.getMethod("has" + methodFieldName);
var hasA = (Boolean) hasMethod.invoke(protoA);
var hasB = (Boolean) hasMethod.invoke(protoB);
if (!hasA.equals(hasB)) {
throwProtoMatchException(methodFieldName, hasA ? "present" : "not present",
hasB ? "present" : "not present");
}
if (!hasA) {
continue;
}
} catch (NoSuchMethodException ignored) {
// pass if there is no has method
} catch (IllegalArgumentException error) {
throw error;
} catch (Throwable error) {
throw new IllegalArgumentException("fromBytes() failed due to error", error);
}
}
try {
var retvalA = method.invoke(protoA);
var retvalB = method.invoke(protoB);
if (isList) {
var listA = (List>) retvalA;
var listB = (List>) retvalB;
if (listA.size() != listB.size()) {
throwProtoMatchException(methodFieldName, "of size " + listA.size(), "of size " + listB.size());
}
for (@Var int i = 0; i < listA.size(); i++) {
// System.out.println("comparing " + thisFieldName + "." + methodFieldName + "[" + i + "]");
requireProtoMatches(listA.get(i), listB.get(i), ignoreSet, methodFieldName + "[" + i + "]");
}
} else {
// System.out.println("comparing " + thisFieldName + "." + methodFieldName);
requireProtoMatches(retvalA, retvalB, ignoreSet, methodFieldName);
}
} catch (IllegalArgumentException error) {
throw error;
} catch (Throwable error) {
throw new IllegalArgumentException("fromBytes() failed due to error", error);
}
}
}
/**
* Generate a hash from a byte array.
*
* @param bytes the byte array
* @return the hash
*/
static byte[] hash(byte[] bytes) {
var digest = new SHA384Digest();
var hash = new byte[digest.getDigestSize()];
digest.update(bytes, 0, bytes.length);
digest.doFinal(hash, 0);
return hash;
}
private static boolean publicKeyIsInSigPairList(ByteString publicKeyBytes, List sigPairList) {
for (var pair : sigPairList) {
if (pair.getPubKeyPrefix().equals(publicKeyBytes)) {
return true;
}
}
return false;
}
/**
* Converts transaction into a scheduled version
*
* @param bodyBuilder the transaction's body builder
* @return the scheduled transaction
*/
protected ScheduleCreateTransaction doSchedule(TransactionBody.Builder bodyBuilder) {
var schedulable = SchedulableTransactionBody.newBuilder()
.setTransactionFee(bodyBuilder.getTransactionFee())
.setMemo(bodyBuilder.getMemo());
onScheduled(schedulable);
var scheduled = new ScheduleCreateTransaction()
.setScheduledTransactionBody(schedulable.build());
if (!transactionIds.isEmpty()) {
scheduled.setTransactionId(transactionIds.get(0));
}
return scheduled;
}
/**
* Extract the scheduled transaction.
*
* @return the scheduled transaction
*/
public ScheduleCreateTransaction schedule() {
requireNotFrozen();
if (!nodeAccountIds.isEmpty()) {
throw new IllegalStateException(
"The underlying transaction for a scheduled transaction cannot have node account IDs set"
);
}
var bodyBuilder = spawnBodyBuilder(null);
onFreeze(bodyBuilder);
return doSchedule(bodyBuilder);
}
/**
* Set the account IDs of the nodes that this transaction will be submitted to.
*
* Providing an explicit node account ID interferes with client-side load balancing of the network. By default, the
* SDK will pre-generate a transaction for 1/3 of the nodes on the network. If a node is down, busy, or otherwise
* reports a fatal error, the SDK will try again with a different node.
*
* @param nodeAccountIds The list of node AccountIds to be set
* @return {@code this}
*/
@Override
public final T setNodeAccountIds(List nodeAccountIds) {
requireNotFrozen();
Objects.requireNonNull(nodeAccountIds);
return super.setNodeAccountIds(nodeAccountIds);
}
/**
* Extract the valid transaction duration.
*
* @return the transaction valid duration
*/
@Nullable
@SuppressFBWarnings(
value = "EI_EXPOSE_REP",
justification = "A Duration can't actually be mutated"
)
public final Duration getTransactionValidDuration() {
return transactionValidDuration;
}
/**
* Sets the duration that this transaction is valid for.
*
* This is defaulted by the SDK to 120 seconds (or two minutes).
*
* @param validDuration The duration to be set
* @return {@code this}
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP2",
justification = "A Duration can't actually be mutated"
)
public final T setTransactionValidDuration(Duration validDuration) {
requireNotFrozen();
Objects.requireNonNull(validDuration);
transactionValidDuration = validDuration;
// noinspection unchecked
return (T) this;
}
/**
* Extract the maximum transaction fee.
*
* @return the maximum transaction fee
*/
@Nullable
public final Hbar getMaxTransactionFee() {
return maxTransactionFee;
}
/**
* Set the maximum transaction fee the operator (paying account) is willing to pay.
*
* @param maxTransactionFee the maximum transaction fee, in tinybars.
* @return {@code this}
*/
public final T setMaxTransactionFee(Hbar maxTransactionFee) {
requireNotFrozen();
Objects.requireNonNull(maxTransactionFee);
this.maxTransactionFee = maxTransactionFee;
// noinspection unchecked
return (T) this;
}
/**
* Extract the default maximum transaction fee.
*
* @return the default maximum transaction fee
*/
public final Hbar getDefaultMaxTransactionFee() {
return defaultMaxTransactionFee;
}
/**
* Extract the memo for the transaction.
*
* @return the memo for the transaction
*/
public final String getTransactionMemo() {
return memo;
}
/**
* Set a note or description that should be recorded in the transaction record (maximum length of 100 characters).
*
* @param memo any notes or descriptions for this transaction.
* @return {@code this}
*/
public final T setTransactionMemo(String memo) {
requireNotFrozen();
Objects.requireNonNull(memo);
this.memo = memo;
// noinspection unchecked
return (T) this;
}
/**
* Extract a byte array representation.
*
* @return the byte array representation
*/
public byte[] toBytes() {
var list = TransactionList.newBuilder();
// If no nodes have been selected yet,
// the new TransactionBody can be used to build a Transaction protobuf object.
if (nodeAccountIds.isEmpty()) {
var bodyBuilder = spawnBodyBuilder(null);
if (!transactionIds.isEmpty()) {
bodyBuilder.setTransactionID(transactionIds.get(0).toProtobuf());
}
onFreeze(bodyBuilder);
var signedTransaction = SignedTransaction.newBuilder()
.setBodyBytes(bodyBuilder.build().toByteString())
.build();
var transaction = com.hedera.hashgraph.sdk.proto.Transaction.newBuilder()
.setSignedTransactionBytes(signedTransaction.toByteString())
.build();
list.addTransactionList(transaction);
} else {
// Generate the SignedTransaction protobuf objects if the Transaction's not frozen.
if (!this.isFrozen()) {
frozenBodyBuilder = spawnBodyBuilder(null);
if (!transactionIds.isEmpty()) {
frozenBodyBuilder.setTransactionID(transactionIds.get(0).toProtobuf());
}
onFreeze(frozenBodyBuilder);
int requiredChunks = getRequiredChunks();
if (!transactionIds.isEmpty()){
generateTransactionIds(transactionIds.get(0), requiredChunks);
}
wipeTransactionLists(requiredChunks);
}
// Build all the Transaction protobuf objects and add them to the TransactionList protobuf object.
buildAllTransactions();
for (var transaction : outerTransactions) {
list.addTransactionList(transaction);
}
}
return list.build().toByteArray();
}
/**
* Extract a byte array of the transaction hash.
*
* @return the transaction hash
*/
public byte[] getTransactionHash() {
if (!this.isFrozen()) {
throw new IllegalStateException(
"transaction must have been frozen before calculating the hash will be stable, try calling `freeze`");
}
transactionIds.setLocked(true);
nodeAccountIds.setLocked(true);
var index = transactionIds.getIndex() * nodeAccountIds.size() + nodeAccountIds.getIndex();
buildTransaction(index);
return hash(outerTransactions.get(index).getSignedTransactionBytes().toByteArray());
}
/**
* Extract the list of account id and hash records.
*
* @return the list of account id and hash records
*/
public Map getTransactionHashPerNode() {
if (!this.isFrozen()) {
throw new IllegalStateException(
"transaction must have been frozen before calculating the hash will be stable, try calling `freeze`");
}
buildAllTransactions();
var hashes = new HashMap();
for (var i = 0; i < outerTransactions.size(); i++) {
hashes.put(nodeAccountIds.get(i), hash(outerTransactions.get(i).getSignedTransactionBytes().toByteArray()));
}
return hashes;
}
@Override
final TransactionId getTransactionIdInternal() {
return transactionIds.getCurrent();
}
/**
* Extract the transaction id.
*
* @return the transaction id
*/
public final TransactionId getTransactionId() {
if (transactionIds.isEmpty() || !this.isFrozen()) {
throw new IllegalStateException("No transaction ID generated yet. Try freezing the transaction or manually setting the transaction ID.");
}
return transactionIds.setLocked(true).getCurrent();
}
/**
* 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 The TransactionId to be set
* @return {@code this}
* @see TransactionId
*/
public final T setTransactionId(TransactionId transactionId) {
requireNotFrozen();
transactionIds.setList(Collections.singletonList(transactionId)).setLocked(true);
// noinspection unchecked
return (T) this;
}
/**
* Should the transaction id be regenerated.
*
* @return should the transaction id be regenerated
*/
public final Boolean getRegenerateTransactionId() {
return regenerateTransactionId;
}
/**
* Regenerate the transaction id.
*
* @param regenerateTransactionId should the transaction id be regenerated
* @return {@code this}
*/
public final T setRegenerateTransactionId(boolean regenerateTransactionId) {
this.regenerateTransactionId = regenerateTransactionId;
// noinspection unchecked
return (T) this;
}
/**
* Sign the transaction.
*
* @param privateKey the private key
* @return the signed transaction
*/
public final T sign(PrivateKey privateKey) {
return signWith(privateKey.getPublicKey(), privateKey::sign);
}
/**
* Sign the transaction.
*
* @param publicKey the public key
* @param transactionSigner the key list
* @return {@code this}
*/
public T signWith(PublicKey publicKey, UnaryOperator transactionSigner) {
if (!isFrozen()) {
throw new IllegalStateException("Signing requires transaction to be frozen");
}
if (keyAlreadySigned(publicKey)) {
// noinspection unchecked
return (T) this;
}
for (int i = 0; i < outerTransactions.size(); i++) {
outerTransactions.set(i, null);
}
publicKeys.add(publicKey);
signers.add(transactionSigner);
// noinspection unchecked
return (T) this;
}
/**
* Sign the transaction with the configured client.
*
* @param client the configured client
* @return the signed transaction
*/
public T signWithOperator(Client client) {
var operator = client.getOperator();
if (operator == null) {
throw new IllegalStateException(
"`client` must have an `operator` to sign with the operator");
}
if (!isFrozen()) {
freezeWith(client);
}
return signWith(operator.publicKey, operator.transactionSigner);
}
/**
* Checks if a public key is already added to the transaction
*
* @param key the public key
* @return if the public key is already added
*/
protected boolean keyAlreadySigned(PublicKey key) {
return publicKeys.contains(key);
}
/**
* Add a signature to the transaction.
*
* @param publicKey the public key
* @param signature the signature
* @return {@code this}
*/
public T addSignature(PublicKey publicKey, byte[] signature) {
requireOneNodeAccountId();
if (!isFrozen()) {
freeze();
}
if (keyAlreadySigned(publicKey)) {
// noinspection unchecked
return (T) this;
}
transactionIds.setLocked(true);
nodeAccountIds.setLocked(true);
for (int i = 0; i < outerTransactions.size(); i++) {
outerTransactions.set(i, null);
}
publicKeys.add(publicKey);
signers.add(null);
sigPairLists.get(0).addSigPair(publicKey.toSignaturePairProtobuf(signature));
// noinspection unchecked
return (T) this;
}
protected Map> getSignaturesAtOffset(int offset) {
var map = new HashMap>(nodeAccountIds.size());
for (int i = 0; i < nodeAccountIds.size(); i++) {
var sigMap = sigPairLists.get(i + offset);
var nodeAccountId = nodeAccountIds.get(i);
var keyMap = map.containsKey(nodeAccountId) ?
Objects.requireNonNull(map.get(nodeAccountId)) :
new HashMap(sigMap.getSigPairCount());
map.put(nodeAccountId, keyMap);
for (var sigPair : sigMap.getSigPairList()) {
keyMap.put(
PublicKey.fromBytes(sigPair.getPubKeyPrefix().toByteArray()),
sigPair.getEd25519().toByteArray()
);
}
}
return map;
}
/**
* Extract list of account id and public keys.
*
* @return the list of account id and public keys
*/
public Map> getSignatures() {
if (!isFrozen()) {
throw new IllegalStateException("Transaction must be frozen in order to have signatures.");
}
if (publicKeys.isEmpty()) {
return Collections.emptyMap();
}
buildAllTransactions();
return getSignaturesAtOffset(0);
}
/**
* Check if transaction is frozen.
*
* @return is the transaction frozen
*/
protected boolean isFrozen() {
return frozenBodyBuilder != null;
}
/**
* Throw an exception if the transaction is frozen.
*/
protected void requireNotFrozen() {
if (isFrozen()) {
throw new IllegalStateException(
"transaction is immutable; it has at least one signature or has been explicitly frozen");
}
}
/**
* Throw an exception if there is not exactly one node id set.
*/
protected void requireOneNodeAccountId() {
if (nodeAccountIds.size() != 1) {
throw new IllegalStateException("transaction did not have exactly one node ID set");
}
}
protected TransactionBody.Builder spawnBodyBuilder(@Nullable Client client) {
var clientDefaultFee = client != null ? client.getDefaultMaxTransactionFee() : null;
var defaultFee = clientDefaultFee != null ? clientDefaultFee : defaultMaxTransactionFee;
var feeHbars = maxTransactionFee != null ? maxTransactionFee : defaultFee;
return TransactionBody.newBuilder()
.setTransactionFee(feeHbars.toTinybars())
.setTransactionValidDuration(DurationConverter.toProtobuf(transactionValidDuration).toBuilder())
.setMemo(memo);
}
/**
* Freeze this transaction from further modification to prepare for signing or serialization.
*
* @return {@code this}
*/
public T freeze() {
return freezeWith(null);
}
/**
* 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 client the configured client
* @return {@code this}
*/
public T freezeWith(@Nullable Client client) {
if (isFrozen()) {
// noinspection unchecked
return (T) this;
}
if (transactionIds.isEmpty()) {
if (client != null) {
var operator = client.getOperator();
if (operator != null) {
// Set a default transaction ID, generated from the operator account ID
transactionIds.setList(Collections.singletonList(TransactionId.generate(operator.accountId)));
} else {
// no client means there must be an explicitly set node ID and transaction ID
throw new IllegalStateException(
"`client` must have an `operator` or `transactionId` must be set");
}
} else {
throw new IllegalStateException(
"Transaction ID must be set, or operator must be provided via freezeWith()");
}
}
if (nodeAccountIds.isEmpty()) {
if (client == null) {
throw new IllegalStateException(
"`client` must be provided or both `nodeId` and `transactionId` must be set");
}
try {
nodeAccountIds.setList(client.network.getNodeAccountIdsForExecute());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
frozenBodyBuilder = spawnBodyBuilder(client).setTransactionID(transactionIds.get(0).toProtobuf());
onFreeze(frozenBodyBuilder);
int requiredChunks = getRequiredChunks();
generateTransactionIds(transactionIds.get(0), requiredChunks);
wipeTransactionLists(requiredChunks);
var clientDefaultRegenerateTransactionId = client != null ? client.getDefaultRegenerateTransactionId() : null;
regenerateTransactionId =
regenerateTransactionId != null ? regenerateTransactionId : clientDefaultRegenerateTransactionId;
// noinspection unchecked
return (T) this;
}
/**
* There must be at least one chunk.
*
* @return there is 1 required chunk
*/
int getRequiredChunks() {
return 1;
}
/**
* Generate transaction id's.
*
* @param initialTransactionId the initial transaction id
* @param count the number of id's to generate.
*/
void generateTransactionIds(TransactionId initialTransactionId, int count) {
var locked = transactionIds.isLocked();
transactionIds.setLocked(false);
if (count == 1) {
transactionIds.setList(Collections.singletonList(initialTransactionId));
return;
}
var nextTransactionId = initialTransactionId.toProtobuf().toBuilder();
transactionIds.ensureCapacity(count);
transactionIds.clear();
for (int i = 0; i < count; i++) {
transactionIds.add(TransactionId.fromProtobuf(nextTransactionId.build()));
// add 1 ns to the validStart to make cascading transaction IDs
var nextValidStart = nextTransactionId.getTransactionValidStart().toBuilder();
nextValidStart.setNanos(nextValidStart.getNanos() + 1);
nextTransactionId.setTransactionValidStart(nextValidStart);
}
transactionIds.setLocked(locked);
}
/**
* Wipe / reset the transaction list.
*
* @param requiredChunks the number of required chunks
*/
void wipeTransactionLists(int requiredChunks) {
if (!transactionIds.isEmpty()) {
Objects.requireNonNull(frozenBodyBuilder).setTransactionID(getTransactionIdInternal().toProtobuf());
}
outerTransactions = new ArrayList<>(nodeAccountIds.size());
sigPairLists = new ArrayList<>(nodeAccountIds.size());
innerSignedTransactions = new ArrayList<>(nodeAccountIds.size());
for (AccountId nodeId : nodeAccountIds) {
sigPairLists.add(SignatureMap.newBuilder());
innerSignedTransactions.add(SignedTransaction.newBuilder()
.setBodyBytes(Objects.requireNonNull(frozenBodyBuilder)
.setNodeAccountID(nodeId.toProtobuf())
.build()
.toByteString()
));
outerTransactions.add(null);
}
}
/**
* Build all the transactions.
*/
void buildAllTransactions() {
transactionIds.setLocked(true);
nodeAccountIds.setLocked(true);
for (var i = 0; i < innerSignedTransactions.size(); ++i) {
buildTransaction(i);
}
}
/**
* Will build the specific transaction at {@code index} This function is only ever called after the transaction is
* frozen.
*
* @param index the index of the transaction to be built
*/
void buildTransaction(int index) {
// Check if transaction is already built.
// Every time a signer is added via sign() or signWith(), all outerTransactions are nullified.
if (
outerTransactions.get(index) != null &&
!outerTransactions.get(index).getSignedTransactionBytes().isEmpty()
) {
return;
}
signTransaction(index);
outerTransactions.set(index, com.hedera.hashgraph.sdk.proto.Transaction.newBuilder()
.setSignedTransactionBytes(
innerSignedTransactions.get(index)
.setSigMap(sigPairLists.get(index))
.build()
.toByteString()
).build());
}
/**
* Will sign the specific transaction at {@code index} This function is only ever called after the transaction is
* frozen.
*
* @param index the index of the transaction to sign
*/
void signTransaction(int index) {
var bodyBytes = innerSignedTransactions.get(index).getBodyBytes().toByteArray();
var thisSigPairList = sigPairLists.get(index).getSigPairList();
for (var i = 0; i < publicKeys.size(); i++) {
if (signers.get(i) == null) {
continue;
}
if (publicKeyIsInSigPairList(ByteString.copyFrom(publicKeys.get(i).toBytesRaw()), thisSigPairList)) {
continue;
}
var signatureBytes = signers.get(i).apply(bodyBytes);
sigPairLists
.get(index)
.addSigPair(publicKeys.get(i).toSignaturePairProtobuf(signatureBytes));
}
}
/**
* Called in {@link #freezeWith(Client)} just before the transaction body is built. The intent is for the derived
* class to assign their data variant to the transaction body.
*/
abstract void onFreeze(TransactionBody.Builder bodyBuilder);
/**
* Called in {@link #schedule()} when converting transaction into a scheduled version.
*/
abstract void onScheduled(SchedulableTransactionBody.Builder scheduled);
@Override
final com.hedera.hashgraph.sdk.proto.Transaction makeRequest() {
var index = nodeAccountIds.getIndex() + (transactionIds.getIndex() * nodeAccountIds.size());
buildTransaction(index);
return outerTransactions.get(index);
}
@Override
TransactionResponse mapResponse(
com.hedera.hashgraph.sdk.proto.TransactionResponse transactionResponse,
AccountId nodeId,
com.hedera.hashgraph.sdk.proto.Transaction request
) {
var transactionId = Objects.requireNonNull(getTransactionIdInternal());
var hash = hash(request.getSignedTransactionBytes().toByteArray());
transactionIds.advance();
return new TransactionResponse(nodeId, transactionId, hash, null);
}
@Override
final Status mapResponseStatus(com.hedera.hashgraph.sdk.proto.TransactionResponse transactionResponse) {
return Status.valueOf(transactionResponse.getNodeTransactionPrecheckCode());
}
abstract void validateChecksums(Client client) throws BadEntityIdException;
/**
* Prepare the transactions to be executed.
*
* @param client the configured client
*/
void onExecute(Client client) {
if (!isFrozen()) {
freezeWith(client);
}
var accountId = Objects.requireNonNull(Objects.requireNonNull(transactionIds.get(0)).accountId);
if (client.isAutoValidateChecksumsEnabled()) {
try {
accountId.validateChecksum(client);
validateChecksums(client);
} catch (BadEntityIdException exc) {
throw new IllegalArgumentException(exc.getMessage());
}
}
var operatorId = client.getOperatorAccountId();
if (operatorId != null && operatorId.equals(accountId)) {
// on execute, sign each transaction with the operator, if present
// and we are signing a transaction that used the default transaction ID
signWithOperator(client);
}
}
@Override
CompletableFuture onExecuteAsync(Client client) {
onExecute(client);
return CompletableFuture.completedFuture(null);
}
@Override
ExecutionState getExecutionState(Status status, com.hedera.hashgraph.sdk.proto.TransactionResponse response) {
if (status == Status.TRANSACTION_EXPIRED) {
if ((regenerateTransactionId != null && !regenerateTransactionId) || transactionIds.isLocked()) {
return ExecutionState.REQUEST_ERROR;
} else {
var firstTransactionId = Objects.requireNonNull(transactionIds.get(0));
var accountId = Objects.requireNonNull(firstTransactionId.accountId);
generateTransactionIds(TransactionId.generate(accountId), transactionIds.size());
wipeTransactionLists(transactionIds.size());
return ExecutionState.RETRY;
}
}
return super.getExecutionState(status, response);
}
@Override
@SuppressWarnings("LiteProtoToString")
public String toString() {
// NOTE: regex is for removing the instance address from the default debug output
TransactionBody.Builder body = spawnBodyBuilder(null);
if (!transactionIds.isEmpty()) {
body.setTransactionID(transactionIds.get(0).toProtobuf());
}
if (!nodeAccountIds.isEmpty()) {
body.setNodeAccountID(nodeAccountIds.get(0).toProtobuf());
}
onFreeze(body);
return body.buildPartial().toString().replaceAll("@[A-Za-z0-9]+", "");
}
}