com.hedera.hashgraph.sdk.Client Maven / Gradle / Ivy
Show all versions of sdk-full Show documentation
/*-
*
* Hedera Java SDK
*
* Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.hedera.hashgraph.sdk;
import static com.hedera.hashgraph.sdk.BaseNodeAddress.PORT_NODE_PLAIN;
import static com.hedera.hashgraph.sdk.BaseNodeAddress.PORT_NODE_TLS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.hedera.hashgraph.sdk.logger.LogLevel;
import com.hedera.hashgraph.sdk.logger.Logger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import javax.annotation.Nullable;
/**
* Managed client for use on the Hedera Hashgraph network.
*/
public final class Client implements AutoCloseable {
static final int DEFAULT_MAX_ATTEMPTS = 10;
static final Duration DEFAULT_MAX_BACKOFF = Duration.ofSeconds(8L);
static final Duration DEFAULT_MIN_BACKOFF = Duration.ofMillis(250L);
static final Duration DEFAULT_MAX_NODE_BACKOFF = Duration.ofHours(1L);
static final Duration DEFAULT_MIN_NODE_BACKOFF = Duration.ofSeconds(8L);
static final Duration DEFAULT_CLOSE_TIMEOUT = Duration.ofSeconds(30L);
static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofMinutes(2L);
static final Duration DEFAULT_GRPC_DEADLINE = Duration.ofSeconds(10L);
static final Duration DEFAULT_NETWORK_UPDATE_PERIOD = Duration.ofHours(24);
// Initial delay of 10 seconds before we update the network for the first time,
// so that this doesn't happen in unit tests.
static final Duration NETWORK_UPDATE_INITIAL_DELAY = Duration.ofSeconds(10);
private static final Hbar DEFAULT_MAX_QUERY_PAYMENT = new Hbar(1);
private static final String MAINNET = "mainnet";
private static final String TESTNET = "testnet";
private static final String PREVIEWNET = "previewnet";
final ExecutorService executor;
private final AtomicReference grpcDeadline = new AtomicReference(DEFAULT_GRPC_DEADLINE);
private final Set subscriptions = ConcurrentHashMap.newKeySet();
@Nullable
Hbar defaultMaxTransactionFee = null;
Hbar defaultMaxQueryPayment = DEFAULT_MAX_QUERY_PAYMENT;
Network network;
MirrorNetwork mirrorNetwork;
@Nullable
private Operator operator;
private Duration requestTimeout = DEFAULT_REQUEST_TIMEOUT;
private Duration closeTimeout = DEFAULT_CLOSE_TIMEOUT;
private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
private volatile Duration maxBackoff = DEFAULT_MAX_BACKOFF;
private volatile Duration minBackoff = DEFAULT_MIN_BACKOFF;
private boolean autoValidateChecksums = false;
private boolean defaultRegenerateTransactionId = true;
// If networkUpdatePeriod is null, any network updates in progress will not complete
@Nullable
private Duration networkUpdatePeriod;
@Nullable
private CompletableFuture networkUpdateFuture;
private Logger logger = new Logger(LogLevel.SILENT);
/**
* Constructor.
*
* @param executor the executor
* @param network the network
* @param mirrorNetwork the mirror network
*/
@VisibleForTesting
Client(
ExecutorService executor,
Network network,
MirrorNetwork mirrorNetwork,
@Nullable Duration networkUpdateInitialDelay,
@Nullable Duration networkUpdatePeriod
) {
this.executor = executor;
this.network = network;
this.mirrorNetwork = mirrorNetwork;
this.networkUpdatePeriod = networkUpdatePeriod;
scheduleNetworkUpdate(networkUpdateInitialDelay);
}
/**
* Extract the executor.
*
* @return the executor service
*/
static ExecutorService createExecutor() {
var threadFactory = new ThreadFactoryBuilder()
.setNameFormat("hedera-sdk-%d")
.setDaemon(true)
.build();
int nThreads = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
*
* Construct a client given a set of nodes.
* It is the responsibility of the caller to ensure that all nodes in the map are part of the
* same Hedera network. Failure to do so will result in undefined behavior.
* The client will load balance all requests to Hedera using a simple round-robin scheme to
* chose nodes to send transactions to. For one transaction, at most 1/3 of the nodes will be tried.
*
* @param networkMap the map of node IDs to node addresses that make up the network.
* @param executor runs the grpc requests asynchronously. Note that calling `close()` method on one of the
* clients will close the executor for all the other clients sharing this executor
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forNetwork(Map networkMap, ExecutorService executor) {
var network = Network.forNetwork(executor, networkMap);
var mirrorNetwork = MirrorNetwork.forNetwork(executor, new ArrayList<>());
return new Client(executor, network, mirrorNetwork, null, null);
}
/**
* Construct a client given a set of nodes.
*
* It is the responsibility of the caller to ensure that all nodes in the map are part of the
* same Hedera network. Failure to do so will result in undefined behavior.
*
*
The client will load balance all requests to Hedera using a simple round-robin scheme to
* chose nodes to send transactions to. For one transaction, at most 1/3 of the nodes will be tried.
*
* @param networkMap the map of node IDs to node addresses that make up the network.
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forNetwork(Map networkMap) {
var executor = createExecutor();
return forNetwork(networkMap, executor);
}
/**
* Set up the client for the selected network.
*
* @param name the selected network
* @return the configured client
*/
public static Client forName(String name) {
return switch (name) {
case MAINNET -> Client.forMainnet();
case TESTNET -> Client.forTestnet();
case PREVIEWNET -> Client.forPreviewnet();
default -> throw new IllegalArgumentException("Name must be one-of `mainnet`, `testnet`, or `previewnet`");
};
}
/**
* Construct a Hedera client pre-configured for Mainnet access.
*
* @param executor runs the grpc requests asynchronously. Note that calling `close()` method on one of the
* clients will close the executor for all the other clients sharing this executor
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forMainnet(ExecutorService executor) {
var network = Network.forMainnet(executor);
var mirrorNetwork = MirrorNetwork.forMainnet(executor);
return new Client(executor, network, mirrorNetwork, NETWORK_UPDATE_INITIAL_DELAY,
DEFAULT_NETWORK_UPDATE_PERIOD);
}
/**
* Construct a Hedera client pre-configured for Testnet
* access.
*
* @param executor runs the grpc requests asynchronously. Note that calling `close()` method on one of the
* clients will close the executor for all the other clients sharing this executor
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forTestnet(ExecutorService executor) {
var network = Network.forTestnet(executor);
var mirrorNetwork = MirrorNetwork.forTestnet(executor);
return new Client(executor, network, mirrorNetwork, NETWORK_UPDATE_INITIAL_DELAY,
DEFAULT_NETWORK_UPDATE_PERIOD);
}
/**
* Construct a Hedera client pre-configured for Preview Testnet
* nodes.
*
* @param executor runs the grpc requests asynchronously. Note that calling `close()` method on one of the
* clients will close the executor for all the other clients sharing this executor
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forPreviewnet(ExecutorService executor) {
var network = Network.forPreviewnet(executor);
var mirrorNetwork = MirrorNetwork.forPreviewnet(executor);
return new Client(executor, network, mirrorNetwork, NETWORK_UPDATE_INITIAL_DELAY,
DEFAULT_NETWORK_UPDATE_PERIOD);
}
/**
* Construct a Hedera client pre-configured for Mainnet access.
*
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forMainnet() {
var executor = createExecutor();
return forMainnet(executor);
}
/**
* Construct a Hedera client pre-configured for Testnet
* access.
*
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forTestnet() {
var executor = createExecutor();
return forTestnet(executor);
}
/**
* Construct a Hedera client pre-configured for Preview Testnet
* nodes.
*
* @return {@link com.hedera.hashgraph.sdk.Client}
*/
public static Client forPreviewnet() {
var executor = createExecutor();
return forPreviewnet(executor);
}
/**
* Configure a client based off the given JSON string.
*
* @param json The json string containing the client configuration
* @return {@link com.hedera.hashgraph.sdk.Client}
* @throws Exception if the config is incorrect
*/
public static Client fromConfig(String json) throws Exception {
return fromConfig(new StringReader(json));
}
/**
* Configure a client based off the given JSON reader.
*
* @param json The Reader containing the client configuration
* @return {@link com.hedera.hashgraph.sdk.Client}
* @throws Exception if the config is incorrect
*/
public static Client fromConfig(Reader json) throws Exception {
Config config = new Gson().fromJson(json, Config.class);
Client client;
if (config.network == null) {
throw new Exception("Network is not set in provided json object");
} else if (config.network.isJsonObject()) {
var networks = config.network.getAsJsonObject();
Map nodes = new HashMap<>(networks.size());
for (Map.Entry entry : networks.entrySet()) {
nodes.put(entry.getValue().toString().replace("\"", ""),
AccountId.fromString(entry.getKey().replace("\"", "")));
}
client = Client.forNetwork(nodes);
if (config.networkName != null) {
var networkNameString = config.networkName.getAsString();
try {
client.setNetworkName(NetworkName.fromString(networkNameString));
} catch (Exception ignored) {
throw new IllegalArgumentException("networkName in config was \"" + networkNameString
+ "\", expected either \"mainnet\", \"testnet\" or \"previewnet\"");
}
}
} else {
String networks = config.network.getAsString();
client = switch (networks) {
case MAINNET -> Client.forMainnet();
case TESTNET -> Client.forTestnet();
case PREVIEWNET -> Client.forPreviewnet();
default -> throw new JsonParseException("Illegal argument for network.");
};
}
if (config.operator != null) {
AccountId operatorAccount = AccountId.fromString(config.operator.accountId);
PrivateKey privateKey = PrivateKey.fromString(config.operator.privateKey);
client.setOperator(operatorAccount, privateKey);
}
//already set in previous set network if?
if (config.mirrorNetwork != null) {
if (config.mirrorNetwork.isJsonArray()) {
var mirrors = config.mirrorNetwork.getAsJsonArray();
List listMirrors = new ArrayList<>(mirrors.size());
for (var i = 0; i < mirrors.size(); i++) {
listMirrors.add(mirrors.get(i).getAsString().replace("\"", ""));
}
client.setMirrorNetwork(listMirrors);
} else {
String mirror = config.mirrorNetwork.getAsString();
switch (mirror) {
case MAINNET -> client.mirrorNetwork = MirrorNetwork.forMainnet(client.executor);
case TESTNET -> client.mirrorNetwork = MirrorNetwork.forTestnet(client.executor);
case PREVIEWNET -> client.mirrorNetwork = MirrorNetwork.forPreviewnet(client.executor);
default -> throw new JsonParseException("Illegal argument for mirrorNetwork.");
}
}
}
return client;
}
/**
* Configure a client based on a JSON file at the given path.
*
* @param fileName The string containing the file path
* @return {@link com.hedera.hashgraph.sdk.Client}
* @throws IOException if IO operations fail
*/
public static Client fromConfigFile(String fileName) throws Exception {
return fromConfigFile(new File(fileName));
}
/**
* Configure a client based on a JSON file.
*
* @param file The file containing the client configuration
* @return {@link com.hedera.hashgraph.sdk.Client}
* @throws IOException if IO operations fail
*/
public static Client fromConfigFile(File file) throws Exception {
return fromConfig(Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8));
}
/**
* Extract the mirror network node list.
*
* @return the list of mirror nodes
*/
synchronized public List getMirrorNetwork() {
return mirrorNetwork.getNetwork();
}
/**
* Set the mirror network nodes.
*
* @param network list of network nodes
* @return {@code this}
* @throws InterruptedException when a thread is interrupted while it's waiting, sleeping, or otherwise occupied
*/
public synchronized Client setMirrorNetwork(List network) throws InterruptedException {
try {
this.mirrorNetwork.setNetwork(network);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
return this;
}
private synchronized void scheduleNetworkUpdate(@Nullable Duration delay) {
if (delay == null) {
networkUpdateFuture = null;
return;
}
networkUpdateFuture = Delayer.delayFor(delay.toMillis(), executor);
networkUpdateFuture.thenRun(() -> {
// Checking networkUpdatePeriod != null must be synchronized, so I've put it in a synchronized method.
requireNetworkUpdatePeriodNotNull(() -> {
new AddressBookQuery().setFileId(FileId.ADDRESS_BOOK).executeAsync(this)
.thenCompose(addressBook -> requireNetworkUpdatePeriodNotNull(() -> {
try {
this.setNetworkFromAddressBook(addressBook);
} catch (Throwable error) {
return CompletableFuture.failedFuture(error);
}
return CompletableFuture.completedFuture(null);
})).exceptionally(error -> {
logger.warn("Failed to update address book via mirror node query ", error);
return null;
});
scheduleNetworkUpdate(networkUpdatePeriod);
return null;
});
});
}
private synchronized CompletionStage> requireNetworkUpdatePeriodNotNull(Supplier> task) {
return networkUpdatePeriod != null ? task.get() : CompletableFuture.completedFuture(null);
}
private void cancelScheduledNetworkUpdate() {
if (networkUpdateFuture != null) {
networkUpdateFuture.cancel(true);
}
}
private void cancelAllSubscriptions() {
subscriptions.forEach(SubscriptionHandle::unsubscribe);
}
void trackSubscription(SubscriptionHandle subscriptionHandle) {
subscriptions.add(subscriptionHandle);
}
void untrackSubscription(SubscriptionHandle subscriptionHandle) {
subscriptions.remove(subscriptionHandle);
}
/**
* Replace all nodes in this Client with the nodes in the Address Book
* and update the address book if necessary.
*
* @param addressBook A list of nodes and their metadata
* @param updateAddressBook whether to update the address book of the network
* @return {@code this}
*/
public synchronized Client setNetworkFromAddressBook(NodeAddressBook addressBook, boolean updateAddressBook)
throws InterruptedException, TimeoutException {
network.setNetwork(Network.addressBookToNetwork(
addressBook.nodeAddresses,
isTransportSecurity() ? PORT_NODE_TLS : PORT_NODE_PLAIN
));
if (updateAddressBook) {
network.setAddressBook(addressBook);
}
return this;
}
/**
* Replace all nodes in this Client with the nodes in the Address Book
*
* @param addressBook A list of nodes and their metadata
* @return {@code this}
* @throws InterruptedException when a thread is interrupted while it's waiting, sleeping, or otherwise occupied
* @throws TimeoutException when shutting down nodes
*/
public synchronized Client setNetworkFromAddressBook(NodeAddressBook addressBook)
throws InterruptedException, TimeoutException {
return setNetworkFromAddressBook(addressBook, false);
}
/**
* Extract the network.
*
* @return the client's network
*/
synchronized public Map getNetwork() {
return network.getNetwork();
}
/**
* Replace all nodes in this Client with a new set of nodes (e.g. for an Address Book update).
*
* @param network a map of node account ID to node URL.
* @return {@code this} for fluent API usage.
* @throws TimeoutException when shutting down nodes
* @throws InterruptedException when a thread is interrupted while it's waiting, sleeping, or otherwise occupied
*/
public synchronized Client setNetwork(Map network)
throws InterruptedException, TimeoutException {
this.network.setNetwork(network);
return this;
}
/**
* Set if transport security should be used to connect to mirror nodes.
*
* If transport security is enabled all connections to mirror nodes will use TLS.
*
* @param transportSecurity - enable or disable transport security for mirror nodes
* @return {@code this} for fluent API usage.
* @deprecated Mirror nodes can only be accessed using TLS
*/
@Deprecated
public Client setMirrorTransportSecurity(boolean transportSecurity) {
return this;
}
/**
* Is tls enabled for consensus nodes.
*
* @return is tls enabled
*/
public boolean isTransportSecurity() {
return network.isTransportSecurity();
}
/**
* Set if transport security should be used to connect to consensus nodes.
*
* If transport security is enabled all connections to consensus nodes will use TLS, and the server's certificate
* hash will be compared to the hash stored in the {@link NodeAddressBook} for the given network.
*
* *Note*: If transport security is enabled, but {@link Client#isVerifyCertificates()} is disabled then server
* certificates will not be verified.
*
* @param transportSecurity enable or disable transport security for consensus nodes
* @return {@code this} for fluent API usage.
* @throws InterruptedException when a thread is interrupted while it's waiting, sleeping, or otherwise occupied
*/
public Client setTransportSecurity(boolean transportSecurity) throws InterruptedException {
network.setTransportSecurity(transportSecurity);
return this;
}
/**
* Is tls enabled for mirror nodes.
*
* @return is tls enabled
*/
public boolean mirrorIsTransportSecurity() {
return mirrorNetwork.isTransportSecurity();
}
/**
* Is certificate verification enabled.
*
* @return is certificate verification enabled
*/
public boolean isVerifyCertificates() {
return network.isVerifyCertificates();
}
/**
* Set if server certificates should be verified against an existing address book
*
* @param verifyCertificates - enable or disable certificate verification
* @return {@code this}
*/
public Client setVerifyCertificates(boolean verifyCertificates) {
network.setVerifyCertificates(verifyCertificates);
return this;
}
/**
* Send a ping to the given node.
*
* @param nodeAccountId Account ID of the node to ping
* @throws TimeoutException when the transaction times out
* @throws PrecheckStatusException when the precheck fails
*/
public Void ping(AccountId nodeAccountId) throws PrecheckStatusException, TimeoutException {
return ping(nodeAccountId, getRequestTimeout());
}
/**
* Send a ping to the given node.
*
* @param nodeAccountId Account ID of the node to ping
* @param timeout The timeout after which the execution attempt will be cancelled.
* @throws TimeoutException when the transaction times out
* @throws PrecheckStatusException when the precheck fails
*/
public Void ping(AccountId nodeAccountId, Duration timeout) throws PrecheckStatusException, TimeoutException {
new AccountBalanceQuery()
.setAccountId(nodeAccountId)
.setNodeAccountIds(Collections.singletonList(nodeAccountId))
.execute(this, timeout);
return null;
}
/**
* Send a ping to the given node asynchronously.
*
* @param nodeAccountId Account ID of the node to ping
* @return an empty future that throws exception if there was an error
*/
public CompletableFuture pingAsync(AccountId nodeAccountId) {
return pingAsync(nodeAccountId, getRequestTimeout());
}
/**
* Send a ping to the given node asynchronously.
*
* @param nodeAccountId Account ID of the node to ping
* @param timeout The timeout after which the execution attempt will be cancelled.
* @return an empty future that throws exception if there was an error
*/
public CompletableFuture pingAsync(AccountId nodeAccountId, Duration timeout) {
var result = new CompletableFuture();
new AccountBalanceQuery()
.setAccountId(nodeAccountId)
.setNodeAccountIds(Collections.singletonList(nodeAccountId))
.executeAsync(this, timeout)
.whenComplete((balance, error) -> {
if (error == null) {
result.complete(null);
} else {
result.completeExceptionally(error);
}
});
return result;
}
/**
* Send a ping to the given node asynchronously.
*
* @param nodeAccountId Account ID of the node to ping
* @param callback a BiConsumer which handles the result or error.
*/
public void pingAsync(AccountId nodeAccountId, BiConsumer callback) {
ConsumerHelper.biConsumer(pingAsync(nodeAccountId), callback);
}
/**
* Send a ping to the given node asynchronously.
*
* @param nodeAccountId Account ID of the node to ping
* @param timeout The timeout after which the execution attempt will be cancelled.
* @param callback a BiConsumer which handles the result or error.
*/
public void pingAsync(AccountId nodeAccountId, Duration timeout, BiConsumer callback) {
ConsumerHelper.biConsumer(pingAsync(nodeAccountId, timeout), callback);
}
/**
* Send a ping to the given node asynchronously.
*
* @param nodeAccountId Account ID of the node to ping
* @param onSuccess a Consumer which consumes the result on success.
* @param onFailure a Consumer which consumes the error on failure.
*/
public void pingAsync(AccountId nodeAccountId, Consumer onSuccess, Consumer onFailure) {
ConsumerHelper.twoConsumers(pingAsync(nodeAccountId), onSuccess, onFailure);
}
/**
* Send a ping to the given node asynchronously.
*
* @param nodeAccountId Account ID of the node to ping
* @param timeout The timeout after which the execution attempt will be cancelled.
* @param onSuccess a Consumer which consumes the result on success.
* @param onFailure a Consumer which consumes the error on failure.
*/
public void pingAsync(AccountId nodeAccountId, Duration timeout, Consumer onSuccess,
Consumer onFailure) {
ConsumerHelper.twoConsumers(pingAsync(nodeAccountId, timeout), onSuccess, onFailure);
}
/**
* Sends pings to all nodes in the client's network. Combines well with setMaxAttempts(1) to remove all dead nodes
* from the network.
*
* @throws TimeoutException when the transaction times out
* @throws PrecheckStatusException when the precheck fails
*/
public synchronized Void pingAll() throws PrecheckStatusException, TimeoutException {
return pingAll(getRequestTimeout());
}
/**
* Sends pings to all nodes in the client's network. Combines well with setMaxAttempts(1) to remove all dead nodes
* from the network.
*
* @param timeoutPerPing The timeout after which each execution attempt will be cancelled.
* @throws TimeoutException when the transaction times out
* @throws PrecheckStatusException when the precheck fails
*/
public synchronized Void pingAll(Duration timeoutPerPing) throws PrecheckStatusException, TimeoutException {
for (var nodeAccountId : network.getNetwork().values()) {
ping(nodeAccountId, timeoutPerPing);
}
return null;
}
/**
* Sends pings to all nodes in the client's network asynchronously. Combines well with setMaxAttempts(1) to remove
* all dead nodes from the network.
*
* @return an empty future that throws exception if there was an error
*/
public synchronized CompletableFuture pingAllAsync() {
return pingAllAsync(getRequestTimeout());
}
/**
* Sends pings to all nodes in the client's network asynchronously. Combines well with setMaxAttempts(1) to remove
* all dead nodes from the network.
*
* @param timeoutPerPing The timeout after which each execution attempt will be cancelled.
* @return an empty future that throws exception if there was an error
*/
public synchronized CompletableFuture pingAllAsync(Duration timeoutPerPing) {
var network = this.network.getNetwork();
var list = new ArrayList>(network.size());
for (var nodeAccountId : network.values()) {
list.add(pingAsync(nodeAccountId, timeoutPerPing));
}
return CompletableFuture.allOf(list.toArray(new CompletableFuture>[0])).thenApply((v) -> null);
}
/**
* Sends pings to all nodes in the client's network asynchronously. Combines well with setMaxAttempts(1) to remove
* all dead nodes from the network.
*
* @param callback a BiConsumer which handles the result or error.
*/
public void pingAllAsync(BiConsumer callback) {
ConsumerHelper.biConsumer(pingAllAsync(), callback);
}
/**
* Sends pings to all nodes in the client's network asynchronously. Combines well with setMaxAttempts(1) to remove
* all dead nodes from the network.
*
* @param timeoutPerPing The timeout after which each execution attempt will be cancelled.
* @param callback a BiConsumer which handles the result or error.
*/
public void pingAllAsync(Duration timeoutPerPing, BiConsumer callback) {
ConsumerHelper.biConsumer(pingAllAsync(timeoutPerPing), callback);
}
/**
* Sends pings to all nodes in the client's network asynchronously. Combines well with setMaxAttempts(1) to remove
* all dead nodes from the network.
*
* @param onSuccess a Consumer which consumes the result on success.
* @param onFailure a Consumer which consumes the error on failure.
*/
public void pingAllAsync(Consumer onSuccess, Consumer onFailure) {
ConsumerHelper.twoConsumers(pingAllAsync(), onSuccess, onFailure);
}
/**
* Sends pings to all nodes in the client's network asynchronously. Combines well with setMaxAttempts(1) to remove
* all dead nodes from the network.
*
* @param timeoutPerPing The timeout after which each execution attempt will be cancelled.
* @param onSuccess a Consumer which consumes the result on success.
* @param onFailure a Consumer which consumes the error on failure.
*/
public void pingAllAsync(Duration timeoutPerPing, Consumer onSuccess, Consumer onFailure) {
ConsumerHelper.twoConsumers(pingAllAsync(timeoutPerPing), onSuccess, onFailure);
}
/**
* Set the account that will, by default, be paying for transactions and queries built with this client.
*
* The operator account ID is used to generate the default transaction ID for all transactions executed with this
* client.
*
* The operator private key is used to sign all transactions executed by this client.
*
* @param accountId The AccountId of the operator
* @param privateKey The PrivateKey of the operator
* @return {@code this}
*/
public synchronized Client setOperator(AccountId accountId, PrivateKey privateKey) {
return setOperatorWith(accountId, privateKey.getPublicKey(), privateKey::sign);
}
/**
* Sets the account that will, by default, by paying for transactions and queries built with this client.
*
* The operator account ID is used to generate a default transaction ID for all transactions executed with this
* client.
*
* The `transactionSigner` is invoked to sign all transactions executed by this client.
*
* @param accountId The AccountId of the operator
* @param publicKey The PrivateKey of the operator
* @param transactionSigner The signer for the operator
* @return {@code this}
*/
public synchronized Client setOperatorWith(AccountId accountId, PublicKey publicKey,
UnaryOperator transactionSigner) {
if (getNetworkName() != null) {
try {
accountId.validateChecksum(this);
} catch (BadEntityIdException exc) {
throw new IllegalArgumentException(
"Tried to set the client operator account ID to an account ID with an invalid checksum: "
+ exc.getMessage()
);
}
}
this.operator = new Operator(accountId, publicKey, transactionSigner);
return this;
}
/**
* Current name of the network; corresponds to ledger ID in entity ID checksum calculations.
*
* @return the network name
* @deprecated use {@link #getLedgerId()} instead
*/
@Nullable
@Deprecated
public synchronized NetworkName getNetworkName() {
var ledgerId = network.getLedgerId();
return ledgerId == null ? null : ledgerId.toNetworkName();
}
/**
* Set the network name to a particular value. Useful when constructing a network which is a subset of an existing
* known network.
*
* @param networkName the desired network
* @return {@code this}
* @deprecated use {@link #setLedgerId(LedgerId)} instead
*/
@Deprecated
public synchronized Client setNetworkName(@Nullable NetworkName networkName) {
this.network.setLedgerId(networkName == null ? null : LedgerId.fromNetworkName(networkName));
return this;
}
/**
* Current LedgerId of the network; corresponds to ledger ID in entity ID checksum calculations.
*
* @return the ledger id
*/
@Nullable
public synchronized LedgerId getLedgerId() {
return network.getLedgerId();
}
/**
* Set the LedgerId to a particular value. Useful when constructing a network which is a subset of an existing known
* network.
*
* @param ledgerId the desired ledger id
* @return {@code this}
*/
public synchronized Client setLedgerId(@Nullable LedgerId ledgerId) {
this.network.setLedgerId(ledgerId);
return this;
}
/**
* Max number of attempts a request executed with this client will do.
*
* @return the maximus attempts
*/
public synchronized int getMaxAttempts() {
return maxAttempts;
}
/**
* Set the max number of attempts a request executed with this client will do.
*
* @param maxAttempts the desired max attempts
* @return {@code this}
*/
public synchronized Client setMaxAttempts(int maxAttempts) {
if (maxAttempts <= 0) {
throw new IllegalArgumentException("maxAttempts must be greater than zero");
}
this.maxAttempts = maxAttempts;
return this;
}
/**
* The maximum amount of time to wait between retries
*
* @return maxBackoff
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP",
justification = "A Duration can't actually be mutated"
)
public Duration getMaxBackoff() {
return maxBackoff;
}
/**
* The maximum amount of time to wait between retries. Every retry attempt will increase the wait time exponentially
* until it reaches this time.
*
* @param maxBackoff The maximum amount of time to wait between retries
* @return {@code this}
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP2",
justification = "A Duration can't actually be mutated"
)
public Client setMaxBackoff(Duration maxBackoff) {
if (maxBackoff == null || maxBackoff.toNanos() < 0) {
throw new IllegalArgumentException("maxBackoff must be a positive duration");
} else if (maxBackoff.compareTo(minBackoff) < 0) {
throw new IllegalArgumentException("maxBackoff must be greater than or equal to minBackoff");
}
this.maxBackoff = maxBackoff;
return this;
}
/**
* The minimum amount of time to wait between retries
*
* @return minBackoff
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP",
justification = "A Duration can't actually be mutated"
)
public Duration getMinBackoff() {
return minBackoff;
}
/**
* The minimum amount of time to wait between retries. When retrying, the delay will start at this time and increase
* exponentially until it reaches the maxBackoff.
*
* @param minBackoff The minimum amount of time to wait between retries
* @return {@code this}
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP2",
justification = "A Duration can't actually be mutated"
)
public Client setMinBackoff(Duration minBackoff) {
if (minBackoff == null || minBackoff.toNanos() < 0) {
throw new IllegalArgumentException("minBackoff must be a positive duration");
} else if (minBackoff.compareTo(maxBackoff) > 0) {
throw new IllegalArgumentException("minBackoff must be less than or equal to maxBackoff");
}
this.minBackoff = minBackoff;
return this;
}
/**
* Max number of times any node in the network can receive a bad gRPC status before being removed from the network.
*
* @return the maximum node attempts
*/
public synchronized int getMaxNodeAttempts() {
return network.getMaxNodeAttempts();
}
/**
* Set the max number of times any node in the network can receive a bad gRPC status before being removed from the
* network.
*
* @param maxNodeAttempts the desired minimum attempts
* @return {@code this}
*/
public synchronized Client setMaxNodeAttempts(int maxNodeAttempts) {
this.network.setMaxNodeAttempts(maxNodeAttempts);
return this;
}
/**
* The minimum backoff time for any node in the network.
*
* @return the wait time
* @deprecated - Use {@link Client#getNodeMaxBackoff()} instead
*/
@Deprecated
public synchronized Duration getNodeWaitTime() {
return getNodeMinBackoff();
}
/**
* Set the minimum backoff time for any node in the network.
*
* @param nodeWaitTime the wait time
* @return the updated client
* @deprecated - Use {@link Client#setNodeMinBackoff(Duration)} ()} instead
*/
@Deprecated
public synchronized Client setNodeWaitTime(Duration nodeWaitTime) {
return setNodeMinBackoff(nodeWaitTime);
}
/**
* The minimum backoff time for any node in the network.
*
* @return the minimum backoff time
*/
public synchronized Duration getNodeMinBackoff() {
return network.getMinNodeBackoff();
}
/**
* Set the minimum backoff time for any node in the network.
*
* @param minBackoff the desired minimum backoff time
* @return {@code this}
*/
public synchronized Client setNodeMinBackoff(Duration minBackoff) {
network.setMinNodeBackoff(minBackoff);
return this;
}
/**
* The maximum backoff time for any node in the network.
*
* @return the maximum node backoff time
*/
public synchronized Duration getNodeMaxBackoff() {
return network.getMaxNodeBackoff();
}
/**
* Set the maximum backoff time for any node in the network.
*
* @param maxBackoff the desired max backoff time
* @return {@code this}
*/
public synchronized Client setNodeMaxBackoff(Duration maxBackoff) {
network.setMaxNodeBackoff(maxBackoff);
return this;
}
/**
* Extract the minimum node readmit time.
*
* @return the minimum node readmit time
*/
public Duration getMinNodeReadmitTime() {
return network.getMinNodeReadmitTime();
}
/**
* Assign the minimum node readmit time.
*
* @param minNodeReadmitTime the requested duration
* @return {@code this}
*/
public Client setMinNodeReadmitTime(Duration minNodeReadmitTime) {
network.setMinNodeReadmitTime(minNodeReadmitTime);
return this;
}
/**
* Extract the node readmit time.
*
* @return the maximum node readmit time
*/
public Duration getMaxNodeReadmitTime() {
return network.getMaxNodeReadmitTime();
}
/**
* Assign the maximum node readmit time.
*
* @param maxNodeReadmitTime the maximum node readmit time
* @return {@code this}
*/
public Client setMaxNodeReadmitTime(Duration maxNodeReadmitTime) {
network.setMaxNodeReadmitTime(maxNodeReadmitTime);
return this;
}
/**
* Set the max amount of nodes that will be chosen per request. By default, the request will use 1/3rd the network
* nodes per request.
*
* @param maxNodesPerTransaction the desired number of nodes
* @return {@code this}
*/
public synchronized Client setMaxNodesPerTransaction(int maxNodesPerTransaction) {
this.network.setMaxNodesPerRequest(maxNodesPerTransaction);
return this;
}
/**
* Enable or disable automatic entity ID checksum validation.
*
* @param value the desired value
* @return {@code this}
*/
public synchronized Client setAutoValidateChecksums(boolean value) {
autoValidateChecksums = value;
return this;
}
/**
* Is automatic entity ID checksum validation enabled.
*
* @return is validation enabled
*/
public synchronized boolean isAutoValidateChecksumsEnabled() {
return autoValidateChecksums;
}
/**
* Get the ID of the operator. Useful when the client was constructed from file.
*
* @return {AccountId}
*/
@Nullable
public synchronized AccountId getOperatorAccountId() {
if (operator == null) {
return null;
}
return operator.accountId;
}
/**
* Get the key of the operator. Useful when the client was constructed from file.
*
* @return {PublicKey}
*/
@Nullable
public synchronized PublicKey getOperatorPublicKey() {
if (operator == null) {
return null;
}
return operator.publicKey;
}
/**
* The default maximum fee used for transactions.
*
* @return the max transaction fee
*/
@Nullable
public synchronized Hbar getDefaultMaxTransactionFee() {
return defaultMaxTransactionFee;
}
/**
* Set the maximum fee to be paid for transactions executed by this client.
*
* Because transaction fees are always maximums, this will simply add a call to
* {@link Transaction#setMaxTransactionFee(Hbar)} on every new transaction. The actual fee assessed for a given
* transaction may be less than this value, but never greater.
*
* @param defaultMaxTransactionFee The Hbar to be set
* @return {@code this}
*/
public synchronized Client setDefaultMaxTransactionFee(Hbar defaultMaxTransactionFee) {
Objects.requireNonNull(defaultMaxTransactionFee);
if (defaultMaxTransactionFee.toTinybars() < 0) {
throw new IllegalArgumentException("maxTransactionFee must be non-negative");
}
this.defaultMaxTransactionFee = defaultMaxTransactionFee;
return this;
}
/**
* Set the maximum fee to be paid for transactions executed by this client.
*
* Because transaction fees are always maximums, this will simply add a call to
* {@link Transaction#setMaxTransactionFee(Hbar)} on every new transaction. The actual fee assessed for a given
* transaction may be less than this value, but never greater.
*
* @param maxTransactionFee The Hbar to be set
* @return {@code this}
* @deprecated Use {@link #setDefaultMaxTransactionFee(Hbar)} instead.
*/
@Deprecated
public synchronized Client setMaxTransactionFee(Hbar maxTransactionFee) {
return setDefaultMaxTransactionFee(maxTransactionFee);
}
/**
* Extract the maximum query payment.
*
* @return the default maximum query payment
*/
public synchronized Hbar getDefaultMaxQueryPayment() {
return defaultMaxQueryPayment;
}
/**
* Set the maximum default payment allowable for queries.
*
* When a query is executed without an explicit {@link Query#setQueryPayment(Hbar)} call, the client will first
* request the cost of the given query from the node it will be submitted to and attach a payment for that amount
* from the operator account on the client.
*
* If the returned value is greater than this value, a {@link MaxQueryPaymentExceededException} will be thrown from
* {@link Query#execute(Client)} or returned in the second callback of
* {@link Query#executeAsync(Client, Consumer, Consumer)}.
*
* Set to 0 to disable automatic implicit payments.
*
* @param defaultMaxQueryPayment The Hbar to be set
* @return {@code this}
*/
public synchronized Client setDefaultMaxQueryPayment(Hbar defaultMaxQueryPayment) {
Objects.requireNonNull(defaultMaxQueryPayment);
if (defaultMaxQueryPayment.toTinybars() < 0) {
throw new IllegalArgumentException("defaultMaxQueryPayment must be non-negative");
}
this.defaultMaxQueryPayment = defaultMaxQueryPayment;
return this;
}
/**
* @param maxQueryPayment The Hbar to be set
* @return {@code this}
* @deprecated Use {@link #setDefaultMaxQueryPayment(Hbar)} instead.
*/
@Deprecated
public synchronized Client setMaxQueryPayment(Hbar maxQueryPayment) {
return setDefaultMaxQueryPayment(maxQueryPayment);
}
/**
* Should the transaction id be regenerated?
*
* @return the default regenerate transaction id
*/
public synchronized boolean getDefaultRegenerateTransactionId() {
return defaultRegenerateTransactionId;
}
/**
* Assign the default regenerate transaction id.
*
* @param regenerateTransactionId should there be a regenerated transaction id
* @return {@code this}
*/
public synchronized Client setDefaultRegenerateTransactionId(boolean regenerateTransactionId) {
this.defaultRegenerateTransactionId = regenerateTransactionId;
return this;
}
/**
* Maximum amount of time a request can run
*
* @return the timeout value
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP",
justification = "A Duration can't actually be mutated"
)
public synchronized Duration getRequestTimeout() {
return requestTimeout;
}
/**
* Set the maximum amount of time a request can run. Used only in async variants of methods.
*
* @param requestTimeout the timeout value
* @return {@code this}
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP2",
justification = "A Duration can't actually be mutated"
)
public synchronized Client setRequestTimeout(Duration requestTimeout) {
this.requestTimeout = Objects.requireNonNull(requestTimeout);
return this;
}
/**
* Maximum amount of time closing a network can take.
*
* @return the timeout value
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP",
justification = "A Duration can't actually be mutated"
)
public Duration getCloseTimeout() {
return closeTimeout;
}
/**
* Set the maximum amount of time closing a network can take.
*
* @param closeTimeout the timeout value
* @return {@code this}
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP2",
justification = "A Duration can't actually be mutated"
)
public Client setCloseTimeout(Duration closeTimeout) {
this.closeTimeout = Objects.requireNonNull(closeTimeout);
network.setCloseTimeout(closeTimeout);
mirrorNetwork.setCloseTimeout(closeTimeout);
return this;
}
/**
* Maximum amount of time a gRPC request can run
*
* @return the gRPC deadline value
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP",
justification = "A Duration can't actually be mutated"
)
public Duration getGrpcDeadline() {
return grpcDeadline.get();
}
/**
* Set the maximum amount of time a gRPC request can run.
*
* @param grpcDeadline the gRPC deadline value
* @return {@code this}
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP2",
justification = "A Duration can't actually be mutated"
)
public Client setGrpcDeadline(Duration grpcDeadline) {
this.grpcDeadline.set(Objects.requireNonNull(grpcDeadline));
return this;
}
/**
* Extract the operator.
*
* @return the operator
*/
@Nullable
synchronized Operator getOperator() {
return this.operator;
}
/**
* Get the period for updating the Address Book
*
* @return the networkUpdatePeriod
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP",
justification = "A Duration can't actually be mutated"
)
@Nullable
public synchronized Duration getNetworkUpdatePeriod() {
return this.networkUpdatePeriod;
}
/**
* Set the period for updating the Address Book
*
*
Note: This method requires API level 33 or higher. It will not work on devices running API versions below 31
* because it uses features introduced in API level 31 (Android 12).
*
*
* @param networkUpdatePeriod the period for updating the Address Book
* @return {@code this}
*/
@SuppressFBWarnings(
value = "EI_EXPOSE_REP2",
justification = "A Duration can't actually be mutated"
)
public synchronized Client setNetworkUpdatePeriod(Duration networkUpdatePeriod) {
cancelScheduledNetworkUpdate();
this.networkUpdatePeriod = networkUpdatePeriod;
scheduleNetworkUpdate(networkUpdatePeriod);
return this;
}
public Logger getLogger() {
return this.logger;
}
public Client setLogger(Logger logger) {
this.logger = logger;
return this;
}
/**
* Initiates an orderly shutdown of all channels (to the Hedera network) in which preexisting transactions or
* queries continue but more would be immediately cancelled.
*
* After this method returns, this client can be re-used. Channels will be re-established as
* needed.
*
* @throws TimeoutException if the mirror network doesn't close in time
*/
@Override
public synchronized void close() throws TimeoutException {
close(closeTimeout);
}
/**
* Initiates an orderly shutdown of all channels (to the Hedera network),
* without closing the ExecutorService {@link #executor}
*
* @throws TimeoutException if the network doesn't close in time
*/
public synchronized void closeChannels() throws TimeoutException {
var closeDeadline = Instant.now().plus(closeTimeout);
networkUpdatePeriod = null;
cancelScheduledNetworkUpdate();
cancelAllSubscriptions();
network.beginClose();
mirrorNetwork.beginClose();
var networkError = network.awaitClose(closeDeadline, null);
var mirrorNetworkError = mirrorNetwork.awaitClose(closeDeadline, networkError);
if (mirrorNetworkError != null) {
if (mirrorNetworkError instanceof TimeoutException ex) {
throw ex;
} else {
throw new RuntimeException(mirrorNetworkError);
}
}
}
/**
* Initiates an orderly shutdown of all channels (to the Hedera network) in which preexisting transactions or
* queries continue but more would be immediately cancelled.
*
*
After this method returns, this client can be re-used. Channels will be re-established as
* needed.
*
* @param timeout The Duration to be set
* @throws TimeoutException if the mirror network doesn't close in time
*/
public synchronized void close(Duration timeout) throws TimeoutException {
var closeDeadline = Instant.now().plus(timeout);
networkUpdatePeriod = null;
cancelScheduledNetworkUpdate();
cancelAllSubscriptions();
network.beginClose();
mirrorNetwork.beginClose();
var networkError = network.awaitClose(closeDeadline, null);
var mirrorNetworkError = mirrorNetwork.awaitClose(closeDeadline, networkError);
// https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
try {
executor.shutdown();
if (!executor.awaitTermination(timeout.getSeconds() / 2, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(timeout.getSeconds() / 2, TimeUnit.SECONDS)) {
logger.warn("Pool did not terminate");
}
}
} catch (InterruptedException ex) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
if (mirrorNetworkError != null) {
if (mirrorNetworkError instanceof TimeoutException ex) {
throw ex;
} else {
throw new RuntimeException(mirrorNetworkError);
}
}
}
static class Operator {
final AccountId accountId;
final PublicKey publicKey;
final UnaryOperator transactionSigner;
Operator(AccountId accountId, PublicKey publicKey, UnaryOperator transactionSigner) {
this.accountId = accountId;
this.publicKey = publicKey;
this.transactionSigner = transactionSigner;
}
}
private static class Config {
@Nullable
private JsonElement network;
@Nullable
private JsonElement networkName;
@Nullable
private ConfigOperator operator;
@Nullable
private JsonElement mirrorNetwork;
private static class ConfigOperator {
@Nullable
private String accountId;
@Nullable
private String privateKey;
}
}
}