package com.hedera.hashgraph.sdk; import com.hedera.hashgraph.proto.Timestamp; import com.hedera.hashgraph.proto.TransactionID; import com.hedera.hashgraph.proto.TransactionIDOrBuilder; import com.hedera.hashgraph.sdk.account.AccountId; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Objects; import java.util.function.Consumer; import javax.annotation.Nullable; // TODO: TransactionId.toString // TODO: TransactionId.fromString public final class TransactionId { public final AccountId accountId; public final Instant validStart; private final TransactionID.Builder inner; @Nullable private static Instant lastInstant; private TransactionId(AccountId accountId, Instant transactionValidStart) { inner = TransactionID.newBuilder() .setAccountID(accountId.toProto()) .setTransactionValidStart( Timestamp.newBuilder() .setSeconds(transactionValidStart.getEpochSecond()) .setNanos(transactionValidStart.getNano())); this.accountId = accountId; this.validStart = transactionValidStart; } /** * Generates a new transaction ID for the given `accountId`. * * <p>Note that transaction IDs are made up of the current time and the account that is * primarily signing the transaction. This account will also be the account that is charged for * any transaction fees. */ public TransactionId(AccountId accountId) { this(accountId, getIncreasingInstant()); } /** * Generate a transaction ID with a given account ID and valid start time. * * <i>Nota bene</i>: executing transactions with the same ID (account ID & account start time) * will throw {@link HederaStatusException} with code {@code DUPLICATE_TRANSACTION}. * <p> * <p> * Use the primary constructor to get an ID with a known-valid {@code transactionValidStart}. * * @param accountId * @param transactionValidStart the time by which the transaction takes effect; must be in the * past by the time it is submitted to the network. */ public static TransactionId withValidStart(AccountId accountId, Instant transactionValidStart) { return new TransactionId(accountId, transactionValidStart); } TransactionId(TransactionIDOrBuilder transactionId) { inner = TransactionID.newBuilder() .setAccountID(transactionId.getAccountID()) .setTransactionValidStart(transactionId.getTransactionValidStart()); accountId = new AccountId(transactionId.getAccountIDOrBuilder()); validStart = TimestampHelper.timestampTo(transactionId.getTransactionValidStart()); } @Internal public TransactionID toProto() { return inner.build(); } @Override public String toString() { Timestamp timestampProto = TimestampHelper.timestampFrom(validStart); return accountId.toString() + "@" + timestampProto.getSeconds() + "." + timestampProto.getNanos(); } @Override public int hashCode() { return Objects.hash(accountId, validStart); } @Override public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof TransactionId)) return false; TransactionId otherId = (TransactionId) other; return accountId.equals(otherId.accountId) && validStart.equals(otherId.validStart); } // RFC: do we want to expose this method and its async version? void waitForConsensus(Client client, @Nullable Duration timeout) throws HederaStatusException { try { // use the receipt query to wait for consensus if (timeout != null) { getReceipt(client, timeout); } else { getReceipt(client); } } catch (HederaReceiptStatusException e) { // these errors mean the transaction was dropped or timed out, we need to bubble them up if (e.status.equalsAny(Status.Busy, Status.Unknown)) { throw e; } // otherwise ignore the status in the receipt; for failed transactions the record might // contain useful context that we don't want to lose/discard } } void waitForConsensusAsync(Client client, @Nullable Duration timeout, Runnable onSuccess, Consumer<HederaThrowable> onError) { // same motivation as synchronous version above Consumer<HederaThrowable> onError2 = e -> { if (e instanceof HederaReceiptStatusException) { // these errors mean the transaction was dropped if (((HederaReceiptStatusException) e).status.equalsAny(Status.Busy, Status.Unknown)) { onError.accept(e); } else { onSuccess.run(); } } }; if (timeout != null) { getReceiptAsync(client, timeout, r -> onSuccess.run(), onError2); } else { getReceiptAsync(client, r -> onSuccess.run(), onError2); } } public TransactionReceipt getReceipt(Client client) throws HederaStatusException { return new TransactionReceiptQuery() .setTransactionId(this) .execute(client); } public TransactionReceipt getReceipt(Client client, Duration timeout) throws HederaStatusException { return new TransactionReceiptQuery() .setTransactionId(this) .execute(client, timeout); } public void getReceiptAsync(Client client, Consumer<TransactionReceipt> onReceipt, Consumer<HederaThrowable> onError) { new TransactionReceiptQuery() .setTransactionId(this) .executeAsync(client, onReceipt, onError); } public void getReceiptAsync(Client client, Duration timeout, Consumer<TransactionReceipt> onReceipt, Consumer<HederaThrowable> onError) { new TransactionReceiptQuery() .setTransactionId(this) .executeAsync(client, timeout, onReceipt, onError); } public TransactionRecord getRecord(Client client) throws HederaStatusException, HederaNetworkException { waitForConsensus(client, null); return new TransactionRecordQuery() .setTransactionId(this) .execute(client); } public TransactionRecord getRecord(Client client, Duration timeout) throws HederaStatusException { waitForConsensus(client, timeout); return new TransactionRecordQuery() .setTransactionId(this) .execute(client, timeout); } public void getRecordAsync(Client client, Consumer<TransactionRecord> onRecord, Consumer<HederaThrowable> onError) { waitForConsensusAsync(client, null, () -> { new TransactionRecordQuery() .setTransactionId(this) .executeAsync(client, onRecord, onError); }, onError); } public void getRecordAsync(Client client, Duration timeout, Consumer<TransactionRecord> onRecord, Consumer<HederaThrowable> onError) { waitForConsensusAsync(client, timeout, () -> { new TransactionRecordQuery() .setTransactionId(this) .executeAsync(client, timeout, onRecord, onError); }, onError); } // `synchronized` is necessary for correctness with multiple threads private static synchronized Instant getIncreasingInstant() { // Allows the transaction to be accepted as long as the // server is not more than 10 seconds behind us final Instant instant = Clock.systemUTC() .instant() .minusSeconds(10); // ensures every instant is at least always greater than the last lastInstant = lastInstant != null && instant.compareTo(lastInstant) <= 0 ? lastInstant.plusNanos(1) : instant; return lastInstant; } }