All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hedera.hashgraph.sdk.Query Maven / Gradle / Ivy

The newest version!
/*-
 *
 * Hedera Java SDK
 *
 * Copyright (C) 2020 - 2022 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 com.google.common.base.MoreObjects;
import com.google.protobuf.InvalidProtocolBufferException;
import com.hedera.hashgraph.sdk.proto.QueryHeader;
import com.hedera.hashgraph.sdk.proto.Response;
import com.hedera.hashgraph.sdk.proto.ResponseHeader;
import com.hedera.hashgraph.sdk.proto.ResponseType;
import com.hedera.hashgraph.sdk.proto.Transaction;
import com.hedera.hashgraph.sdk.proto.TransactionBody;
import io.grpc.MethodDescriptor;
import java8.util.concurrent.CompletableFuture;
import java8.util.function.BiConsumer;
import java8.util.function.Consumer;
import org.threeten.bp.Duration;
import org.threeten.bp.Instant;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Base class for all queries that can be submitted to Hedera.
 *
 * @param  The output type of the query.
 * @param  The type of the query itself. Used to enable chaining.
 */
public abstract class Query> extends Executable {
    private final com.hedera.hashgraph.sdk.proto.Query.Builder builder;

    private final QueryHeader.Builder headerBuilder;

    /**
     * The transaction ID
     */
    @Nullable
    protected TransactionId paymentTransactionId = null;

    /**
     * List of payment transactions
     */
    @Nullable
    protected List paymentTransactions = null;
    @Nullable
    private Client.Operator paymentOperator = null;
    @Nullable
    private Hbar queryPayment = null;

    @Nullable
    private Hbar maxQueryPayment = null;

    @Nullable
    private Hbar chosenQueryPayment = null;

    /**
     * Constructor.
     */
    Query() {
        builder = com.hedera.hashgraph.sdk.proto.Query.newBuilder();
        headerBuilder = QueryHeader.newBuilder();
    }

    /**
     * Create a payment transaction.
     *
     * @param paymentTransactionId      the transaction id
     * @param nodeId                    the node id
     * @param operator                  the operator
     * @param paymentAmount             the amount
     * @return                          the new payment transaction
     */
    private static Transaction makePaymentTransaction(
        TransactionId paymentTransactionId,
        AccountId nodeId,
        Client.Operator operator,
        Hbar paymentAmount
    ) {
        return new TransferTransaction()
            .setTransactionId(paymentTransactionId)
            .setNodeAccountIds(Collections.singletonList(nodeId))
            .setMaxTransactionFee(new Hbar(1)) // 1 Hbar
            .addHbarTransfer(operator.accountId, paymentAmount.negated())
            .addHbarTransfer(nodeId, paymentAmount)
            .freeze()
            .signWith(operator.publicKey, operator.transactionSigner)
            .makeRequest();
    }

    /**
     * Set an explicit payment amount for this query.
     * 

* The client will submit exactly this amount for the payment of this query. Hedera * will not return any remainder. * * @param queryPayment The explicit payment amount to set * @return {@code this} */ public T setQueryPayment(Hbar queryPayment) { this.queryPayment = queryPayment; // noinspection unchecked return (T) this; } /** * Set the maximum payment allowable for this query. *

* 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 maxQueryPayment The maximum payment amount to set * @return {@code this} */ public T setMaxQueryPayment(Hbar maxQueryPayment) { this.maxQueryPayment = maxQueryPayment; // noinspection unchecked return (T) this; } /** * Fetch the expected cost. * * @param client the client * @return the cost in hbar * @throws TimeoutException when the transaction times out * @throws PrecheckStatusException when the precheck fails */ public Hbar getCost(Client client) throws TimeoutException, PrecheckStatusException { return getCost(client, client.getRequestTimeout()); } /** * Fetch the expected cost. * * @param client the client * @param timeout The timeout after which the execution attempt will be cancelled. * @return the cost in hbar * @throws TimeoutException when the transaction times out * @throws PrecheckStatusException when the precheck fails */ public Hbar getCost(Client client, Duration timeout) throws TimeoutException, PrecheckStatusException { initWithNodeIds(client); return getCostExecutable().setNodeAccountIds(Objects.requireNonNull(getNodeAccountIds())).execute(client, timeout); } /** * Fetch the expected cost asynchronously. * * @param client the client * @return Future result of the cost in hbar */ public CompletableFuture getCostAsync(Client client) { return getCostAsync(client, client.getRequestTimeout()); } /** * Fetch the expected cost asynchronously. * * @param client the client * @param timeout The timeout after which the execution attempt will be cancelled. * @return Future result of the cost in hbar */ public CompletableFuture getCostAsync(Client client, Duration timeout) { initWithNodeIds(client); return getCostExecutable().setNodeAccountIds(Objects.requireNonNull(getNodeAccountIds())).executeAsync(client, timeout); } /** * Fetch the expected cost asynchronously. * * @param client the client * @param timeout The timeout after which the execution attempt will be cancelled. * @param callback a BiConsumer which handles the result or error. */ public void getCostAsync(Client client, Duration timeout, BiConsumer callback) { ConsumerHelper.biConsumer(getCostAsync(client, timeout), callback); } /** * Fetch the expected cost asynchronously. * * @param client the client * @param callback a BiConsumer which handles the result or error. */ public void getCostAsync(Client client, BiConsumer callback) { ConsumerHelper.biConsumer(getCostAsync(client), callback); } /** * Fetch the expected cost asynchronously. * * @param client the client * @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 getCostAsync(Client client, Duration timeout, Consumer onSuccess, Consumer onFailure) { ConsumerHelper.twoConsumers(getCostAsync(client, timeout), onSuccess, onFailure); } /** * Fetch the expected cost asynchronously. * * @param client the client * @param onSuccess a Consumer which consumes the result on success. * @param onFailure a Consumer which consumes the error on failure. */ public void getCostAsync(Client client, Consumer onSuccess, Consumer onFailure) { ConsumerHelper.twoConsumers(getCostAsync(client), onSuccess, onFailure); } /** * Does this query require a payment? * * @return does this query require a payment */ boolean isPaymentRequired() { // nearly all queries require a payment return true; } /** * Called in {@link #makeRequest} just before the query is built. The intent is for the derived * class to assign their data variant to the query. */ abstract void onMakeRequest(com.hedera.hashgraph.sdk.proto.Query.Builder queryBuilder, QueryHeader header); /** * The derived class should access its response header and return. */ abstract ResponseHeader mapResponseHeader(Response response); /** * The derived class should access its request header and return. */ abstract QueryHeader mapRequestHeader(com.hedera.hashgraph.sdk.proto.Query request); /** * Crate the new Query. * * @return the new Query */ private Query getCostExecutable() { return new QueryCostQuery(); } /** * Validate the checksums. */ abstract void validateChecksums(Client client) throws BadEntityIdException; /** * Retrieve the operator from the configured client. * * @param client the configured client * @return the operator */ Client.Operator getOperatorFromClient(Client client) { var operator = client.getOperator(); if (operator == null) { throw new IllegalStateException( "`client` must have an `operator` or an explicit payment transaction must be provided"); } return operator; } @Override void onExecute(Client client) throws TimeoutException, PrecheckStatusException { var grpcCostQuery = new GrpcCostQuery(client); if (grpcCostQuery.isNotRequired()) { return; } if (grpcCostQuery.getCost() == null) { grpcCostQuery.setCost(getCost(client)); if (grpcCostQuery.shouldError()) { throw grpcCostQuery.mapError(); } } grpcCostQuery.finish(); } @Override CompletableFuture onExecuteAsync(Client client) { var grpcCostQuery = new GrpcCostQuery(client); if (grpcCostQuery.isNotRequired()) { return CompletableFuture.completedFuture(null); } return CompletableFuture.supplyAsync(() -> { if (grpcCostQuery.getCost() == null) { // No payment was specified so we need to go ask // This is a query in its own right so we use a nested future here return getCostAsync(client).thenCompose(cost -> { grpcCostQuery.setCost(cost); if (grpcCostQuery.shouldError()) { return CompletableFuture.failedFuture(grpcCostQuery.mapError()); } return CompletableFuture.completedFuture(null); }); } return CompletableFuture.completedFuture(null); }, client.executor) .thenCompose(x -> x) .thenAccept((paymentAmount) -> { grpcCostQuery.finish(); }); } private void initWithNodeIds(Client client) { if (client.isAutoValidateChecksumsEnabled()) { try { validateChecksums(client); } catch (BadEntityIdException exc) { throw new IllegalArgumentException(exc.getMessage()); } } if (nodeAccountIds.size() == 0) { // Get a list of node AccountId's if the user has not set them manually. try { nodeAccountIds.setList(client.network.getNodeAccountIdsForExecute()); } catch (InterruptedException e) { throw new RuntimeException(e); } } } /** * Retrieve the transaction at the given index. * * @param index the index * @return the transaction */ Transaction getPaymentTransaction(int index) { var paymentTx = Objects.requireNonNull(paymentTransactions).get(index); if (paymentTx != null) { return paymentTx; } else { if (paymentTransactionId == null) { paymentTransactionId = TransactionId.generate(Objects.requireNonNull(paymentOperator).accountId); } var newPaymentTx = makePaymentTransaction( paymentTransactionId, nodeAccountIds.get(index), paymentOperator, Objects.requireNonNull(chosenQueryPayment) ); paymentTransactions.set(index, newPaymentTx); return newPaymentTx; } } @Override final com.hedera.hashgraph.sdk.proto.Query makeRequest() { // If payment is required, set the next payment transaction on the query if (isPaymentRequired() && paymentTransactions != null) { headerBuilder.setPayment(getPaymentTransaction(nodeAccountIds.getIndex())); } // Delegate to the derived class to apply the header because the common header struct is // within the nested type onMakeRequest(builder, headerBuilder.setResponseType(ResponseType.ANSWER_ONLY).build()); return builder.build(); } @Override Status mapResponseStatus(Response response) { var preCheckCode = mapResponseHeader(response).getNodeTransactionPrecheckCode(); return Status.valueOf(preCheckCode); } @Override @Nullable TransactionId getTransactionIdInternal() { // this is only called on an error about either the payment transaction or missing a payment transaction // as we make sure the latter can't happen, this will never be null return paymentTransactionId; } /** * Extract the transaction id. * * @return the transaction id */ @Nullable public TransactionId getPaymentTransactionId() { return paymentTransactionId; } /** * Assign the transaction id. * * @param paymentTransactionId the transaction id * @return {@code this} */ @Nullable public T setPaymentTransactionId(TransactionId paymentTransactionId) { this.paymentTransactionId = paymentTransactionId; // noinspection unchecked return (T) this; } @Override @SuppressWarnings("LiteProtoToString") public String toString() { var request = makeRequest(); StringBuilder builder = new StringBuilder(request.toString().replaceAll("(?m)^# com.hedera.hashgraph.sdk.proto.Query.*", "")); var queryHeader = mapRequestHeader(request); if (queryHeader.hasPayment()) { builder.append("\n"); try { // the replaceAll() is for removing the class name from Transaction Body builder.append(TransactionBody.parseFrom(queryHeader.getPayment().getBodyBytes()).toString().replaceAll("(?m)^# com.hedera.hashgraph.sdk.proto.TransactionBuilder.*", "")); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } } return builder.toString(); } private class GrpcCostQuery { private final Hbar maxCost; private final boolean notRequired; private Client.Operator operator; private Hbar cost; GrpcCostQuery(Client client) { Query.this.initWithNodeIds(client); cost = Query.this.queryPayment; notRequired = (Query.this.paymentTransactions != null) || !Query.this.isPaymentRequired(); maxCost = MoreObjects.firstNonNull(Query.this.maxQueryPayment, client.defaultMaxQueryPayment); if (!notRequired) { operator = Query.this.getOperatorFromClient(client); } } public Client.Operator getOperator() { return operator; } public Hbar getCost() { return cost; } public boolean isNotRequired() { return notRequired; } GrpcCostQuery setCost(Hbar cost) { this.cost = cost; return this; } boolean shouldError() { // Check if this is below our configured maximum query payment return cost.compareTo(maxCost) > 0; } MaxQueryPaymentExceededException mapError() { return new MaxQueryPaymentExceededException(Query.this, cost, maxCost); } void finish() { Query.this.chosenQueryPayment = cost; Query.this.paymentOperator = operator; Query.this.paymentTransactions = new ArrayList<>(Query.this.nodeAccountIds.size()); for (int i = 0; i < Query.this.nodeAccountIds.size(); i++) { Query.this.paymentTransactions.add(null); } } } @SuppressWarnings("NullableDereference") private class QueryCostQuery extends Query { @Override void validateChecksums(Client client) throws BadEntityIdException { } @Override void onMakeRequest(com.hedera.hashgraph.sdk.proto.Query.Builder queryBuilder, QueryHeader header) { headerBuilder.setResponseType(ResponseType.COST_ANSWER); // COST_ANSWER requires a payment to pass validation but doesn't actually process it // yes, this transaction is completely invalid // that is okay // now go back to sleep // without this, an error of MISSING_QUERY_HEADER is returned headerBuilder.setPayment(new TransferTransaction() .setNodeAccountIds(Collections.singletonList(new AccountId(0))) .setTransactionId(TransactionId.withValidStart(new AccountId(0), Instant.ofEpochSecond(0))) .freeze() .makeRequest()); Query.this.onMakeRequest(queryBuilder, headerBuilder.build()); } @Override ResponseHeader mapResponseHeader(Response response) { return Query.this.mapResponseHeader(response); } @Override QueryHeader mapRequestHeader(com.hedera.hashgraph.sdk.proto.Query request) { return Query.this.mapRequestHeader(request); } @Override Hbar mapResponse(Response response, AccountId nodeId, com.hedera.hashgraph.sdk.proto.Query request) { return Hbar.fromTinybars(mapResponseHeader(response).getCost()); } @Override MethodDescriptor getMethodDescriptor() { return Query.this.getMethodDescriptor(); } @Override boolean isPaymentRequired() { // combo breaker return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy