diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e4cc35449..fb216f3f3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -179,7 +179,7 @@ jobs:
uses: hiero-ledger/hiero-solo-action@4d42a74e8e644a2753f3bb7a2afa429305375b14 # v0.16
with:
installMirrorNode: true
- mirrorNodeVersion: v0.145.2
+ mirrorNodeVersion: v0.152.0
hieroVersion: v0.70.0-rc.2
- name: Build SDK
@@ -254,7 +254,7 @@ jobs:
installMirrorNode: true
dualMode: true
hieroVersion: v0.68.0
- mirrorNodeVersion: v0.142.0
+ mirrorNodeVersion: v0.152.0
- name: Build SDK
run: ./gradlew assemble
@@ -309,7 +309,7 @@ jobs:
uses: hiero-ledger/hiero-solo-action@4d42a74e8e644a2753f3bb7a2afa429305375b14 # v0.16
with:
installMirrorNode: true
- mirrorNodeVersion: v0.145.2
+ mirrorNodeVersion: v0.152.0
hieroVersion: v0.70.0-rc.2
- name: Build SDK
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 314bf0ded..5a239919e 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
@@ -110,7 +110,7 @@ private static FeeEstimateResponse estimateWithStateMode(Client client, Transfer
printNodeFee(stateEstimate);
printServiceFee(stateEstimate);
printTotalFee(stateEstimate);
- printNotes(stateEstimate);
+ System.out.println("\nHigh Volume Multiplier: " + stateEstimate.getHighVolumeMultiplier());
return stateEstimate;
}
@@ -148,15 +148,6 @@ private static void printTotalFee(FeeEstimateResponse estimate) {
System.out.println("Total Estimated Fee: " + Hbar.fromTinybars(estimate.getTotal() / 100));
}
- private static void printNotes(FeeEstimateResponse estimate) {
- if (!estimate.getNotes().isEmpty()) {
- System.out.println("\nNotes:");
- for (String note : estimate.getNotes()) {
- System.out.println(" - " + note);
- }
- }
- }
-
private static FeeEstimateResponse estimateWithIntrinsicMode(Client client, TransferTransaction tx)
throws Exception {
System.out.println("\n=== Estimating Fees with INTRINSIC Mode ===");
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 bec12a5f6..1fca0c4b5 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java
@@ -8,7 +8,7 @@
*/
public enum FeeEstimateMode {
/**
- * Default mode: uses latest known state.
+ * 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
@@ -17,7 +17,7 @@ public enum FeeEstimateMode {
STATE(0),
/**
- * Intrinsic mode: ignores state-dependent factors.
+ * Default mode: ignores state-dependent factors.
*
* This mode calculates fees based only on the intrinsic properties of
* the transaction itself, ignoring dynamic network conditions. This
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 f2c2e859d..d362c9c47 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateQuery.java
@@ -30,6 +30,7 @@ public class FeeEstimateQuery {
@Nullable
private com.hedera.hashgraph.sdk.proto.Transaction transaction = null;
+ private int highVolumeThrottle = 0;
private int maxAttempts = 10;
private Duration maxBackoff = Duration.ofSeconds(8L);
@@ -64,7 +65,7 @@ public FeeEstimateMode getMode() {
/**
* Set the mode for fee estimation.
*
- * Defaults to {@link FeeEstimateMode#STATE} if not set.
+ * Defaults to {@link FeeEstimateMode#INTRINSIC} if not set.
*
* @param mode the fee estimate mode
* @return {@code this}
@@ -75,6 +76,32 @@ public FeeEstimateQuery setMode(FeeEstimateMode mode) {
return this;
}
+ /**
+ * Extract the high-volume throttle utilization in basis points.
+ *
+ * @return the high-volume throttle value (0–10000)
+ */
+ public int getHighVolumeThrottle() {
+ return highVolumeThrottle;
+ }
+
+ /**
+ * Set the high-volume throttle utilization in basis points (0–10000, where 10000 = 100%).
+ *
+ * When non-zero, the mirror node returns a high-volume pricing multiplier
+ * in the response.
+ *
+ * @param highVolumeThrottle the throttle utilization in basis points
+ * @return {@code this}
+ */
+ public FeeEstimateQuery setHighVolumeThrottle(int highVolumeThrottle) {
+ if (highVolumeThrottle < 0 || highVolumeThrottle > 10000) {
+ throw new IllegalArgumentException("highVolumeThrottle must be between 0 and 10000");
+ }
+ this.highVolumeThrottle = highVolumeThrottle;
+ return this;
+ }
+
/**
* Extract the transaction to estimate fees for.
*
@@ -178,7 +205,7 @@ public FeeEstimateResponse execute(Client client) throws IOException, Interrupte
* @throws InterruptedException if the operation is interrupted
*/
public FeeEstimateResponse execute(Client client, Duration timeout) throws IOException, InterruptedException {
- var resolvedMode = mode != null ? mode : FeeEstimateMode.STATE;
+ var resolvedMode = mode != null ? mode : FeeEstimateMode.INTRINSIC;
var requestPayload = getRequestPayload();
var url = buildUrl(client, resolvedMode);
@@ -253,7 +280,7 @@ public CompletableFuture executeAsync(Client client) {
* @return the fee estimate response
*/
public CompletableFuture executeAsync(Client client, Duration timeout) {
- var resolvedMode = mode != null ? mode : FeeEstimateMode.STATE;
+ var resolvedMode = mode != null ? mode : FeeEstimateMode.INTRINSIC;
CompletableFuture returnFuture = new CompletableFuture<>();
executeAsync(client, timeout, resolvedMode, returnFuture, 1);
return returnFuture;
@@ -352,7 +379,11 @@ private byte[] getRequestPayload() {
private String buildUrl(Client client, FeeEstimateMode resolvedMode) {
// Keep mode casing consistent with JS SDK (uppercase)
- return client.getMirrorRestBaseUrl() + "/network/fees?mode=" + resolvedMode.toString();
+ String url = client.getMirrorRestBaseUrl() + "/network/fees?mode=" + resolvedMode.toString();
+ if (highVolumeThrottle > 0) {
+ url += "&high_volume_throttle=" + highVolumeThrottle;
+ }
+ return url;
}
private HttpRequest buildHttpRequest(String url, Duration timeout, byte[] payload) {
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 5d646a301..146f476ed 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateResponse.java
@@ -4,9 +4,6 @@
import com.google.common.base.MoreObjects;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -46,11 +43,12 @@ public final class FeeEstimateResponse {
private final FeeEstimate serviceFee;
/**
- * An array of strings for any caveats.
+ * The high-volume throttle multiplier returned by the mirror node.
*
- * For example: ["Fallback to worst-case due to missing state"]
+ * When non-zero high-volume throttle utilization is requested, this value
+ * will be greater than or equal to 1.
*/
- private final List notes;
+ private final long highVolumeMultiplier;
/**
* The sum of the network, node, and service subtotals in tinycents.
@@ -60,24 +58,24 @@ public final class FeeEstimateResponse {
/**
* Constructor.
*
- * @param mode the fee estimate mode used
- * @param networkFee the network fee component
- * @param nodeFee the node fee estimate
- * @param notes the list of notes/caveats
- * @param serviceFee the service fee estimate
- * @param total the total fee in tinycents
+ * @param mode the fee estimate mode used
+ * @param networkFee the network fee component
+ * @param nodeFee the node fee estimate
+ * @param highVolumeMultiplier the high-volume throttle multiplier
+ * @param serviceFee the service fee estimate
+ * @param total the total fee in tinycents
*/
FeeEstimateResponse(
FeeEstimateMode mode,
@Nullable NetworkFee networkFee,
@Nullable FeeEstimate nodeFee,
- List notes,
+ long highVolumeMultiplier,
@Nullable FeeEstimate serviceFee,
long total) {
this.mode = mode;
this.networkFee = networkFee;
this.nodeFee = nodeFee;
- this.notes = Collections.unmodifiableList(new ArrayList<>(notes));
+ this.highVolumeMultiplier = highVolumeMultiplier;
this.serviceFee = serviceFee;
this.total = total;
}
@@ -96,7 +94,7 @@ static FeeEstimateResponse fromJson(String json, FeeEstimateMode defaultMode) {
parseModeFromJson(root, defaultMode),
parseNetworkFeeFromJson(root),
parseFeeEstimateFromJson(root, "node"),
- parseNotesFromJson(root),
+ parseHighVolumeMultiplierFromJson(root),
parseFeeEstimateFromJson(root, "service"),
parseTotalFromJson(root));
}
@@ -148,17 +146,15 @@ private static FeeEstimate parseFeeEstimateFromJson(JsonObject root, String fiel
}
/**
- * Parse notes from JSON.
+ * Parse high-volume multiplier from JSON.
*
* @param root the JSON object
- * @return the list of notes
+ * @return the high-volume multiplier value, or 0 if not present
*/
- private static List parseNotesFromJson(JsonObject root) {
- List notes = new ArrayList<>();
- if (root.has("notes") && root.get("notes").isJsonArray()) {
- root.getAsJsonArray("notes").forEach(element -> notes.add(element.getAsString()));
- }
- return notes;
+ private static long parseHighVolumeMultiplierFromJson(JsonObject root) {
+ return root.has("high_volume_multiplier")
+ ? root.get("high_volume_multiplier").getAsLong()
+ : 0L;
}
/**
@@ -201,12 +197,12 @@ public FeeEstimate getNodeFee() {
}
/**
- * Extract the list of notes/caveats.
+ * Extract the high-volume throttle multiplier.
*
- * @return an unmodifiable list of notes
+ * @return the high-volume multiplier
*/
- public List getNotes() {
- return notes;
+ public long getHighVolumeMultiplier() {
+ return highVolumeMultiplier;
}
/**
@@ -234,7 +230,7 @@ public String toString() {
.add("mode", mode)
.add("network", networkFee)
.add("node", nodeFee)
- .add("notes", notes)
+ .add("highVolumeMultiplier", highVolumeMultiplier)
.add("service", serviceFee)
.add("total", total)
.toString();
@@ -250,14 +246,14 @@ public boolean equals(Object o) {
}
return total == that.total
&& mode == that.mode
+ && highVolumeMultiplier == that.highVolumeMultiplier
&& Objects.equals(networkFee, that.networkFee)
&& Objects.equals(nodeFee, that.nodeFee)
- && Objects.equals(notes, that.notes)
&& Objects.equals(serviceFee, that.serviceFee);
}
@Override
public int hashCode() {
- return Objects.hash(mode, networkFee, nodeFee, notes, serviceFee, total);
+ return Objects.hash(mode, networkFee, nodeFee, highVolumeMultiplier, serviceFee, 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 db2676990..1da82504a 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/FeeExtra.java
@@ -15,12 +15,12 @@ public final class FeeExtra {
/**
* The charged count of items as calculated by max(0, count - included).
*/
- private final int charged;
+ private final long charged;
/**
* The actual count of items received.
*/
- private final int count;
+ private final long count;
/**
* The fee price per unit in tinycents.
@@ -30,7 +30,7 @@ public final class FeeExtra {
/**
* The count of this "extra" that is included for free.
*/
- private final int included;
+ private final long included;
/**
* The unique name of this extra fee as defined in the fee schedule.
@@ -55,7 +55,7 @@ public final class FeeExtra {
* @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) {
+ FeeExtra(long charged, long count, long feePerUnit, long included, @Nullable String name, long subtotal) {
this.charged = charged;
this.count = count;
this.feePerUnit = feePerUnit;
@@ -71,10 +71,10 @@ public final class FeeExtra {
* @return the new FeeExtra
*/
static FeeExtra fromJson(com.google.gson.JsonObject feeExtra) {
- int charged = getInt(feeExtra, "charged");
- int count = getInt(feeExtra, "count");
+ long charged = getLong(feeExtra, "charged");
+ long count = getLong(feeExtra, "count");
long feePerUnit = getLong(feeExtra, "fee_per_unit");
- int included = getInt(feeExtra, "included");
+ long included = getLong(feeExtra, "included");
String name = feeExtra.has("name") && !feeExtra.get("name").isJsonNull()
? feeExtra.get("name").getAsString()
: null;
@@ -88,7 +88,7 @@ static FeeExtra fromJson(com.google.gson.JsonObject feeExtra) {
*
* @return the charged count of items
*/
- public int getCharged() {
+ public long getCharged() {
return charged;
}
@@ -97,7 +97,7 @@ public int getCharged() {
*
* @return the actual count of items
*/
- public int getCount() {
+ public long getCount() {
return count;
}
@@ -115,7 +115,7 @@ public long getFeePerUnit() {
*
* @return the count included for free
*/
- public int getIncluded() {
+ public long getIncluded() {
return included;
}
@@ -171,10 +171,6 @@ public int hashCode() {
return Objects.hash(charged, count, feePerUnit, included, name, subtotal);
}
- private static int getInt(com.google.gson.JsonObject object, String key) {
- return object.get(key).getAsInt();
- }
-
private static long getLong(com.google.gson.JsonObject object, String key) {
return object.get(key).getAsLong();
}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java
index c19a7c759..1655ad8c0 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/FeeEstimateQueryMockTest.java
@@ -94,6 +94,48 @@ void succeedsOnFirstAttempt() throws IOException, InterruptedException {
assertThat(response.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC);
assertThat(response.getTotal()).isEqualTo(3 * 10 + 10 + 20);
+ assertThat(response.getHighVolumeMultiplier()).isEqualTo(1);
+ assertThat(stub.requestCount()).isEqualTo(1);
+ assertThat(stub.getLastQueryParams()).doesNotContain("high_volume_throttle");
+ }
+
+ @Test
+ @DisplayName("Given a FeeEstimateQuery without explicit mode, it defaults to INTRINSIC")
+ void defaultsToIntrinsic() throws IOException, InterruptedException {
+ query.setTransaction(DUMMY_TRANSACTION);
+
+ stub.enqueue(new StubResponse(200, newSuccessResponse(FeeEstimateMode.INTRINSIC, 3, 10, 20)));
+
+ var response = query.execute(client);
+
+ assertThat(stub.getLastQueryParams()).contains("mode=INTRINSIC");
+ assertThat(response.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC);
+ }
+
+ @Test
+ @DisplayName("Given a FeeEstimateQuery with high volume throttle, it sends the parameter in the URL")
+ void sendsHighVolumeThrottle() throws IOException, InterruptedException {
+ query.setTransaction(DUMMY_TRANSACTION).setHighVolumeThrottle(5000);
+
+ stub.enqueue(new StubResponse(200, newSuccessResponse(FeeEstimateMode.INTRINSIC, 3, 10, 20)));
+
+ var response = query.execute(client);
+
+ assertThat(stub.getLastQueryParams()).contains("high_volume_throttle=5000");
+ assertThat(response.getHighVolumeMultiplier()).isGreaterThanOrEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("Given a FeeEstimateQuery receives HTTP 400, it does not retry")
+ void doesNotRetryOn400() {
+ query.setTransaction(DUMMY_TRANSACTION).setMaxAttempts(3);
+
+ stub.enqueue(new StubResponse(400, "bad request"));
+
+ org.assertj.core.api.Assertions.assertThatThrownBy(() -> query.execute(client))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("400");
+
assertThat(stub.requestCount()).isEqualTo(1);
}
@@ -107,11 +149,10 @@ private static String newSuccessResponse(
"network": {"multiplier": %d, "subtotal": %d},
"node": {"base": %d, "extras": []},
"service": {"base": %d, "extras": []},
- "notes": [],
+ "high_volume_multiplier": 1,
"total": %d
}
- """
- .formatted(mode, networkMultiplier, networkSubtotal, nodeBase, serviceBase, total);
+ """.formatted(mode, networkMultiplier, networkSubtotal, nodeBase, serviceBase, total);
}
private static final class StubResponse {
@@ -127,6 +168,7 @@ private static final class StubResponse {
private static final class StubMirrorRestServer {
private final Queue responses = new ArrayDeque<>();
private int observedRequests = 0;
+ private String lastQueryParams;
private HttpServer server;
private int port;
@@ -135,6 +177,7 @@ void start() throws IOException {
port = server.getAddress().getPort();
server.createContext("/api/v1/network/fees", exchange -> {
observedRequests++;
+ lastQueryParams = exchange.getRequestURI().getQuery();
var response = responses.poll();
assertThat(response)
.as("response should be queued before invoking network fee estimation")
@@ -176,6 +219,10 @@ int getPort() {
return port;
}
+ String getLastQueryParams() {
+ return lastQueryParams;
+ }
+
void verify() {
assertThat(responses)
.as("all queued responses should have been served")
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 1ba0894db..4458ed891 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
@@ -107,8 +107,8 @@ void transferTransactionIntrinsicModeFeeEstimate() throws Throwable {
@Test
@DisplayName(
- "Given a TransferTransaction without explicit mode, when fee estimate is requested, then STATE mode is used by default")
- void transferTransactionDefaultModeIsState() throws Throwable {
+ "Given a TransferTransaction without explicit mode, when fee estimate is requested, then INTRINSIC mode is used by default")
+ void transferTransactionDefaultModeIsIntrinsic() throws Throwable {
try (var testEnv = createFeeEstimateTestEnv()) {
var transaction = new TransferTransaction()
.addHbarTransfer(testEnv.operatorId, Hbar.fromTinybars(-1))
@@ -121,7 +121,31 @@ void transferTransactionDefaultModeIsState() throws Throwable {
var response = new FeeEstimateQuery().setTransaction(transaction).execute(testEnv.client);
assertFeeComponentsPresent(response);
- assertThat(response.getMode()).isEqualTo(FeeEstimateMode.STATE);
+ assertThat(response.getMode()).isEqualTo(FeeEstimateMode.INTRINSIC);
+ assertComponentTotalsConsistent(response);
+ }
+ }
+
+ @Test
+ @DisplayName(
+ "Given a TransferTransaction with high volume throttle, when fee estimate is requested, then high volume multiplier is returned")
+ void feeEstimateQueryWithHighVolumeThrottle() throws Throwable {
+ try (var testEnv = createFeeEstimateTestEnv()) {
+ 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)
+ .setHighVolumeThrottle(5000)
+ .execute(testEnv.client);
+
+ assertFeeComponentsPresent(response);
+ assertThat(response.getHighVolumeMultiplier()).isGreaterThanOrEqualTo(1);
assertComponentTotalsConsistent(response);
}
}
@@ -382,8 +406,8 @@ private static void assertFeeComponentsPresent(FeeEstimateResponse response) {
assertThat(response.getServiceFee().getBase()).isGreaterThanOrEqualTo(0);
assertThat(response.getServiceFee().getExtras()).isNotNull();
- // Notes and total
- assertThat(response.getNotes()).isNotNull();
+ // High volume multiplier and total
+ assertThat(response.getHighVolumeMultiplier()).isGreaterThanOrEqualTo(1);
assertThat(response.getTotal()).isGreaterThan(0);
}