Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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 {

Check warning on line 54 in examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

examples/src/main/java/com/hedera/hashgraph/sdk/examples/FeeEstimateQueryExample.java#L54

Method FeeEstimateQueryExample::main has 84 lines of code (limit is 50)
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.getNetworkFee().getMultiplier());
System.out.println(" Subtotal: " + stateEstimate.getNetworkFee().getSubtotal() + " tinycents");

// Node fee breakdown
System.out.println("\nNode Fee:");
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();
}
System.out.println(" Node Total: " + nodeTotal + " tinycents");

// Service fee breakdown
System.out.println("\nService Fee:");
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();
}
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.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));

/*
* 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!");
}
}

130 changes: 130 additions & 0 deletions sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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).
* <p>
* 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<FeeExtra> extras;

/**
* Constructor.
*
* @param base the base fee price in tinycents
* @param extras the list of extra fees
*/
FeeEstimate(long base, List<FeeExtra> 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<FeeExtra> 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<FeeExtra> 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);
}
}
56 changes: 56 additions & 0 deletions sdk/src/main/java/com/hedera/hashgraph/sdk/FeeEstimateMode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
package com.hedera.hashgraph.sdk;

/**
* Enum for the fee estimate mode.
* <p>
* Determines how the fee estimate is calculated for a transaction.
*/
public enum FeeEstimateMode {
/**
* Default mode: uses latest known state.
* <p>
* 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.
* <p>
* 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";
};
}
}
Loading