From e26e233662cc9fe367a86a83a3dc4843c624eb20 Mon Sep 17 00:00:00 2001 From: emiliyank Date: Wed, 29 Oct 2025 17:18:29 +0200 Subject: [PATCH 1/4] hip 1261 - initial commit Signed-off-by: emiliyank --- .../com/hedera/hashgraph/sdk/FeeDataType.java | 1 + .../com/hedera/hashgraph/sdk/FeeEstimate.java | 131 +++++++ .../hedera/hashgraph/sdk/FeeEstimateMode.java | 58 ++++ .../hashgraph/sdk/FeeEstimateQuery.java | 307 +++++++++++++++++ .../hashgraph/sdk/FeeEstimateResponse.java | 237 +++++++++++++ .../com/hedera/hashgraph/sdk/FeeExtra.java | 214 ++++++++++++ .../com/hedera/hashgraph/sdk/FreezeType.java | 2 +- .../com/hedera/hashgraph/sdk/NetworkFee.java | 104 ++++++ .../java/com/hedera/hashgraph/sdk/Status.java | 58 ++-- sdk/src/main/proto/mirror/fee.proto | 152 ++++++++ .../proto/mirror/mirror_network_service.proto | 23 +- .../FeeEstimateQueryIntegrationTest.java | 324 ++++++++++++++++++ 12 files changed, 1559 insertions(+), 52 deletions(-) create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java create mode 100644 sdk/src/main/proto/mirror/fee.proto create mode 100644 sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java index c3b23ac68b..f168bd03c9 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java @@ -54,6 +54,7 @@ public enum FeeDataType { */ SUBMIT_MESSAGE_WITH_CUSTOM_FEES(SubType.SUBMIT_MESSAGE_WITH_CUSTOM_FEES); + final SubType code; FeeDataType(SubType code) { diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java new file mode 100644 index 0000000000..6ab2081c71 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import com.google.common.base.MoreObjects; +import com.google.protobuf.InvalidProtocolBufferException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * The fee estimate for a fee component (node or service). + *

+ * Includes the base fee and any extras associated with it. + */ +public final class FeeEstimate { + /** + * The base fee price, in tinycents. + */ + private final long base; + + /** + * The extra fees that apply for this fee component. + */ + private final List extras; + + /** + * Constructor. + * + * @param base the base fee price in tinycents + * @param extras the list of extra fees + */ + FeeEstimate(long base, List extras) { + this.base = base; + this.extras = Collections.unmodifiableList(new ArrayList<>(extras)); + } + + /** + * Create a FeeEstimate from a protobuf. + * + * @param feeEstimate the protobuf + * @return the new FeeEstimate + */ + static FeeEstimate fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate feeEstimate) { + List extras = new ArrayList<>(feeEstimate.getExtrasCount()); + for (var extraProto : feeEstimate.getExtrasList()) { + extras.add(FeeExtra.fromProtobuf(extraProto)); + } + return new FeeEstimate(feeEstimate.getBase(), extras); + } + + /** + * Create a FeeEstimate from a byte array. + * + * @param bytes the byte array + * @return the new FeeEstimate + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + public static FeeEstimate fromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.parseFrom(bytes) + .toBuilder() + .build()); + } + + /** + * Extract the base fee price in tinycents. + * + * @return the base fee price in tinycents + */ + public long getBase() { + return base; + } + + /** + * Extract the list of extra fees. + * + * @return an unmodifiable list of extra fees + */ + public List getExtras() { + return extras; + } + + /** + * Convert the fee estimate to a protobuf. + * + * @return the protobuf + */ + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate toProtobuf() { + var builder = com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.newBuilder().setBase(base); + + for (var extra : extras) { + builder.addExtras(extra.toProtobuf()); + } + + return builder.build(); + } + + /** + * Convert the fee estimate to a byte array. + * + * @return the byte array + */ + public byte[] toBytes() { + return toProtobuf().toByteArray(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("base", base) + .add("extras", extras) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FeeEstimate that)) { + return false; + } + return base == that.base && Objects.equals(extras, that.extras); + } + + @Override + public int hashCode() { + return Objects.hash(base, extras); + } +} + diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java new file mode 100644 index 0000000000..47e55cbf73 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +/** + * Enum for the fee estimate mode. + *

+ * Determines how the fee estimate is calculated for a transaction. + */ +public enum FeeEstimateMode { + /** + * Default mode: uses latest known state. + *

+ * This mode calculates fees based on the current state of the network, + * taking into account all state-dependent factors such as current + * exchange rates, gas prices, and network congestion. + */ + STATE(0), + + /** + * Intrinsic mode: ignores state-dependent factors. + *

+ * This mode calculates fees based only on the intrinsic properties of + * the transaction itself, ignoring dynamic network conditions. This + * provides a baseline estimate that doesn't fluctuate with network state. + */ + INTRINSIC(1); + + final int code; + + FeeEstimateMode(int code) { + this.code = code; + } + + /** + * Convert a protobuf-encoded fee estimate mode value to the corresponding enum. + * + * @param code the protobuf-encoded value + * @return the corresponding FeeEstimateMode + * @throws IllegalArgumentException if the code is not recognized + */ + static FeeEstimateMode valueOf(int code) { + return switch (code) { + case 0 -> STATE; + case 1 -> INTRINSIC; + default -> throw new IllegalArgumentException( + "(BUG) unhandled FeeEstimateMode code: " + code); + }; + } + + @Override + public String toString() { + return switch (this) { + case STATE -> "STATE"; + case INTRINSIC -> "INTRINSIC"; + }; + } +} + diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java new file mode 100644 index 0000000000..382833c0b7 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import com.hedera.hashgraph.sdk.proto.mirror.NetworkServiceGrpc; +import io.grpc.CallOptions; +import io.grpc.ClientCall; +import io.grpc.Deadline; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.ClientCalls; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Query the mirror node for fee estimates for a transaction. + *

+ * This query allows users, SDKs, and tools to estimate expected fees without + * submitting transactions to the network. + */ +public class FeeEstimateQuery { + private static final Logger LOGGER = LoggerFactory.getLogger(FeeEstimateQuery.class); + + @Nullable + private FeeEstimateMode mode = null; + + @Nullable + private com.hedera.hashgraph.sdk.proto.Transaction transaction = null; + + private int maxAttempts = 10; + private Duration maxBackoff = Duration.ofSeconds(8L); + + /** + * Constructor. + */ + public FeeEstimateQuery() {} + + private static boolean shouldRetry(Throwable throwable) { + if (throwable instanceof StatusRuntimeException statusRuntimeException) { + var code = statusRuntimeException.getStatus().getCode(); + var description = statusRuntimeException.getStatus().getDescription(); + + return (code == io.grpc.Status.Code.UNAVAILABLE) + || (code == io.grpc.Status.Code.RESOURCE_EXHAUSTED) + || (code == Status.Code.INTERNAL + && description != null + && Executable.RST_STREAM.matcher(description).matches()); + } + + return false; + } + + /** + * Extract the fee estimate mode. + * + * @return the fee estimate mode that was set, or null if not set + */ + @Nullable + public FeeEstimateMode getMode() { + return mode; + } + + /** + * Set the mode for fee estimation. + *

+ * Defaults to {@link FeeEstimateMode#STATE} if not set. + * + * @param mode the fee estimate mode + * @return {@code this} + */ + public FeeEstimateQuery setMode(FeeEstimateMode mode) { + Objects.requireNonNull(mode); + this.mode = mode; + return this; + } + + /** + * Extract the transaction to estimate fees for. + * + * @return the transaction that was set, or null if not set + */ + @Nullable + public com.hedera.hashgraph.sdk.proto.Transaction getTransaction() { + return transaction; + } + + /** + * Set the transaction to estimate fees for. + *

+ * This should be the raw HAPI transaction that will be estimated. + * + * @param transaction the transaction proto + * @return {@code this} + */ + public FeeEstimateQuery setTransaction(com.hedera.hashgraph.sdk.proto.Transaction transaction) { + Objects.requireNonNull(transaction); + this.transaction = transaction; + return this; + } + + /** + * Set the transaction to estimate fees for from a Transaction object. + * + * @param transaction the transaction to estimate + * @return {@code this} + */ + public > FeeEstimateQuery setTransaction( + com.hedera.hashgraph.sdk.Transaction transaction) { + Objects.requireNonNull(transaction); + this.transaction = transaction.makeRequest(); + return this; + } + + /** + * Extract the maximum number of attempts. + * + * @return the maximum number of attempts + */ + public int getMaxAttempts() { + return maxAttempts; + } + + /** + * Set the maximum number of attempts for the query. + * + * @param maxAttempts the maximum number of attempts + * @return {@code this} + */ + public FeeEstimateQuery setMaxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; + return this; + } + + /** + * Extract the maximum backoff duration. + * + * @return the maximum backoff duration + */ + public Duration getMaxBackoff() { + return maxBackoff; + } + + /** + * Set the maximum backoff duration for retry attempts. + * + * @param maxBackoff the maximum backoff duration + * @return {@code this} + */ + public FeeEstimateQuery setMaxBackoff(Duration maxBackoff) { + Objects.requireNonNull(maxBackoff); + if (maxBackoff.toMillis() < 500L) { + throw new IllegalArgumentException("maxBackoff must be at least 500 ms"); + } + this.maxBackoff = maxBackoff; + return this; + } + + /** + * Execute the query with preset timeout. + * + * @param client the client object + * @return the fee estimate response + */ + public FeeEstimateResponse execute(Client client) { + return execute(client, client.getRequestTimeout()); + } + + /** + * Execute the query with user supplied timeout. + * + * @param client the client object + * @param timeout the user supplied timeout + * @return the fee estimate response + */ + public FeeEstimateResponse execute(Client client, Duration timeout) { + var deadline = Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS); + for (int attempt = 1; true; attempt++) { + try { + var responseProto = ClientCalls.blockingUnaryCall(buildCall(client, deadline), buildQuery()); + return FeeEstimateResponse.fromProtobuf(responseProto); + } catch (Throwable error) { + if (!shouldRetry(error) || attempt >= maxAttempts) { + LOGGER.error("Error attempting to get fee estimate", error); + throw error; + } + warnAndDelay(attempt, error); + } + } + } + + /** + * Execute the query with preset timeout asynchronously. + * + * @param client the client object + * @return the fee estimate response + */ + public CompletableFuture executeAsync(Client client) { + return executeAsync(client, client.getRequestTimeout()); + } + + /** + * Execute the query with user supplied timeout asynchronously. + * + * @param client the client object + * @param timeout the user supplied timeout + * @return the fee estimate response + */ + public CompletableFuture executeAsync(Client client, Duration timeout) { + var deadline = Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS); + CompletableFuture returnFuture = new CompletableFuture<>(); + executeAsync(client, deadline, returnFuture, 1); + return returnFuture; + } + + /** + * Execute the query asynchronously (internal implementation). + * + * @param client the client object + * @param deadline the deadline for the call + * @param returnFuture the future to complete with the result + * @param attempt the current attempt number + */ + void executeAsync( + Client client, Deadline deadline, CompletableFuture returnFuture, int attempt) { + try { + var futureStub = NetworkServiceGrpc.newFutureStub( + client.mirrorNetwork.getNextMirrorNode().getChannel()) + .withDeadline(deadline); + + var responseFuture = futureStub.getFeeEstimate(buildQuery()); + + FutureConverter.toCompletableFuture(responseFuture) + .whenComplete((response, error) -> { + if (error != null) { + if (attempt >= maxAttempts || !shouldRetry(error)) { + LOGGER.error("Error attempting to get fee estimate", error); + returnFuture.completeExceptionally(error); + return; + } + warnAndDelay(attempt, error); + executeAsync(client, deadline, returnFuture, attempt + 1); + } else { + returnFuture.complete(FeeEstimateResponse.fromProtobuf(response)); + } + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + returnFuture.completeExceptionally(e); + } + } + + /** + * Build the FeeEstimateQuery protobuf message. + * + * @return the protobuf query + */ + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery buildQuery() { + var builder = com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery.newBuilder(); + + if (mode != null) { + builder.setModeValue(mode.code); + } else { + // Default to STATE mode + builder.setModeValue(FeeEstimateMode.STATE.code); + } + + if (transaction != null) { + builder.setTransaction(transaction); + } + + return builder.build(); + } + + private ClientCall< + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery, + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse> + buildCall(Client client, Deadline deadline) { + try { + return client.mirrorNetwork + .getNextMirrorNode() + .getChannel() + .newCall(NetworkServiceGrpc.getGetFeeEstimateMethod(), CallOptions.DEFAULT.withDeadline(deadline)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void warnAndDelay(int attempt, Throwable error) { + var delay = Math.min(500 * (long) Math.pow(2, attempt), maxBackoff.toMillis()); + LOGGER.warn( + "Error fetching fee estimate during attempt #{}. Waiting {} ms before next attempt: {}", + attempt, + delay, + error.getMessage()); + + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} + diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java new file mode 100644 index 0000000000..b687881d2d --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import com.google.common.base.MoreObjects; +import com.google.protobuf.InvalidProtocolBufferException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * The response containing the estimated transaction fees. + *

+ * This response provides a breakdown of the network, node, and service fees, + * along with the total estimated cost in tinycents. + */ +public final class FeeEstimateResponse { + /** + * The mode that was used to calculate the fees. + */ + private final FeeEstimateMode mode; + + /** + * The network fee component which covers the cost of gossip, consensus, + * signature verifications, fee payment, and storage. + */ + @Nullable + private final NetworkFee network; + + /** + * The node fee component which is to be paid to the node that submitted the + * transaction to the network. This fee exists to compensate the node for the + * work it performed to pre-check the transaction before submitting it, and + * incentivizes the node to accept new transactions from users. + */ + @Nullable + private final FeeEstimate node; + + /** + * An array of strings for any caveats. + *

+ * For example: ["Fallback to worst-case due to missing state"] + */ + private final List notes; + + /** + * The service fee component which covers execution costs, state saved in the + * Merkle tree, and additional costs to the blockchain storage. + */ + @Nullable + private final FeeEstimate service; + + /** + * The sum of the network, node, and service subtotals in tinycents. + */ + private final long total; + + /** + * Constructor. + * + * @param mode the fee estimate mode used + * @param network the network fee component + * @param node the node fee estimate + * @param notes the list of notes/caveats + * @param service the service fee estimate + * @param total the total fee in tinycents + */ + FeeEstimateResponse( + FeeEstimateMode mode, + @Nullable NetworkFee network, + @Nullable FeeEstimate node, + List notes, + @Nullable FeeEstimate service, + long total) { + this.mode = mode; + this.network = network; + this.node = node; + this.notes = Collections.unmodifiableList(new ArrayList<>(notes)); + this.service = service; + this.total = total; + } + + /** + * Create a FeeEstimateResponse from a protobuf. + * + * @param response the protobuf + * @return the new FeeEstimateResponse + */ + static FeeEstimateResponse fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse response) { + var mode = FeeEstimateMode.valueOf(response.getModeValue()); + var network = response.hasNetwork() ? NetworkFee.fromProtobuf(response.getNetwork()) : null; + var node = response.hasNode() ? FeeEstimate.fromProtobuf(response.getNode()) : null; + var notes = new ArrayList<>(response.getNotesList()); + var service = response.hasService() ? FeeEstimate.fromProtobuf(response.getService()) : null; + var total = response.getTotal(); + + return new FeeEstimateResponse(mode, network, node, notes, service, total); + } + + /** + * Create a FeeEstimateResponse from a byte array. + * + * @param bytes the byte array + * @return the new FeeEstimateResponse + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + public static FeeEstimateResponse fromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse.parseFrom(bytes) + .toBuilder() + .build()); + } + + /** + * Extract the fee estimate mode used. + * + * @return the fee estimate mode + */ + public FeeEstimateMode getMode() { + return mode; + } + + /** + * Extract the network fee component. + * + * @return the network fee component, or null if not set + */ + @Nullable + public NetworkFee getNetwork() { + return network; + } + + /** + * Extract the node fee estimate. + * + * @return the node fee estimate, or null if not set + */ + @Nullable + public FeeEstimate getNode() { + return node; + } + + /** + * Extract the list of notes/caveats. + * + * @return an unmodifiable list of notes + */ + public List getNotes() { + return notes; + } + + /** + * Extract the service fee estimate. + * + * @return the service fee estimate, or null if not set + */ + @Nullable + public FeeEstimate getService() { + return service; + } + + /** + * Extract the total fee in tinycents. + * + * @return the total fee in tinycents + */ + public long getTotal() { + return total; + } + + /** + * Convert the fee estimate response to a protobuf. + * + * @return the protobuf + */ + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse toProtobuf() { + var builder = com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse.newBuilder() + .setModeValue(mode.code) + .setTotal(total) + .addAllNotes(notes); + + if (network != null) { + builder.setNetwork(network.toProtobuf()); + } + if (node != null) { + builder.setNode(node.toProtobuf()); + } + if (service != null) { + builder.setService(service.toProtobuf()); + } + + return builder.build(); + } + + /** + * Convert the fee estimate response to a byte array. + * + * @return the byte array + */ + public byte[] toBytes() { + return toProtobuf().toByteArray(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("mode", mode) + .add("network", network) + .add("node", node) + .add("notes", notes) + .add("service", service) + .add("total", total) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FeeEstimateResponse that)) { + return false; + } + return total == that.total + && mode == that.mode + && Objects.equals(network, that.network) + && Objects.equals(node, that.node) + && Objects.equals(notes, that.notes) + && Objects.equals(service, that.service); + } + + @Override + public int hashCode() { + return Objects.hash(mode, network, node, notes, service, total); + } +} + diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java new file mode 100644 index 0000000000..78724713b6 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import com.google.common.base.MoreObjects; +import com.google.protobuf.InvalidProtocolBufferException; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * The extra fee charged for the transaction. + *

+ * Represents additional fees that apply for specific fee components, + * such as charges beyond included amounts. + */ +public final class FeeExtra { + /** + * The charged count of items as calculated by max(0, count - included). + */ + private final int charged; + + /** + * The actual count of items received. + */ + private final int count; + + /** + * The fee price per unit in tinycents. + */ + private final long feePerUnit; + + /** + * The count of this "extra" that is included for free. + */ + private final int included; + + /** + * The unique name of this extra fee as defined in the fee schedule. + */ + @Nullable + private final String name; + + /** + * The subtotal in tinycents for this extra fee. + *

+ * Calculated by multiplying the charged count by the fee_per_unit. + */ + private final long subtotal; + + /** + * Constructor. + * + * @param charged the charged count of items + * @param count the actual count of items + * @param feePerUnit the fee price per unit in tinycents + * @param included the count included for free + * @param name the unique name of this extra fee + * @param subtotal the subtotal in tinycents + */ + FeeExtra(int charged, int count, long feePerUnit, int included, @Nullable String name, long subtotal) { + this.charged = charged; + this.count = count; + this.feePerUnit = feePerUnit; + this.included = included; + this.name = name; + this.subtotal = subtotal; + } + + /** + * Create a FeeExtra from a protobuf. + * + * @param feeExtra the protobuf + * @return the new FeeExtra + */ + static FeeExtra fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeExtra feeExtra) { + return new FeeExtra( + feeExtra.getCharged(), + feeExtra.getCount(), + feeExtra.getFeePerUnit(), + feeExtra.getIncluded(), + feeExtra.getName().isEmpty() ? null : feeExtra.getName(), + feeExtra.getSubtotal()); + } + + /** + * Create a FeeExtra from a byte array. + * + * @param bytes the byte array + * @return the new FeeExtra + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + public static FeeExtra fromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeExtra.parseFrom(bytes) + .toBuilder() + .build()); + } + + /** + * Extract the charged count of items. + * + * @return the charged count of items + */ + public int getCharged() { + return charged; + } + + /** + * Extract the actual count of items. + * + * @return the actual count of items + */ + public int getCount() { + return count; + } + + /** + * Extract the fee price per unit in tinycents. + * + * @return the fee price per unit in tinycents + */ + public long getFeePerUnit() { + return feePerUnit; + } + + /** + * Extract the count included for free. + * + * @return the count included for free + */ + public int getIncluded() { + return included; + } + + /** + * Extract the unique name of this extra fee. + * + * @return the unique name of this extra fee, or null if not set + */ + @Nullable + public String getName() { + return name; + } + + /** + * Extract the subtotal in tinycents. + * + * @return the subtotal in tinycents + */ + public long getSubtotal() { + return subtotal; + } + + /** + * Convert the fee extra to a protobuf. + * + * @return the protobuf + */ + com.hedera.hashgraph.sdk.proto.mirror.FeeExtra toProtobuf() { + var builder = com.hedera.hashgraph.sdk.proto.mirror.FeeExtra.newBuilder() + .setCharged(charged) + .setCount(count) + .setFeePerUnit(feePerUnit) + .setIncluded(included) + .setSubtotal(subtotal); + + if (name != null) { + builder.setName(name); + } + + return builder.build(); + } + + /** + * Convert the fee extra to a byte array. + * + * @return the byte array + */ + public byte[] toBytes() { + return toProtobuf().toByteArray(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("charged", charged) + .add("count", count) + .add("feePerUnit", feePerUnit) + .add("included", included) + .add("name", name) + .add("subtotal", subtotal) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FeeExtra that)) { + return false; + } + return charged == that.charged + && count == that.count + && feePerUnit == that.feePerUnit + && included == that.included + && subtotal == that.subtotal + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(charged, count, feePerUnit, included, name, subtotal); + } +} + diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java index 9ebc2d304f..f6897f64e1 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java @@ -115,7 +115,7 @@ static FreezeType valueOf(com.hedera.hashgraph.sdk.proto.FreezeType code) { case UNRECOGNIZED -> // NOTE: Protobuf deserialization will not give us the code on the wire throw new IllegalArgumentException( - "network returned unrecognized response code; your SDK may be out of date"); + "network returned unrecognized response code; your SDK may be out of date"); }; } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java new file mode 100644 index 0000000000..7bb626b686 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import com.google.common.base.MoreObjects; +import com.google.protobuf.InvalidProtocolBufferException; + +/** + * The network fee component which covers the cost of gossip, consensus, + * signature verifications, fee payment, and storage. + */ +public final class NetworkFee { + /** + * Multiplied by the node fee to determine the total network fee. + */ + private final int multiplier; + + /** + * The subtotal in tinycents for the network fee component which is calculated by + * multiplying the node subtotal by the network multiplier. + */ + private final long subtotal; + + /** + * Constructor. + * + * @param multiplier the network fee multiplier + * @param subtotal the network fee subtotal in tinycents + */ + NetworkFee(int multiplier, long subtotal) { + this.multiplier = multiplier; + this.subtotal = subtotal; + } + + /** + * Create a NetworkFee from a protobuf. + * + * @param networkFee the protobuf + * @return the new NetworkFee + */ + static NetworkFee fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.NetworkFee networkFee) { + return new NetworkFee(networkFee.getMultiplier(), networkFee.getSubtotal()); + } + + /** + * Create a NetworkFee from a byte array. + * + * @param bytes the byte array + * @return the new NetworkFee + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + public static NetworkFee fromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.NetworkFee.parseFrom(bytes) + .toBuilder() + .build()); + } + + /** + * Extract the network fee multiplier. + * + * @return the network fee multiplier + */ + public int getMultiplier() { + return multiplier; + } + + /** + * Extract the network fee subtotal in tinycents. + * + * @return the network fee subtotal in tinycents + */ + public long getSubtotal() { + return subtotal; + } + + /** + * Convert the network fee to a protobuf. + * + * @return the protobuf + */ + com.hedera.hashgraph.sdk.proto.mirror.NetworkFee toProtobuf() { + return com.hedera.hashgraph.sdk.proto.mirror.NetworkFee.newBuilder() + .setMultiplier(multiplier) + .setSubtotal(subtotal) + .build(); + } + + /** + * Convert the network fee to a byte array. + * + * @return the byte array + */ + public byte[] toBytes() { + return toProtobuf().toByteArray(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("multiplier", multiplier) + .add("subtotal", subtotal) + .toString(); + } +} + diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java index 9f84417ac7..107efd95f6 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java @@ -2,6 +2,7 @@ package com.hedera.hashgraph.sdk; import com.hedera.hashgraph.sdk.proto.ResponseCodeEnum; + import java.util.Objects; /** @@ -1053,8 +1054,7 @@ public enum Status { /** * Only tokens of type FUNGIBLE_COMMON can have fractional fees */ - CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON( - ResponseCodeEnum.CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), + CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON(ResponseCodeEnum.CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), /** * The provided custom fee schedule key was invalid @@ -1094,8 +1094,7 @@ public enum Status { /** * An AccountAmount token transfers list referenced a token type other than FUNGIBLE_COMMON */ - ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON( - ResponseCodeEnum.ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), + ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON(ResponseCodeEnum.ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), /** * All the NFTs allowed in the current price regime have already been minted @@ -1120,8 +1119,7 @@ public enum Status { /** * The sender account in the token transfer transaction could not afford a custom fee */ - INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE( - ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE), + INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE(ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE), /** * Currently no more than 4,294,967,295 NFTs may be minted for a given unique token type @@ -1131,8 +1129,7 @@ public enum Status { /** * Only tokens of type NON_FUNGIBLE_UNIQUE can have royalty fees */ - CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE( - ResponseCodeEnum.CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE), + CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE(ResponseCodeEnum.CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE), /** * The account has reached the limit on the automatic associations count. @@ -1142,15 +1139,13 @@ public enum Status { /** * Already existing automatic associations are more than the new maximum automatic associations. */ - EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT( - ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT), + EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT(ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT), /** * Cannot set the number of automatic associations for an account more than the maximum allowed * token associations tokens.maxPerAccount. */ - REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT( - ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT), + REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT(ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT), /** * Token is paused. This Token cannot be a part of any kind of Transaction until unpaused. @@ -1380,8 +1375,7 @@ public enum Status { /** * The scheduled transaction could not be created because it's expiration_time was less than or equal to the consensus time. */ - SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME( - ResponseCodeEnum.SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME), + SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME(ResponseCodeEnum.SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME), /** * The scheduled transaction could not be created because it would cause throttles to be violated on the specified expiration_time. @@ -1908,13 +1902,7 @@ public enum Status { * The GRPC proxy endpoint is set in the NodeCreate or NodeUpdate transaction, * which the network does not support. */ - GRPC_WEB_PROXY_NOT_SUPPORTED(ResponseCodeEnum.GRPC_WEB_PROXY_NOT_SUPPORTED), - - /** - * An NFT transfers list referenced a token type other than NON_FUNGIBLE_UNIQUE. - */ - NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE( - ResponseCodeEnum.NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE); + GRPC_WEB_PROXY_NOT_SUPPORTED(ResponseCodeEnum.GRPC_WEB_PROXY_NOT_SUPPORTED); final ResponseCodeEnum code; @@ -2128,8 +2116,7 @@ static Status valueOf(ResponseCodeEnum code) { case FRACTIONAL_FEE_MAX_AMOUNT_LESS_THAN_MIN_AMOUNT -> FRACTIONAL_FEE_MAX_AMOUNT_LESS_THAN_MIN_AMOUNT; case CUSTOM_SCHEDULE_ALREADY_HAS_NO_FEES -> CUSTOM_SCHEDULE_ALREADY_HAS_NO_FEES; case CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON -> CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON; - case CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> - CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; + case CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; case INVALID_CUSTOM_FEE_SCHEDULE_KEY -> INVALID_CUSTOM_FEE_SCHEDULE_KEY; case INVALID_TOKEN_MINT_METADATA -> INVALID_TOKEN_MINT_METADATA; case INVALID_TOKEN_BURN_METADATA -> INVALID_TOKEN_BURN_METADATA; @@ -2137,22 +2124,17 @@ static Status valueOf(ResponseCodeEnum code) { case ACCOUNT_STILL_OWNS_NFTS -> ACCOUNT_STILL_OWNS_NFTS; case TREASURY_MUST_OWN_BURNED_NFT -> TREASURY_MUST_OWN_BURNED_NFT; case ACCOUNT_DOES_NOT_OWN_WIPED_NFT -> ACCOUNT_DOES_NOT_OWN_WIPED_NFT; - case ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> - ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; + case ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; case MAX_NFTS_IN_PRICE_REGIME_HAVE_BEEN_MINTED -> MAX_NFTS_IN_PRICE_REGIME_HAVE_BEEN_MINTED; case PAYER_ACCOUNT_DELETED -> PAYER_ACCOUNT_DELETED; case CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH -> CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH; case CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS -> CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS; - case INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE -> - INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE; + case INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE -> INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE; case SERIAL_NUMBER_LIMIT_REACHED -> SERIAL_NUMBER_LIMIT_REACHED; - case CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE -> - CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE; + case CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE -> CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE; case NO_REMAINING_AUTOMATIC_ASSOCIATIONS -> NO_REMAINING_AUTOMATIC_ASSOCIATIONS; - case EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT -> - EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT; - case REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT -> - REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT; + case EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT -> EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT; + case REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT -> REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT; case TOKEN_IS_PAUSED -> TOKEN_IS_PAUSED; case TOKEN_HAS_NO_PAUSE_KEY -> TOKEN_HAS_NO_PAUSE_KEY; case INVALID_PAUSE_KEY -> INVALID_PAUSE_KEY; @@ -2195,8 +2177,7 @@ static Status valueOf(ResponseCodeEnum code) { case DELEGATING_SPENDER_CANNOT_GRANT_APPROVE_FOR_ALL -> DELEGATING_SPENDER_CANNOT_GRANT_APPROVE_FOR_ALL; case DELEGATING_SPENDER_DOES_NOT_HAVE_APPROVE_FOR_ALL -> DELEGATING_SPENDER_DOES_NOT_HAVE_APPROVE_FOR_ALL; case SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE -> SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE; - case SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME -> - SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME; + case SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME -> SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME; case SCHEDULE_FUTURE_THROTTLE_EXCEEDED -> SCHEDULE_FUTURE_THROTTLE_EXCEEDED; case SCHEDULE_FUTURE_GAS_LIMIT_EXCEEDED -> SCHEDULE_FUTURE_GAS_LIMIT_EXCEEDED; case INVALID_ETHEREUM_TRANSACTION -> INVALID_ETHEREUM_TRANSACTION; @@ -2289,12 +2270,13 @@ static Status valueOf(ResponseCodeEnum code) { case THROTTLE_GROUP_LCM_OVERFLOW -> THROTTLE_GROUP_LCM_OVERFLOW; case AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN -> AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN; case GRPC_WEB_PROXY_NOT_SUPPORTED -> GRPC_WEB_PROXY_NOT_SUPPORTED; - case NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE -> - NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE; case UNRECOGNIZED -> // NOTE: Protobuf deserialization will not give us the code on the wire throw new IllegalArgumentException( - "network returned unrecognized response code; your SDK may be out of date"); + "network returned unrecognized response code; your SDK may be out of date"); + default -> + throw new IllegalArgumentException( + "unhandled response code: " + code + "; your SDK may be out of date"); }; } diff --git a/sdk/src/main/proto/mirror/fee.proto b/sdk/src/main/proto/mirror/fee.proto new file mode 100644 index 0000000000..d19be014de --- /dev/null +++ b/sdk/src/main/proto/mirror/fee.proto @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package proto.mirror; +option java_package = "com.hedera.hashgraph.sdk.proto.mirror"; +option java_multiple_files = true; + +import "transaction.proto"; + +/** + * Determines whether the fee estimation depends on network state (e.g., whether an account exists or requires creation + * during a transfer). + */ +enum EstimateMode { + /* + * Estimate based on intrinsic properties plus the latest known state (e.g., check if accounts + * exist, load token associations). This is the default if no mode is specified. + */ + STATE = 0; + + /* + * Estimate based solely on the transaction's inherent properties (e.g., size, signatures, keys). Ignores + * state-dependent factors. + */ + INTRINSIC = 1; +} + +/** + * Request object for users, SDKs, and tools to query expected fees without + * submitting transactions to the network. + */ +message FeeEstimateQuery { + /** + * The mode of fee estimation. Defaults to `STATE` if omitted. + */ + EstimateMode mode = 1; + + /** + * The raw HAPI transaction that should be estimated. + */ + .proto.Transaction transaction = 2; +} + +/** + * The response containing the estimated transaction fees. + */ +message FeeEstimateResponse { + /** + * The mode that was used to calculate the fees. + */ + EstimateMode mode = 1; + + /** + * The network fee component which covers the cost of gossip, consensus, + * signature verifications, fee payment, and storage. + */ + NetworkFee network = 2; + + /** + * The node fee component which is to be paid to the node that submitted the + * transaction to the network. This fee exists to compensate the node for the + * work it performed to pre-check the transaction before submitting it, and + * incentivizes the node to accept new transactions from users. + */ + FeeEstimate node = 3; + + /** + * An array of strings for any caveats (e.g., ["Fallback to worst-case due to missing state"]). + */ + repeated string notes = 4; + + /** + * The service fee component which covers execution costs, state saved in the + * Merkle tree, and additional costs to the blockchain storage. + */ + FeeEstimate service = 5; + + /** + * The sum of the network, node, and service subtotals in tinycents. + */ + uint64 total = 6; +} + +/** + * The fee estimate for the network component. Includes the base fee and any + * extras associated with it. + */ +message FeeEstimate { + /** + * The base fee price, in tinycents. + */ + uint64 base = 1; + + /** + * The extra fees that apply for this fee component. + */ + repeated FeeExtra extras = 2; +} + +/** + * The extra fee charged for the transaction. + */ +message FeeExtra { + /** + * The charged count of items as calculated by `max(0, count - included)`. + */ + uint32 charged = 1; + + /** + * The actual count of items received. + */ + uint32 count = 2; + + /** + * The fee price per unit in tinycents. + */ + uint64 fee_per_unit = 3; + + /** + * The count of this "extra" that is included for free. + */ + uint32 included = 4; + + /** + * The unique name of this extra fee as defined in the fee schedule. + */ + string name = 5; + + /** + * The subtotal in tinycents for this extra fee. Calculated by multiplying the + * charged count by the fee_per_unit. + */ + uint64 subtotal = 6; +} + +/** + * The network fee component which covers the cost of gossip, consensus, + * signature verifications, fee payment, and storage. + */ +message NetworkFee { + /** + * Multiplied by the node fee to determine the total network fee. + */ + uint32 multiplier = 1; + + /** + * The subtotal in tinycents for the network fee component which is calculated by + * multiplying the node subtotal by the network multiplier. + */ + uint64 subtotal = 2; +} diff --git a/sdk/src/main/proto/mirror/mirror_network_service.proto b/sdk/src/main/proto/mirror/mirror_network_service.proto index 460da29f30..8d7bee8795 100644 --- a/sdk/src/main/proto/mirror/mirror_network_service.proto +++ b/sdk/src/main/proto/mirror/mirror_network_service.proto @@ -20,38 +20,35 @@ syntax = "proto3"; -package com.hedera.mirror.api.proto; +package proto.mirror; option java_multiple_files = true; // Required for the reactor-grpc generator to work correctly option java_package = "com.hedera.hashgraph.sdk.proto.mirror"; import "basic_types.proto"; -import "timestamp.proto"; +import "mirror/fee.proto"; /** * Request object to query an address book for its list of nodes */ message AddressBookQuery { - /** - * The ID of the address book file on the network. Can be either 0.0.101 or 0.0.102. - */ - .proto.FileID file_id = 1; - - /** - * The maximum number of node addresses to receive before stopping. If not set or set to zero it will return all node addresses in the database. - */ - int32 limit = 2; + .proto.FileID file_id = 1; // The ID of the address book file on the network. Can be either 0.0.101 or 0.0.102. + int32 limit = 2; // The maximum number of node addresses to receive before stopping. If not set or set to zero it will return all node addresses in the database. } /** * Provides cross network APIs like address book queries */ service NetworkService { - /* + /** + * Query to estimate the fees when submitting a transaction to the network. + */ + rpc getFeeEstimate(FeeEstimateQuery) returns (FeeEstimateResponse); + + /** * Query for an address book and return its nodes. The nodes are returned in ascending order by node ID. The * response is not guaranteed to be a byte-for-byte equivalent to the NodeAddress in the Hedera file on * the network since it is reconstructed from a normalized database table. */ rpc getNodes (AddressBookQuery) returns (stream .proto.NodeAddress); } - diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java new file mode 100644 index 0000000000..29332d9037 --- /dev/null +++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk.test.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import com.hedera.hashgraph.sdk.AccountCreateTransaction; +import com.hedera.hashgraph.sdk.FeeEstimateMode; +import com.hedera.hashgraph.sdk.FeeEstimateQuery; +import com.hedera.hashgraph.sdk.PrivateKey; +import com.hedera.hashgraph.sdk.TokenCreateTransaction; +import com.hedera.hashgraph.sdk.TokenSupplyType; +import com.hedera.hashgraph.sdk.TokenType; +import com.hedera.hashgraph.sdk.TransferTransaction; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FeeEstimateQueryIntegrationTest { + + @Test + @DisplayName("Can estimate fees for TokenCreateTransaction") + void canEstimateFeesForTokenCreateTransaction() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + // Skip if not running against a mirror node that supports fee estimation + if (!testEnv.isLocalNode) { + return; + } + + // Create a TokenCreateTransaction + var transaction = new TokenCreateTransaction() + .setTokenName("Test Token") + .setTokenSymbol("TEST") + .setDecimals(3) + .setInitialSupply(1000000) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(testEnv.operatorKey) + .setSupplyKey(testEnv.operatorKey) + .setTokenType(TokenType.FUNGIBLE_COMMON) + .setSupplyType(TokenSupplyType.INFINITE) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + // Wait for mirror node to be ready + Thread.sleep(2000); + + // Request fee estimate with STATE mode (default) + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.STATE) + .execute(testEnv.client); + + // Verify response structure + assertThat(response).isNotNull(); + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); + assertThat(response.getTotal()).isGreaterThan(0); + + // Verify network fee component + assertThat(response.getNetwork()).isNotNull(); + assertThat(response.getNetwork().getMultiplier()).isGreaterThan(0); + assertThat(response.getNetwork().getSubtotal()).isGreaterThan(0); + + // Verify node fee estimate + assertThat(response.getNode()).isNotNull(); + assertThat(response.getNode().getBase()).isGreaterThan(0); + // Node fee may or may not have extras + assertThat(response.getNode().getExtras()).isNotNull(); + + // Verify service fee estimate + assertThat(response.getService()).isNotNull(); + assertThat(response.getService().getBase()).isGreaterThan(0); + // Service fee for token creation should have extras for token operations + assertThat(response.getService().getExtras()).isNotNull(); + + // Verify notes (may be empty) + assertThat(response.getNotes()).isNotNull(); + + // Verify that network fee = node subtotal * multiplier (approximately) + var nodeSubtotal = response.getNode().getBase(); + var expectedNetworkFee = nodeSubtotal * response.getNetwork().getMultiplier(); + assertThat(response.getNetwork().getSubtotal()) + .isCloseTo(expectedNetworkFee, org.assertj.core.data.Percentage.withPercentage(1.0)); + } + } + + @Test + @DisplayName("Can estimate fees with INTRINSIC mode") + void canEstimateFeesWithIntrinsicMode() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + if (!testEnv.isLocalNode) { + return; + } + + var transaction = new TokenCreateTransaction() + .setTokenName("Test Token") + .setTokenSymbol("TEST") + .setDecimals(3) + .setInitialSupply(1000000) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(testEnv.operatorKey) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + Thread.sleep(2000); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.INTRINSIC) + .execute(testEnv.client); + + assertThat(response).isNotNull(); + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC); + assertThat(response.getTotal()).isGreaterThan(0); + assertThat(response.getNetwork()).isNotNull(); + assertThat(response.getNode()).isNotNull(); + assertThat(response.getService()).isNotNull(); + } + } + + @Test + @DisplayName("Can estimate fees for AccountCreateTransaction") + void canEstimateFeesForAccountCreateTransaction() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + if (!testEnv.isLocalNode) { + return; + } + + var newKey = PrivateKey.generateED25519(); + var transaction = new AccountCreateTransaction() + .setKey(newKey) + .setInitialBalance(com.hedera.hashgraph.sdk.Hbar.from(10)) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + Thread.sleep(2000); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .execute(testEnv.client); + + assertThat(response).isNotNull(); + assertThat(response.getTotal()).isGreaterThan(0); + assertThat(response.getNetwork()).isNotNull(); + assertThat(response.getNode()).isNotNull(); + assertThat(response.getService()).isNotNull(); + + // Account creation should have a base fee + assertThat(response.getService().getBase()).isGreaterThan(0); + } + } + + @Test + @DisplayName("Can estimate fees for TransferTransaction") + void canEstimateFeesForTransferTransaction() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + if (!testEnv.isLocalNode) { + return; + } + + var transaction = new TransferTransaction() + .addHbarTransfer(testEnv.operatorId, com.hedera.hashgraph.sdk.Hbar.from(-1)) + .addHbarTransfer(new com.hedera.hashgraph.sdk.AccountId(0, 0, 3), com.hedera.hashgraph.sdk.Hbar.from(1)) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + Thread.sleep(2000); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .execute(testEnv.client); + + assertThat(response).isNotNull(); + assertThat(response.getTotal()).isGreaterThan(0); + + // Transfer transactions should have lower fees than complex transactions + assertThat(response.getService().getBase()).isGreaterThan(0); + } + } + + @Test + @DisplayName("State mode and intrinsic mode return different estimates") + void stateAndIntrinsicModesReturnDifferentEstimates() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + if (!testEnv.isLocalNode) { + return; + } + + var transaction = new TokenCreateTransaction() + .setTokenName("Test Token") + .setTokenSymbol("TEST") + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(testEnv.operatorKey) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + Thread.sleep(2000); + + var stateResponse = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.STATE) + .execute(testEnv.client); + + var intrinsicResponse = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.INTRINSIC) + .execute(testEnv.client); + + assertThat(stateResponse).isNotNull(); + assertThat(intrinsicResponse).isNotNull(); + + // Both should have valid totals + assertThat(stateResponse.getTotal()).isGreaterThan(0); + assertThat(intrinsicResponse.getTotal()).isGreaterThan(0); + + // The estimates may differ based on state-dependent factors + // We just verify both modes work and return reasonable values + assertThat(stateResponse.getMode()).isEqualTo(FeeEstimateMode.STATE); + assertThat(intrinsicResponse.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC); + } + } + + @Test + @DisplayName("Can execute query asynchronously") + void canExecuteQueryAsynchronously() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + if (!testEnv.isLocalNode) { + return; + } + + var transaction = new TransferTransaction() + .addHbarTransfer(testEnv.operatorId, com.hedera.hashgraph.sdk.Hbar.from(-1)) + .addHbarTransfer(new com.hedera.hashgraph.sdk.AccountId(0, 0, 3), com.hedera.hashgraph.sdk.Hbar.from(1)) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + Thread.sleep(2000); + + var responseFuture = new FeeEstimateQuery() + .setTransaction(transaction) + .executeAsync(testEnv.client); + + // Verify the async call completes successfully + assertThatNoException().isThrownBy(() -> { + var response = responseFuture.get(); + assertThat(response).isNotNull(); + assertThat(response.getTotal()).isGreaterThan(0); + }); + } + } + + @Test + @DisplayName("Response includes appropriate fields for extras") + void responseIncludesAppropriateFieldsForExtras() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + if (!testEnv.isLocalNode) { + return; + } + + var transaction = new TokenCreateTransaction() + .setTokenName("Test Token") + .setTokenSymbol("TEST") + .setDecimals(3) + .setInitialSupply(1000000) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(testEnv.operatorKey) + .setSupplyKey(testEnv.operatorKey) + .setWipeKey(testEnv.operatorKey) + .setFreezeKey(testEnv.operatorKey) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + Thread.sleep(2000); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .execute(testEnv.client); + + assertThat(response).isNotNull(); + + // Check if service fee has extras + if (response.getService() != null && !response.getService().getExtras().isEmpty()) { + for (var extra : response.getService().getExtras()) { + // Verify extra fields are populated + assertThat(extra.getCount()).isGreaterThanOrEqualTo(0); + assertThat(extra.getCharged()).isGreaterThanOrEqualTo(0); + assertThat(extra.getIncluded()).isGreaterThanOrEqualTo(0); + assertThat(extra.getFeePerUnit()).isGreaterThanOrEqualTo(0); + assertThat(extra.getSubtotal()).isGreaterThanOrEqualTo(0); + // Name may or may not be set + // Charged = max(0, count - included) + assertThat(extra.getCharged()).isEqualTo(Math.max(0, extra.getCount() - extra.getIncluded())); + // Subtotal = charged * feePerUnit + assertThat(extra.getSubtotal()).isEqualTo(extra.getCharged() * extra.getFeePerUnit()); + } + } + } + } + + @Test + @DisplayName("Can handle transaction without freezing") + void canHandleTransactionWithoutFreezing() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + if (!testEnv.isLocalNode) { + return; + } + + var transaction = new TransferTransaction() + .addHbarTransfer(testEnv.operatorId, com.hedera.hashgraph.sdk.Hbar.from(-1)) + .addHbarTransfer(new com.hedera.hashgraph.sdk.AccountId(0, 0, 3), com.hedera.hashgraph.sdk.Hbar.from(1)); + + // Freeze and sign within the query + transaction.freezeWith(testEnv.client).signWithOperator(testEnv.client); + + Thread.sleep(2000); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .execute(testEnv.client); + + assertThat(response).isNotNull(); + assertThat(response.getTotal()).isGreaterThan(0); + } + } +} + From 5d97cbebb93660b5c21a08493afca9a723303dd4 Mon Sep 17 00:00:00 2001 From: emiliyank Date: Thu, 30 Oct 2025 15:31:00 +0200 Subject: [PATCH 2/4] hip 1261 - fix imports in proto files Signed-off-by: emiliyank --- .../sdk/examples/FeeEstimateQueryExample.java | 203 ++++++++++++ .../com/hedera/hashgraph/sdk/FeeDataType.java | 1 - .../com/hedera/hashgraph/sdk/FeeEstimate.java | 7 +- .../hedera/hashgraph/sdk/FeeEstimateMode.java | 4 +- .../hashgraph/sdk/FeeEstimateQuery.java | 49 ++- .../hashgraph/sdk/FeeEstimateResponse.java | 4 +- .../com/hedera/hashgraph/sdk/FeeExtra.java | 4 +- .../com/hedera/hashgraph/sdk/FreezeType.java | 2 +- .../com/hedera/hashgraph/sdk/NetworkFee.java | 4 +- .../java/com/hedera/hashgraph/sdk/Status.java | 47 ++- sdk/src/main/proto/mirror/fee.proto | 1 - .../proto/mirror/mirror_network_service.proto | 22 +- .../FeeEstimateQueryIntegrationTest.java | 311 ++---------------- 13 files changed, 308 insertions(+), 351 deletions(-) create mode 100644 examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java diff --git a/examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java new file mode 100644 index 0000000000..901b7dd635 --- /dev/null +++ b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk.examples; + +import com.hedera.hashgraph.sdk.*; +import com.hedera.hashgraph.sdk.logger.LogLevel; +import com.hedera.hashgraph.sdk.logger.Logger; +import io.github.cdimascio.dotenv.Dotenv; +import java.util.Objects; + +/** + * How to estimate transaction fees using the mirror node's fee estimation service. + *

+ * This example demonstrates: + * 1. Creating and freezing a transfer transaction + * 2. Estimating fees with STATE mode (considers current network state) + * 3. Estimating fees with INTRINSIC mode (only transaction properties) + * 4. Displaying detailed fee breakdowns + */ +class FeeEstimateQueryExample { + + /* + * See .env.sample in the examples folder root for how to specify values below + * or set environment variables with the same names. + */ + + /** + * Operator's account ID. + * Used to sign and pay for operations on Hedera. + */ + private static final AccountId OPERATOR_ID = + AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + + /** + * Operator's private key. + */ + private static final PrivateKey OPERATOR_KEY = + PrivateKey.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY"))); + + /** + * HEDERA_NETWORK defaults to testnet if not specified in dotenv file. + * Network can be: localhost, testnet, previewnet or mainnet. + */ + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + /** + * SDK_LOG_LEVEL defaults to SILENT if not specified in dotenv file. + * Log levels can be: TRACE, DEBUG, INFO, WARN, ERROR, SILENT. + *

+ * Important pre-requisite: set simple logger log level to same level as the SDK_LOG_LEVEL, + * for example via VM options: -Dorg.slf4j.simpleLogger.log.org.hiero=trace + */ + private static final String SDK_LOG_LEVEL = Dotenv.load().get("SDK_LOG_LEVEL", "SILENT"); + + public static void main(String[] args) throws Exception { + System.out.println("Fee Estimate Example Start!"); + + /* + * Step 0: + * Create and configure the SDK Client. + */ + Client client = ClientHelper.forName(HEDERA_NETWORK); + // All generated transactions will be paid by this account and signed by this key. + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + // Attach logger to the SDK Client. + client.setLogger(new Logger(LogLevel.valueOf(SDK_LOG_LEVEL))); + + // Create a recipient account for the example + AccountId recipientId = AccountId.fromString("0.0.3"); + + /* + * Step 1: + * Create and freeze a transfer transaction. + * The transaction must be frozen before it can be estimated. + */ + System.out.println("\n=== Creating Transfer Transaction ==="); + Hbar transferAmount = Hbar.from(1); + + TransferTransaction tx = new TransferTransaction() + .addHbarTransfer(OPERATOR_ID, transferAmount.negated()) + .addHbarTransfer(recipientId, transferAmount) + .setTransactionMemo("Fee estimate example") + .freezeWith(client); + + // Sign the transaction (required for accurate fee estimation) + tx.signWithOperator(client); + + System.out.println("Transaction created: Transfer " + transferAmount + " from " + + OPERATOR_ID + " to " + recipientId); + + /* + * Step 2: + * Estimate fees with STATE mode (default). + * STATE mode considers the current network state (e.g., whether accounts exist, + * token associations, etc.) for more accurate fee estimation. + */ + System.out.println("\n=== Estimating Fees with STATE Mode ==="); + + FeeEstimateResponse stateEstimate = new FeeEstimateQuery() + .setMode(FeeEstimateMode.STATE) + .setTransaction(tx) + .execute(client); + + System.out.println("Mode: " + stateEstimate.getMode()); + + // Network fee breakdown + System.out.println("\nNetwork Fee:"); + System.out.println(" Multiplier: " + stateEstimate.getNetwork().getMultiplier()); + System.out.println(" Subtotal: " + stateEstimate.getNetwork().getSubtotal() + " tinycents"); + + // Node fee breakdown + System.out.println("\nNode Fee:"); + System.out.println(" Base: " + stateEstimate.getNode().getBase() + " tinycents"); + long nodeTotal = stateEstimate.getNode().getBase(); + for (FeeExtra extra : stateEstimate.getNode().getExtras()) { + System.out.println(" Extra - " + extra.getName() + ": " + extra.getSubtotal() + " tinycents"); + nodeTotal += extra.getSubtotal(); + } + System.out.println(" Node Total: " + nodeTotal + " tinycents"); + + // Service fee breakdown + System.out.println("\nService Fee:"); + System.out.println(" Base: " + stateEstimate.getService().getBase() + " tinycents"); + long serviceTotal = stateEstimate.getService().getBase(); + for (FeeExtra extra : stateEstimate.getService().getExtras()) { + System.out.println(" Extra - " + extra.getName() + ": " + extra.getSubtotal() + " tinycents"); + serviceTotal += extra.getSubtotal(); + } + System.out.println(" Service Total: " + serviceTotal + " tinycents"); + + // Total fee + System.out.println("\nTotal Estimated Fee: " + stateEstimate.getTotal() + " tinycents"); + System.out.println("Total Estimated Fee: " + Hbar.fromTinybars(stateEstimate.getTotal() / 100)); + + // Display any notes/caveats + if (!stateEstimate.getNotes().isEmpty()) { + System.out.println("\nNotes:"); + for (String note : stateEstimate.getNotes()) { + System.out.println(" - " + note); + } + } + + /* + * Step 3: + * Estimate fees with INTRINSIC mode. + * INTRINSIC mode only considers the transaction's inherent properties + * (size, signatures, keys) and ignores state-dependent factors. + */ + System.out.println("\n=== Estimating Fees with INTRINSIC Mode ==="); + + FeeEstimateResponse intrinsicEstimate = new FeeEstimateQuery() + .setMode(FeeEstimateMode.INTRINSIC) + .setTransaction(tx) + .execute(client); + + System.out.println("Mode: " + intrinsicEstimate.getMode()); + System.out.println("Network Fee Subtotal: " + intrinsicEstimate.getNetwork().getSubtotal() + " tinycents"); + System.out.println("Node Fee Base: " + intrinsicEstimate.getNode().getBase() + " tinycents"); + System.out.println("Service Fee Base: " + intrinsicEstimate.getService().getBase() + " tinycents"); + System.out.println("Total Estimated Fee: " + intrinsicEstimate.getTotal() + " tinycents"); + System.out.println("Total Estimated Fee: " + Hbar.fromTinybars(intrinsicEstimate.getTotal() / 100)); + + /* + * Step 4: + * Compare STATE vs INTRINSIC mode estimates. + */ + System.out.println("\n=== Comparison ==="); + System.out.println("STATE mode total: " + stateEstimate.getTotal() + " tinycents"); + System.out.println("INTRINSIC mode total: " + intrinsicEstimate.getTotal() + " tinycents"); + long difference = Math.abs(stateEstimate.getTotal() - intrinsicEstimate.getTotal()); + System.out.println("Difference: " + difference + " tinycents"); + + /* + * Step 5: + * Demonstrate fee estimation for a token creation transaction. + */ + System.out.println("\n=== Estimating Token Creation Fees ==="); + + TokenCreateTransaction tokenTx = new TokenCreateTransaction() + .setTokenName("Example Token") + .setTokenSymbol("EXT") + .setDecimals(3) + .setInitialSupply(1000000) + .setTreasuryAccountId(OPERATOR_ID) + .setAdminKey(OPERATOR_KEY) + .freezeWith(client) + .signWithOperator(client); + + FeeEstimateResponse tokenEstimate = new FeeEstimateQuery() + .setMode(FeeEstimateMode.STATE) + .setTransaction(tokenTx) + .execute(client); + + System.out.println("Token Creation Estimated Fee: " + tokenEstimate.getTotal() + " tinycents"); + System.out.println("Token Creation Estimated Fee: " + Hbar.fromTinybars(tokenEstimate.getTotal() / 100)); + + /* + * Clean up: + */ + client.close(); + System.out.println("\nExample complete!"); + } +} + diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java index f168bd03c9..c3b23ac68b 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeDataType.java @@ -54,7 +54,6 @@ public enum FeeDataType { */ SUBMIT_MESSAGE_WITH_CUSTOM_FEES(SubType.SUBMIT_MESSAGE_WITH_CUSTOM_FEES); - final SubType code; FeeDataType(SubType code) { diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java index 6ab2081c71..545d7e4b39 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java @@ -57,8 +57,7 @@ static FeeEstimate fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimat * @throws InvalidProtocolBufferException when there is an issue with the protobuf */ public static FeeEstimate fromBytes(byte[] bytes) throws InvalidProtocolBufferException { - return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.parseFrom(bytes) - .toBuilder() + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.parseFrom(bytes).toBuilder() .build()); } @@ -86,7 +85,8 @@ public List getExtras() { * @return the protobuf */ com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate toProtobuf() { - var builder = com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.newBuilder().setBase(base); + var builder = + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.newBuilder().setBase(base); for (var extra : extras) { builder.addExtras(extra.toProtobuf()); @@ -128,4 +128,3 @@ public int hashCode() { return Objects.hash(base, extras); } } - diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java index 47e55cbf73..262ee0802d 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java @@ -42,8 +42,7 @@ static FeeEstimateMode valueOf(int code) { return switch (code) { case 0 -> STATE; case 1 -> INTRINSIC; - default -> throw new IllegalArgumentException( - "(BUG) unhandled FeeEstimateMode code: " + code); + default -> throw new IllegalArgumentException("(BUG) unhandled FeeEstimateMode code: " + code); }; } @@ -55,4 +54,3 @@ public String toString() { }; } } - diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java index 382833c0b7..a0832724a4 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java @@ -226,31 +226,31 @@ public CompletableFuture executeAsync(Client client, Durati */ void executeAsync( Client client, Deadline deadline, CompletableFuture returnFuture, int attempt) { - try { - var futureStub = NetworkServiceGrpc.newFutureStub( - client.mirrorNetwork.getNextMirrorNode().getChannel()) - .withDeadline(deadline); - - var responseFuture = futureStub.getFeeEstimate(buildQuery()); - - FutureConverter.toCompletableFuture(responseFuture) - .whenComplete((response, error) -> { - if (error != null) { - if (attempt >= maxAttempts || !shouldRetry(error)) { - LOGGER.error("Error attempting to get fee estimate", error); - returnFuture.completeExceptionally(error); - return; - } - warnAndDelay(attempt, error); - executeAsync(client, deadline, returnFuture, attempt + 1); - } else { - returnFuture.complete(FeeEstimateResponse.fromProtobuf(response)); + ClientCalls.asyncUnaryCall( + buildCall(client, deadline), + buildQuery(), + new io.grpc.stub.StreamObserver() { + @Override + public void onNext(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse response) { + returnFuture.complete(FeeEstimateResponse.fromProtobuf(response)); + } + + @Override + public void onError(Throwable error) { + if (attempt >= maxAttempts || !shouldRetry(error)) { + LOGGER.error("Error attempting to get fee estimate", error); + returnFuture.completeExceptionally(error); + return; } - }); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - returnFuture.completeExceptionally(e); - } + warnAndDelay(attempt, error); + executeAsync(client, deadline, returnFuture, attempt + 1); + } + + @Override + public void onCompleted() { + // Response already handled in onNext + } + }); } /** @@ -304,4 +304,3 @@ private void warnAndDelay(int attempt, Throwable error) { } } } - diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java index b687881d2d..f68f7f2d8f 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java @@ -106,8 +106,7 @@ static FeeEstimateResponse fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.Fe * @throws InvalidProtocolBufferException when there is an issue with the protobuf */ public static FeeEstimateResponse fromBytes(byte[] bytes) throws InvalidProtocolBufferException { - return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse.parseFrom(bytes) - .toBuilder() + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse.parseFrom(bytes).toBuilder() .build()); } @@ -234,4 +233,3 @@ public int hashCode() { return Objects.hash(mode, network, node, notes, service, total); } } - diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java index 78724713b6..b3e8814723 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java @@ -89,8 +89,7 @@ static FeeExtra fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeExtra feeE * @throws InvalidProtocolBufferException when there is an issue with the protobuf */ public static FeeExtra fromBytes(byte[] bytes) throws InvalidProtocolBufferException { - return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeExtra.parseFrom(bytes) - .toBuilder() + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.FeeExtra.parseFrom(bytes).toBuilder() .build()); } @@ -211,4 +210,3 @@ public int hashCode() { return Objects.hash(charged, count, feePerUnit, included, name, subtotal); } } - diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java index f6897f64e1..9ebc2d304f 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FreezeType.java @@ -115,7 +115,7 @@ static FreezeType valueOf(com.hedera.hashgraph.sdk.proto.FreezeType code) { case UNRECOGNIZED -> // NOTE: Protobuf deserialization will not give us the code on the wire throw new IllegalArgumentException( - "network returned unrecognized response code; your SDK may be out of date"); + "network returned unrecognized response code; your SDK may be out of date"); }; } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java index 7bb626b686..db4b5f5309 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/NetworkFee.java @@ -49,8 +49,7 @@ static NetworkFee fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.NetworkFee * @throws InvalidProtocolBufferException when there is an issue with the protobuf */ public static NetworkFee fromBytes(byte[] bytes) throws InvalidProtocolBufferException { - return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.NetworkFee.parseFrom(bytes) - .toBuilder() + return fromProtobuf(com.hedera.hashgraph.sdk.proto.mirror.NetworkFee.parseFrom(bytes).toBuilder() .build()); } @@ -101,4 +100,3 @@ public String toString() { .toString(); } } - diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java index 107efd95f6..aa2f58cdf7 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java @@ -2,7 +2,6 @@ package com.hedera.hashgraph.sdk; import com.hedera.hashgraph.sdk.proto.ResponseCodeEnum; - import java.util.Objects; /** @@ -1054,7 +1053,8 @@ public enum Status { /** * Only tokens of type FUNGIBLE_COMMON can have fractional fees */ - CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON(ResponseCodeEnum.CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), + CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON( + ResponseCodeEnum.CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), /** * The provided custom fee schedule key was invalid @@ -1094,7 +1094,8 @@ public enum Status { /** * An AccountAmount token transfers list referenced a token type other than FUNGIBLE_COMMON */ - ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON(ResponseCodeEnum.ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), + ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON( + ResponseCodeEnum.ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON), /** * All the NFTs allowed in the current price regime have already been minted @@ -1119,7 +1120,8 @@ public enum Status { /** * The sender account in the token transfer transaction could not afford a custom fee */ - INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE(ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE), + INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE( + ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE), /** * Currently no more than 4,294,967,295 NFTs may be minted for a given unique token type @@ -1129,7 +1131,8 @@ public enum Status { /** * Only tokens of type NON_FUNGIBLE_UNIQUE can have royalty fees */ - CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE(ResponseCodeEnum.CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE), + CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE( + ResponseCodeEnum.CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE), /** * The account has reached the limit on the automatic associations count. @@ -1139,13 +1142,15 @@ public enum Status { /** * Already existing automatic associations are more than the new maximum automatic associations. */ - EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT(ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT), + EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT( + ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT), /** * Cannot set the number of automatic associations for an account more than the maximum allowed * token associations tokens.maxPerAccount. */ - REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT(ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT), + REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT( + ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT), /** * Token is paused. This Token cannot be a part of any kind of Transaction until unpaused. @@ -1375,7 +1380,8 @@ public enum Status { /** * The scheduled transaction could not be created because it's expiration_time was less than or equal to the consensus time. */ - SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME(ResponseCodeEnum.SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME), + SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME( + ResponseCodeEnum.SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME), /** * The scheduled transaction could not be created because it would cause throttles to be violated on the specified expiration_time. @@ -2116,7 +2122,8 @@ static Status valueOf(ResponseCodeEnum code) { case FRACTIONAL_FEE_MAX_AMOUNT_LESS_THAN_MIN_AMOUNT -> FRACTIONAL_FEE_MAX_AMOUNT_LESS_THAN_MIN_AMOUNT; case CUSTOM_SCHEDULE_ALREADY_HAS_NO_FEES -> CUSTOM_SCHEDULE_ALREADY_HAS_NO_FEES; case CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON -> CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON; - case CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; + case CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> + CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; case INVALID_CUSTOM_FEE_SCHEDULE_KEY -> INVALID_CUSTOM_FEE_SCHEDULE_KEY; case INVALID_TOKEN_MINT_METADATA -> INVALID_TOKEN_MINT_METADATA; case INVALID_TOKEN_BURN_METADATA -> INVALID_TOKEN_BURN_METADATA; @@ -2124,17 +2131,22 @@ static Status valueOf(ResponseCodeEnum code) { case ACCOUNT_STILL_OWNS_NFTS -> ACCOUNT_STILL_OWNS_NFTS; case TREASURY_MUST_OWN_BURNED_NFT -> TREASURY_MUST_OWN_BURNED_NFT; case ACCOUNT_DOES_NOT_OWN_WIPED_NFT -> ACCOUNT_DOES_NOT_OWN_WIPED_NFT; - case ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; + case ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON -> + ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON; case MAX_NFTS_IN_PRICE_REGIME_HAVE_BEEN_MINTED -> MAX_NFTS_IN_PRICE_REGIME_HAVE_BEEN_MINTED; case PAYER_ACCOUNT_DELETED -> PAYER_ACCOUNT_DELETED; case CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH -> CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH; case CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS -> CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS; - case INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE -> INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE; + case INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE -> + INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE; case SERIAL_NUMBER_LIMIT_REACHED -> SERIAL_NUMBER_LIMIT_REACHED; - case CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE -> CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE; + case CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE -> + CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE; case NO_REMAINING_AUTOMATIC_ASSOCIATIONS -> NO_REMAINING_AUTOMATIC_ASSOCIATIONS; - case EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT -> EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT; - case REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT -> REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT; + case EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT -> + EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT; + case REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT -> + REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT; case TOKEN_IS_PAUSED -> TOKEN_IS_PAUSED; case TOKEN_HAS_NO_PAUSE_KEY -> TOKEN_HAS_NO_PAUSE_KEY; case INVALID_PAUSE_KEY -> INVALID_PAUSE_KEY; @@ -2177,7 +2189,8 @@ static Status valueOf(ResponseCodeEnum code) { case DELEGATING_SPENDER_CANNOT_GRANT_APPROVE_FOR_ALL -> DELEGATING_SPENDER_CANNOT_GRANT_APPROVE_FOR_ALL; case DELEGATING_SPENDER_DOES_NOT_HAVE_APPROVE_FOR_ALL -> DELEGATING_SPENDER_DOES_NOT_HAVE_APPROVE_FOR_ALL; case SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE -> SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE; - case SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME -> SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME; + case SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME -> + SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME; case SCHEDULE_FUTURE_THROTTLE_EXCEEDED -> SCHEDULE_FUTURE_THROTTLE_EXCEEDED; case SCHEDULE_FUTURE_GAS_LIMIT_EXCEEDED -> SCHEDULE_FUTURE_GAS_LIMIT_EXCEEDED; case INVALID_ETHEREUM_TRANSACTION -> INVALID_ETHEREUM_TRANSACTION; @@ -2273,10 +2286,10 @@ static Status valueOf(ResponseCodeEnum code) { case UNRECOGNIZED -> // NOTE: Protobuf deserialization will not give us the code on the wire throw new IllegalArgumentException( - "network returned unrecognized response code; your SDK may be out of date"); + "network returned unrecognized response code; your SDK may be out of date"); default -> throw new IllegalArgumentException( - "unhandled response code: " + code + "; your SDK may be out of date"); + "unhandled response code: " + code + "; your SDK may be out of date"); }; } diff --git a/sdk/src/main/proto/mirror/fee.proto b/sdk/src/main/proto/mirror/fee.proto index d19be014de..0816699c22 100644 --- a/sdk/src/main/proto/mirror/fee.proto +++ b/sdk/src/main/proto/mirror/fee.proto @@ -2,7 +2,6 @@ syntax = "proto3"; -package proto.mirror; option java_package = "com.hedera.hashgraph.sdk.proto.mirror"; option java_multiple_files = true; diff --git a/sdk/src/main/proto/mirror/mirror_network_service.proto b/sdk/src/main/proto/mirror/mirror_network_service.proto index 8d7bee8795..65c0de39af 100644 --- a/sdk/src/main/proto/mirror/mirror_network_service.proto +++ b/sdk/src/main/proto/mirror/mirror_network_service.proto @@ -20,20 +20,28 @@ syntax = "proto3"; -package proto.mirror; +package com.hedera.mirror.api.proto; option java_multiple_files = true; // Required for the reactor-grpc generator to work correctly option java_package = "com.hedera.hashgraph.sdk.proto.mirror"; import "basic_types.proto"; +import "timestamp.proto"; import "mirror/fee.proto"; /** * Request object to query an address book for its list of nodes */ message AddressBookQuery { - .proto.FileID file_id = 1; // The ID of the address book file on the network. Can be either 0.0.101 or 0.0.102. - int32 limit = 2; // The maximum number of node addresses to receive before stopping. If not set or set to zero it will return all node addresses in the database. + /** + * The ID of the address book file on the network. Can be either 0.0.101 or 0.0.102. + */ + .proto.FileID file_id = 1; + + /** + * The maximum number of node addresses to receive before stopping. If not set or set to zero it will return all node addresses in the database. + */ + int32 limit = 2; } /** @@ -41,14 +49,14 @@ message AddressBookQuery { */ service NetworkService { /** - * Query to estimate the fees when submitting a transaction to the network. - */ + * Query to estimate the fees when submitting a transaction to the network. + */ rpc getFeeEstimate(FeeEstimateQuery) returns (FeeEstimateResponse); - - /** + /* * Query for an address book and return its nodes. The nodes are returned in ascending order by node ID. The * response is not guaranteed to be a byte-for-byte equivalent to the NodeAddress in the Hedera file on * the network since it is reconstructed from a normalized database table. */ rpc getNodes (AddressBookQuery) returns (stream .proto.NodeAddress); } + diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java index 29332d9037..ab11dee6f5 100644 --- a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java +++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java @@ -2,31 +2,20 @@ package com.hedera.hashgraph.sdk.test.integration; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import com.hedera.hashgraph.sdk.AccountCreateTransaction; -import com.hedera.hashgraph.sdk.FeeEstimateMode; -import com.hedera.hashgraph.sdk.FeeEstimateQuery; -import com.hedera.hashgraph.sdk.PrivateKey; -import com.hedera.hashgraph.sdk.TokenCreateTransaction; -import com.hedera.hashgraph.sdk.TokenSupplyType; -import com.hedera.hashgraph.sdk.TokenType; -import com.hedera.hashgraph.sdk.TransferTransaction; +import com.hedera.hashgraph.sdk.*; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class FeeEstimateQueryIntegrationTest { @Test - @DisplayName("Can estimate fees for TokenCreateTransaction") - void canEstimateFeesForTokenCreateTransaction() throws Exception { + @DisplayName("Given a TokenCreateTransaction, when fee estimate is requested, " + + "then response includes service fees for token creation and network fees") + void tokenCreateTransactionFeeEstimate() throws Throwable { try (var testEnv = new IntegrationTestEnv(1)) { - // Skip if not running against a mirror node that supports fee estimation - if (!testEnv.isLocalNode) { - return; - } - // Create a TokenCreateTransaction + // Given: A TokenCreateTransaction is created var transaction = new TokenCreateTransaction() .setTokenName("Test Token") .setTokenSymbol("TEST") @@ -34,291 +23,47 @@ void canEstimateFeesForTokenCreateTransaction() throws Exception { .setInitialSupply(1000000) .setTreasuryAccountId(testEnv.operatorId) .setAdminKey(testEnv.operatorKey) - .setSupplyKey(testEnv.operatorKey) - .setTokenType(TokenType.FUNGIBLE_COMMON) - .setSupplyType(TokenSupplyType.INFINITE) .freezeWith(testEnv.client) .signWithOperator(testEnv.client); - // Wait for mirror node to be ready + // Wait for mirror node to sync Thread.sleep(2000); - // Request fee estimate with STATE mode (default) - var response = new FeeEstimateQuery() + // When: A fee estimate is requested + FeeEstimateResponse response = new FeeEstimateQuery() .setTransaction(transaction) .setMode(FeeEstimateMode.STATE) .execute(testEnv.client); - // Verify response structure + // Then: The response includes appropriate fees assertThat(response).isNotNull(); - assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); - assertThat(response.getTotal()).isGreaterThan(0); - - // Verify network fee component - assertThat(response.getNetwork()).isNotNull(); - assertThat(response.getNetwork().getMultiplier()).isGreaterThan(0); - assertThat(response.getNetwork().getSubtotal()).isGreaterThan(0); - // Verify node fee estimate - assertThat(response.getNode()).isNotNull(); - assertThat(response.getNode().getBase()).isGreaterThan(0); - // Node fee may or may not have extras - assertThat(response.getNode().getExtras()).isNotNull(); + // Verify mode is returned correctly + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); - // Verify service fee estimate + // Verify service fees are present (token creation has service fees) assertThat(response.getService()).isNotNull(); - assertThat(response.getService().getBase()).isGreaterThan(0); - // Service fee for token creation should have extras for token operations - assertThat(response.getService().getExtras()).isNotNull(); - - // Verify notes (may be empty) - assertThat(response.getNotes()).isNotNull(); - - // Verify that network fee = node subtotal * multiplier (approximately) - var nodeSubtotal = response.getNode().getBase(); - var expectedNetworkFee = nodeSubtotal * response.getNetwork().getMultiplier(); - assertThat(response.getNetwork().getSubtotal()) - .isCloseTo(expectedNetworkFee, org.assertj.core.data.Percentage.withPercentage(1.0)); - } - } + // assertThat(response.getService().getBase()).isGreaterThan(0); TODO currently there is a stub + // implementation of the estimateFee which always returns 0 - @Test - @DisplayName("Can estimate fees with INTRINSIC mode") - void canEstimateFeesWithIntrinsicMode() throws Exception { - try (var testEnv = new IntegrationTestEnv(1)) { - if (!testEnv.isLocalNode) { - return; - } - - var transaction = new TokenCreateTransaction() - .setTokenName("Test Token") - .setTokenSymbol("TEST") - .setDecimals(3) - .setInitialSupply(1000000) - .setTreasuryAccountId(testEnv.operatorId) - .setAdminKey(testEnv.operatorKey) - .freezeWith(testEnv.client) - .signWithOperator(testEnv.client); - - Thread.sleep(2000); - - var response = new FeeEstimateQuery() - .setTransaction(transaction) - .setMode(FeeEstimateMode.INTRINSIC) - .execute(testEnv.client); - - assertThat(response).isNotNull(); - assertThat(response.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC); - assertThat(response.getTotal()).isGreaterThan(0); + // Verify network fees are present (transaction size-based fees) assertThat(response.getNetwork()).isNotNull(); - assertThat(response.getNode()).isNotNull(); - assertThat(response.getService()).isNotNull(); - } - } + // assertThat(response.getNetwork().getSubtotal()).isGreaterThan(0); - @Test - @DisplayName("Can estimate fees for AccountCreateTransaction") - void canEstimateFeesForAccountCreateTransaction() throws Exception { - try (var testEnv = new IntegrationTestEnv(1)) { - if (!testEnv.isLocalNode) { - return; - } + // Verify total fee is calculated + // assertThat(response.getTotal()).isGreaterThan(0); - var newKey = PrivateKey.generateED25519(); - var transaction = new AccountCreateTransaction() - .setKey(newKey) - .setInitialBalance(com.hedera.hashgraph.sdk.Hbar.from(10)) - .freezeWith(testEnv.client) - .signWithOperator(testEnv.client); - - Thread.sleep(2000); - - var response = new FeeEstimateQuery() - .setTransaction(transaction) - .execute(testEnv.client); - - assertThat(response).isNotNull(); - assertThat(response.getTotal()).isGreaterThan(0); - assertThat(response.getNetwork()).isNotNull(); + // Verify node fees are present assertThat(response.getNode()).isNotNull(); - assertThat(response.getService()).isNotNull(); - - // Account creation should have a base fee - assertThat(response.getService().getBase()).isGreaterThan(0); - } - } - - @Test - @DisplayName("Can estimate fees for TransferTransaction") - void canEstimateFeesForTransferTransaction() throws Exception { - try (var testEnv = new IntegrationTestEnv(1)) { - if (!testEnv.isLocalNode) { - return; - } - - var transaction = new TransferTransaction() - .addHbarTransfer(testEnv.operatorId, com.hedera.hashgraph.sdk.Hbar.from(-1)) - .addHbarTransfer(new com.hedera.hashgraph.sdk.AccountId(0, 0, 3), com.hedera.hashgraph.sdk.Hbar.from(1)) - .freezeWith(testEnv.client) - .signWithOperator(testEnv.client); - - Thread.sleep(2000); - - var response = new FeeEstimateQuery() - .setTransaction(transaction) - .execute(testEnv.client); - - assertThat(response).isNotNull(); - assertThat(response.getTotal()).isGreaterThan(0); - - // Transfer transactions should have lower fees than complex transactions - assertThat(response.getService().getBase()).isGreaterThan(0); - } - } - - @Test - @DisplayName("State mode and intrinsic mode return different estimates") - void stateAndIntrinsicModesReturnDifferentEstimates() throws Exception { - try (var testEnv = new IntegrationTestEnv(1)) { - if (!testEnv.isLocalNode) { - return; - } - - var transaction = new TokenCreateTransaction() - .setTokenName("Test Token") - .setTokenSymbol("TEST") - .setTreasuryAccountId(testEnv.operatorId) - .setAdminKey(testEnv.operatorKey) - .freezeWith(testEnv.client) - .signWithOperator(testEnv.client); - - Thread.sleep(2000); - - var stateResponse = new FeeEstimateQuery() - .setTransaction(transaction) - .setMode(FeeEstimateMode.STATE) - .execute(testEnv.client); - - var intrinsicResponse = new FeeEstimateQuery() - .setTransaction(transaction) - .setMode(FeeEstimateMode.INTRINSIC) - .execute(testEnv.client); - - assertThat(stateResponse).isNotNull(); - assertThat(intrinsicResponse).isNotNull(); - - // Both should have valid totals - assertThat(stateResponse.getTotal()).isGreaterThan(0); - assertThat(intrinsicResponse.getTotal()).isGreaterThan(0); - - // The estimates may differ based on state-dependent factors - // We just verify both modes work and return reasonable values - assertThat(stateResponse.getMode()).isEqualTo(FeeEstimateMode.STATE); - assertThat(intrinsicResponse.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC); - } - } - - @Test - @DisplayName("Can execute query asynchronously") - void canExecuteQueryAsynchronously() throws Exception { - try (var testEnv = new IntegrationTestEnv(1)) { - if (!testEnv.isLocalNode) { - return; - } - - var transaction = new TransferTransaction() - .addHbarTransfer(testEnv.operatorId, com.hedera.hashgraph.sdk.Hbar.from(-1)) - .addHbarTransfer(new com.hedera.hashgraph.sdk.AccountId(0, 0, 3), com.hedera.hashgraph.sdk.Hbar.from(1)) - .freezeWith(testEnv.client) - .signWithOperator(testEnv.client); - - Thread.sleep(2000); - - var responseFuture = new FeeEstimateQuery() - .setTransaction(transaction) - .executeAsync(testEnv.client); - - // Verify the async call completes successfully - assertThatNoException().isThrownBy(() -> { - var response = responseFuture.get(); - assertThat(response).isNotNull(); - assertThat(response.getTotal()).isGreaterThan(0); - }); - } - } - - @Test - @DisplayName("Response includes appropriate fields for extras") - void responseIncludesAppropriateFieldsForExtras() throws Exception { - try (var testEnv = new IntegrationTestEnv(1)) { - if (!testEnv.isLocalNode) { - return; - } - - var transaction = new TokenCreateTransaction() - .setTokenName("Test Token") - .setTokenSymbol("TEST") - .setDecimals(3) - .setInitialSupply(1000000) - .setTreasuryAccountId(testEnv.operatorId) - .setAdminKey(testEnv.operatorKey) - .setSupplyKey(testEnv.operatorKey) - .setWipeKey(testEnv.operatorKey) - .setFreezeKey(testEnv.operatorKey) - .freezeWith(testEnv.client) - .signWithOperator(testEnv.client); - - Thread.sleep(2000); - - var response = new FeeEstimateQuery() - .setTransaction(transaction) - .execute(testEnv.client); - - assertThat(response).isNotNull(); - - // Check if service fee has extras - if (response.getService() != null && !response.getService().getExtras().isEmpty()) { - for (var extra : response.getService().getExtras()) { - // Verify extra fields are populated - assertThat(extra.getCount()).isGreaterThanOrEqualTo(0); - assertThat(extra.getCharged()).isGreaterThanOrEqualTo(0); - assertThat(extra.getIncluded()).isGreaterThanOrEqualTo(0); - assertThat(extra.getFeePerUnit()).isGreaterThanOrEqualTo(0); - assertThat(extra.getSubtotal()).isGreaterThanOrEqualTo(0); - // Name may or may not be set - // Charged = max(0, count - included) - assertThat(extra.getCharged()).isEqualTo(Math.max(0, extra.getCount() - extra.getIncluded())); - // Subtotal = charged * feePerUnit - assertThat(extra.getSubtotal()).isEqualTo(extra.getCharged() * extra.getFeePerUnit()); - } - } - } - } - - @Test - @DisplayName("Can handle transaction without freezing") - void canHandleTransactionWithoutFreezing() throws Exception { - try (var testEnv = new IntegrationTestEnv(1)) { - if (!testEnv.isLocalNode) { - return; - } - - var transaction = new TransferTransaction() - .addHbarTransfer(testEnv.operatorId, com.hedera.hashgraph.sdk.Hbar.from(-1)) - .addHbarTransfer(new com.hedera.hashgraph.sdk.AccountId(0, 0, 3), com.hedera.hashgraph.sdk.Hbar.from(1)); - - // Freeze and sign within the query - transaction.freezeWith(testEnv.client).signWithOperator(testEnv.client); - - Thread.sleep(2000); - - var response = new FeeEstimateQuery() - .setTransaction(transaction) - .execute(testEnv.client); - - assertThat(response).isNotNull(); - assertThat(response.getTotal()).isGreaterThan(0); + // assertThat(response.getNode().getBase()).isGreaterThan(0); + + // Log the fee breakdown for debugging + System.out.println("Token Create Fee Estimate:"); + System.out.println(" Mode: " + response.getMode()); + System.out.println(" Service Base: " + response.getService().getBase()); + System.out.println(" Network Subtotal: " + response.getNetwork().getSubtotal()); + System.out.println(" Node Base: " + response.getNode().getBase()); + System.out.println(" Total: " + response.getTotal()); } } } - From 38e3e228f333048aebe218bbd0423162c24f04b8 Mon Sep 17 00:00:00 2001 From: emiliyank Date: Fri, 31 Oct 2025 14:38:58 +0200 Subject: [PATCH 3/4] hip 1261 - add more tests Signed-off-by: emiliyank --- .../sdk/examples/FeeEstimateQueryExample.java | 22 +- .../hashgraph/sdk/FeeEstimateQuery.java | 1 + .../hashgraph/sdk/FeeEstimateResponse.java | 74 ++--- .../java/com/hedera/hashgraph/sdk/Status.java | 13 +- .../sdk/FeeEstimateQueryMockTest.java | 187 ++++++++++++ .../FeeEstimateQueryIntegrationTest.java | 288 ++++++++++++++++-- sdk/src/testIntegration/java/module-info.java | 2 + 7 files changed, 508 insertions(+), 79 deletions(-) create mode 100644 sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java diff --git a/examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java index 901b7dd635..e74f414764 100644 --- a/examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java +++ b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java @@ -104,14 +104,14 @@ public static void main(String[] args) throws Exception { // Network fee breakdown System.out.println("\nNetwork Fee:"); - System.out.println(" Multiplier: " + stateEstimate.getNetwork().getMultiplier()); - System.out.println(" Subtotal: " + stateEstimate.getNetwork().getSubtotal() + " tinycents"); + System.out.println(" Multiplier: " + stateEstimate.getNetworkFee().getMultiplier()); + System.out.println(" Subtotal: " + stateEstimate.getNetworkFee().getSubtotal() + " tinycents"); // Node fee breakdown System.out.println("\nNode Fee:"); - System.out.println(" Base: " + stateEstimate.getNode().getBase() + " tinycents"); - long nodeTotal = stateEstimate.getNode().getBase(); - for (FeeExtra extra : stateEstimate.getNode().getExtras()) { + System.out.println(" Base: " + stateEstimate.getNodeFee().getBase() + " tinycents"); + long nodeTotal = stateEstimate.getNodeFee().getBase(); + for (FeeExtra extra : stateEstimate.getNodeFee().getExtras()) { System.out.println(" Extra - " + extra.getName() + ": " + extra.getSubtotal() + " tinycents"); nodeTotal += extra.getSubtotal(); } @@ -119,9 +119,9 @@ public static void main(String[] args) throws Exception { // Service fee breakdown System.out.println("\nService Fee:"); - System.out.println(" Base: " + stateEstimate.getService().getBase() + " tinycents"); - long serviceTotal = stateEstimate.getService().getBase(); - for (FeeExtra extra : stateEstimate.getService().getExtras()) { + System.out.println(" Base: " + stateEstimate.getServiceFee().getBase() + " tinycents"); + long serviceTotal = stateEstimate.getServiceFee().getBase(); + for (FeeExtra extra : stateEstimate.getServiceFee().getExtras()) { System.out.println(" Extra - " + extra.getName() + ": " + extra.getSubtotal() + " tinycents"); serviceTotal += extra.getSubtotal(); } @@ -153,9 +153,9 @@ public static void main(String[] args) throws Exception { .execute(client); System.out.println("Mode: " + intrinsicEstimate.getMode()); - System.out.println("Network Fee Subtotal: " + intrinsicEstimate.getNetwork().getSubtotal() + " tinycents"); - System.out.println("Node Fee Base: " + intrinsicEstimate.getNode().getBase() + " tinycents"); - System.out.println("Service Fee Base: " + intrinsicEstimate.getService().getBase() + " tinycents"); + System.out.println("Network Fee Subtotal: " + intrinsicEstimate.getNetworkFee().getSubtotal() + " tinycents"); + System.out.println("Node Fee Base: " + intrinsicEstimate.getNodeFee().getBase() + " tinycents"); + System.out.println("Service Fee Base: " + intrinsicEstimate.getServiceFee().getBase() + " tinycents"); System.out.println("Total Estimated Fee: " + intrinsicEstimate.getTotal() + " tinycents"); System.out.println("Total Estimated Fee: " + Hbar.fromTinybars(intrinsicEstimate.getTotal() / 100)); diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java index a0832724a4..26dce735a1 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java @@ -45,6 +45,7 @@ private static boolean shouldRetry(Throwable throwable) { var description = statusRuntimeException.getStatus().getDescription(); return (code == io.grpc.Status.Code.UNAVAILABLE) + || (code == io.grpc.Status.Code.DEADLINE_EXCEEDED) || (code == io.grpc.Status.Code.RESOURCE_EXHAUSTED) || (code == Status.Code.INTERNAL && description != null diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java index f68f7f2d8f..e2388d9781 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java @@ -26,7 +26,7 @@ public final class FeeEstimateResponse { * signature verifications, fee payment, and storage. */ @Nullable - private final NetworkFee network; + private final NetworkFee networkFee; /** * The node fee component which is to be paid to the node that submitted the @@ -35,7 +35,14 @@ public final class FeeEstimateResponse { * incentivizes the node to accept new transactions from users. */ @Nullable - private final FeeEstimate node; + private final FeeEstimate nodeFee; + + /** + * The service fee component which covers execution costs, state saved in the + * Merkle tree, and additional costs to the blockchain storage. + */ + @Nullable + private final FeeEstimate serviceFee; /** * An array of strings for any caveats. @@ -44,13 +51,6 @@ public final class FeeEstimateResponse { */ private final List notes; - /** - * The service fee component which covers execution costs, state saved in the - * Merkle tree, and additional costs to the blockchain storage. - */ - @Nullable - private final FeeEstimate service; - /** * The sum of the network, node, and service subtotals in tinycents. */ @@ -60,24 +60,24 @@ public final class FeeEstimateResponse { * Constructor. * * @param mode the fee estimate mode used - * @param network the network fee component - * @param node the node fee estimate + * @param networkFee the network fee component + * @param nodeFee the node fee estimate * @param notes the list of notes/caveats - * @param service the service fee estimate + * @param serviceFee the service fee estimate * @param total the total fee in tinycents */ FeeEstimateResponse( FeeEstimateMode mode, - @Nullable NetworkFee network, - @Nullable FeeEstimate node, + @Nullable NetworkFee networkFee, + @Nullable FeeEstimate nodeFee, List notes, - @Nullable FeeEstimate service, + @Nullable FeeEstimate serviceFee, long total) { this.mode = mode; - this.network = network; - this.node = node; + this.networkFee = networkFee; + this.nodeFee = nodeFee; this.notes = Collections.unmodifiableList(new ArrayList<>(notes)); - this.service = service; + this.serviceFee = serviceFee; this.total = total; } @@ -125,8 +125,8 @@ public FeeEstimateMode getMode() { * @return the network fee component, or null if not set */ @Nullable - public NetworkFee getNetwork() { - return network; + public NetworkFee getNetworkFee() { + return networkFee; } /** @@ -135,8 +135,8 @@ public NetworkFee getNetwork() { * @return the node fee estimate, or null if not set */ @Nullable - public FeeEstimate getNode() { - return node; + public FeeEstimate getNodeFee() { + return nodeFee; } /** @@ -154,8 +154,8 @@ public List getNotes() { * @return the service fee estimate, or null if not set */ @Nullable - public FeeEstimate getService() { - return service; + public FeeEstimate getServiceFee() { + return serviceFee; } /** @@ -178,14 +178,14 @@ com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse toProtobuf() { .setTotal(total) .addAllNotes(notes); - if (network != null) { - builder.setNetwork(network.toProtobuf()); + if (networkFee != null) { + builder.setNetwork(networkFee.toProtobuf()); } - if (node != null) { - builder.setNode(node.toProtobuf()); + if (nodeFee != null) { + builder.setNode(nodeFee.toProtobuf()); } - if (service != null) { - builder.setService(service.toProtobuf()); + if (serviceFee != null) { + builder.setService(serviceFee.toProtobuf()); } return builder.build(); @@ -204,10 +204,10 @@ public byte[] toBytes() { public String toString() { return MoreObjects.toStringHelper(this) .add("mode", mode) - .add("network", network) - .add("node", node) + .add("network", networkFee) + .add("node", nodeFee) .add("notes", notes) - .add("service", service) + .add("service", serviceFee) .add("total", total) .toString(); } @@ -222,14 +222,14 @@ public boolean equals(Object o) { } return total == that.total && mode == that.mode - && Objects.equals(network, that.network) - && Objects.equals(node, that.node) + && Objects.equals(networkFee, that.networkFee) + && Objects.equals(nodeFee, that.nodeFee) && Objects.equals(notes, that.notes) - && Objects.equals(service, that.service); + && Objects.equals(serviceFee, that.serviceFee); } @Override public int hashCode() { - return Objects.hash(mode, network, node, notes, service, total); + return Objects.hash(mode, networkFee, nodeFee, notes, serviceFee, total); } } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java index aa2f58cdf7..9f84417ac7 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java @@ -1908,7 +1908,13 @@ public enum Status { * The GRPC proxy endpoint is set in the NodeCreate or NodeUpdate transaction, * which the network does not support. */ - GRPC_WEB_PROXY_NOT_SUPPORTED(ResponseCodeEnum.GRPC_WEB_PROXY_NOT_SUPPORTED); + GRPC_WEB_PROXY_NOT_SUPPORTED(ResponseCodeEnum.GRPC_WEB_PROXY_NOT_SUPPORTED), + + /** + * An NFT transfers list referenced a token type other than NON_FUNGIBLE_UNIQUE. + */ + NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE( + ResponseCodeEnum.NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE); final ResponseCodeEnum code; @@ -2283,13 +2289,12 @@ static Status valueOf(ResponseCodeEnum code) { case THROTTLE_GROUP_LCM_OVERFLOW -> THROTTLE_GROUP_LCM_OVERFLOW; case AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN -> AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN; case GRPC_WEB_PROXY_NOT_SUPPORTED -> GRPC_WEB_PROXY_NOT_SUPPORTED; + case NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE -> + NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE; case UNRECOGNIZED -> // NOTE: Protobuf deserialization will not give us the code on the wire throw new IllegalArgumentException( "network returned unrecognized response code; your SDK may be out of date"); - default -> - throw new IllegalArgumentException( - "unhandled response code: " + code + "; your SDK may be out of date"); }; } diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java new file mode 100644 index 0000000000..2ee203c716 --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.ByteString; +import com.hedera.hashgraph.sdk.proto.mirror.NetworkServiceGrpc; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FeeEstimateQueryMockTest { + + private static final com.hedera.hashgraph.sdk.proto.Transaction DUMMY_TRANSACTION = + com.hedera.hashgraph.sdk.proto.Transaction.newBuilder() + .setSignedTransactionBytes(ByteString.copyFromUtf8("dummy")) + .build(); + + private Client client; + private FeeEstimateServiceStub feeEstimateServiceStub; + private Server server; + private FeeEstimateQuery query; + + @BeforeEach + void setUp() throws Exception { + client = Client.forNetwork(Collections.emptyMap()); + client.setRequestTimeout(Duration.ofSeconds(10)); + // FIX: Use unique in-process server name for each test run + String serverName = "test-" + System.nanoTime(); + + client.setMirrorNetwork(Collections.singletonList("in-process:" + serverName)); + + feeEstimateServiceStub = new FeeEstimateServiceStub(); + server = InProcessServerBuilder.forName(serverName) // FIX: unique name here + .addService(feeEstimateServiceStub) + .directExecutor() + .build() + .start(); + + query = new FeeEstimateQuery(); + } + + @AfterEach + void tearDown() throws Exception { + // Verify the stub received and processed all requests + feeEstimateServiceStub.verify(); + + // FIX: ensure proper cleanup between tests + if (server != null) { + server.shutdownNow(); // FIX: force shutdown to avoid lingering registrations + server.awaitTermination(1, TimeUnit.SECONDS); + } + if (client != null) { + client.close(); + } + } + + @Test + @DisplayName( + "Given a FeeEstimateQuery is executed when the Mirror service is unavailable, when the query is executed, then it retries according to the existing query retry policy for UNAVAILABLE errors") + void retriesOnUnavailableErrors() { + query.setTransaction(DUMMY_TRANSACTION).setMaxAttempts(3).setMaxBackoff(Duration.ofMillis(500)); + + var expectedRequest = com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery.newBuilder() + .setModeValue(FeeEstimateMode.STATE.code) + .setTransaction(DUMMY_TRANSACTION) + .build(); + + feeEstimateServiceStub.enqueueError( + expectedRequest, Status.UNAVAILABLE.withDescription("transient").asRuntimeException()); + feeEstimateServiceStub.enqueue(expectedRequest, newSuccessResponse(FeeEstimateMode.STATE, 2, 6, 8)); + + var response = query.execute(client); + + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); + assertThat(response.getTotal()).isEqualTo(26); + assertThat(feeEstimateServiceStub.requestCount()).isEqualTo(2); + } + + @Test + @DisplayName( + "Given a FeeEstimateQuery times out, when the query is executed, then it retries according to the existing query retry policy for DEADLINE_EXCEEDED errors") + void retriesOnDeadlineExceededErrors() { + query.setTransaction(DUMMY_TRANSACTION).setMaxAttempts(3).setMaxBackoff(Duration.ofMillis(500)); + + var expectedRequest = com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery.newBuilder() + .setModeValue(FeeEstimateMode.STATE.code) + .setTransaction(DUMMY_TRANSACTION) + .build(); + + feeEstimateServiceStub.enqueueError( + expectedRequest, + Status.DEADLINE_EXCEEDED.withDescription("timeout").asRuntimeException()); + feeEstimateServiceStub.enqueue(expectedRequest, newSuccessResponse(FeeEstimateMode.STATE, 4, 8, 20)); + + var response = query.execute(client); + + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); + assertThat(response.getTotal()).isEqualTo(60); + assertThat(feeEstimateServiceStub.requestCount()).isEqualTo(2); + } + + private static com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse newSuccessResponse( + FeeEstimateMode mode, int networkMultiplier, long nodeBase, long serviceBase) { + long networkSubtotal = nodeBase * networkMultiplier; + long total = networkSubtotal + nodeBase + serviceBase; + return com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse.newBuilder() + .setModeValue(mode.code) + .setNetwork(com.hedera.hashgraph.sdk.proto.mirror.NetworkFee.newBuilder() + .setMultiplier(networkMultiplier) + .setSubtotal(networkSubtotal) + .build()) + .setNode(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.newBuilder() + .setBase(nodeBase) + .build()) + .setService(com.hedera.hashgraph.sdk.proto.mirror.FeeEstimate.newBuilder() + .setBase(serviceBase) + .build()) + .setTotal(total) + .build(); + } + + private static class FeeEstimateServiceStub extends NetworkServiceGrpc.NetworkServiceImplBase { + private final Queue expectedRequests = + new ArrayDeque<>(); + private final Queue responses = new ArrayDeque<>(); + private int observedRequests = 0; + + void enqueue( + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery request, + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse response) { + expectedRequests.add(request); + responses.add(response); + } + + void enqueueError( + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery request, StatusRuntimeException error) { + expectedRequests.add(request); + responses.add(error); + } + + int requestCount() { + return observedRequests; + } + + void verify() { + assertThat(expectedRequests).isEmpty(); + assertThat(responses).isEmpty(); + } + + @Override + public void getFeeEstimate( + com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateQuery request, + StreamObserver responseObserver) { + observedRequests++; + var expected = expectedRequests.poll(); + assertThat(expected) + .as("expected request to be queued before invoking getFeeEstimate") + .isNotNull(); + assertThat(request).isEqualTo(expected); + + var response = responses.poll(); + assertThat(response) + .as("response or error should be queued before invoking getFeeEstimate") + .isNotNull(); + + if (response instanceof StatusRuntimeException error) { + responseObserver.onError(error); + return; + } + + responseObserver.onNext((com.hedera.hashgraph.sdk.proto.mirror.FeeEstimateResponse) response); + responseObserver.onCompleted(); + } + } +} diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java index ab11dee6f5..6729ad40e4 100644 --- a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java +++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java @@ -4,11 +4,15 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hedera.hashgraph.sdk.*; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class FeeEstimateQueryIntegrationTest { + private static final long MIRROR_SYNC_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(2); + @Test @DisplayName("Given a TokenCreateTransaction, when fee estimate is requested, " + "then response includes service fees for token creation and network fees") @@ -26,8 +30,7 @@ void tokenCreateTransactionFeeEstimate() throws Throwable { .freezeWith(testEnv.client) .signWithOperator(testEnv.client); - // Wait for mirror node to sync - Thread.sleep(2000); + waitForMirrorNodeSync(); // When: A fee estimate is requested FeeEstimateResponse response = new FeeEstimateQuery() @@ -36,34 +39,265 @@ void tokenCreateTransactionFeeEstimate() throws Throwable { .execute(testEnv.client); // Then: The response includes appropriate fees - assertThat(response).isNotNull(); + assertFeeComponentsPresent(response); + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); + assertComponentTotalsConsistent(response); + } + } + + @Test + @DisplayName( + "Given a TransferTransaction, when fee estimate is requested in STATE mode, then all components are returned") + void transferTransactionStateModeFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new TransferTransaction() + .addHbarTransfer(testEnv.operatorId, Hbar.fromTinybars(-1)) + .addHbarTransfer(AccountId.fromString("0.0.3"), Hbar.fromTinybars(1)) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.STATE) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); + assertComponentTotalsConsistent(response); + } + } + + @Test + @Disabled + @DisplayName( + "Given a TransferTransaction, when fee estimate is requested in INTRINSIC mode, then components are returned without state dependencies") + void transferTransactionIntrinsicModeFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new TransferTransaction() + .addHbarTransfer(testEnv.operatorId, Hbar.fromTinybars(-1)) + .addHbarTransfer(AccountId.fromString("0.0.3"), Hbar.fromTinybars(1)) + .freezeWith(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.INTRINSIC) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertThat(response.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC); + assertComponentTotalsConsistent(response); + } + } + + @Test + @DisplayName( + "Given a TransferTransaction without explicit mode, when fee estimate is requested, then STATE mode is used by default") + void transferTransactionDefaultModeIsState() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new TransferTransaction() + .addHbarTransfer(testEnv.operatorId, Hbar.fromTinybars(-1)) + .addHbarTransfer(AccountId.fromString("0.0.3"), Hbar.fromTinybars(1)) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery().setTransaction(transaction).execute(testEnv.client); - // Verify mode is returned correctly + assertFeeComponentsPresent(response); assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE); + assertComponentTotalsConsistent(response); + } + } + + @Test + @DisplayName("Given a TokenMintTransaction, when fee estimate is requested, then extras are returned for minting") + void tokenMintTransactionFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new TokenMintTransaction() + .setTokenId(TokenId.fromString("0.0.1234")) + .setAmount(10) + .freezeWith(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.INTRINSIC) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertThat(response.getNodeFee().getExtras()).isNotNull(); + assertComponentTotalsConsistent(response); + } + } + + @Test + @DisplayName("Given a TopicCreateTransaction, when fee estimate is requested, then service fees are included") + void topicCreateTransactionFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new TopicCreateTransaction() + .setTopicMemo("integration test topic") + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.STATE) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertComponentTotalsConsistent(response); + } + } + + @Test + @DisplayName("Given a ContractCreateTransaction, when fee estimate is requested, then execution fees are returned") + void contractCreateTransactionFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new ContractCreateTransaction() + .setBytecode(new byte[] {1, 2, 3}) + .setGas(1000) + .setAdminKey(testEnv.operatorKey) + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.STATE) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertComponentTotalsConsistent(response); + } + } + + @Test + @DisplayName("Given a FileCreateTransaction, when fee estimate is requested, then storage fees are included") + void fileCreateTransactionFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new FileCreateTransaction() + .setKeys(testEnv.operatorKey) + .setContents("integration test file") + .freezeWith(testEnv.client) + .signWithOperator(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.STATE) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertComponentTotalsConsistent(response); + } + } + + @Test + @Disabled + @DisplayName( + "Given a FileAppendTransaction spanning multiple chunks, when fee estimate is requested, then aggregated totals are returned") + void fileAppendTransactionFeeEstimateAggregatesChunks() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new FileAppendTransaction() + .setFileId(FileId.fromString("0.0.1234")) + .setContents(new byte[5000]) + .freezeWith(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.INTRINSIC) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertComponentTotalsConsistent(response); + } + } + + @Test + @DisplayName( + "Given a TopicMessageSubmitTransaction smaller than a chunk, when fee estimate is requested, then a single chunk estimate is returned") + void topicMessageSubmitSingleChunkFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new TopicMessageSubmitTransaction() + .setTopicId(TopicId.fromString("0.0.1234")) + .setMessage(new byte[128]) + .freezeWith(testEnv.client); + + waitForMirrorNodeSync(); - // Verify service fees are present (token creation has service fees) - assertThat(response.getService()).isNotNull(); - // assertThat(response.getService().getBase()).isGreaterThan(0); TODO currently there is a stub - // implementation of the estimateFee which always returns 0 - - // Verify network fees are present (transaction size-based fees) - assertThat(response.getNetwork()).isNotNull(); - // assertThat(response.getNetwork().getSubtotal()).isGreaterThan(0); - - // Verify total fee is calculated - // assertThat(response.getTotal()).isGreaterThan(0); - - // Verify node fees are present - assertThat(response.getNode()).isNotNull(); - // assertThat(response.getNode().getBase()).isGreaterThan(0); - - // Log the fee breakdown for debugging - System.out.println("Token Create Fee Estimate:"); - System.out.println(" Mode: " + response.getMode()); - System.out.println(" Service Base: " + response.getService().getBase()); - System.out.println(" Network Subtotal: " + response.getNetwork().getSubtotal()); - System.out.println(" Node Base: " + response.getNode().getBase()); - System.out.println(" Total: " + response.getTotal()); + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.INTRINSIC) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertComponentTotalsConsistent(response); } } + + @Test + @Disabled + @DisplayName( + "Given a TopicMessageSubmitTransaction larger than a chunk, when fee estimate is requested, then multi-chunk totals are aggregated") + void topicMessageSubmitMultipleChunkFeeEstimate() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + var transaction = new TopicMessageSubmitTransaction() + .setTopicId(TopicId.fromString("0.0.1234")) + .setMessage(new byte[5000]) + .freezeWith(testEnv.client); + + waitForMirrorNodeSync(); + + var response = new FeeEstimateQuery() + .setTransaction(transaction) + .setMode(FeeEstimateMode.INTRINSIC) + .execute(testEnv.client); + + assertFeeComponentsPresent(response); + assertComponentTotalsConsistent(response); + } + } + + private static void waitForMirrorNodeSync() throws InterruptedException { + Thread.sleep(MIRROR_SYNC_DELAY_MILLIS); + } + + private static long subtotal(FeeEstimate estimate) { + return estimate.getBase() + + estimate.getExtras().stream().mapToLong(FeeExtra::getSubtotal).sum(); + } + + private static void assertFeeComponentsPresent(FeeEstimateResponse response) { + // TODO adjust when NetworkService.getFeeEstimate has actual implementation + assertThat(response).isNotNull(); + assertThat(response.getNetworkFee()).isNotNull(); + assertThat(response.getNodeFee()).isNotNull(); + assertThat(response.getServiceFee()).isNotNull(); + assertThat(response.getNotes()).isNotNull(); + } + + private static void assertComponentTotalsConsistent(FeeEstimateResponse response) { + // TODO adjust when NetworkService.getFeeEstimate has actual implementation + var network = response.getNetworkFee(); + var node = response.getNodeFee(); + var service = response.getServiceFee(); + + var nodeSubtotal = subtotal(node); + var serviceSubtotal = subtotal(service); + + assertThat(network.getSubtotal()).isEqualTo(nodeSubtotal * network.getMultiplier()); + assertThat(response.getTotal()).isEqualTo(network.getSubtotal() + nodeSubtotal + serviceSubtotal); + } } diff --git a/sdk/src/testIntegration/java/module-info.java b/sdk/src/testIntegration/java/module-info.java index 921a0eb751..d4f45c82e7 100644 --- a/sdk/src/testIntegration/java/module-info.java +++ b/sdk/src/testIntegration/java/module-info.java @@ -2,6 +2,8 @@ module com.hedera.hashgraph.sdk.test.integration { requires com.hedera.hashgraph.sdk; requires com.esaulpaugh.headlong; + requires io.grpc.inprocess; + requires io.grpc; requires org.assertj.core; requires org.bouncycastle.provider; requires org.junit.jupiter.api; From 8361b298a71982df4c101efd912cde2d47e25d93 Mon Sep 17 00:00:00 2001 From: emiliyank Date: Mon, 10 Nov 2025 15:28:47 +0200 Subject: [PATCH 4/4] add test for INVALID_ARGUMENT available with mirror node 0.142 Signed-off-by: emiliyank --- .../FeeEstimateQueryIntegrationTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java index 6729ad40e4..efb9365690 100644 --- a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java +++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/FeeEstimateQueryIntegrationTest.java @@ -2,8 +2,12 @@ package com.hedera.hashgraph.sdk.test.integration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.google.protobuf.ByteString; import com.hedera.hashgraph.sdk.*; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; @@ -270,6 +274,31 @@ void topicMessageSubmitMultipleChunkFeeEstimate() throws Throwable { } } + @Test + @DisplayName( + "Given a FeeEstimateQuery with a malformed transaction, when the query is executed, then it returns an INVALID_ARGUMENT error and does not retry") + void malformedTransactionReturnsInvalidArgumentError() throws Throwable { + try (var testEnv = new IntegrationTestEnv(1)) { + + // Given: A malformed transaction payload (invalid signed bytes) + ByteString invalidBytes = ByteString.copyFrom(new byte[] {0x00, 0x01, 0x02, 0x03}); + var malformedTransaction = com.hedera.hashgraph.sdk.proto.Transaction.newBuilder() + .setSignedTransactionBytes(invalidBytes) + .build(); + + waitForMirrorNodeSync(); + + // When/Then: Executing the fee estimate query should throw INVALID_ARGUMENT + assertThatThrownBy(() -> new FeeEstimateQuery() + .setTransaction(malformedTransaction) + .setMode(FeeEstimateMode.STATE) + .execute(testEnv.client)) + .isInstanceOf(StatusRuntimeException.class) + .extracting(ex -> ((StatusRuntimeException) ex).getStatus().getCode()) + .isEqualTo(Status.Code.INVALID_ARGUMENT); + } + } + private static void waitForMirrorNodeSync() throws InterruptedException { Thread.sleep(MIRROR_SYNC_DELAY_MILLIS); }